summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/media_probe.py
blob: e0c59e3158cc8064f64ee6ace74b40c1fa50f745 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
"""
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"}

# Source video codecs whose MSE codec string is real but which no mainstream
# browser can actually decode via MediaSource on most desktop platforms — HEVC
# has no royalty-free decoder in Chrome/Firefox on Linux (and is spotty even
# on platforms with one). Found live: a real HEVC/EAC3 WEB-DL reported
# "Codec not supported for streaming: hev1.1.6.L93.B0,mp4a.40.2" from
# MediaSource.isTypeSupported, even though ffprobe/VLC play it fine. VP9/AV1
# are not in this set — those decode natively in every mainstream browser.
BROWSER_INCOMPATIBLE_VIDEO_CODECS = frozenset({"hevc"})


async def probe_video(
    path: str,
) -> tuple[str | None, float, bool, int | None, int | None, str | None]:
    """
    Probe video file with ffprobe, return (MSE codec string, duration,
    has_audio, width, height, raw video codec name).

    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 is copied
    whenever the browser can decode it directly; the raw codec name is
    returned alongside the MSE string so the caller can decide whether this
    source needs a real re-encode instead (BROWSER_INCOMPATIBLE_VIDEO_CODECS
    above) — the MSE string alone can't drive that decision, since it still
    faithfully reports "hev1..." for a source this pipeline cannot actually
    deliver copied.

    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).
    """
    from meshbay_node.platform import ffprobe_cmd
    proc = await asyncio.create_subprocess_exec(
        ffprobe_cmd(), "-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 = ""
    raw_codec_name: str | None = None
    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", "")
            raw_codec_name = cn or None
            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, raw_codec_name
    codec = f"{v_codec},mp4a.40.2" if has_audio else v_codec
    return codec, duration, has_audio, width, height, raw_codec_name