"""Audio streaming — direct byte-range stream, transcoded quality, and HLS. ``GET /stream/{id}`` streams the master with Range support, or a cached Opus rendition when ``?quality=`` is set (a cache miss falls back to the master and warms the cache in the background — playback never waits on ffmpeg). ``/hls/*`` serves the cached HLS rendition (generated by the ``transcode_track`` worker). """ import re import uuid from typing import Annotated import anyio from fastapi import APIRouter, Header, Query, Response from fastapi.responses import FileResponse, StreamingResponse from app.api.deps import StreamingServiceDep, StreamUser, TranscodeServiceDep from app.domain.errors import NotFoundError from app.workers.queue import enqueue_transcode_quiet router = APIRouter(prefix="/stream", tags=["streaming"]) _HLS_PLAYLIST_TYPE = "application/vnd.apple.mpegurl" _HLS_SEGMENT_TYPE = "video/mp2t" _OPUS_TYPE = "audio/ogg" _SEGMENT_LINE_RE = re.compile(r"^(seg_\d+\.ts)$", re.MULTILINE) @router.get("/{track_id}") async def stream_track( track_id: uuid.UUID, service: StreamingServiceDep, transcode: TranscodeServiceDep, _user: StreamUser, range_header: Annotated[str | None, Header(alias="Range")] = None, quality: Annotated[str | None, Query()] = None, ) -> Response: # A quality rendition, if one is cached; otherwise fall back to the master # and enqueue generation so the next play gets it (graceful degradation). if quality and quality != "original": cached = await transcode.resolve_quality_file(track_id, quality) if cached is not None: return FileResponse(cached, media_type=_OPUS_TYPE) await enqueue_transcode_quiet(track_id, quality=quality, hls=False) result = await service.open_stream(track_id, range_header) headers = { "Accept-Ranges": "bytes", "Content-Length": str(result.content_length), } if result.is_partial: headers["Content-Range"] = f"bytes {result.start}-{result.end}/{result.total_size}" status_code = 206 else: status_code = 200 return StreamingResponse( result.stream, status_code=status_code, headers=headers, media_type=result.content_type, ) @router.get("/{track_id}/hls/playlist.m3u8") async def stream_hls_playlist( track_id: uuid.UUID, transcode: TranscodeServiceDep, _user: StreamUser, token: Annotated[str | None, Query()] = None, ) -> Response: """Serve the cached HLS playlist. On a miss, kick off generation and 404 so the client retries. Segment URLs are relative; when the request carried a ``?token=`` (players can't set an Authorization header), it's appended to each segment line so the segment requests authenticate the same way.""" path = await transcode.hls_playlist(track_id) if path is None: await enqueue_transcode_quiet(track_id, hls=True) raise NotFoundError("HLS rendition is being prepared; retry shortly.") body = await anyio.to_thread.run_sync(path.read_text) if token: body = _SEGMENT_LINE_RE.sub(rf"\1?token={token}", body) return Response(body, media_type=_HLS_PLAYLIST_TYPE) @router.get("/{track_id}/hls/{segment}") async def stream_hls_segment( track_id: uuid.UUID, segment: str, transcode: TranscodeServiceDep, _user: StreamUser, ) -> FileResponse: path = transcode.hls_segment(track_id, segment) if path is None: raise NotFoundError("Segment not found.") return FileResponse(path, media_type=_HLS_SEGMENT_TYPE)