avbridge 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +120 -0
- package/LICENSE +21 -0
- package/README.md +415 -0
- package/dist/avi-M5B4SHRM.cjs +164 -0
- package/dist/avi-M5B4SHRM.cjs.map +1 -0
- package/dist/avi-POCGZ4JX.js +162 -0
- package/dist/avi-POCGZ4JX.js.map +1 -0
- package/dist/chunk-5ISVAODK.js +80 -0
- package/dist/chunk-5ISVAODK.js.map +1 -0
- package/dist/chunk-F7YS2XOA.cjs +2966 -0
- package/dist/chunk-F7YS2XOA.cjs.map +1 -0
- package/dist/chunk-FKM7QBZU.js +2957 -0
- package/dist/chunk-FKM7QBZU.js.map +1 -0
- package/dist/chunk-J5MCMN3S.js +27 -0
- package/dist/chunk-J5MCMN3S.js.map +1 -0
- package/dist/chunk-L4NPOJ36.cjs +180 -0
- package/dist/chunk-L4NPOJ36.cjs.map +1 -0
- package/dist/chunk-NZU7W256.cjs +29 -0
- package/dist/chunk-NZU7W256.cjs.map +1 -0
- package/dist/chunk-PQTZS7OA.js +147 -0
- package/dist/chunk-PQTZS7OA.js.map +1 -0
- package/dist/chunk-WD2ZNQA7.js +177 -0
- package/dist/chunk-WD2ZNQA7.js.map +1 -0
- package/dist/chunk-Y5FYF5KG.cjs +153 -0
- package/dist/chunk-Y5FYF5KG.cjs.map +1 -0
- package/dist/chunk-Z2FJ5TJC.cjs +82 -0
- package/dist/chunk-Z2FJ5TJC.cjs.map +1 -0
- package/dist/element.cjs +433 -0
- package/dist/element.cjs.map +1 -0
- package/dist/element.d.cts +158 -0
- package/dist/element.d.ts +158 -0
- package/dist/element.js +431 -0
- package/dist/element.js.map +1 -0
- package/dist/index.cjs +576 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +80 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +554 -0
- package/dist/index.js.map +1 -0
- package/dist/libav-http-reader-FPYDBMYK.cjs +16 -0
- package/dist/libav-http-reader-FPYDBMYK.cjs.map +1 -0
- package/dist/libav-http-reader-NQJVY273.js +3 -0
- package/dist/libav-http-reader-NQJVY273.js.map +1 -0
- package/dist/libav-import-2JURFHEW.js +8 -0
- package/dist/libav-import-2JURFHEW.js.map +1 -0
- package/dist/libav-import-GST2AMPL.cjs +30 -0
- package/dist/libav-import-GST2AMPL.cjs.map +1 -0
- package/dist/libav-loader-KA2MAWLM.js +3 -0
- package/dist/libav-loader-KA2MAWLM.js.map +1 -0
- package/dist/libav-loader-ZHOERPHW.cjs +12 -0
- package/dist/libav-loader-ZHOERPHW.cjs.map +1 -0
- package/dist/player-BBwbCkdL.d.cts +365 -0
- package/dist/player-BBwbCkdL.d.ts +365 -0
- package/dist/source-SC6ZEQYR.cjs +28 -0
- package/dist/source-SC6ZEQYR.cjs.map +1 -0
- package/dist/source-ZFS4H7J3.js +3 -0
- package/dist/source-ZFS4H7J3.js.map +1 -0
- package/dist/variant-routing-GOHB2RZN.cjs +12 -0
- package/dist/variant-routing-GOHB2RZN.cjs.map +1 -0
- package/dist/variant-routing-JOBWXYKD.js +3 -0
- package/dist/variant-routing-JOBWXYKD.js.map +1 -0
- package/package.json +95 -0
- package/src/classify/index.ts +1 -0
- package/src/classify/rules.ts +214 -0
- package/src/convert/index.ts +2 -0
- package/src/convert/remux.ts +522 -0
- package/src/convert/transcode.ts +329 -0
- package/src/diagnostics.ts +99 -0
- package/src/element/avbridge-player.ts +576 -0
- package/src/element.ts +19 -0
- package/src/events.ts +71 -0
- package/src/index.ts +42 -0
- package/src/libav-stubs.d.ts +24 -0
- package/src/player.ts +455 -0
- package/src/plugins/builtin.ts +37 -0
- package/src/plugins/registry.ts +32 -0
- package/src/probe/avi.ts +242 -0
- package/src/probe/index.ts +59 -0
- package/src/probe/mediabunny.ts +194 -0
- package/src/strategies/fallback/audio-output.ts +293 -0
- package/src/strategies/fallback/clock.ts +7 -0
- package/src/strategies/fallback/decoder.ts +660 -0
- package/src/strategies/fallback/index.ts +170 -0
- package/src/strategies/fallback/libav-import.ts +27 -0
- package/src/strategies/fallback/libav-loader.ts +190 -0
- package/src/strategies/fallback/variant-routing.ts +43 -0
- package/src/strategies/fallback/video-renderer.ts +216 -0
- package/src/strategies/hybrid/decoder.ts +641 -0
- package/src/strategies/hybrid/index.ts +139 -0
- package/src/strategies/native.ts +107 -0
- package/src/strategies/remux/annexb.ts +112 -0
- package/src/strategies/remux/index.ts +79 -0
- package/src/strategies/remux/mse.ts +234 -0
- package/src/strategies/remux/pipeline.ts +254 -0
- package/src/subtitles/index.ts +91 -0
- package/src/subtitles/render.ts +62 -0
- package/src/subtitles/srt.ts +62 -0
- package/src/subtitles/vtt.ts +5 -0
- package/src/types-shim.d.ts +3 -0
- package/src/types.ts +360 -0
- package/src/util/codec-strings.ts +86 -0
- package/src/util/libav-http-reader.ts +315 -0
- package/src/util/source.ts +274 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AudioCodec,
|
|
3
|
+
AudioTrackInfo,
|
|
4
|
+
Classification,
|
|
5
|
+
ContainerKind,
|
|
6
|
+
MediaContext,
|
|
7
|
+
VideoCodec,
|
|
8
|
+
VideoTrackInfo,
|
|
9
|
+
} from "../types.js";
|
|
10
|
+
import { mp4MimeFor, mseSupports } from "../util/codec-strings.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Codecs we know `<video>` and MSE support across modern desktop + Android.
|
|
14
|
+
* The decision to remux instead of decode hinges on this list.
|
|
15
|
+
*/
|
|
16
|
+
const NATIVE_VIDEO_CODECS = new Set<VideoCodec>(["h264", "h265", "vp8", "vp9", "av1"]);
|
|
17
|
+
const NATIVE_AUDIO_CODECS = new Set<AudioCodec>([
|
|
18
|
+
"aac",
|
|
19
|
+
"mp3",
|
|
20
|
+
"opus",
|
|
21
|
+
"vorbis",
|
|
22
|
+
"flac",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Codecs no major browser plays, period. These force the WASM fallback.
|
|
27
|
+
*/
|
|
28
|
+
const FALLBACK_VIDEO_CODECS = new Set<VideoCodec>(["wmv3", "vc1", "mpeg4", "rv40", "mpeg2", "mpeg1", "theora"]);
|
|
29
|
+
const FALLBACK_AUDIO_CODECS = new Set<AudioCodec>(["wmav2", "wmapro", "ac3", "eac3"]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Containers `<video>` plays directly. Anything else with otherwise-supported
|
|
33
|
+
* codecs is a remux candidate — IF mediabunny can read the container.
|
|
34
|
+
*/
|
|
35
|
+
const NATIVE_CONTAINERS = new Set<ContainerKind>([
|
|
36
|
+
"mp4",
|
|
37
|
+
"mov",
|
|
38
|
+
"webm",
|
|
39
|
+
"ogg",
|
|
40
|
+
"wav",
|
|
41
|
+
"mp3",
|
|
42
|
+
"flac",
|
|
43
|
+
"adts",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Containers mediabunny can demux. The remux strategy feeds the source through
|
|
48
|
+
* mediabunny → fMP4 → MSE, so the source container must be one mediabunny
|
|
49
|
+
* understands. AVI, ASF, FLV are NOT in this set — mediabunny rejects them
|
|
50
|
+
* with "unsupported or unrecognizable format". Files in those containers with
|
|
51
|
+
* otherwise-native codecs (e.g. AVI + H.264 + MP3) must go to the fallback
|
|
52
|
+
* strategy even though the *codecs* are browser-supported.
|
|
53
|
+
*
|
|
54
|
+
* MPEG-TS is in this set: mediabunny demuxes it natively, but browsers
|
|
55
|
+
* cannot play `<video src="*.ts">` directly (MPEG-TS is HLS-only), so even
|
|
56
|
+
* a TS file with H.264 + AAC has to go through the remux path.
|
|
57
|
+
*/
|
|
58
|
+
const REMUXABLE_CONTAINERS = new Set<ContainerKind>([
|
|
59
|
+
"mp4",
|
|
60
|
+
"mov",
|
|
61
|
+
"mkv",
|
|
62
|
+
"webm",
|
|
63
|
+
"ogg",
|
|
64
|
+
"wav",
|
|
65
|
+
"mp3",
|
|
66
|
+
"flac",
|
|
67
|
+
"adts",
|
|
68
|
+
"mpegts",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Pure classification — no I/O, no async. Test-friendly.
|
|
73
|
+
*/
|
|
74
|
+
export function classifyContext(ctx: MediaContext): Classification {
|
|
75
|
+
const video = ctx.videoTracks[0];
|
|
76
|
+
const audio = ctx.audioTracks[0];
|
|
77
|
+
|
|
78
|
+
// Audio-only files: mediabunny handles all the common ones natively.
|
|
79
|
+
if (!video) {
|
|
80
|
+
if (NATIVE_CONTAINERS.has(ctx.container) && (!audio || NATIVE_AUDIO_CODECS.has(audio.codec))) {
|
|
81
|
+
return {
|
|
82
|
+
class: "NATIVE",
|
|
83
|
+
strategy: "native",
|
|
84
|
+
reason: `audio-only ${ctx.container} with native codec`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (audio && FALLBACK_AUDIO_CODECS.has(audio.codec)) {
|
|
88
|
+
return {
|
|
89
|
+
class: "FALLBACK_REQUIRED",
|
|
90
|
+
strategy: "fallback",
|
|
91
|
+
reason: `audio codec "${audio.codec}" requires WASM decode`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (REMUXABLE_CONTAINERS.has(ctx.container)) {
|
|
95
|
+
return {
|
|
96
|
+
class: "REMUX_CANDIDATE",
|
|
97
|
+
strategy: "remux",
|
|
98
|
+
reason: `audio-only file in non-native container "${ctx.container}"`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
class: "FALLBACK_REQUIRED",
|
|
103
|
+
strategy: "fallback",
|
|
104
|
+
reason: `audio-only file in "${ctx.container}" (not remuxable by mediabunny)`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Video paths.
|
|
109
|
+
if (FALLBACK_VIDEO_CODECS.has(video.codec)) {
|
|
110
|
+
return {
|
|
111
|
+
class: "FALLBACK_REQUIRED",
|
|
112
|
+
strategy: "fallback",
|
|
113
|
+
reason: `video codec "${video.codec}" has no browser decoder; WASM fallback required`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (audio && FALLBACK_AUDIO_CODECS.has(audio.codec)) {
|
|
117
|
+
return {
|
|
118
|
+
class: "FALLBACK_REQUIRED",
|
|
119
|
+
strategy: "fallback",
|
|
120
|
+
reason: `audio codec "${audio.codec}" has no browser decoder; WASM fallback required`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!NATIVE_VIDEO_CODECS.has(video.codec)) {
|
|
125
|
+
return {
|
|
126
|
+
class: "FALLBACK_REQUIRED",
|
|
127
|
+
strategy: "fallback",
|
|
128
|
+
reason: `unknown video codec "${video.codec}", routing to fallback`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Codecs are native. Now decide between NATIVE and REMUX based on the
|
|
133
|
+
// container and codec quirks.
|
|
134
|
+
const isNativeContainer = NATIVE_CONTAINERS.has(ctx.container);
|
|
135
|
+
|
|
136
|
+
if (isNativeContainer && isSafeNativeCombo(video, audio)) {
|
|
137
|
+
// Confirm with the browser when we have access to MediaSource.
|
|
138
|
+
const mime = mp4MimeFor(video, audio);
|
|
139
|
+
if (mime && mseSupports(mime)) {
|
|
140
|
+
return {
|
|
141
|
+
class: "NATIVE",
|
|
142
|
+
strategy: "native",
|
|
143
|
+
reason: `${ctx.container} + ${video.codec}${audio ? "/" + audio.codec : ""} plays natively`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (mime == null || typeof MediaSource === "undefined") {
|
|
147
|
+
// No MSE in this environment (e.g. tests) — trust the heuristic.
|
|
148
|
+
return {
|
|
149
|
+
class: "NATIVE",
|
|
150
|
+
strategy: "native",
|
|
151
|
+
reason: `${ctx.container} + ${video.codec}${audio ? "/" + audio.codec : ""} (heuristic native)`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (isNativeContainer && isRiskyNative(video)) {
|
|
157
|
+
return {
|
|
158
|
+
class: "RISKY_NATIVE",
|
|
159
|
+
strategy: "native",
|
|
160
|
+
reason: `${video.codec} ${video.profile ?? ""} ${video.bitDepth ?? 8}-bit may stutter on mobile; will escalate to remux on stall`,
|
|
161
|
+
fallbackChain: ["remux", "hybrid", "fallback"],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Codecs are native but the container isn't. Can we remux?
|
|
166
|
+
// Remuxing goes through mediabunny, which only supports certain containers.
|
|
167
|
+
// AVI/ASF/FLV are NOT in that set — mediabunny rejects them at Input().
|
|
168
|
+
if (REMUXABLE_CONTAINERS.has(ctx.container)) {
|
|
169
|
+
return {
|
|
170
|
+
class: "REMUX_CANDIDATE",
|
|
171
|
+
strategy: "remux",
|
|
172
|
+
reason: `${ctx.container} container with native-supported codecs — remux to fragmented MP4 for reliable playback`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Container is unreadable by mediabunny (AVI, ASF, FLV, etc.) but codecs
|
|
177
|
+
// are browser-supported. Use the hybrid strategy (libav demux + WebCodecs
|
|
178
|
+
// hardware decode) when WebCodecs is available; otherwise full WASM decode.
|
|
179
|
+
if (webCodecsAvailable()) {
|
|
180
|
+
return {
|
|
181
|
+
class: "HYBRID_CANDIDATE",
|
|
182
|
+
strategy: "hybrid",
|
|
183
|
+
reason: `${ctx.container} container requires libav demux; codecs (${video.codec}${audio ? "/" + audio.codec : ""}) are hardware-decodable via WebCodecs`,
|
|
184
|
+
fallbackChain: ["fallback"],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
class: "FALLBACK_REQUIRED",
|
|
189
|
+
strategy: "fallback",
|
|
190
|
+
reason: `${ctx.container} container cannot be remuxed by mediabunny; falling back to WASM decode (${video.codec}${audio ? "/" + audio.codec : ""})`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function webCodecsAvailable(): boolean {
|
|
195
|
+
return typeof globalThis.VideoDecoder !== "undefined";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isSafeNativeCombo(video: VideoTrackInfo, audio?: AudioTrackInfo): boolean {
|
|
199
|
+
if (video.codec === "h264") {
|
|
200
|
+
// 8-bit yuv420p H.264 is the safe combo. Hi10P / 4:2:2 / 4:4:4 are not.
|
|
201
|
+
if (video.bitDepth && video.bitDepth > 8) return false;
|
|
202
|
+
if (video.pixelFormat && !/yuv420p$/.test(video.pixelFormat)) return false;
|
|
203
|
+
}
|
|
204
|
+
if (audio && !NATIVE_AUDIO_CODECS.has(audio.codec)) return false;
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isRiskyNative(video: VideoTrackInfo): boolean {
|
|
209
|
+
if (video.bitDepth && video.bitDepth > 8) return true;
|
|
210
|
+
if (video.pixelFormat && /yuv4(2[24]|44)/.test(video.pixelFormat)) return true;
|
|
211
|
+
if (video.width > 3840 || video.height > 2160) return true;
|
|
212
|
+
if (video.fps && video.fps > 60) return true;
|
|
213
|
+
return false;
|
|
214
|
+
}
|