import os import re from django.conf import settings from django.http import StreamingHttpResponse, Http404, HttpResponseNotModified, FileResponse from django.views.static import was_modified_since from wsgiref.util import FileWrapper def serve_video_with_range(request, path): """ Serve a media file with HTTP Range support. Required for HTML5 video seeking in Chrome/Safari using the Django development server. """ clean_path = path.lstrip('/') full_path = os.path.normpath(os.path.join(settings.MEDIA_ROOT, clean_path)) # Security check to prevent directory traversal if not full_path.startswith(os.path.normpath(settings.MEDIA_ROOT)): raise Http404("Invalid path") if not os.path.exists(full_path): raise Http404(f"File {path} not found") statobj = os.stat(full_path) size = statobj.st_size # Very simple content type mapping for videos content_type = "video/mp4" if full_path.endswith('.webm'): content_type = "video/webm" elif full_path.endswith('.mkv'): content_type = "video/x-matroska" elif full_path.endswith('.svg'): content_type = "image/svg+xml" elif full_path.endswith('.png'): content_type = "image/png" elif full_path.endswith('.jpg') or full_path.endswith('.jpeg'): content_type = "image/jpeg" range_header = request.META.get('HTTP_RANGE', '').strip() if range_header.startswith('bytes='): range_match = re.match(r'bytes=(\d+)-(\d*)', range_header) if range_match: first_byte, last_byte = range_match.groups() first_byte = int(first_byte) last_byte = int(last_byte) if last_byte else size - 1 if last_byte >= size: last_byte = size - 1 length = last_byte - first_byte + 1 def file_iterator(file_path, offset=0, bytes_to_read=None): with open(file_path, 'rb') as f: f.seek(offset) remaining = bytes_to_read while remaining > 0: chunk_size = min(8192, remaining) data = f.read(chunk_size) if not data: break yield data remaining -= len(data) resp = StreamingHttpResponse( file_iterator(full_path, offset=first_byte, bytes_to_read=length), status=206, content_type=content_type ) resp['Content-Range'] = f'bytes {first_byte}-{last_byte}/{size}' resp['Content-Length'] = str(length) resp['Accept-Ranges'] = 'bytes' return resp # Fallback to standard 200 FileResponse if no range resp = FileResponse(open(full_path, 'rb'), content_type=content_type) resp['Content-Length'] = str(size) resp['Accept-Ranges'] = 'bytes' return resp