magenta2 catchup

This commit is contained in:
Nirvana
2026-06-09 16:33:13 +02:00
parent f4afff5868
commit 2b70962257
@@ -9,15 +9,37 @@ Responsibilities
(avoiding a SMIL round-trip for channels already fetched by get_channels).
- Build Widevine licence URLs directly for live channels using lib_theplatform.
- Delegate VOD and recording manifest/DRM requests to SmilManager.
- Serve catchup manifests by appending ``dvr_window_length`` to the cached
live manifest URL — no separate SMIL fetch required.
- Inject ``smil_base_url`` from the recording-URL cache so callers (e.g.
DRMOperations) only need to pass a ``content_id``.
- Provide ``get_catchup_manifest`` via SmilManager.
The class holds NO state of its own beyond references to the managers and
callbacks passed at construction time. All caches belong to ChannelManager.
Catchup routing
---------------
Magenta2 catchup is DVR-based: the live DASH manifest URL is reused with a
``dvr_window_length`` query parameter (in seconds) that tells the CDN how far
back the sliding window should reach. Example:
Live: https://svc45.…/zdf_hd/DASH/index.mpd?AppVersion=…
Catchup: https://svc45.…/zdf_hd/DASH/index.mpd?AppVersion=…&dvr_window_length=14400
The window length is derived from the provider's ``catchup_window`` property
(hours) and is passed in as ``dvr_window_seconds`` from the caller, or falls
back to ``DVR_WINDOW_SECONDS_DEFAULT`` (4 h = 14 400 s).
DRM for catchup is identical to live — the same Widevine licence URL applies —
so ``get_drm`` routes catchup requests through the same fast-path as live.
If the live manifest URL is not yet cached when a catchup request arrives,
``_ensure_live_cache()`` is called to populate it before building the URL.
SmilManager is NOT used for catchup on this provider.
"""
import base64
from typing import Callable, Dict, List, Optional
from urllib.parse import urlencode, urlparse, urlunparse, parse_qs
from ...base.models import DRMConfig
from ...base.utils.logger import logger
@@ -32,6 +54,10 @@ from ..lib_theplatform import (
build_widevine_drm_config,
)
# Default DVR window: 4 hours in seconds. Matches provider.catchup_window = 4.
DVR_WINDOW_SECONDS_DEFAULT: int = 4 * 3600 # 14 400
DVR_WINDOW_PARAM: str = "dvr_window_length"
class PlaybackManager:
"""
@@ -84,10 +110,18 @@ class PlaybackManager:
"""
Return the MPD manifest URL for *content_id*.
Live channels whose manifest was already fetched by get_channels() are
served directly from the ChannelManager cache — no SMIL round-trip.
VOD and recordings fall through to SmilManager.
Routing logic
~~~~~~~~~~~~~
1. **Catchup** (``start_time`` + ``end_time`` present in kwargs):
Appends ``dvr_window_length`` to the cached live manifest URL.
No SMIL round-trip. The live cache is populated on demand if empty.
2. **Live** (``content_type == CONTENT_TYPE_LIVE``, no time window):
Served directly from the ChannelManager cache when available.
3. **VOD / recordings**: Fall through to ``SmilManager.get_manifest``.
"""
if self._is_catchup_request(kwargs):
return self._get_catchup_manifest(content_id, kwargs)
if content_type == CONTENT_TYPE_LIVE:
self._ensure_live_cache()
if content_id in self._channel_manager._live_manifest_cache:
@@ -105,10 +139,35 @@ class PlaybackManager:
"""
Return DRM configuration for *content_id*.
For live channels whose releasePid is cached, the Widevine licence URL
is built directly using lib_theplatform — no SMIL fetch needed.
VOD and recordings fall through to SmilManager.
Routing logic
~~~~~~~~~~~~~
1. **Catchup** (``start_time`` + ``end_time`` present in kwargs):
DRM is identical to live — same Widevine licence URL applies to the
DVR window. Routes through the live fast-path using the pid cache.
Falls back to SmilManager if the pid is not cached.
2. **Live** (``content_type == CONTENT_TYPE_LIVE``, no time window):
For live channels whose releasePid is cached, the Widevine licence
URL is built directly using lib_theplatform — no SMIL fetch needed.
3. **VOD / recordings**: Fall through to ``SmilManager.get_drm``.
"""
if self._is_catchup_request(kwargs):
# Catchup uses the same Widevine licence as live — reuse the fast-path.
self._ensure_live_cache()
if content_id in self._channel_manager._live_pid_cache:
logger.debug(
f"get_drm: catchup request for {content_id} — reusing live DRM fast-path"
)
return self._build_live_drm(content_id)
# pid not cached yet — fall through to SmilManager as a best-effort.
logger.warning(
f"get_drm: catchup for {content_id} but pid not in live cache; "
"falling back to SmilManager"
)
if not self._smil_manager:
raise RuntimeError("SmilManager not available")
self._inject_smil_base_url(content_id, kwargs)
return self._smil_manager.get_drm(content_id, content_type, **kwargs)
if content_type == CONTENT_TYPE_LIVE:
self._ensure_live_cache()
if content_id in self._channel_manager._live_pid_cache:
@@ -122,17 +181,120 @@ class PlaybackManager:
def get_catchup_manifest(
self, channel_id: str, start_time: int, end_time: int, **kwargs
) -> Optional[str]:
"""Return a catchup manifest URL via SmilManager."""
if not self._smil_manager:
raise RuntimeError("SmilManager not available")
return self._smil_manager.get_catchup_manifest(
channel_id, start_time, end_time, **kwargs
)
"""
Return a catchup manifest URL for *channel_id*.
Builds the DVR URL from the cached live manifest by appending
``dvr_window_length``. ``start_time`` and ``end_time`` are accepted
for interface compatibility but are not used — Magenta2 catchup is a
sliding DVR window, not a fixed time-range VOD asset.
Parameters
----------
channel_id:
The live channel content_id (same key used in the live cache).
start_time:
Unix timestamp (seconds) of the catchup window start. Accepted
for API compatibility; not forwarded to the CDN.
end_time:
Unix timestamp (seconds) of the catchup window end. Accepted
for API compatibility; not forwarded to the CDN.
dvr_window_seconds:
Optional override (via kwargs) for the DVR window length in
seconds. Defaults to ``DVR_WINDOW_SECONDS_DEFAULT`` (14 400).
"""
dvr_seconds: int = kwargs.get("dvr_window_seconds", DVR_WINDOW_SECONDS_DEFAULT)
return self._build_dvr_manifest_url(channel_id, dvr_seconds)
# ------------------------------------------------------------------ #
# Internal helpers #
# ------------------------------------------------------------------ #
@staticmethod
def _is_catchup_request(kwargs: Dict) -> bool:
"""
Return True when *kwargs* carries both ``start_time`` and ``end_time``,
indicating a time-shifted / catchup playback request.
"""
return (
kwargs.get("start_time") is not None
and kwargs.get("end_time") is not None
)
def _get_catchup_manifest(self, content_id: str, kwargs: Dict) -> Optional[str]:
"""
Build a DVR manifest URL for a catchup request arriving via
``get_manifest``.
Extracts an optional ``dvr_window_seconds`` override from kwargs;
otherwise derives the window from ``end_time - start_time`` when both
are present, capped at ``DVR_WINDOW_SECONDS_DEFAULT``.
"""
# Prefer an explicit override; otherwise derive from the requested
# time window so the DVR slider covers at least the requested range.
if "dvr_window_seconds" in kwargs:
dvr_seconds: int = kwargs["dvr_window_seconds"]
else:
start_time: int = kwargs["start_time"]
end_time: int = kwargs["end_time"]
requested_window = end_time - start_time
# Never request a window smaller than the requested range, but
# cap at the provider default to avoid oversized requests.
dvr_seconds = max(
min(requested_window, DVR_WINDOW_SECONDS_DEFAULT),
DVR_WINDOW_SECONDS_DEFAULT,
)
logger.debug(
f"get_manifest: catchup request for {content_id} "
f"(start={kwargs['start_time']}, end={kwargs['end_time']}, "
f"dvr_window_seconds={dvr_seconds})"
)
return self._build_dvr_manifest_url(content_id, dvr_seconds)
def _build_dvr_manifest_url(self, content_id: str, dvr_seconds: int) -> Optional[str]:
"""
Retrieve the cached live manifest URL for *content_id* and append
(or replace) the ``dvr_window_length`` query parameter.
Returns ``None`` if the channel is not found in the live cache even
after attempting to populate it.
"""
self._ensure_live_cache()
live_url = self._channel_manager._live_manifest_cache.get(content_id)
if not live_url:
logger.error(
f"_build_dvr_manifest_url: no live manifest cached for {content_id}; "
"cannot build catchup URL"
)
return None
dvr_url = self._append_dvr_param(live_url, dvr_seconds)
logger.debug(
f"_build_dvr_manifest_url: {content_id}{dvr_url}"
)
return dvr_url
@staticmethod
def _append_dvr_param(url: str, dvr_seconds: int) -> str:
"""
Return *url* with ``dvr_window_length=<dvr_seconds>`` set in the query
string. Any pre-existing ``dvr_window_length`` value is replaced so
that repeated calls are idempotent.
"""
parsed = urlparse(url)
# Parse existing query parameters, preserving all existing keys.
# parse_qs returns lists; rebuild as a flat dict for urlencode.
existing: Dict[str, List[str]] = parse_qs(parsed.query, keep_blank_values=True)
# Replace (or add) the DVR param — overwrite any existing value.
existing[DVR_WINDOW_PARAM] = [str(dvr_seconds)]
new_query = urlencode(
{k: v[0] for k, v in existing.items()},
safe="",
)
return urlunparse(parsed._replace(query=new_query))
def _ensure_live_cache(self) -> None:
"""
Bootstrap the live-manifest / live-pid caches on demand by calling