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.
Files changed (103) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/LICENSE +21 -0
  3. package/README.md +415 -0
  4. package/dist/avi-M5B4SHRM.cjs +164 -0
  5. package/dist/avi-M5B4SHRM.cjs.map +1 -0
  6. package/dist/avi-POCGZ4JX.js +162 -0
  7. package/dist/avi-POCGZ4JX.js.map +1 -0
  8. package/dist/chunk-5ISVAODK.js +80 -0
  9. package/dist/chunk-5ISVAODK.js.map +1 -0
  10. package/dist/chunk-F7YS2XOA.cjs +2966 -0
  11. package/dist/chunk-F7YS2XOA.cjs.map +1 -0
  12. package/dist/chunk-FKM7QBZU.js +2957 -0
  13. package/dist/chunk-FKM7QBZU.js.map +1 -0
  14. package/dist/chunk-J5MCMN3S.js +27 -0
  15. package/dist/chunk-J5MCMN3S.js.map +1 -0
  16. package/dist/chunk-L4NPOJ36.cjs +180 -0
  17. package/dist/chunk-L4NPOJ36.cjs.map +1 -0
  18. package/dist/chunk-NZU7W256.cjs +29 -0
  19. package/dist/chunk-NZU7W256.cjs.map +1 -0
  20. package/dist/chunk-PQTZS7OA.js +147 -0
  21. package/dist/chunk-PQTZS7OA.js.map +1 -0
  22. package/dist/chunk-WD2ZNQA7.js +177 -0
  23. package/dist/chunk-WD2ZNQA7.js.map +1 -0
  24. package/dist/chunk-Y5FYF5KG.cjs +153 -0
  25. package/dist/chunk-Y5FYF5KG.cjs.map +1 -0
  26. package/dist/chunk-Z2FJ5TJC.cjs +82 -0
  27. package/dist/chunk-Z2FJ5TJC.cjs.map +1 -0
  28. package/dist/element.cjs +433 -0
  29. package/dist/element.cjs.map +1 -0
  30. package/dist/element.d.cts +158 -0
  31. package/dist/element.d.ts +158 -0
  32. package/dist/element.js +431 -0
  33. package/dist/element.js.map +1 -0
  34. package/dist/index.cjs +576 -0
  35. package/dist/index.cjs.map +1 -0
  36. package/dist/index.d.cts +80 -0
  37. package/dist/index.d.ts +80 -0
  38. package/dist/index.js +554 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/libav-http-reader-FPYDBMYK.cjs +16 -0
  41. package/dist/libav-http-reader-FPYDBMYK.cjs.map +1 -0
  42. package/dist/libav-http-reader-NQJVY273.js +3 -0
  43. package/dist/libav-http-reader-NQJVY273.js.map +1 -0
  44. package/dist/libav-import-2JURFHEW.js +8 -0
  45. package/dist/libav-import-2JURFHEW.js.map +1 -0
  46. package/dist/libav-import-GST2AMPL.cjs +30 -0
  47. package/dist/libav-import-GST2AMPL.cjs.map +1 -0
  48. package/dist/libav-loader-KA2MAWLM.js +3 -0
  49. package/dist/libav-loader-KA2MAWLM.js.map +1 -0
  50. package/dist/libav-loader-ZHOERPHW.cjs +12 -0
  51. package/dist/libav-loader-ZHOERPHW.cjs.map +1 -0
  52. package/dist/player-BBwbCkdL.d.cts +365 -0
  53. package/dist/player-BBwbCkdL.d.ts +365 -0
  54. package/dist/source-SC6ZEQYR.cjs +28 -0
  55. package/dist/source-SC6ZEQYR.cjs.map +1 -0
  56. package/dist/source-ZFS4H7J3.js +3 -0
  57. package/dist/source-ZFS4H7J3.js.map +1 -0
  58. package/dist/variant-routing-GOHB2RZN.cjs +12 -0
  59. package/dist/variant-routing-GOHB2RZN.cjs.map +1 -0
  60. package/dist/variant-routing-JOBWXYKD.js +3 -0
  61. package/dist/variant-routing-JOBWXYKD.js.map +1 -0
  62. package/package.json +95 -0
  63. package/src/classify/index.ts +1 -0
  64. package/src/classify/rules.ts +214 -0
  65. package/src/convert/index.ts +2 -0
  66. package/src/convert/remux.ts +522 -0
  67. package/src/convert/transcode.ts +329 -0
  68. package/src/diagnostics.ts +99 -0
  69. package/src/element/avbridge-player.ts +576 -0
  70. package/src/element.ts +19 -0
  71. package/src/events.ts +71 -0
  72. package/src/index.ts +42 -0
  73. package/src/libav-stubs.d.ts +24 -0
  74. package/src/player.ts +455 -0
  75. package/src/plugins/builtin.ts +37 -0
  76. package/src/plugins/registry.ts +32 -0
  77. package/src/probe/avi.ts +242 -0
  78. package/src/probe/index.ts +59 -0
  79. package/src/probe/mediabunny.ts +194 -0
  80. package/src/strategies/fallback/audio-output.ts +293 -0
  81. package/src/strategies/fallback/clock.ts +7 -0
  82. package/src/strategies/fallback/decoder.ts +660 -0
  83. package/src/strategies/fallback/index.ts +170 -0
  84. package/src/strategies/fallback/libav-import.ts +27 -0
  85. package/src/strategies/fallback/libav-loader.ts +190 -0
  86. package/src/strategies/fallback/variant-routing.ts +43 -0
  87. package/src/strategies/fallback/video-renderer.ts +216 -0
  88. package/src/strategies/hybrid/decoder.ts +641 -0
  89. package/src/strategies/hybrid/index.ts +139 -0
  90. package/src/strategies/native.ts +107 -0
  91. package/src/strategies/remux/annexb.ts +112 -0
  92. package/src/strategies/remux/index.ts +79 -0
  93. package/src/strategies/remux/mse.ts +234 -0
  94. package/src/strategies/remux/pipeline.ts +254 -0
  95. package/src/subtitles/index.ts +91 -0
  96. package/src/subtitles/render.ts +62 -0
  97. package/src/subtitles/srt.ts +62 -0
  98. package/src/subtitles/vtt.ts +5 -0
  99. package/src/types-shim.d.ts +3 -0
  100. package/src/types.ts +360 -0
  101. package/src/util/codec-strings.ts +86 -0
  102. package/src/util/libav-http-reader.ts +315 -0
  103. 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";