"""HLS manifest proxy service (Phase 3.3). Rewrites HLS manifests and proxies .ts segments so the browser treats them as same-origin, enabling Web Audio API access to the audio track. """ import logging import re from typing import AsyncGenerator from urllib.parse import quote, urljoin import httpx from fastapi.responses import StreamingResponse logger = logging.getLogger(__name__) class HLSProxyService: """Streams and rewrites HLS manifests; proxies .ts segments with zero re-encoding.""" async def rewrite_manifest(self, upstream_url: str, upstream: httpx.Response) -> AsyncGenerator[str, None]: base_url = upstream_url async for line in upstream.aiter_lines(): rewritten = self._rewrite_line(line, base_url) yield rewritten + "\n" def _rewrite_line(self, line: str, base_url: str) -> str: stripped = line.rstrip("\r\n") if not stripped: return stripped if stripped.startswith("#"): if stripped.startswith("#EXT-X-KEY:") and 'URI="' in stripped: return self._rewrite_key_uri(stripped, base_url) return stripped if "://" in stripped: absolute_uri = stripped else: absolute_uri = urljoin(base_url, stripped) return self._build_proxy_url_for_uri(absolute_uri) def _rewrite_key_uri(self, line: str, base_url: str) -> str: match = re.match(r'(#EXT-X-KEY:.*URI=")(.+?)(".*)', line) if not match: return line prefix, uri, suffix = match.group(1), match.group(2), match.group(3) if "://" in uri: absolute_uri = uri else: absolute_uri = urljoin(base_url, uri) proxy_uri = self._build_proxy_url_for_uri(absolute_uri) return f"{prefix}{proxy_uri}{suffix}" def _resolve_url(self, uri: str, base_url: str) -> str: return urljoin(base_url, uri) def _build_proxy_url_for_uri(self, absolute_uri: str) -> str: encoded = quote(absolute_uri, safe="") if absolute_uri.endswith(".m3u8"): return f"/api/v1/youtube/proxy/manifest.m3u8?url={encoded}" return f"/api/v1/youtube/proxy/segment.ts?url={encoded}" async def proxy_segment(self, upstream_url: str) -> StreamingResponse: async with httpx.AsyncClient(timeout=30.0) as client: req = client.build_request("GET", upstream_url) upstream = await client.send(req, stream=True) return StreamingResponse( upstream.aiter_bytes(), status_code=upstream.status_code, media_type="video/mp2t", headers={"access-control-allow-origin": "*"}, )