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:
@@ -7,6 +7,7 @@ from sqlalchemy.orm import sessionmaker, joinedload
|
||||
from sqlalchemy import create_engine, or_, and_, text
|
||||
from models.models import Event, EventMedia, EventException
|
||||
from dateutil.rrule import rrulestr
|
||||
from urllib.request import Request, urlopen
|
||||
from datetime import timezone
|
||||
|
||||
# Load .env only in development to mirror server/database.py behavior
|
||||
@@ -256,6 +257,56 @@ def format_event_with_media(event):
|
||||
f"[Scheduler] Using website URL for event_media_id={media.id} (type={event.event_type.value}): {media.url}")
|
||||
_media_decision_logged.add(media.id)
|
||||
|
||||
# Add other event types (video, message, etc.) here as needed...
|
||||
# Handle video events
|
||||
elif event.event_type.value == "video":
|
||||
filename = os.path.basename(media.file_path) if media.file_path else "video"
|
||||
# Use streaming endpoint for better video playback support
|
||||
stream_url = f"{API_BASE_URL}/api/eventmedia/stream/{media.id}/{filename}"
|
||||
|
||||
# Best-effort: probe the streaming endpoint for cheap metadata (HEAD request)
|
||||
mime_type = None
|
||||
size = None
|
||||
accept_ranges = False
|
||||
try:
|
||||
req = Request(stream_url, method='HEAD')
|
||||
with urlopen(req, timeout=2) as resp:
|
||||
# getheader returns None if missing
|
||||
mime_type = resp.getheader('Content-Type')
|
||||
length = resp.getheader('Content-Length')
|
||||
if length:
|
||||
try:
|
||||
size = int(length)
|
||||
except Exception:
|
||||
size = None
|
||||
accept_ranges = (resp.getheader('Accept-Ranges') or '').lower() == 'bytes'
|
||||
except Exception as e:
|
||||
# Don't fail the scheduler for probe errors; log once per media
|
||||
if media.id not in _media_decision_logged:
|
||||
logging.debug(f"[Scheduler] HEAD probe for media_id={media.id} failed: {e}")
|
||||
|
||||
event_dict["video"] = {
|
||||
"type": "media",
|
||||
"url": stream_url,
|
||||
"autoplay": getattr(event, "autoplay", True),
|
||||
"loop": getattr(event, "loop", False),
|
||||
"volume": getattr(event, "volume", 0.8),
|
||||
# Best-effort metadata to help clients decide how to stream
|
||||
"mime_type": mime_type,
|
||||
"size": size,
|
||||
"accept_ranges": accept_ranges,
|
||||
# Optional richer info (may be null if not available): duration (seconds), resolution, bitrate
|
||||
"duration": None,
|
||||
"resolution": None,
|
||||
"bitrate": None,
|
||||
"qualities": [],
|
||||
"thumbnails": [],
|
||||
"checksum": None,
|
||||
}
|
||||
if media.id not in _media_decision_logged:
|
||||
logging.debug(
|
||||
f"[Scheduler] Using video streaming URL for event_media_id={media.id}: {filename}")
|
||||
_media_decision_logged.add(media.id)
|
||||
|
||||
# Add other event types (message, etc.) here as needed...
|
||||
|
||||
return event_dict
|
||||
|
||||
Reference in New Issue
Block a user