feat(transcode): optimize + on-the-fly quality + HLS streaming (§6.6)
POST /tracks/{id}/optimize enqueues a transcode; GET /stream/{id}?quality=
serves a cached Opus rendition (miss → master + background warm); GET
/stream/{id}/hls/{playlist.m3u8,segment} serves the worker-generated HLS
rendition (AAC-in-TS), with ?token= propagated onto segment URLs for players
that can't set headers. Hexagonal: Transcoder port, FfmpegTranscoder adapter,
TranscodeService + cache-path helpers, transcode_track worker (idempotent),
schemas + deps wiring. ffmpeg already in the image.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""Transcode service + cache-path helpers (Group B / plan §6.6).
|
||||
|
||||
Cache layout under ``transcode_cache_path``::
|
||||
|
||||
{track_id}/opus_{kbps}.opus # direct quality renditions
|
||||
{track_id}/hls/playlist.m3u8 # HLS rendition (AAC-in-TS)
|
||||
{track_id}/hls/seg_000.ts …
|
||||
|
||||
The request side (streaming router) only *reads* the cache — misses fall back to
|
||||
the original file and enqueue generation. The worker (``transcode_task``) writes
|
||||
it. Path helpers are module-level so both sides agree on locations without one
|
||||
importing the other.
|
||||
"""
|
||||
|
||||
import re
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from app.domain.errors import NotFoundError
|
||||
from app.domain.ports import TrackRepository
|
||||
|
||||
# Stream-quality name (matches the user-settings ``StreamQuality``) → Opus
|
||||
# bitrate. ``original`` is absent: it means "serve the master, no transcode".
|
||||
QUALITY_BITRATE: dict[str, int] = {"high": 128, "medium": 96, "low": 64}
|
||||
|
||||
# Single HLS rendition bitrate (AAC). One rendition keeps the MVP simple; a
|
||||
# multi-bitrate ladder can come later.
|
||||
HLS_BITRATE = 128
|
||||
|
||||
# Only these segment names may be served, guarding the segment route against
|
||||
# path traversal.
|
||||
_SEGMENT_RE = re.compile(r"^seg_\d{3,}\.ts$")
|
||||
|
||||
|
||||
def bitrate_for_quality(quality: str) -> int | None:
|
||||
"""Opus bitrate for a quality name, or ``None`` for ``original``/unknown."""
|
||||
return QUALITY_BITRATE.get(quality)
|
||||
|
||||
|
||||
def track_cache_dir(root: Path, track_id: uuid.UUID) -> Path:
|
||||
return root / str(track_id)
|
||||
|
||||
|
||||
def opus_path(root: Path, track_id: uuid.UUID, bitrate_kbps: int) -> Path:
|
||||
return track_cache_dir(root, track_id) / f"opus_{bitrate_kbps}.opus"
|
||||
|
||||
|
||||
def hls_dir(root: Path, track_id: uuid.UUID) -> Path:
|
||||
return track_cache_dir(root, track_id) / "hls"
|
||||
|
||||
|
||||
def hls_playlist_path(root: Path, track_id: uuid.UUID) -> Path:
|
||||
return hls_dir(root, track_id) / "playlist.m3u8"
|
||||
|
||||
|
||||
def hls_segment_path(root: Path, track_id: uuid.UUID, name: str) -> Path | None:
|
||||
"""Resolve a segment file, or ``None`` if the name is not a valid segment."""
|
||||
if not _SEGMENT_RE.fullmatch(name):
|
||||
return None
|
||||
return hls_dir(root, track_id) / name
|
||||
|
||||
|
||||
class TranscodeService:
|
||||
"""Request-side cache lookups for transcoded renditions."""
|
||||
|
||||
def __init__(self, *, tracks: TrackRepository, cache_root: Path) -> None:
|
||||
self._tracks = tracks
|
||||
self._root = cache_root
|
||||
|
||||
async def _require_streamable(self, track_id: uuid.UUID) -> None:
|
||||
track = await self._tracks.get_by_id(track_id)
|
||||
if track is None:
|
||||
raise NotFoundError("Track not found.")
|
||||
if track.storage_uri is None:
|
||||
raise NotFoundError("Track is not yet downloaded.")
|
||||
|
||||
async def resolve_quality_file(
|
||||
self, track_id: uuid.UUID, quality: str
|
||||
) -> Path | None:
|
||||
"""Cached Opus file for ``quality`` if present, else ``None`` (caller
|
||||
falls back to the master and enqueues generation). ``original`` → None."""
|
||||
bitrate = bitrate_for_quality(quality)
|
||||
if bitrate is None:
|
||||
return None
|
||||
path = opus_path(self._root, track_id, bitrate)
|
||||
return path if path.exists() else None
|
||||
|
||||
async def hls_playlist(self, track_id: uuid.UUID) -> Path | None:
|
||||
"""Cached HLS playlist if generated, else ``None`` (validates the track
|
||||
exists so an unknown id 404s rather than silently missing)."""
|
||||
await self._require_streamable(track_id)
|
||||
path = hls_playlist_path(self._root, track_id)
|
||||
return path if path.exists() else None
|
||||
|
||||
def hls_segment(self, track_id: uuid.UUID, name: str) -> Path | None:
|
||||
path = hls_segment_path(self._root, track_id, name)
|
||||
if path is None or not path.exists():
|
||||
return None
|
||||
return path
|
||||
Reference in New Issue
Block a user