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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
"""
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
from dataclasses import dataclass, field
_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"})
@dataclass(frozen=True)
class AudioTrack:
"""
One selectable audio track.
**`ordinal` is the position among the audio streams, not the container
stream index**, because that is what `-map 0:a:<n>` takes. A file whose
audio sits at container indices 1, 2 and 3 has ordinals 0, 1 and 2, and
mapping `0:a:1` on the container index would silently serve the third
track — the failure this field's name exists to prevent.
"""
ordinal: int
language: str | None
title: str | None
codec_name: str | None
channels: int | None
@dataclass
class VideoProbe:
"""
What one ffprobe call says about a video file.
A dataclass rather than the tuple this used to return: the tuple had six
positional fields, `has_audio` sat third and `raw_codec_name` sixth, and
adding a seventh for the track list would have made every call site a
counting exercise.
"""
codec: str | None
duration: float
has_audio: bool
width: int | None
height: int | None
raw_codec_name: str | None
audio_tracks: list[AudioTrack] = field(default_factory=list)
async def probe_video(path: str) -> VideoProbe:
"""
Probe a video file with ffprobe.
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.
**A `None` codec string means "this must be re-encoded", not "this cannot
be played".** Only the four codecs a browser can decode through MediaSource
are mapped; everything else — MPEG-4 Part 2 (Xvid, DivX), MPEG-2, VC-1,
WMV, Theora — has no string to report because there is no browser decoder
to report it to, and the streaming path answers that by re-encoding to
H264. It answered it by refusing until 2026-09-09, which read to the
operator as a broken file rather than as an unwired code path.
**Every audio track is reported, not just the first.** The streaming path
transcodes audio unconditionally, so serving the second track costs exactly
what serving the first costs and the choice is the viewer's to make; a
library of dubbed films is one where the first track is a language half the
group does not want. `has_audio` stays as the single question the muxing
decisions ask, and is now `bool(audio_tracks)`.
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,channels",
"-show_entries", "stream_tags=language,title",
"-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
width: int | None = None
height: int | None = None
audio_tracks: list[AudioTrack] = []
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":
tags = s.get("tags") or {}
audio_tracks.append(AudioTrack(
# Counted here, never read from `s["index"]` — see AudioTrack.
ordinal=len(audio_tracks),
language=(tags.get("language") or "").strip() or None,
title=(tags.get("title") or "").strip() or None,
codec_name=s.get("codec_name") or None,
channels=s.get("channels"),
))
has_audio = bool(audio_tracks)
codec = None
if v_codec:
codec = f"{v_codec},mp4a.40.2" if has_audio else v_codec
return VideoProbe(
codec=codec,
duration=duration,
has_audio=has_audio,
width=width,
height=height,
raw_codec_name=raw_codec_name,
audio_tracks=audio_tracks,
)
|