avbridge 2.2.1 → 2.5.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 +153 -1
- package/NOTICE.md +2 -2
- package/README.md +2 -3
- package/THIRD_PARTY_LICENSES.md +2 -2
- package/dist/avi-2JPBSHGA.js +183 -0
- package/dist/avi-2JPBSHGA.js.map +1 -0
- package/dist/avi-F6WZJK5T.cjs +185 -0
- package/dist/avi-F6WZJK5T.cjs.map +1 -0
- package/dist/{avi-GCGM7OJI.js → avi-NJXAXUXK.js} +9 -3
- package/dist/avi-NJXAXUXK.js.map +1 -0
- package/dist/{avi-6SJLWIWW.cjs → avi-W6L3BTWU.cjs} +10 -4
- package/dist/avi-W6L3BTWU.cjs.map +1 -0
- package/dist/chunk-2IJ66NTD.cjs +212 -0
- package/dist/chunk-2IJ66NTD.cjs.map +1 -0
- package/dist/{chunk-ILKDNBSE.js → chunk-2XW2O3YI.cjs} +55 -10
- package/dist/chunk-2XW2O3YI.cjs.map +1 -0
- package/dist/chunk-5KVLE6YI.js +167 -0
- package/dist/chunk-5KVLE6YI.js.map +1 -0
- package/dist/chunk-5YAWWKA3.js +18 -0
- package/dist/chunk-5YAWWKA3.js.map +1 -0
- package/dist/chunk-CPJLFFCC.js +189 -0
- package/dist/chunk-CPJLFFCC.js.map +1 -0
- package/dist/chunk-CPZ7PXAM.cjs +240 -0
- package/dist/chunk-CPZ7PXAM.cjs.map +1 -0
- package/dist/{chunk-WD2ZNQA7.js → chunk-DCSOQH2N.js} +7 -4
- package/dist/chunk-DCSOQH2N.js.map +1 -0
- package/dist/{chunk-HZLQNKFN.cjs → chunk-E76AMWI4.js} +40 -15
- package/dist/chunk-E76AMWI4.js.map +1 -0
- package/dist/chunk-F3LQJKXK.cjs +20 -0
- package/dist/chunk-F3LQJKXK.cjs.map +1 -0
- package/dist/chunk-IAYKFGFG.js +200 -0
- package/dist/chunk-IAYKFGFG.js.map +1 -0
- package/dist/{chunk-DMWARSEF.js → chunk-KY2GPCT7.js} +788 -697
- package/dist/chunk-KY2GPCT7.js.map +1 -0
- package/dist/chunk-LUFA47FP.js +19 -0
- package/dist/chunk-LUFA47FP.js.map +1 -0
- package/dist/chunk-NNVOHKXJ.cjs +204 -0
- package/dist/chunk-NNVOHKXJ.cjs.map +1 -0
- package/dist/chunk-Q2VUO52Z.cjs +374 -0
- package/dist/chunk-Q2VUO52Z.cjs.map +1 -0
- package/dist/chunk-QDJLQR53.cjs +22 -0
- package/dist/chunk-QDJLQR53.cjs.map +1 -0
- package/dist/chunk-S4WAZC2T.cjs +173 -0
- package/dist/chunk-S4WAZC2T.cjs.map +1 -0
- package/dist/chunk-SMH6IOP2.js +368 -0
- package/dist/chunk-SMH6IOP2.js.map +1 -0
- package/dist/chunk-SR3MPV4D.js +237 -0
- package/dist/chunk-SR3MPV4D.js.map +1 -0
- package/dist/{chunk-UF2N5L63.cjs → chunk-TBW26OPP.cjs} +800 -710
- package/dist/chunk-TBW26OPP.cjs.map +1 -0
- package/dist/chunk-X2K3GIWE.js +235 -0
- package/dist/chunk-X2K3GIWE.js.map +1 -0
- package/dist/{chunk-L4NPOJ36.cjs → chunk-Z33SBWL5.cjs} +7 -4
- package/dist/chunk-Z33SBWL5.cjs.map +1 -0
- package/dist/chunk-ZCUXHW55.cjs +242 -0
- package/dist/chunk-ZCUXHW55.cjs.map +1 -0
- package/dist/element-browser.js +1282 -503
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +59 -5
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +39 -1
- package/dist/element.d.ts +39 -1
- package/dist/element.js +58 -4
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +605 -327
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -4
- package/dist/index.d.ts +48 -4
- package/dist/index.js +528 -319
- package/dist/index.js.map +1 -1
- package/dist/libav-demux-H2GS46GH.cjs +27 -0
- package/dist/{libav-http-reader-NQJVY273.js.map → libav-demux-H2GS46GH.cjs.map} +1 -1
- package/dist/libav-demux-OWZ4T2YW.js +6 -0
- package/dist/{libav-http-reader-FPYDBMYK.cjs.map → libav-demux-OWZ4T2YW.js.map} +1 -1
- package/dist/libav-http-reader-AZLE7YFS.cjs +16 -0
- package/dist/libav-http-reader-AZLE7YFS.cjs.map +1 -0
- package/dist/libav-http-reader-WXG3Z7AI.js +3 -0
- package/dist/libav-http-reader-WXG3Z7AI.js.map +1 -0
- package/dist/{libav-import-GST2AMPL.cjs → libav-import-2ZVKV2E7.cjs} +2 -2
- package/dist/{libav-import-GST2AMPL.cjs.map → libav-import-2ZVKV2E7.cjs.map} +1 -1
- package/dist/{libav-import-2JURFHEW.js → libav-import-6MGLCXVQ.js} +2 -2
- package/dist/{libav-import-2JURFHEW.js.map → libav-import-6MGLCXVQ.js.map} +1 -1
- package/dist/{player-U2NPmFvA.d.cts → player-B6WB74RD.d.cts} +62 -3
- package/dist/{player-U2NPmFvA.d.ts → player-B6WB74RD.d.ts} +62 -3
- package/dist/player.cjs +5631 -0
- package/dist/player.cjs.map +1 -0
- package/dist/player.d.cts +699 -0
- package/dist/player.d.ts +699 -0
- package/dist/player.js +5629 -0
- package/dist/player.js.map +1 -0
- package/dist/remux-OBSMIENG.cjs +35 -0
- package/dist/remux-OBSMIENG.cjs.map +1 -0
- package/dist/remux-WBYIZBBX.js +10 -0
- package/dist/remux-WBYIZBBX.js.map +1 -0
- package/dist/source-4TZ6KMNV.js +4 -0
- package/dist/{source-FFZ7TW2B.js.map → source-4TZ6KMNV.js.map} +1 -1
- package/dist/source-7YLO6E7X.cjs +29 -0
- package/dist/{source-CN43EI7Z.cjs.map → source-7YLO6E7X.cjs.map} +1 -1
- package/dist/source-MTX5ELUZ.js +4 -0
- package/dist/source-MTX5ELUZ.js.map +1 -0
- package/dist/source-VFLXLOCN.cjs +29 -0
- package/dist/source-VFLXLOCN.cjs.map +1 -0
- package/dist/subtitles-4T74JRGT.js +4 -0
- package/dist/subtitles-4T74JRGT.js.map +1 -0
- package/dist/subtitles-QUH4LPI4.cjs +29 -0
- package/dist/subtitles-QUH4LPI4.cjs.map +1 -0
- package/dist/variant-routing-434STYAB.js +3 -0
- package/dist/{variant-routing-JOBWXYKD.js.map → variant-routing-434STYAB.js.map} +1 -1
- package/dist/variant-routing-HONNAA6R.cjs +12 -0
- package/dist/{variant-routing-GOHB2RZN.cjs.map → variant-routing-HONNAA6R.cjs.map} +1 -1
- package/package.json +9 -1
- package/src/classify/rules.ts +27 -5
- package/src/convert/remux.ts +9 -35
- package/src/convert/transcode-libav.ts +691 -0
- package/src/convert/transcode.ts +53 -12
- package/src/element/avbridge-player.ts +861 -0
- package/src/element/avbridge-video.ts +54 -0
- package/src/element/player-icons.ts +25 -0
- package/src/element/player-styles.ts +472 -0
- package/src/errors.ts +53 -0
- package/src/index.ts +23 -0
- package/src/player-element.ts +18 -0
- package/src/player.ts +118 -27
- package/src/plugins/builtin.ts +2 -2
- package/src/probe/avi.ts +4 -0
- package/src/probe/index.ts +40 -10
- package/src/strategies/fallback/audio-output.ts +31 -0
- package/src/strategies/fallback/decoder.ts +179 -175
- package/src/strategies/fallback/index.ts +48 -6
- package/src/strategies/fallback/libav-import.ts +9 -1
- package/src/strategies/fallback/variant-routing.ts +7 -13
- package/src/strategies/fallback/video-renderer.ts +231 -32
- package/src/strategies/hybrid/decoder.ts +219 -200
- package/src/strategies/hybrid/index.ts +48 -7
- package/src/strategies/native.ts +6 -3
- package/src/strategies/remux/index.ts +14 -2
- package/src/strategies/remux/mse.ts +12 -2
- package/src/strategies/remux/pipeline.ts +72 -12
- package/src/subtitles/index.ts +7 -3
- package/src/subtitles/render.ts +8 -0
- package/src/types.ts +53 -1
- package/src/util/libav-demux.ts +405 -0
- package/src/util/libav-http-reader.ts +5 -1
- package/src/util/source.ts +28 -8
- package/src/util/transport.ts +26 -0
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
- package/dist/avi-6SJLWIWW.cjs.map +0 -1
- package/dist/avi-GCGM7OJI.js.map +0 -1
- package/dist/chunk-DMWARSEF.js.map +0 -1
- package/dist/chunk-HZLQNKFN.cjs.map +0 -1
- package/dist/chunk-ILKDNBSE.js.map +0 -1
- package/dist/chunk-J5MCMN3S.js +0 -27
- package/dist/chunk-J5MCMN3S.js.map +0 -1
- package/dist/chunk-L4NPOJ36.cjs.map +0 -1
- package/dist/chunk-NZU7W256.cjs +0 -29
- package/dist/chunk-NZU7W256.cjs.map +0 -1
- package/dist/chunk-UF2N5L63.cjs.map +0 -1
- package/dist/chunk-WD2ZNQA7.js.map +0 -1
- package/dist/libav-http-reader-FPYDBMYK.cjs +0 -16
- package/dist/libav-http-reader-NQJVY273.js +0 -3
- package/dist/source-CN43EI7Z.cjs +0 -28
- package/dist/source-FFZ7TW2B.js +0 -3
- package/dist/variant-routing-GOHB2RZN.cjs +0 -12
- package/dist/variant-routing-JOBWXYKD.js +0 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClockSource } from "./audio-output.js";
|
|
2
|
+
import { SubtitleOverlay } from "../../subtitles/render.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Renders decoded `VideoFrame`s into a 2D canvas overlaid on the user's
|
|
@@ -20,6 +21,13 @@ import type { ClockSource } from "./audio-output.js";
|
|
|
20
21
|
* decoder was still warming up, and every frame was already in the past by
|
|
21
22
|
* the time it landed in the queue.
|
|
22
23
|
*/
|
|
24
|
+
// Periodic debug log — throttled to once per second so it doesn't
|
|
25
|
+
// flood the console at 60Hz rAF rate.
|
|
26
|
+
function isDebug(): boolean {
|
|
27
|
+
return typeof globalThis !== "undefined" && !!(globalThis as Record<string, unknown>).AVBRIDGE_DEBUG;
|
|
28
|
+
}
|
|
29
|
+
let lastDebugLog = 0;
|
|
30
|
+
|
|
23
31
|
export class VideoRenderer {
|
|
24
32
|
private canvas: HTMLCanvasElement;
|
|
25
33
|
private ctx: CanvasRenderingContext2D;
|
|
@@ -35,6 +43,31 @@ export class VideoRenderer {
|
|
|
35
43
|
private lastPaintWall = 0;
|
|
36
44
|
/** Minimum ms between paints — paces video at roughly source fps. */
|
|
37
45
|
private paintIntervalMs: number;
|
|
46
|
+
/** Cumulative count of frames skipped because all PTS are in the future. */
|
|
47
|
+
private ticksWaiting = 0;
|
|
48
|
+
/** Cumulative count of ticks where PTS mode painted a frame. */
|
|
49
|
+
private ticksPainted = 0;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Subtitle overlay div attached to the stage wrapper alongside the
|
|
53
|
+
* canvas. Created lazily when subtitle tracks are attached via the
|
|
54
|
+
* target's `<track>` children. Canvas strategies (hybrid, fallback)
|
|
55
|
+
* hide the <video>, so we can't rely on the browser's native cue
|
|
56
|
+
* rendering; we read TextTrack.cues and render into this overlay.
|
|
57
|
+
*/
|
|
58
|
+
private subtitleOverlay: SubtitleOverlay | null = null;
|
|
59
|
+
private subtitleTrack: TextTrack | null = null;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Calibration offset (microseconds) between video PTS and audio clock.
|
|
63
|
+
* Video PTS and AudioContext.currentTime can drift ~0.1% relative to
|
|
64
|
+
* each other (different clock domains). Over 45 minutes that's 2.6s.
|
|
65
|
+
* We measure the offset on the first painted frame and update it
|
|
66
|
+
* periodically so the PTS comparison stays calibrated.
|
|
67
|
+
*/
|
|
68
|
+
private ptsCalibrationUs = 0;
|
|
69
|
+
private ptsCalibrated = false;
|
|
70
|
+
private lastCalibrationWall = 0;
|
|
38
71
|
|
|
39
72
|
/** Resolves once the first decoded frame has been enqueued. */
|
|
40
73
|
readonly firstFrameReady: Promise<void>;
|
|
@@ -89,6 +122,15 @@ export class VideoRenderer {
|
|
|
89
122
|
}
|
|
90
123
|
target.style.visibility = "hidden";
|
|
91
124
|
|
|
125
|
+
// Create a subtitle overlay on the same parent as the canvas so cues
|
|
126
|
+
// appear over the rendered video. Shows nothing until a TextTrack
|
|
127
|
+
// gets attached via attachSubtitleTracks.
|
|
128
|
+
const overlayParent = parent instanceof HTMLElement ? parent : document.body;
|
|
129
|
+
this.subtitleOverlay = new SubtitleOverlay(overlayParent);
|
|
130
|
+
// Watch for <track> children on the target <video>. When one is
|
|
131
|
+
// added, grab its TextTrack and poll cues from it each tick.
|
|
132
|
+
this.watchTextTracks(target);
|
|
133
|
+
|
|
92
134
|
const ctx = this.canvas.getContext("2d");
|
|
93
135
|
if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
|
|
94
136
|
this.ctx = ctx;
|
|
@@ -134,10 +176,95 @@ export class VideoRenderer {
|
|
|
134
176
|
}
|
|
135
177
|
}
|
|
136
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Watch the target <video>'s textTracks list. When a track is added,
|
|
181
|
+
* grab it and start polling cues on each render tick. Existing tracks
|
|
182
|
+
* (if any) are picked up immediately.
|
|
183
|
+
*/
|
|
184
|
+
private watchTextTracks(target: HTMLVideoElement): void {
|
|
185
|
+
const pick = () => {
|
|
186
|
+
if (this.subtitleTrack) return;
|
|
187
|
+
const tracks = target.textTracks;
|
|
188
|
+
if (isDebug()) {
|
|
189
|
+
// eslint-disable-next-line no-console
|
|
190
|
+
console.log(`[avbridge:subs] watchTextTracks pick() — ${tracks.length} tracks`);
|
|
191
|
+
}
|
|
192
|
+
for (let i = 0; i < tracks.length; i++) {
|
|
193
|
+
const t = tracks[i];
|
|
194
|
+
if (isDebug()) {
|
|
195
|
+
// eslint-disable-next-line no-console
|
|
196
|
+
console.log(`[avbridge:subs] track ${i}: kind=${t.kind} mode=${t.mode} cues=${t.cues?.length ?? 0}`);
|
|
197
|
+
}
|
|
198
|
+
if (t.kind === "subtitles" || t.kind === "captions") {
|
|
199
|
+
this.subtitleTrack = t;
|
|
200
|
+
t.mode = "hidden"; // hidden means "cues available via API, don't render"
|
|
201
|
+
if (isDebug()) {
|
|
202
|
+
// eslint-disable-next-line no-console
|
|
203
|
+
console.log(`[avbridge:subs] picked track, mode=hidden`);
|
|
204
|
+
}
|
|
205
|
+
// Listen for cue load completion
|
|
206
|
+
const trackEl = target.querySelector(`track[srclang="${t.language}"]`) as HTMLTrackElement | null;
|
|
207
|
+
if (trackEl) {
|
|
208
|
+
trackEl.addEventListener("load", () => {
|
|
209
|
+
if (isDebug()) {
|
|
210
|
+
// eslint-disable-next-line no-console
|
|
211
|
+
console.log(`[avbridge:subs] track element loaded, cues=${t.cues?.length ?? 0}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
trackEl.addEventListener("error", (ev) => {
|
|
215
|
+
// eslint-disable-next-line no-console
|
|
216
|
+
console.warn(`[avbridge:subs] track element error:`, ev);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
pick();
|
|
224
|
+
if (typeof target.textTracks.addEventListener === "function") {
|
|
225
|
+
target.textTracks.addEventListener("addtrack", (e) => {
|
|
226
|
+
if (isDebug()) {
|
|
227
|
+
// eslint-disable-next-line no-console
|
|
228
|
+
console.log("[avbridge:subs] addtrack event fired");
|
|
229
|
+
}
|
|
230
|
+
void e;
|
|
231
|
+
pick();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private _loggedCues = false;
|
|
237
|
+
|
|
238
|
+
/** Find the active cue (if any) for the given media time. */
|
|
239
|
+
private updateSubtitles(): void {
|
|
240
|
+
if (!this.subtitleOverlay || !this.subtitleTrack) return;
|
|
241
|
+
const cues = this.subtitleTrack.cues;
|
|
242
|
+
if (!cues || cues.length === 0) return;
|
|
243
|
+
if (isDebug() && !this._loggedCues) {
|
|
244
|
+
this._loggedCues = true;
|
|
245
|
+
// eslint-disable-next-line no-console
|
|
246
|
+
console.log(`[avbridge:subs] cues available: ${cues.length}, first start=${cues[0].startTime}, last end=${cues[cues.length-1].endTime}`);
|
|
247
|
+
}
|
|
248
|
+
const t = this.clock.now();
|
|
249
|
+
let activeText = "";
|
|
250
|
+
for (let i = 0; i < cues.length; i++) {
|
|
251
|
+
const c = cues[i];
|
|
252
|
+
if (t >= c.startTime && t <= c.endTime) {
|
|
253
|
+
const vttCue = c as VTTCue & { text?: string };
|
|
254
|
+
activeText = vttCue.text ?? "";
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Strip VTT tags for plain rendering (e.g. <c.en> voice tags)
|
|
259
|
+
this.subtitleOverlay.setText(activeText.replace(/<[^>]+>/g, ""));
|
|
260
|
+
}
|
|
261
|
+
|
|
137
262
|
private tick(): void {
|
|
138
263
|
if (this.destroyed) return;
|
|
139
264
|
this.rafHandle = requestAnimationFrame(this.tick);
|
|
140
265
|
|
|
266
|
+
this.updateSubtitles();
|
|
267
|
+
|
|
141
268
|
if (this.queue.length === 0) return;
|
|
142
269
|
|
|
143
270
|
const playing = this.clock.isPlaying();
|
|
@@ -154,45 +281,109 @@ export class VideoRenderer {
|
|
|
154
281
|
return;
|
|
155
282
|
}
|
|
156
283
|
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// per-frame audio-gate that caused massive overflow during decode bursts.
|
|
284
|
+
// PTS-based painting: find the latest frame whose presentation time
|
|
285
|
+
// has arrived (timestamp ≤ audio clock), paint it, and discard any
|
|
286
|
+
// older frames. This produces correct cadence at any display refresh
|
|
287
|
+
// rate and any source fps — no 3:2 pulldown artifacts.
|
|
162
288
|
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// timestamps, packed B-frames, and odd frame durations. The correction
|
|
169
|
-
// is deliberately gentle (one frame at a time) so it doesn't cause
|
|
170
|
-
// visible stuttering.
|
|
171
|
-
const wallNow = performance.now();
|
|
172
|
-
if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
|
|
289
|
+
// Fallback: if frame timestamps are unreliable (all zero, synthetic),
|
|
290
|
+
// fall back to wall-clock pacing as before.
|
|
291
|
+
const rawAudioNowUs = this.clock.now() * 1_000_000;
|
|
292
|
+
const headTs = this.queue[0].timestamp ?? 0;
|
|
293
|
+
const hasPts = headTs > 0 || this.queue.length > 1;
|
|
173
294
|
|
|
174
|
-
if (
|
|
295
|
+
if (hasPts) {
|
|
296
|
+
// Calibration: video PTS and audio clock (AudioContext.currentTime)
|
|
297
|
+
// live in different clock domains with a fixed offset (different epoch)
|
|
298
|
+
// plus a small rate drift (~7ms/s). We snap the offset on first paint
|
|
299
|
+
// and re-snap every 10 seconds. Between snaps, max drift is ~70ms
|
|
300
|
+
// (under 2 frames at 24fps, below lip-sync perception threshold).
|
|
301
|
+
const wallNow = performance.now();
|
|
302
|
+
if (!this.ptsCalibrated || wallNow - this.lastCalibrationWall > 10_000) {
|
|
303
|
+
this.ptsCalibrationUs = headTs - rawAudioNowUs;
|
|
304
|
+
this.ptsCalibrated = true;
|
|
305
|
+
this.lastCalibrationWall = wallNow;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
|
|
309
|
+
const frameDurationUs = this.paintIntervalMs * 1000;
|
|
310
|
+
const deadlineUs = audioNowUs + frameDurationUs;
|
|
175
311
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
312
|
+
let bestIdx = -1;
|
|
313
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
314
|
+
const ts = this.queue[i].timestamp ?? 0;
|
|
315
|
+
if (ts <= deadlineUs) {
|
|
316
|
+
bestIdx = i;
|
|
317
|
+
} else {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (bestIdx < 0) {
|
|
323
|
+
this.ticksWaiting++;
|
|
324
|
+
if (isDebug()) {
|
|
325
|
+
const now = performance.now();
|
|
326
|
+
if (now - lastDebugLog > 1000) {
|
|
327
|
+
const headPtsMs = (headTs / 1000).toFixed(1);
|
|
328
|
+
const audioMs = (audioNowUs / 1000).toFixed(1);
|
|
329
|
+
const rawDriftMs = ((headTs - rawAudioNowUs) / 1000).toFixed(1);
|
|
330
|
+
const calibMs = (this.ptsCalibrationUs / 1000).toFixed(1);
|
|
331
|
+
// eslint-disable-next-line no-console
|
|
332
|
+
console.log(
|
|
333
|
+
`[avbridge:renderer] WAIT q=${this.queue.length} headPTS=${headPtsMs}ms calibAudio=${audioMs}ms ` +
|
|
334
|
+
`rawDrift=${rawDriftMs}ms calib=${calibMs}ms painted=${this.framesPainted} dropped=${this.framesDroppedLate}`,
|
|
335
|
+
);
|
|
336
|
+
lastDebugLog = now;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
192
339
|
return;
|
|
193
340
|
}
|
|
341
|
+
|
|
342
|
+
// Only drop frames that are more than 2 frame-durations behind.
|
|
343
|
+
const dropThresholdUs = audioNowUs - frameDurationUs * 2;
|
|
344
|
+
let dropped = 0;
|
|
345
|
+
while (bestIdx > 0) {
|
|
346
|
+
const ts = this.queue[0].timestamp ?? 0;
|
|
347
|
+
if (ts < dropThresholdUs) {
|
|
348
|
+
this.queue.shift()?.close();
|
|
349
|
+
this.framesDroppedLate++;
|
|
350
|
+
bestIdx--;
|
|
351
|
+
dropped++;
|
|
352
|
+
} else {
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.ticksPainted++;
|
|
358
|
+
|
|
359
|
+
if (isDebug()) {
|
|
360
|
+
const now = performance.now();
|
|
361
|
+
if (now - lastDebugLog > 1000) {
|
|
362
|
+
const paintedTs = (this.queue[0]?.timestamp ?? 0);
|
|
363
|
+
const audioMs = (audioNowUs / 1000).toFixed(1);
|
|
364
|
+
const ptsMs = (paintedTs / 1000).toFixed(1);
|
|
365
|
+
const rawDriftMs = ((paintedTs - rawAudioNowUs) / 1000).toFixed(1);
|
|
366
|
+
const calibMs = (this.ptsCalibrationUs / 1000).toFixed(1);
|
|
367
|
+
// eslint-disable-next-line no-console
|
|
368
|
+
console.log(
|
|
369
|
+
`[avbridge:renderer] PAINT q=${this.queue.length} calibAudio=${audioMs}ms nextPTS=${ptsMs}ms ` +
|
|
370
|
+
`rawDrift=${rawDriftMs}ms calib=${calibMs}ms dropped=${dropped} total_drops=${this.framesDroppedLate} painted=${this.framesPainted}`,
|
|
371
|
+
);
|
|
372
|
+
lastDebugLog = now;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const frame = this.queue.shift()!;
|
|
377
|
+
this.paint(frame);
|
|
378
|
+
frame.close();
|
|
379
|
+
this.lastPaintWall = performance.now();
|
|
380
|
+
return;
|
|
194
381
|
}
|
|
195
382
|
|
|
383
|
+
// Wall-clock fallback: used when timestamps are unreliable (all zero).
|
|
384
|
+
const wallNow = performance.now();
|
|
385
|
+
if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
|
|
386
|
+
|
|
196
387
|
const frame = this.queue.shift()!;
|
|
197
388
|
this.paint(frame);
|
|
198
389
|
frame.close();
|
|
@@ -222,8 +413,14 @@ export class VideoRenderer {
|
|
|
222
413
|
|
|
223
414
|
/** Discard all queued frames. Used by seek to drop stale buffers. */
|
|
224
415
|
flush(): void {
|
|
416
|
+
const count = this.queue.length;
|
|
225
417
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
226
418
|
this.prerolled = false;
|
|
419
|
+
this.ptsCalibrated = false; // recalibrate at new seek position
|
|
420
|
+
if (isDebug() && count > 0) {
|
|
421
|
+
// eslint-disable-next-line no-console
|
|
422
|
+
console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
|
|
423
|
+
}
|
|
227
424
|
}
|
|
228
425
|
|
|
229
426
|
stats(): Record<string, unknown> {
|
|
@@ -239,6 +436,8 @@ export class VideoRenderer {
|
|
|
239
436
|
this.destroyed = true;
|
|
240
437
|
if (this.rafHandle != null) cancelAnimationFrame(this.rafHandle);
|
|
241
438
|
this.flush();
|
|
439
|
+
if (this.subtitleOverlay) { this.subtitleOverlay.destroy(); this.subtitleOverlay = null; }
|
|
440
|
+
this.subtitleTrack = null;
|
|
242
441
|
this.canvas.remove();
|
|
243
442
|
this.target.style.visibility = "";
|
|
244
443
|
}
|