72 lines
2.9 KiB
Python
72 lines
2.9 KiB
Python
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
|