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:
RobbStarkAustria
2025-10-25 16:48:14 +00:00
parent e6c19c189f
commit 38800cec68
14 changed files with 453 additions and 83 deletions

View File

@@ -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