feat(video): add streamable video events & dashboard controls
Add end-to-end support for video events: server streaming, scheduler metadata, API fields, and dashboard UI. - Server: range-capable streaming endpoint with byte-range support. - Scheduler: emits `video` object; best-effort HEAD probe adds `mime_type`, `size`, `accept_ranges`; placeholders for richer metadata (duration/resolution/bitrate/qualities/thumbnails). - API/DB: accept and persist `event_media_id`, `autoplay`, `loop`, `volume` for video events. - Frontend: Event modal supports video selection + playback options; FileManager increased upload size and client-side duration check (max 10 minutes). - Docs/UX: bumped program-info, added UX-only changelog and updated Copilot instructions for contributors. - Notes: metadata extraction (ffprobe), checksum persistence, and HLS/DASH transcoding are recommended follow-ups (separate changes).
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from re import A
|
||||
from flask import Blueprint, request, jsonify, send_from_directory
|
||||
from flask import Blueprint, request, jsonify, send_from_directory, Response, send_file
|
||||
from server.permissions import editor_or_higher
|
||||
from server.database import Session
|
||||
from models.models import EventMedia, MediaType, Conversion, ConversionStatus
|
||||
@@ -7,6 +7,7 @@ from server.task_queue import get_queue
|
||||
from server.worker import convert_event_media_to_pdf
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
|
||||
eventmedia_bp = Blueprint('eventmedia', __name__, url_prefix='/api/eventmedia')
|
||||
|
||||
@@ -304,3 +305,63 @@ def get_media_by_id(media_id):
|
||||
}
|
||||
session.close()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
# --- Video Streaming with Range Request Support ---
|
||||
@eventmedia_bp.route('/stream/<int:media_id>/<path:filename>', methods=['GET'])
|
||||
def stream_video(media_id, filename):
|
||||
"""Stream video files with range request support for seeking"""
|
||||
session = Session()
|
||||
media = session.query(EventMedia).get(media_id)
|
||||
if not media or not media.file_path:
|
||||
session.close()
|
||||
return jsonify({'error': 'Video not found'}), 404
|
||||
|
||||
file_path = os.path.join(MEDIA_ROOT, media.file_path)
|
||||
if not os.path.exists(file_path):
|
||||
session.close()
|
||||
return jsonify({'error': 'File not found'}), 404
|
||||
|
||||
session.close()
|
||||
|
||||
# Determine MIME type based on file extension
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
mime_types = {
|
||||
'.mp4': 'video/mp4',
|
||||
'.webm': 'video/webm',
|
||||
'.ogv': 'video/ogg',
|
||||
'.avi': 'video/x-msvideo',
|
||||
'.mkv': 'video/x-matroska',
|
||||
'.mov': 'video/quicktime',
|
||||
'.wmv': 'video/x-ms-wmv',
|
||||
'.flv': 'video/x-flv',
|
||||
'.mpg': 'video/mpeg',
|
||||
'.mpeg': 'video/mpeg',
|
||||
}
|
||||
mime_type = mime_types.get(ext, 'video/mp4')
|
||||
|
||||
# Support range requests for video seeking
|
||||
range_header = request.headers.get('Range', None)
|
||||
if not range_header:
|
||||
return send_file(file_path, mimetype=mime_type)
|
||||
|
||||
size = os.path.getsize(file_path)
|
||||
byte_start, byte_end = 0, size - 1
|
||||
|
||||
match = re.search(r'bytes=(\d+)-(\d*)', range_header)
|
||||
if match:
|
||||
byte_start = int(match.group(1))
|
||||
if match.group(2):
|
||||
byte_end = int(match.group(2))
|
||||
|
||||
length = byte_end - byte_start + 1
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
f.seek(byte_start)
|
||||
data = f.read(length)
|
||||
|
||||
response = Response(data, 206, mimetype=mime_type, direct_passthrough=True)
|
||||
response.headers.add('Content-Range', f'bytes {byte_start}-{byte_end}/{size}')
|
||||
response.headers.add('Accept-Ranges', 'bytes')
|
||||
response.headers.add('Content-Length', str(length))
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user