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,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Audio output for the fallback strategy.
|
|
3
|
+
*
|
|
4
|
+
* Owns the **media-time clock** for fallback playback. Audio is the master:
|
|
5
|
+
* decoded video frames are presented based on what `now()` returns here.
|
|
6
|
+
*
|
|
7
|
+
* State machine:
|
|
8
|
+
*
|
|
9
|
+
* ┌──────┐ schedule() ┌──────┐ ┌────────┐
|
|
10
|
+
* │ idle │ ───────────▶ │ idle │ ── start() ──▶│ playing│
|
|
11
|
+
* └──────┘ (queues) └──────┘ └────┬───┘
|
|
12
|
+
* ▲ │
|
|
13
|
+
* │ │ pause()
|
|
14
|
+
* │ ▼
|
|
15
|
+
* │ ┌────────┐
|
|
16
|
+
* └────────────── reset(t) ─────────────── ── │ paused │
|
|
17
|
+
* └────────┘
|
|
18
|
+
*
|
|
19
|
+
* - **idle**: AudioContext is suspended (no playback). `schedule()` queues
|
|
20
|
+
* samples in `pendingQueue`; `now()` returns `mediaTimeOfAnchor`.
|
|
21
|
+
* - **playing**: AudioContext is running. `schedule()` writes directly to
|
|
22
|
+
* the audio graph at the right time. `now()` advances with `ctx.currentTime`.
|
|
23
|
+
* - **paused**: AudioContext is suspended. `now()` returns the media time
|
|
24
|
+
* captured at pause. `start()` resumes.
|
|
25
|
+
*
|
|
26
|
+
* Key invariant: between any two `start()` calls, `mediaTimeOfNext` (the
|
|
27
|
+
* media time of the next sample to be scheduled) must equal the media time
|
|
28
|
+
* the playback is at. This is what makes the cold-start race go away — we
|
|
29
|
+
* never schedule audio with a stale wall-clock anchor.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
interface PendingChunk {
|
|
33
|
+
samples: Float32Array;
|
|
34
|
+
channels: number;
|
|
35
|
+
sampleRate: number;
|
|
36
|
+
frameCount: number;
|
|
37
|
+
durationSec: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ClockSource {
|
|
41
|
+
/** Current media time in seconds. */
|
|
42
|
+
now(): number;
|
|
43
|
+
/** True if media is currently playing (audio scheduler is running). */
|
|
44
|
+
isPlaying(): boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class AudioOutput implements ClockSource {
|
|
48
|
+
private ctx: AudioContext;
|
|
49
|
+
private gain: GainNode;
|
|
50
|
+
|
|
51
|
+
private state: "idle" | "playing" | "paused" = "idle";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wall-clock fallback mode. When true, this output behaves as if audio
|
|
55
|
+
* is unavailable — `now()` advances from `performance.now()` instead of
|
|
56
|
+
* the audio context, `schedule()` is a no-op, and `bufferAhead()` returns
|
|
57
|
+
* Infinity so the session's `waitForBuffer()` doesn't block on audio.
|
|
58
|
+
*
|
|
59
|
+
* Set by the decoder via {@link setNoAudio} when audio decode init fails.
|
|
60
|
+
* This is what lets video play even when the audio codec isn't supported
|
|
61
|
+
* by the loaded libav variant.
|
|
62
|
+
*/
|
|
63
|
+
private noAudio = false;
|
|
64
|
+
/** Wall-clock anchor (ms from `performance.now()`) for noAudio mode. */
|
|
65
|
+
private wallAnchorMs = 0;
|
|
66
|
+
|
|
67
|
+
/** Media time at which the next sample will be scheduled. */
|
|
68
|
+
private mediaTimeOfNext = 0;
|
|
69
|
+
|
|
70
|
+
/** Anchor: media time `mediaTimeOfAnchor` corresponds to ctx time `ctxTimeAtAnchor`. */
|
|
71
|
+
private mediaTimeOfAnchor = 0;
|
|
72
|
+
private ctxTimeAtAnchor = 0;
|
|
73
|
+
|
|
74
|
+
private pendingQueue: PendingChunk[] = [];
|
|
75
|
+
|
|
76
|
+
private framesScheduled = 0;
|
|
77
|
+
private destroyed = false;
|
|
78
|
+
|
|
79
|
+
constructor() {
|
|
80
|
+
this.ctx = new AudioContext();
|
|
81
|
+
this.gain = this.ctx.createGain();
|
|
82
|
+
this.gain.connect(this.ctx.destination);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Switch into wall-clock fallback mode. Called by the decoder when no
|
|
87
|
+
* audio decoder could be initialized for the source. Once set, this
|
|
88
|
+
* output drives playback time from `performance.now()` and ignores
|
|
89
|
+
* any incoming audio samples.
|
|
90
|
+
*/
|
|
91
|
+
setNoAudio(): void {
|
|
92
|
+
this.noAudio = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── ClockSource ────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
now(): number {
|
|
98
|
+
if (this.noAudio) {
|
|
99
|
+
if (this.state === "playing") {
|
|
100
|
+
return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1000;
|
|
101
|
+
}
|
|
102
|
+
return this.mediaTimeOfAnchor;
|
|
103
|
+
}
|
|
104
|
+
if (this.state === "playing") {
|
|
105
|
+
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor);
|
|
106
|
+
}
|
|
107
|
+
return this.mediaTimeOfAnchor;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isPlaying(): boolean {
|
|
111
|
+
return this.state === "playing";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Buffering ─────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* How many seconds of audio are buffered ahead of the current playback
|
|
118
|
+
* position. While idle, this counts the pending queue. While playing,
|
|
119
|
+
* it counts how far `mediaTimeOfNext` is ahead of `now()`.
|
|
120
|
+
*/
|
|
121
|
+
bufferAhead(): number {
|
|
122
|
+
// In wall-clock mode, no samples are ever scheduled — the buffer is
|
|
123
|
+
// genuinely empty. Callers that want to gate cold-start should check
|
|
124
|
+
// {@link isNoAudio} and skip the audio gate entirely instead.
|
|
125
|
+
if (this.noAudio) return 0;
|
|
126
|
+
if (this.state === "idle") {
|
|
127
|
+
let sec = 0;
|
|
128
|
+
for (const c of this.pendingQueue) sec += c.durationSec;
|
|
129
|
+
return sec;
|
|
130
|
+
}
|
|
131
|
+
return Math.max(0, this.mediaTimeOfNext - this.now());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** True if this output is in wall-clock fallback mode (no audio decode). */
|
|
135
|
+
isNoAudio(): boolean {
|
|
136
|
+
return this.noAudio;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Schedule a chunk of decoded samples. Queues internally while idle (cold
|
|
141
|
+
* start or post-seek), schedules directly to the audio graph while playing.
|
|
142
|
+
* In wall-clock mode, samples are silently discarded.
|
|
143
|
+
*/
|
|
144
|
+
schedule(samples: Float32Array, channels: number, sampleRate: number): void {
|
|
145
|
+
if (this.destroyed || this.noAudio) return;
|
|
146
|
+
const frameCount = samples.length / channels;
|
|
147
|
+
const durationSec = frameCount / sampleRate;
|
|
148
|
+
|
|
149
|
+
if (this.state === "idle" || this.state === "paused") {
|
|
150
|
+
this.pendingQueue.push({ samples, channels, sampleRate, frameCount, durationSec });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.scheduleNow(samples, channels, sampleRate, frameCount);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private scheduleNow(
|
|
158
|
+
samples: Float32Array,
|
|
159
|
+
channels: number,
|
|
160
|
+
sampleRate: number,
|
|
161
|
+
frameCount: number,
|
|
162
|
+
): void {
|
|
163
|
+
const buffer = this.ctx.createBuffer(channels, frameCount, sampleRate);
|
|
164
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
165
|
+
const channelData = buffer.getChannelData(ch);
|
|
166
|
+
for (let i = 0; i < frameCount; i++) {
|
|
167
|
+
channelData[i] = samples[i * channels + ch];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const node = this.ctx.createBufferSource();
|
|
171
|
+
node.buffer = buffer;
|
|
172
|
+
node.connect(this.gain);
|
|
173
|
+
|
|
174
|
+
// Convert media time → ctx time using the anchor.
|
|
175
|
+
const ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
|
|
176
|
+
const safeStart = Math.max(ctxStart, this.ctx.currentTime);
|
|
177
|
+
node.start(safeStart);
|
|
178
|
+
|
|
179
|
+
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
180
|
+
this.framesScheduled++;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Start (or resume) playback. On a cold start (or after a reset), drains
|
|
187
|
+
* the pending queue scheduling all queued samples to play starting at
|
|
188
|
+
* `ctx.currentTime + STARTUP_DELAY`. On resume from pause, just re-anchors
|
|
189
|
+
* the media↔ctx time mapping and unsuspends the context.
|
|
190
|
+
*/
|
|
191
|
+
async start(): Promise<void> {
|
|
192
|
+
if (this.destroyed || this.state === "playing") return;
|
|
193
|
+
|
|
194
|
+
// Wall-clock mode: no audio context involved. Anchor to performance.now()
|
|
195
|
+
// and let `now()` advance from there. The renderer's tick loop will see
|
|
196
|
+
// `isPlaying() === true` and start painting frames.
|
|
197
|
+
if (this.noAudio) {
|
|
198
|
+
this.wallAnchorMs = performance.now();
|
|
199
|
+
this.state = "playing";
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (this.ctx.state === "suspended") {
|
|
204
|
+
await this.ctx.resume();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (this.state === "paused") {
|
|
208
|
+
// Resume: media time should continue from where we paused. ctx.currentTime
|
|
209
|
+
// is preserved across suspend/resume, so re-anchoring it to "now" with
|
|
210
|
+
// the same mediaTimeOfAnchor gives a continuous clock.
|
|
211
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
212
|
+
this.state = "playing";
|
|
213
|
+
// Drain anything that was scheduled while paused.
|
|
214
|
+
const drain = this.pendingQueue;
|
|
215
|
+
this.pendingQueue = [];
|
|
216
|
+
for (const c of drain) {
|
|
217
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Cold start (or post-seek). Anchor: the first sample we scheduled lands
|
|
223
|
+
// at ctxTimeAtAnchor (a tiny bit in the future), and that ctx time
|
|
224
|
+
// corresponds to media time mediaTimeOfAnchor.
|
|
225
|
+
const STARTUP_DELAY = 0.05;
|
|
226
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime + STARTUP_DELAY;
|
|
227
|
+
this.mediaTimeOfNext = this.mediaTimeOfAnchor;
|
|
228
|
+
this.state = "playing";
|
|
229
|
+
|
|
230
|
+
const drain = this.pendingQueue;
|
|
231
|
+
this.pendingQueue = [];
|
|
232
|
+
for (const c of drain) {
|
|
233
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Pause playback. Suspends the audio context. */
|
|
238
|
+
async pause(): Promise<void> {
|
|
239
|
+
if (this.state !== "playing") return;
|
|
240
|
+
this.mediaTimeOfAnchor = this.now();
|
|
241
|
+
this.state = "paused";
|
|
242
|
+
if (this.noAudio) return;
|
|
243
|
+
if (this.ctx.state === "running") {
|
|
244
|
+
await this.ctx.suspend();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Reset to a new media time. Discards all queued and scheduled audio,
|
|
250
|
+
* disconnects the gain node so any in-flight scheduled buffers are cut
|
|
251
|
+
* off, and returns to the idle state. Used by `seek()`.
|
|
252
|
+
*
|
|
253
|
+
* After reset, callers should re-buffer audio (the decoder will start
|
|
254
|
+
* supplying new samples) and then call `start()` to resume playback.
|
|
255
|
+
*/
|
|
256
|
+
async reset(newMediaTime: number): Promise<void> {
|
|
257
|
+
if (this.noAudio) {
|
|
258
|
+
this.pendingQueue = [];
|
|
259
|
+
this.mediaTimeOfAnchor = newMediaTime;
|
|
260
|
+
this.wallAnchorMs = performance.now();
|
|
261
|
+
this.state = "idle";
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try { this.gain.disconnect(); } catch { /* ignore */ }
|
|
266
|
+
this.gain = this.ctx.createGain();
|
|
267
|
+
this.gain.connect(this.ctx.destination);
|
|
268
|
+
|
|
269
|
+
this.pendingQueue = [];
|
|
270
|
+
this.mediaTimeOfAnchor = newMediaTime;
|
|
271
|
+
this.mediaTimeOfNext = newMediaTime;
|
|
272
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
273
|
+
this.state = "idle";
|
|
274
|
+
|
|
275
|
+
if (this.ctx.state === "running") {
|
|
276
|
+
await this.ctx.suspend();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
stats(): Record<string, unknown> {
|
|
281
|
+
return {
|
|
282
|
+
framesScheduled: this.framesScheduled,
|
|
283
|
+
bufferAhead: this.bufferAhead(),
|
|
284
|
+
audioState: this.state,
|
|
285
|
+
clockMode: this.noAudio ? "wall" : "audio",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
destroy(): void {
|
|
290
|
+
this.destroyed = true;
|
|
291
|
+
try { this.ctx.close(); } catch { /* ignore */ }
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ClockSource` is the abstraction the renderer uses to ask "what time is it
|
|
3
|
+
* and are we playing?" In the fallback strategy that role is played by
|
|
4
|
+
* `AudioOutput`, which owns the actual media-time state machine. This file
|
|
5
|
+
* is a re-export so callers don't need to know.
|
|
6
|
+
*/
|
|
7
|
+
export type { ClockSource } from "./audio-output.js";
|