diff --git a/drb-c2-core/app/internal/storage.py b/drb-c2-core/app/internal/storage.py index db64040..18af633 100644 --- a/drb-c2-core/app/internal/storage.py +++ b/drb-c2-core/app/internal/storage.py @@ -44,11 +44,33 @@ def _safe_audio_filename(filename: str, call_id: str) -> str: The original extension is preserved only if it's a known audio type. """ ext = os.path.splitext(filename)[-1].lower() if filename else "" - if ext not in (".mp3", ".wav", ".ogg", ".m4a", ".aac", ".flac"): + if ext not in AUDIO_CONTENT_TYPES: ext = ".mp3" return f"{call_id}{ext}" +# Extension → Content-Type. The node used to send nothing but 16 kbps MP3, so +# "audio/mpeg" was hardcoded at every point audio is written or served; it now +# sends FLAC (lossless, for Whisper's benefit — see call_recorder.py's AUDIO_* +# constants) and a stored object mislabelled audio/mpeg will not play in a +# browser. Old .mp3 objects keep working: the map is keyed off the real +# extension, not off what the current node happens to produce. +AUDIO_CONTENT_TYPES = { + ".flac": "audio/flac", + ".mp3": "audio/mpeg", + ".wav": "audio/wav", + ".ogg": "audio/ogg", + ".m4a": "audio/mp4", + ".aac": "audio/aac", +} + + +def content_type_for(name: str) -> str: + """Content-Type for a stored audio object, by extension. Defaults to MP3.""" + ext = os.path.splitext(name or "")[-1].lower() + return AUDIO_CONTENT_TYPES.get(ext, "audio/mpeg") + + async def upload_audio(data: bytes, filename: str, call_id: str = "") -> Optional[str]: """Upload audio bytes to GCS and return the canonical gs:// URI, or None if disabled.""" if not settings.gcs_bucket: @@ -65,7 +87,7 @@ async def upload_audio(data: bytes, filename: str, call_id: str = "") -> Optiona else: client = storage.Client() blob = client.bucket(settings.gcs_bucket).blob(blob_path) - blob.upload_from_string(data, content_type="audio/mpeg") + blob.upload_from_string(data, content_type=content_type_for(safe_name)) try: await asyncio.to_thread(_upload) diff --git a/drb-c2-core/app/routers/media.py b/drb-c2-core/app/routers/media.py index 09de51a..67a35ad 100644 --- a/drb-c2-core/app/routers/media.py +++ b/drb-c2-core/app/routers/media.py @@ -13,7 +13,7 @@ service-account private key on the VM — is involved anywhere in this path. """ from fastapi import APIRouter, HTTPException, Query, Response from app.internal import firestore as fstore -from app.internal.storage import verify_audio_link, gcs_uri_for_call, download_audio +from app.internal.storage import verify_audio_link, gcs_uri_for_call, download_audio, content_type_for router = APIRouter(prefix="/media", tags=["media"]) @@ -42,12 +42,18 @@ async def get_call_audio( return Response( content=data, - media_type="audio/mpeg", + # Was hardcoded audio/mpeg. The node now uploads FLAC, and a browser + # will not play a FLAC body labelled audio/mpeg. Derived from the stored + # object's extension so old .mp3 recordings keep working unchanged. + media_type=content_type_for(gcs_uri), headers={ "Content-Length": str(len(data)), - # Recordings are small (16 kbps mono — a 30s call is ~60 KB), so the - # whole body is sent at once and the browser seeks within its own - # buffer. Range support would only matter for long files. + # Whole body at once, no Range support. This was comfortable at + # 16 kbps mono (~60 KB for a 30 s call); FLAC is ~1.3 MB/min, so a + # long call is now tens of MB and the browser must download all of + # it before playback starts. Acceptable for typical few-second + # transmissions, but this is the change that makes Range support + # actually matter — see DEFERRED.md. "Accept-Ranges": "none", # Immutable content, but the URL expires — cache privately only. "Cache-Control": "private, max-age=3600",