""" ffprobe wrapper shared by stream-time codec detection (transport/webrtc_server.py) and index-time technical-field enrichment (indexer/enrich.py). Split out of webrtc_server.py so the indexer package (which webrtc_server.py already imports from) can call it too without a circular import. """ import asyncio import json _H264_PROFILES = {"Baseline": "42", "Main": "4d", "High": "64", "High 10": "6e"} async def probe_video(path: str) -> tuple[str | None, float, bool, int | None, int | None]: """ Probe video file with ffprobe, return (MSE codec string, duration, has_audio, width, height). The audio half of the codec string is always "mp4a.40.2" (AAC-LC) or absent — never the source's real audio codec — because the streaming path always transcodes audio to AAC and never copies it: MSE in every mainstream browser only decodes AAC/Opus, and a source codec outside that (AC-3, E-AC-3, DTS, ...) is at best silently unplayable and at worst, for E-AC-3 at least, makes ffmpeg itself refuse to write the fragmented MP4 header ("Cannot write moov atom before EAC3 packets parsed" — reproduced against a real 5.1 E-AC-3 WEB-DL). Video stays whatever it actually is: it is always copied, never transcoded. width/height come from the same ffprobe call (one extra `-show_entries` field, no second process spawn) — resolution is deliberately never guessed from the filename (docs/mediacenter.md §3.5). """ proc = await asyncio.create_subprocess_exec( "ffprobe", "-v", "error", "-show_entries", "stream=codec_name,profile,level,codec_type,width,height", "-show_entries", "format=duration", "-of", "json", path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, _ = await proc.communicate() info = json.loads(stdout) duration = float(info.get("format", {}).get("duration", 0)) v_codec = "" has_audio = False width: int | None = None height: int | None = None for s in info.get("streams", []): if s.get("codec_type") == "video" and not v_codec: cn = s.get("codec_name", "") if cn == "h264": p = _H264_PROFILES.get(s.get("profile", "High"), "64") lvl = int(s.get("level", 40)) v_codec = f"avc1.{p}00{lvl:02x}" elif cn == "hevc": v_codec = "hev1.1.6.L93.B0" elif cn == "vp9": v_codec = "vp09.00.10.08" elif cn == "av1": v_codec = "av01.0.01M.08" width = s.get("width") height = s.get("height") elif s.get("codec_type") == "audio": has_audio = True if not v_codec: return None, duration, has_audio, width, height codec = f"{v_codec},mp4a.40.2" if has_audio else v_codec return codec, duration, has_audio, width, height