avbridge 2.12.0 → 2.13.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 +177 -0
- package/README.md +33 -0
- package/dist/{avi-EQE6AR75.cjs → avi-32UABODO.cjs} +12 -4
- package/dist/avi-32UABODO.cjs.map +1 -0
- package/dist/{avi-Y3N325WZ.cjs → avi-5BPR6QUX.cjs} +12 -4
- package/dist/avi-5BPR6QUX.cjs.map +1 -0
- package/dist/{avi-NNHH4AAA.js → avi-BLIH7KKV.js} +12 -4
- package/dist/avi-BLIH7KKV.js.map +1 -0
- package/dist/{avi-S7EY54YA.js → avi-GX2H34IQ.js} +12 -4
- package/dist/avi-GX2H34IQ.js.map +1 -0
- package/dist/{chunk-2LNXMGT6.js → chunk-5CX7BVVV.js} +5 -5
- package/dist/{chunk-2LNXMGT6.js.map → chunk-5CX7BVVV.js.map} +1 -1
- package/dist/{chunk-5Y5BTB5D.js → chunk-B76QWPFM.js} +3 -3
- package/dist/{chunk-5Y5BTB5D.js.map → chunk-B76QWPFM.js.map} +1 -1
- package/dist/{chunk-GJBNLPGI.cjs → chunk-E5MAM2P4.cjs} +9 -9
- package/dist/{chunk-GJBNLPGI.cjs.map → chunk-E5MAM2P4.cjs.map} +1 -1
- package/dist/{chunk-7EF4VTUS.cjs → chunk-OFJYEITB.cjs} +489 -113
- package/dist/chunk-OFJYEITB.cjs.map +1 -0
- package/dist/{chunk-HBHSUGNI.cjs → chunk-VLI3Y6IJ.cjs} +5 -5
- package/dist/{chunk-HBHSUGNI.cjs.map → chunk-VLI3Y6IJ.cjs.map} +1 -1
- package/dist/{chunk-Z26PXRUY.js → chunk-VOC24LYF.js} +486 -110
- package/dist/chunk-VOC24LYF.js.map +1 -0
- package/dist/element-browser.js +492 -130
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +3 -3
- package/dist/element.js +2 -2
- package/dist/index.cjs +18 -18
- package/dist/index.js +6 -6
- package/dist/player.cjs +658 -170
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +36 -4
- package/dist/player.d.ts +36 -4
- package/dist/player.js +658 -170
- package/dist/player.js.map +1 -1
- package/dist/{remux-VPKCLHHM.cjs → remux-NSBJFMLG.cjs} +9 -9
- package/dist/{remux-VPKCLHHM.cjs.map → remux-NSBJFMLG.cjs.map} +1 -1
- package/dist/{remux-7TA4FKTY.js → remux-PHUHO3VV.js} +4 -4
- package/dist/{remux-7TA4FKTY.js.map → remux-PHUHO3VV.js.map} +1 -1
- package/package.json +1 -1
- package/src/element/avbridge-player.ts +223 -43
- package/src/probe/avi.ts +34 -2
- package/src/strategies/fallback/audio-output.ts +164 -35
- package/src/strategies/fallback/decoder.ts +467 -60
- package/src/strategies/fallback/video-renderer.ts +209 -29
- package/src/strategies/hybrid/decoder.ts +56 -28
- package/src/strategies/remux/pipeline.ts +12 -3
- 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/vendor/libav/avbridge/libav-avbridge.mjs +1 -1
- package/dist/avi-EQE6AR75.cjs.map +0 -1
- package/dist/avi-NNHH4AAA.js.map +0 -1
- package/dist/avi-S7EY54YA.js.map +0 -1
- package/dist/avi-Y3N325WZ.cjs.map +0 -1
- package/dist/chunk-7EF4VTUS.cjs.map +0 -1
- package/dist/chunk-Z26PXRUY.js.map +0 -1
package/dist/element-browser.js
CHANGED
|
@@ -29957,7 +29957,7 @@ async function probeWithLibav(source, sniffed) {
|
|
|
29957
29957
|
codec: ffmpegToAvbridgeVideo(codecName),
|
|
29958
29958
|
width: codecpar?.width ?? 0,
|
|
29959
29959
|
height: codecpar?.height ?? 0,
|
|
29960
|
-
fps: framerate(stream)
|
|
29960
|
+
fps: await framerate(libav, stream)
|
|
29961
29961
|
});
|
|
29962
29962
|
} else if (stream.codec_type === libav.AVMEDIA_TYPE_AUDIO) {
|
|
29963
29963
|
audioTracks.push({
|
|
@@ -29985,7 +29985,7 @@ async function probeWithLibav(source, sniffed) {
|
|
|
29985
29985
|
duration
|
|
29986
29986
|
};
|
|
29987
29987
|
}
|
|
29988
|
-
function framerate(stream) {
|
|
29988
|
+
async function framerate(libav, stream) {
|
|
29989
29989
|
if (typeof stream.avg_frame_rate_num === "number" && stream.avg_frame_rate_den) {
|
|
29990
29990
|
return stream.avg_frame_rate_num / stream.avg_frame_rate_den;
|
|
29991
29991
|
}
|
|
@@ -29993,6 +29993,14 @@ function framerate(stream) {
|
|
|
29993
29993
|
if (stream.avg_frame_rate.den === 0) return void 0;
|
|
29994
29994
|
return stream.avg_frame_rate.num / stream.avg_frame_rate.den;
|
|
29995
29995
|
}
|
|
29996
|
+
try {
|
|
29997
|
+
const num = await libav.AVCodecParameters_framerate_num?.(stream.codecpar);
|
|
29998
|
+
const den = await libav.AVCodecParameters_framerate_den?.(stream.codecpar);
|
|
29999
|
+
if (typeof num === "number" && typeof den === "number" && den > 0 && num > 0) {
|
|
30000
|
+
return num / den;
|
|
30001
|
+
}
|
|
30002
|
+
} catch {
|
|
30003
|
+
}
|
|
29996
30004
|
return void 0;
|
|
29997
30005
|
}
|
|
29998
30006
|
async function safeDuration(libav, fmt_ctx) {
|
|
@@ -32247,22 +32255,25 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
32247
32255
|
}
|
|
32248
32256
|
}
|
|
32249
32257
|
let mimePromise = null;
|
|
32258
|
+
const myToken = pumpToken;
|
|
32250
32259
|
const writable = new WritableStream({
|
|
32251
32260
|
write: async (chunk) => {
|
|
32252
|
-
if (destroyed) return;
|
|
32261
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
32253
32262
|
if (!sink) {
|
|
32254
32263
|
const mime = await (mimePromise ??= output.getMimeType());
|
|
32264
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
32255
32265
|
sink = new MseSink({ mime, video });
|
|
32256
32266
|
await sink.ready();
|
|
32267
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
32257
32268
|
if (pendingStartTime > 0) {
|
|
32258
32269
|
sink.invalidate(pendingStartTime);
|
|
32259
32270
|
}
|
|
32260
32271
|
sink.setPlayOnSeek(pendingAutoPlay);
|
|
32261
32272
|
}
|
|
32262
|
-
while (sink && !destroyed && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
|
|
32273
|
+
while (sink && !destroyed && pumpToken === myToken && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
|
|
32263
32274
|
await new Promise((r) => setTimeout(r, 500));
|
|
32264
32275
|
}
|
|
32265
|
-
if (destroyed) return;
|
|
32276
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
32266
32277
|
sink.append(chunk.data);
|
|
32267
32278
|
stats.bytesWritten += chunk.data.byteLength;
|
|
32268
32279
|
stats.fragments++;
|
|
@@ -32530,7 +32541,21 @@ var VideoRenderer = class {
|
|
|
32530
32541
|
framesPainted = 0;
|
|
32531
32542
|
framesDroppedLate = 0;
|
|
32532
32543
|
framesDroppedOverflow = 0;
|
|
32544
|
+
/** True once the head frame has been painted as a pre-roll poster
|
|
32545
|
+
* since the last flush. Used to ensure pre-roll paints exactly one
|
|
32546
|
+
* frame (held static) during the post-seek discard window. */
|
|
32533
32547
|
prerolled = false;
|
|
32548
|
+
/** PTS (µs) of the most recently painted frame. Used as the calibration
|
|
32549
|
+
* reference on the first post-flush snap: the pre-roll path paints one
|
|
32550
|
+
* frame *before* PTS-based playback starts, so the queue head's PTS at
|
|
32551
|
+
* first PTS-based paint is the *next* frame, off by one frameDur from
|
|
32552
|
+
* the actually-displayed frame. Calibrating against the painted frame
|
|
32553
|
+
* instead of the queue head removes that one-frame offset and yields
|
|
32554
|
+
* calib ≈ 0 instead of +frameDur. */
|
|
32555
|
+
lastPaintedPtsUs = 0;
|
|
32556
|
+
hasLastPaintedPts = false;
|
|
32557
|
+
/** Audio-clock reading (ms) at the previous paint, for overlay Δaud. */
|
|
32558
|
+
lastPaintAudMs = 0;
|
|
32534
32559
|
/** Wall-clock time of the last paint, in ms (performance.now()). */
|
|
32535
32560
|
lastPaintWall = 0;
|
|
32536
32561
|
/** Minimum ms between paints — paces video at roughly source fps. */
|
|
@@ -32561,32 +32586,47 @@ var VideoRenderer = class {
|
|
|
32561
32586
|
/** Resolves once the first decoded frame has been enqueued. */
|
|
32562
32587
|
firstFrameReady;
|
|
32563
32588
|
resolveFirstFrame;
|
|
32564
|
-
/**
|
|
32589
|
+
/**
|
|
32590
|
+
* True once at least one frame has been enqueued *since the last flush*.
|
|
32591
|
+
* Used by `readyState` — initial cold-start reports HAVE_NOTHING until
|
|
32592
|
+
* any frame has arrived, and after a seek we want the same semantics
|
|
32593
|
+
* (HAVE_NOTHING until post-seek frames arrive), so the cumulative
|
|
32594
|
+
* `framesPainted > 0` that used to live here was wrong: it kept the
|
|
32595
|
+
* state "true forever" after the first frame ever, so post-seek
|
|
32596
|
+
* `waitForBuffer()` would exit immediately with an empty queue and
|
|
32597
|
+
* leave video frozen while audio kept going.
|
|
32598
|
+
*/
|
|
32565
32599
|
hasFrames() {
|
|
32566
|
-
return this.queue.length > 0 || this.
|
|
32600
|
+
return this.queue.length > 0 || this.hasEverEnqueuedSinceFlush;
|
|
32567
32601
|
}
|
|
32602
|
+
hasEverEnqueuedSinceFlush = false;
|
|
32568
32603
|
/** Current depth of the frame queue. Used by the decoder for backpressure. */
|
|
32569
32604
|
queueDepth() {
|
|
32570
32605
|
return this.queue.length;
|
|
32571
32606
|
}
|
|
32572
32607
|
/**
|
|
32573
|
-
*
|
|
32574
|
-
*
|
|
32575
|
-
*
|
|
32576
|
-
*
|
|
32577
|
-
*
|
|
32608
|
+
* Cap the decoder may fill the queue up to. Used by the decoder's
|
|
32609
|
+
* enqueue-side discard logic (it closes new frames instead of pushing
|
|
32610
|
+
* them when this is reached). Sized so a long post-seek catch-up
|
|
32611
|
+
* fits — the decoder produces frames at PTS T_kf onwards rapidly
|
|
32612
|
+
* while the demuxer is chewing through pre-target audio; if the
|
|
32613
|
+
* queue can hold the whole post-seek burst, the renderer plays
|
|
32614
|
+
* smoothly from pre-roll without a frozen-video gap when audio.start
|
|
32615
|
+
* fires. At ~340 KB per SD frame the cap is ~85 MB peak; at HD it's
|
|
32616
|
+
* larger but still bounded.
|
|
32578
32617
|
*/
|
|
32579
|
-
queueHighWater =
|
|
32618
|
+
queueHighWater = 256;
|
|
32580
32619
|
enqueue(frame) {
|
|
32581
32620
|
if (this.destroyed) {
|
|
32582
32621
|
frame.close();
|
|
32583
32622
|
return;
|
|
32584
32623
|
}
|
|
32585
32624
|
this.queue.push(frame);
|
|
32625
|
+
this.hasEverEnqueuedSinceFlush = true;
|
|
32586
32626
|
if (this.queue.length === 1 && this.framesPainted === 0) {
|
|
32587
32627
|
this.resolveFirstFrame();
|
|
32588
32628
|
}
|
|
32589
|
-
while (this.queue.length >
|
|
32629
|
+
while (this.queue.length > this.queueHighWater + 8) {
|
|
32590
32630
|
this.queue.shift()?.close();
|
|
32591
32631
|
this.framesDroppedOverflow++;
|
|
32592
32632
|
}
|
|
@@ -32668,12 +32708,9 @@ var VideoRenderer = class {
|
|
|
32668
32708
|
if (this.queue.length === 0) return;
|
|
32669
32709
|
const playing = this.clock.isPlaying();
|
|
32670
32710
|
if (!playing) {
|
|
32671
|
-
if (!this.prerolled) {
|
|
32672
|
-
const head = this.queue.shift();
|
|
32673
|
-
this.paint(head);
|
|
32674
|
-
head.close();
|
|
32711
|
+
if (!this.prerolled && this.queue.length > 0) {
|
|
32675
32712
|
this.prerolled = true;
|
|
32676
|
-
this.
|
|
32713
|
+
this.paint(this.queue[0]);
|
|
32677
32714
|
}
|
|
32678
32715
|
return;
|
|
32679
32716
|
}
|
|
@@ -32682,14 +32719,29 @@ var VideoRenderer = class {
|
|
|
32682
32719
|
const hasPts = headTs > 0 || this.queue.length > 1;
|
|
32683
32720
|
if (hasPts) {
|
|
32684
32721
|
const wallNow2 = performance.now();
|
|
32685
|
-
if (!this.ptsCalibrated
|
|
32686
|
-
this.
|
|
32722
|
+
if (!this.ptsCalibrated) {
|
|
32723
|
+
const anchorUs = (this.clock.anchorTime?.() ?? this.clock.now()) * 1e6;
|
|
32724
|
+
const referencePtsUs = this.hasLastPaintedPts ? this.lastPaintedPtsUs : headTs;
|
|
32725
|
+
this.ptsCalibrationUs = referencePtsUs - anchorUs;
|
|
32687
32726
|
this.ptsCalibrated = true;
|
|
32688
32727
|
this.lastCalibrationWall = wallNow2;
|
|
32728
|
+
if (isDebug()) {
|
|
32729
|
+
console.log(
|
|
32730
|
+
`[avbridge:renderer] CALIB-FIRST audioAnchor=${(anchorUs / 1e3).toFixed(1)}ms prerolledPTS=${this.hasLastPaintedPts ? (this.lastPaintedPtsUs / 1e3).toFixed(1) : "n/a"}ms queueHeadPTS=${(headTs / 1e3).toFixed(1)}ms rawAudioNow=${(rawAudioNowUs / 1e3).toFixed(1)}ms \u2192 calib=${(this.ptsCalibrationUs / 1e3).toFixed(1)}ms`
|
|
32731
|
+
);
|
|
32732
|
+
}
|
|
32733
|
+
} else if (wallNow2 - this.lastCalibrationWall > 1e4) {
|
|
32734
|
+
const oldCalib = this.ptsCalibrationUs;
|
|
32735
|
+
this.ptsCalibrationUs = headTs - rawAudioNowUs;
|
|
32736
|
+
this.lastCalibrationWall = wallNow2;
|
|
32737
|
+
if (isDebug()) {
|
|
32738
|
+
console.log(
|
|
32739
|
+
`[avbridge:renderer] CALIB-RESNAP headPTS=${(headTs / 1e3).toFixed(1)}ms rawAudioNow=${(rawAudioNowUs / 1e3).toFixed(1)}ms calib ${(oldCalib / 1e3).toFixed(1)}ms \u2192 ${(this.ptsCalibrationUs / 1e3).toFixed(1)}ms (\u0394=${((this.ptsCalibrationUs - oldCalib) / 1e3).toFixed(1)}ms after 10s)`
|
|
32740
|
+
);
|
|
32741
|
+
}
|
|
32689
32742
|
}
|
|
32690
32743
|
const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
|
|
32691
|
-
const
|
|
32692
|
-
const deadlineUs = audioNowUs + frameDurationUs;
|
|
32744
|
+
const deadlineUs = audioNowUs;
|
|
32693
32745
|
let bestIdx = -1;
|
|
32694
32746
|
for (let i = 0; i < this.queue.length; i++) {
|
|
32695
32747
|
const ts = this.queue[i].timestamp ?? 0;
|
|
@@ -32716,19 +32768,21 @@ var VideoRenderer = class {
|
|
|
32716
32768
|
}
|
|
32717
32769
|
return;
|
|
32718
32770
|
}
|
|
32719
|
-
const
|
|
32771
|
+
const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
|
|
32720
32772
|
let dropped = 0;
|
|
32721
|
-
|
|
32722
|
-
|
|
32723
|
-
|
|
32773
|
+
const initialBestIdx = bestIdx;
|
|
32774
|
+
if (!_relaxDrop) {
|
|
32775
|
+
while (bestIdx > 0) {
|
|
32724
32776
|
this.queue.shift()?.close();
|
|
32725
32777
|
this.framesDroppedLate++;
|
|
32726
32778
|
bestIdx--;
|
|
32727
32779
|
dropped++;
|
|
32728
|
-
} else {
|
|
32729
|
-
break;
|
|
32730
32780
|
}
|
|
32731
32781
|
}
|
|
32782
|
+
const paintTs = this.queue[0]?.timestamp ?? 0;
|
|
32783
|
+
if (isDebug()) {
|
|
32784
|
+
console.log(`[TRACE] PAINT bestIdx_initial=${initialBestIdx} dropped=${dropped} paintPts=${(paintTs / 1e3).toFixed(1)}ms audioNow=${(audioNowUs / 1e3).toFixed(1)}ms deadline=${(deadlineUs / 1e3).toFixed(1)}ms queueLen=${this.queue.length} wall=${performance.now().toFixed(0)}`);
|
|
32785
|
+
}
|
|
32732
32786
|
this.ticksPainted++;
|
|
32733
32787
|
if (isDebug()) {
|
|
32734
32788
|
const now = performance.now();
|
|
@@ -32764,6 +32818,33 @@ var VideoRenderer = class {
|
|
|
32764
32818
|
}
|
|
32765
32819
|
try {
|
|
32766
32820
|
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
32821
|
+
if (isDebug()) {
|
|
32822
|
+
const wallNow = performance.now();
|
|
32823
|
+
const audNowMs = this.clock.now() * 1e3;
|
|
32824
|
+
const ptsMs = (frame.timestamp ?? 0) / 1e3;
|
|
32825
|
+
const dWall = this.lastPaintWall > 0 ? wallNow - this.lastPaintWall : 0;
|
|
32826
|
+
const dAud = this.lastPaintAudMs > 0 ? audNowMs - this.lastPaintAudMs : 0;
|
|
32827
|
+
const dPts = this.hasLastPaintedPts ? ptsMs - this.lastPaintedPtsUs / 1e3 : 0;
|
|
32828
|
+
this.ctx.save();
|
|
32829
|
+
this.ctx.font = "bold 18px monospace";
|
|
32830
|
+
const lines = [
|
|
32831
|
+
`#${this.framesPainted + 1} pts=${ptsMs.toFixed(0)} aud=${audNowMs.toFixed(0)} wall=${wallNow.toFixed(0)}`,
|
|
32832
|
+
`\u0394pts=${dPts.toFixed(0)} \u0394aud=${dAud.toFixed(0)} \u0394wall=${dWall.toFixed(0)}`
|
|
32833
|
+
];
|
|
32834
|
+
const lineHeight = 22;
|
|
32835
|
+
const padTop = 6;
|
|
32836
|
+
const stripH = padTop + lineHeight * lines.length;
|
|
32837
|
+
this.ctx.fillStyle = "rgba(0,0,0,0.7)";
|
|
32838
|
+
this.ctx.fillRect(0, 0, this.canvas.width, stripH);
|
|
32839
|
+
this.ctx.fillStyle = "#0f0";
|
|
32840
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32841
|
+
this.ctx.fillText(lines[i], 8, padTop + lineHeight * (i + 1) - 4);
|
|
32842
|
+
}
|
|
32843
|
+
this.ctx.restore();
|
|
32844
|
+
}
|
|
32845
|
+
this.lastPaintedPtsUs = frame.timestamp ?? 0;
|
|
32846
|
+
this.hasLastPaintedPts = true;
|
|
32847
|
+
this.lastPaintAudMs = this.clock.now() * 1e3;
|
|
32767
32848
|
this.framesPainted++;
|
|
32768
32849
|
} catch (err) {
|
|
32769
32850
|
if (this.framesPainted === 0 && this.framesDroppedLate === 0) {
|
|
@@ -32776,17 +32857,30 @@ var VideoRenderer = class {
|
|
|
32776
32857
|
const count = this.queue.length;
|
|
32777
32858
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
32778
32859
|
this.prerolled = false;
|
|
32860
|
+
this.hasLastPaintedPts = false;
|
|
32779
32861
|
this.ptsCalibrated = false;
|
|
32862
|
+
this.hasEverEnqueuedSinceFlush = false;
|
|
32780
32863
|
if (isDebug() && count > 0) {
|
|
32781
32864
|
console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
|
|
32782
32865
|
}
|
|
32783
32866
|
}
|
|
32784
32867
|
stats() {
|
|
32868
|
+
let queueSpanMs = 0;
|
|
32869
|
+
let queueHeadMs = 0;
|
|
32870
|
+
let queueTailMs = 0;
|
|
32871
|
+
if (this.queue.length > 0) {
|
|
32872
|
+
queueHeadMs = Math.round((this.queue[0].timestamp ?? 0) / 1e3);
|
|
32873
|
+
queueTailMs = Math.round((this.queue[this.queue.length - 1].timestamp ?? 0) / 1e3);
|
|
32874
|
+
queueSpanMs = Math.max(0, queueTailMs - queueHeadMs);
|
|
32875
|
+
}
|
|
32785
32876
|
return {
|
|
32786
32877
|
framesPainted: this.framesPainted,
|
|
32787
32878
|
framesDroppedLate: this.framesDroppedLate,
|
|
32788
32879
|
framesDroppedOverflow: this.framesDroppedOverflow,
|
|
32789
|
-
queueDepth: this.queue.length
|
|
32880
|
+
queueDepth: this.queue.length,
|
|
32881
|
+
queueHeadMs,
|
|
32882
|
+
queueTailMs,
|
|
32883
|
+
queueSpanMs
|
|
32790
32884
|
};
|
|
32791
32885
|
}
|
|
32792
32886
|
destroy() {
|
|
@@ -32804,6 +32898,9 @@ var VideoRenderer = class {
|
|
|
32804
32898
|
};
|
|
32805
32899
|
|
|
32806
32900
|
// src/strategies/fallback/audio-output.ts
|
|
32901
|
+
function isDebug2() {
|
|
32902
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
32903
|
+
}
|
|
32807
32904
|
var AudioOutput = class {
|
|
32808
32905
|
ctx;
|
|
32809
32906
|
gain;
|
|
@@ -32825,6 +32922,16 @@ var AudioOutput = class {
|
|
|
32825
32922
|
mediaTimeOfNext = 0;
|
|
32826
32923
|
/** Anchor: media time `mediaTimeOfAnchor` corresponds to ctx time `ctxTimeAtAnchor`. */
|
|
32827
32924
|
mediaTimeOfAnchor = 0;
|
|
32925
|
+
/**
|
|
32926
|
+
* Ctx time at which the first audible chunk will start playing. `-1`
|
|
32927
|
+
* before any chunk has been scheduled successfully (clock is frozen);
|
|
32928
|
+
* the actual ctx time once one has. The renderer's `clock.now()` uses
|
|
32929
|
+
* this to avoid advancing during the silent-gap window between
|
|
32930
|
+
* `audio.start()` and the first chunk that schedules without being
|
|
32931
|
+
* dropped — that gap is what produces the "audio-less fast-forward"
|
|
32932
|
+
* the user sees post-seek when the gate releases on video-only grace.
|
|
32933
|
+
*/
|
|
32934
|
+
firstAudibleCtxStart = -1;
|
|
32828
32935
|
ctxTimeAtAnchor = 0;
|
|
32829
32936
|
pendingQueue = [];
|
|
32830
32937
|
framesScheduled = 0;
|
|
@@ -32897,10 +33004,16 @@ var AudioOutput = class {
|
|
|
32897
33004
|
return this.mediaTimeOfAnchor;
|
|
32898
33005
|
}
|
|
32899
33006
|
if (this.state === "playing") {
|
|
33007
|
+
if (this.firstAudibleCtxStart < 0) {
|
|
33008
|
+
return this.mediaTimeOfAnchor;
|
|
33009
|
+
}
|
|
32900
33010
|
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
|
|
32901
33011
|
}
|
|
32902
33012
|
return this.mediaTimeOfAnchor;
|
|
32903
33013
|
}
|
|
33014
|
+
anchorTime() {
|
|
33015
|
+
return this.mediaTimeOfAnchor;
|
|
33016
|
+
}
|
|
32904
33017
|
isPlaying() {
|
|
32905
33018
|
return this.state === "playing";
|
|
32906
33019
|
}
|
|
@@ -32927,18 +33040,81 @@ var AudioOutput = class {
|
|
|
32927
33040
|
* Schedule a chunk of decoded samples. Queues internally while idle (cold
|
|
32928
33041
|
* start or post-seek), schedules directly to the audio graph while playing.
|
|
32929
33042
|
* In wall-clock mode, samples are silently discarded.
|
|
33043
|
+
*
|
|
33044
|
+
* `ptsSec` is the chunk's source-domain content PTS in seconds, from
|
|
33045
|
+
* the demuxer. When provided, the chunk plays at the ctx-time
|
|
33046
|
+
* corresponding to that PTS — so pre-target audio after a seek
|
|
33047
|
+
* naturally drops (its computed `ctxStart` falls in the past) and
|
|
33048
|
+
* post-target audio plays at its true content time, without any
|
|
33049
|
+
* external trim or anchor rebase. When `ptsSec` is null (cold start
|
|
33050
|
+
* with no PTS yet, or codecs whose packet→frame mapping isn't 1:1),
|
|
33051
|
+
* the chunk is scheduled sequentially after `mediaTimeOfNext` — the
|
|
33052
|
+
* pre-refactor behavior.
|
|
32930
33053
|
*/
|
|
32931
|
-
schedule(samples, channels, sampleRate) {
|
|
33054
|
+
schedule(samples, channels, sampleRate, ptsSec) {
|
|
32932
33055
|
if (this.destroyed || this.noAudio) return;
|
|
32933
33056
|
const frameCount = samples.length / channels;
|
|
32934
33057
|
const durationSec = frameCount / sampleRate;
|
|
33058
|
+
const hasPts = ptsSec != null && Number.isFinite(ptsSec);
|
|
33059
|
+
if (hasPts && ptsSec + durationSec / this._rate < this.mediaTimeOfAnchor) {
|
|
33060
|
+
return;
|
|
33061
|
+
}
|
|
32935
33062
|
if (this.state === "idle" || this.state === "paused") {
|
|
32936
|
-
this.pendingQueue.push({
|
|
33063
|
+
this.pendingQueue.push({
|
|
33064
|
+
samples,
|
|
33065
|
+
channels,
|
|
33066
|
+
sampleRate,
|
|
33067
|
+
frameCount,
|
|
33068
|
+
durationSec,
|
|
33069
|
+
ptsSec: hasPts ? ptsSec : null
|
|
33070
|
+
});
|
|
32937
33071
|
return;
|
|
32938
33072
|
}
|
|
32939
|
-
this.scheduleNow(
|
|
33073
|
+
this.scheduleNow(
|
|
33074
|
+
samples,
|
|
33075
|
+
channels,
|
|
33076
|
+
sampleRate,
|
|
33077
|
+
frameCount,
|
|
33078
|
+
hasPts ? ptsSec : null
|
|
33079
|
+
);
|
|
32940
33080
|
}
|
|
32941
|
-
scheduleNow(samples, channels, sampleRate, frameCount) {
|
|
33081
|
+
scheduleNow(samples, channels, sampleRate, frameCount, ptsSec) {
|
|
33082
|
+
const durationSec = frameCount / sampleRate;
|
|
33083
|
+
let ctxStart;
|
|
33084
|
+
if (ptsSec != null) {
|
|
33085
|
+
ctxStart = this.ctxTimeAtAnchor + (ptsSec - this.mediaTimeOfAnchor) / this._rate;
|
|
33086
|
+
if (isDebug2()) {
|
|
33087
|
+
console.log(`[TRACE-AUD] PTS sched #${this.framesScheduled} pts=${ptsSec.toFixed(3)} dur=${durationSec.toFixed(4)} ctxStart=${ctxStart.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)} anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} mtNext=${this.mediaTimeOfNext.toFixed(3)} rate=${this._rate}`);
|
|
33088
|
+
}
|
|
33089
|
+
if (ctxStart < this.ctx.currentTime - 1e-3) {
|
|
33090
|
+
if (isDebug2()) {
|
|
33091
|
+
console.log(`[TRACE-AUD] DROP late chunk pts=${ptsSec.toFixed(3)} ctxStart=${ctxStart.toFixed(4)} < ctxNow=${this.ctx.currentTime.toFixed(4)}`);
|
|
33092
|
+
}
|
|
33093
|
+
return;
|
|
33094
|
+
}
|
|
33095
|
+
if (this.firstAudibleCtxStart < 0) {
|
|
33096
|
+
this.firstAudibleCtxStart = ctxStart;
|
|
33097
|
+
this.mediaTimeOfAnchor = ptsSec;
|
|
33098
|
+
this.ctxTimeAtAnchor = ctxStart;
|
|
33099
|
+
if (isDebug2()) {
|
|
33100
|
+
console.log(`[TRACE-AUD] UNFREEZE clock \u2014 first audible chunk pts=${ptsSec.toFixed(3)} ctxStart=${ctxStart.toFixed(4)} \u2192 anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)}`);
|
|
33101
|
+
}
|
|
33102
|
+
}
|
|
33103
|
+
const endMediaTime = ptsSec + durationSec / this._rate;
|
|
33104
|
+
if (endMediaTime > this.mediaTimeOfNext) {
|
|
33105
|
+
this.mediaTimeOfNext = endMediaTime;
|
|
33106
|
+
}
|
|
33107
|
+
} else {
|
|
33108
|
+
ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
33109
|
+
console.warn(`[TRACE-AUD] LEGACY (no PTS) sched dur=${durationSec.toFixed(4)} ctxStart=${ctxStart.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)}`);
|
|
33110
|
+
if (ctxStart < this.ctx.currentTime) {
|
|
33111
|
+
console.warn(`[TRACE-AUD] REBASE anchor was=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor was=${this.ctxTimeAtAnchor.toFixed(4)} \u2192 anchor=${this.mediaTimeOfNext.toFixed(3)} ctxAnchor=${this.ctx.currentTime.toFixed(4)}`);
|
|
33112
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
33113
|
+
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
33114
|
+
ctxStart = this.ctx.currentTime;
|
|
33115
|
+
}
|
|
33116
|
+
this.mediaTimeOfNext += durationSec;
|
|
33117
|
+
}
|
|
32942
33118
|
const buffer = this.ctx.createBuffer(channels, frameCount, sampleRate);
|
|
32943
33119
|
for (let ch = 0; ch < channels; ch++) {
|
|
32944
33120
|
const channelData = buffer.getChannelData(ch);
|
|
@@ -32950,14 +33126,7 @@ var AudioOutput = class {
|
|
|
32950
33126
|
node3.buffer = buffer;
|
|
32951
33127
|
node3.connect(this.gain);
|
|
32952
33128
|
if (this._rate !== 1) node3.playbackRate.value = this._rate;
|
|
32953
|
-
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
32954
|
-
if (ctxStart < this.ctx.currentTime) {
|
|
32955
|
-
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
32956
|
-
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
32957
|
-
ctxStart = this.ctx.currentTime;
|
|
32958
|
-
}
|
|
32959
33129
|
node3.start(ctxStart);
|
|
32960
|
-
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
32961
33130
|
this.framesScheduled++;
|
|
32962
33131
|
}
|
|
32963
33132
|
// ── Lifecycle ─────────────────────────────────────────────────────────
|
|
@@ -32982,12 +33151,15 @@ var AudioOutput = class {
|
|
|
32982
33151
|
} catch {
|
|
32983
33152
|
}
|
|
32984
33153
|
if (this.state === "paused") {
|
|
33154
|
+
if (isDebug2()) {
|
|
33155
|
+
console.log(`[TRACE-AUD] START(resume) anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} \u2192 ctxAnchor=${this.ctx.currentTime.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)} pendingCount=${this.pendingQueue.length}`);
|
|
33156
|
+
}
|
|
32985
33157
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
32986
33158
|
this.state = "playing";
|
|
32987
33159
|
const drain2 = this.pendingQueue;
|
|
32988
33160
|
this.pendingQueue = [];
|
|
32989
33161
|
for (const c of drain2) {
|
|
32990
|
-
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
33162
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
|
|
32991
33163
|
}
|
|
32992
33164
|
return;
|
|
32993
33165
|
}
|
|
@@ -32995,10 +33167,13 @@ var AudioOutput = class {
|
|
|
32995
33167
|
this.ctxTimeAtAnchor = this.ctx.currentTime + STARTUP_DELAY;
|
|
32996
33168
|
this.mediaTimeOfNext = this.mediaTimeOfAnchor;
|
|
32997
33169
|
this.state = "playing";
|
|
33170
|
+
if (isDebug2()) {
|
|
33171
|
+
console.log(`[TRACE-AUD] START(cold) anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} mtNext=${this.mediaTimeOfNext.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(4)} pendingCount=${this.pendingQueue.length}`);
|
|
33172
|
+
}
|
|
32998
33173
|
const drain = this.pendingQueue;
|
|
32999
33174
|
this.pendingQueue = [];
|
|
33000
33175
|
for (const c of drain) {
|
|
33001
|
-
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
33176
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
|
|
33002
33177
|
}
|
|
33003
33178
|
}
|
|
33004
33179
|
/** Pause playback. Suspends the audio context. */
|
|
@@ -33024,6 +33199,9 @@ var AudioOutput = class {
|
|
|
33024
33199
|
* supplying new samples) and then call `start()` to resume playback.
|
|
33025
33200
|
*/
|
|
33026
33201
|
async reset(newMediaTime) {
|
|
33202
|
+
if (isDebug2()) {
|
|
33203
|
+
console.log(`[TRACE-AUD] RESET to=${newMediaTime.toFixed(3)} prev_anchor=${this.mediaTimeOfAnchor.toFixed(3)} prev_mtNext=${this.mediaTimeOfNext.toFixed(3)} prev_ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)} state=${this.state}`);
|
|
33204
|
+
}
|
|
33027
33205
|
if (this.noAudio) {
|
|
33028
33206
|
this.pendingQueue = [];
|
|
33029
33207
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
@@ -33042,6 +33220,7 @@ var AudioOutput = class {
|
|
|
33042
33220
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
33043
33221
|
this.mediaTimeOfNext = newMediaTime;
|
|
33044
33222
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
33223
|
+
this.firstAudibleCtxStart = -1;
|
|
33045
33224
|
this.state = "idle";
|
|
33046
33225
|
if (this.ctx.state === "running") {
|
|
33047
33226
|
await this.ctx.suspend();
|
|
@@ -33225,28 +33404,6 @@ function asUint8(x) {
|
|
|
33225
33404
|
const ta = x;
|
|
33226
33405
|
return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
|
|
33227
33406
|
}
|
|
33228
|
-
function sanitizeFrameTimestamp(frame, nextUs, fallbackTimeBase) {
|
|
33229
|
-
const lo = frame.pts ?? 0;
|
|
33230
|
-
const hi = frame.ptshi ?? 0;
|
|
33231
|
-
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
33232
|
-
if (isInvalid) {
|
|
33233
|
-
const us2 = nextUs();
|
|
33234
|
-
frame.pts = us2;
|
|
33235
|
-
frame.ptshi = 0;
|
|
33236
|
-
return;
|
|
33237
|
-
}
|
|
33238
|
-
const tb = fallbackTimeBase ?? [1, 1e6];
|
|
33239
|
-
const pts64 = hi * 4294967296 + lo;
|
|
33240
|
-
const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
|
|
33241
|
-
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
33242
|
-
frame.pts = us;
|
|
33243
|
-
frame.ptshi = us < 0 ? -1 : 0;
|
|
33244
|
-
return;
|
|
33245
|
-
}
|
|
33246
|
-
const fallback = nextUs();
|
|
33247
|
-
frame.pts = fallback;
|
|
33248
|
-
frame.ptshi = 0;
|
|
33249
|
-
}
|
|
33250
33407
|
|
|
33251
33408
|
// src/strategies/hybrid/decoder.ts
|
|
33252
33409
|
async function startHybridDecoder(opts) {
|
|
@@ -33328,6 +33485,7 @@ async function startHybridDecoder(opts) {
|
|
|
33328
33485
|
}
|
|
33329
33486
|
let bsfCtx = null;
|
|
33330
33487
|
let bsfPkt = null;
|
|
33488
|
+
let bsfRequiredButMissing = false;
|
|
33331
33489
|
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
33332
33490
|
try {
|
|
33333
33491
|
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
@@ -33338,13 +33496,19 @@ async function startHybridDecoder(opts) {
|
|
|
33338
33496
|
bsfPkt = await libav.av_packet_alloc();
|
|
33339
33497
|
dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
|
|
33340
33498
|
} else {
|
|
33341
|
-
|
|
33499
|
+
bsfRequiredButMissing = true;
|
|
33342
33500
|
bsfCtx = null;
|
|
33343
33501
|
}
|
|
33344
33502
|
} catch (err) {
|
|
33345
|
-
|
|
33503
|
+
bsfRequiredButMissing = true;
|
|
33346
33504
|
bsfCtx = null;
|
|
33347
33505
|
bsfPkt = null;
|
|
33506
|
+
dbg.warn("bsf", `hybrid: mpeg4_unpack_bframes BSF init failed: ${err.message}`);
|
|
33507
|
+
}
|
|
33508
|
+
if (bsfRequiredButMissing) {
|
|
33509
|
+
console.error(
|
|
33510
|
+
"[avbridge] MPEG-4 Part 2 (DivX/Xvid) detected but mpeg4_unpack_bframes BSF is unavailable in this libav variant. Files with packed B-frames will play with incorrect frame ordering. Rebuild the libav variant with the `avbsf` fragment included."
|
|
33511
|
+
);
|
|
33348
33512
|
}
|
|
33349
33513
|
}
|
|
33350
33514
|
async function applyBSF(packets) {
|
|
@@ -33354,7 +33518,6 @@ async function startHybridDecoder(opts) {
|
|
|
33354
33518
|
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
33355
33519
|
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
33356
33520
|
if (sendErr < 0) {
|
|
33357
|
-
out.push(pkt);
|
|
33358
33521
|
continue;
|
|
33359
33522
|
}
|
|
33360
33523
|
while (true) {
|
|
@@ -33368,10 +33531,13 @@ async function startHybridDecoder(opts) {
|
|
|
33368
33531
|
async function flushBSF() {
|
|
33369
33532
|
if (!bsfCtx || !bsfPkt) return;
|
|
33370
33533
|
try {
|
|
33371
|
-
|
|
33372
|
-
|
|
33373
|
-
|
|
33374
|
-
|
|
33534
|
+
if (libav.av_bsf_flush) {
|
|
33535
|
+
await libav.av_bsf_flush(bsfCtx);
|
|
33536
|
+
} else {
|
|
33537
|
+
while (true) {
|
|
33538
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
33539
|
+
if (err < 0) break;
|
|
33540
|
+
}
|
|
33375
33541
|
}
|
|
33376
33542
|
} catch {
|
|
33377
33543
|
}
|
|
@@ -33385,7 +33551,6 @@ async function startHybridDecoder(opts) {
|
|
|
33385
33551
|
let videoChunksFed = 0;
|
|
33386
33552
|
let bufferedUntilSec = 0;
|
|
33387
33553
|
let syntheticVideoUs = 0;
|
|
33388
|
-
let syntheticAudioUs = 0;
|
|
33389
33554
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
33390
33555
|
const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
|
|
33391
33556
|
const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
|
|
@@ -33417,7 +33582,13 @@ async function startHybridDecoder(opts) {
|
|
|
33417
33582
|
}
|
|
33418
33583
|
}
|
|
33419
33584
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
33420
|
-
await decodeAudioBatch(
|
|
33585
|
+
await decodeAudioBatch(
|
|
33586
|
+
audioPackets,
|
|
33587
|
+
myToken,
|
|
33588
|
+
/*flush*/
|
|
33589
|
+
false,
|
|
33590
|
+
audioTimeBase
|
|
33591
|
+
);
|
|
33421
33592
|
}
|
|
33422
33593
|
if (myToken !== pumpToken || destroyed) return;
|
|
33423
33594
|
await new Promise((r) => setTimeout(r, 0));
|
|
@@ -33464,8 +33635,11 @@ async function startHybridDecoder(opts) {
|
|
|
33464
33635
|
}
|
|
33465
33636
|
}
|
|
33466
33637
|
}
|
|
33467
|
-
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
33638
|
+
async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
|
|
33468
33639
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
33640
|
+
const pktPtsSec = pkts.map(
|
|
33641
|
+
(p) => tb ? packetPtsSec(p, tb) : null
|
|
33642
|
+
);
|
|
33469
33643
|
const AUDIO_SUB_BATCH = 4;
|
|
33470
33644
|
let allFrames = [];
|
|
33471
33645
|
for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
|
|
@@ -33503,22 +33677,13 @@ async function startHybridDecoder(opts) {
|
|
|
33503
33677
|
}
|
|
33504
33678
|
if (myToken !== pumpToken || destroyed) return;
|
|
33505
33679
|
const frames = allFrames;
|
|
33506
|
-
for (
|
|
33680
|
+
for (let i = 0; i < frames.length; i++) {
|
|
33507
33681
|
if (myToken !== pumpToken || destroyed) return;
|
|
33508
|
-
|
|
33509
|
-
f,
|
|
33510
|
-
() => {
|
|
33511
|
-
const ts = syntheticAudioUs;
|
|
33512
|
-
const samples2 = f.nb_samples ?? 1024;
|
|
33513
|
-
const sampleRate = f.sample_rate ?? 44100;
|
|
33514
|
-
syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
|
|
33515
|
-
return ts;
|
|
33516
|
-
},
|
|
33517
|
-
audioTimeBase
|
|
33518
|
-
);
|
|
33682
|
+
const f = frames[i];
|
|
33519
33683
|
const samples = libavFrameToInterleavedFloat32(f);
|
|
33520
33684
|
if (samples) {
|
|
33521
|
-
|
|
33685
|
+
const pts = pktPtsSec[i] ?? null;
|
|
33686
|
+
opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
|
|
33522
33687
|
audioFramesDecoded++;
|
|
33523
33688
|
}
|
|
33524
33689
|
}
|
|
@@ -33628,7 +33793,6 @@ async function startHybridDecoder(opts) {
|
|
|
33628
33793
|
}
|
|
33629
33794
|
await flushBSF();
|
|
33630
33795
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
33631
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
33632
33796
|
pumpRunning = pumpLoop(newToken).catch(
|
|
33633
33797
|
(err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
|
|
33634
33798
|
);
|
|
@@ -33667,7 +33831,6 @@ async function startHybridDecoder(opts) {
|
|
|
33667
33831
|
}
|
|
33668
33832
|
await flushBSF();
|
|
33669
33833
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
33670
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
33671
33834
|
pumpRunning = pumpLoop(newToken).catch(
|
|
33672
33835
|
(err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
|
|
33673
33836
|
);
|
|
@@ -33683,6 +33846,7 @@ async function startHybridDecoder(opts) {
|
|
|
33683
33846
|
videoChunksFed,
|
|
33684
33847
|
audioFramesDecoded,
|
|
33685
33848
|
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
33849
|
+
bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
|
|
33686
33850
|
videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
|
|
33687
33851
|
// Confirmed transport info — see fallback decoder for the pattern.
|
|
33688
33852
|
_transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
|
|
@@ -33921,6 +34085,9 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
33921
34085
|
// src/strategies/fallback/decoder.ts
|
|
33922
34086
|
init_libav_loader();
|
|
33923
34087
|
init_debug();
|
|
34088
|
+
function isDebug3() {
|
|
34089
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
34090
|
+
}
|
|
33924
34091
|
async function startDecoder(opts) {
|
|
33925
34092
|
const variant = "avbridge";
|
|
33926
34093
|
const libav = await loadLibav(variant);
|
|
@@ -33984,6 +34151,7 @@ async function startDecoder(opts) {
|
|
|
33984
34151
|
}
|
|
33985
34152
|
let bsfCtx = null;
|
|
33986
34153
|
let bsfPkt = null;
|
|
34154
|
+
let bsfRequiredButMissing = false;
|
|
33987
34155
|
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
33988
34156
|
try {
|
|
33989
34157
|
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
@@ -33994,13 +34162,19 @@ async function startDecoder(opts) {
|
|
|
33994
34162
|
bsfPkt = await libav.av_packet_alloc();
|
|
33995
34163
|
dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
|
|
33996
34164
|
} else {
|
|
33997
|
-
|
|
34165
|
+
bsfRequiredButMissing = true;
|
|
33998
34166
|
bsfCtx = null;
|
|
33999
34167
|
}
|
|
34000
34168
|
} catch (err) {
|
|
34001
|
-
|
|
34169
|
+
bsfRequiredButMissing = true;
|
|
34002
34170
|
bsfCtx = null;
|
|
34003
34171
|
bsfPkt = null;
|
|
34172
|
+
dbg.warn("bsf", `mpeg4_unpack_bframes BSF init failed: ${err.message}`);
|
|
34173
|
+
}
|
|
34174
|
+
if (bsfRequiredButMissing) {
|
|
34175
|
+
console.error(
|
|
34176
|
+
"[avbridge] MPEG-4 Part 2 (DivX/Xvid) detected but mpeg4_unpack_bframes BSF is unavailable in this libav variant. Files with packed B-frames will play with incorrect frame ordering (backwards PTS jumps, heavy late-drop stuttering). Rebuild the libav variant with the `avbsf` fragment included. See docs/dev/POSTMORTEMS.md for details."
|
|
34177
|
+
);
|
|
34004
34178
|
}
|
|
34005
34179
|
}
|
|
34006
34180
|
async function applyBSF(packets) {
|
|
@@ -34010,7 +34184,6 @@ async function startDecoder(opts) {
|
|
|
34010
34184
|
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
34011
34185
|
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
34012
34186
|
if (sendErr < 0) {
|
|
34013
|
-
out.push(pkt);
|
|
34014
34187
|
continue;
|
|
34015
34188
|
}
|
|
34016
34189
|
while (true) {
|
|
@@ -34024,10 +34197,13 @@ async function startDecoder(opts) {
|
|
|
34024
34197
|
async function flushBSF() {
|
|
34025
34198
|
if (!bsfCtx || !bsfPkt) return;
|
|
34026
34199
|
try {
|
|
34027
|
-
|
|
34028
|
-
|
|
34029
|
-
|
|
34030
|
-
|
|
34200
|
+
if (libav.av_bsf_flush) {
|
|
34201
|
+
await libav.av_bsf_flush(bsfCtx);
|
|
34202
|
+
} else {
|
|
34203
|
+
while (true) {
|
|
34204
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
34205
|
+
if (err < 0) break;
|
|
34206
|
+
}
|
|
34031
34207
|
}
|
|
34032
34208
|
} catch {
|
|
34033
34209
|
}
|
|
@@ -34043,8 +34219,28 @@ async function startDecoder(opts) {
|
|
|
34043
34219
|
let watchdogSlowSinceMs = 0;
|
|
34044
34220
|
let watchdogSlowWarned = false;
|
|
34045
34221
|
let watchdogOverflowWarned = false;
|
|
34046
|
-
let
|
|
34047
|
-
let
|
|
34222
|
+
let lastContentUs = -1;
|
|
34223
|
+
let firstValidPtsLoggedSinceSeek = false;
|
|
34224
|
+
let seenFirstAudioPacketSinceSeek = false;
|
|
34225
|
+
let seekTargetSec = 0;
|
|
34226
|
+
let diagPktsLoggedSinceSeek = 0;
|
|
34227
|
+
let diagFramesLoggedSinceSeek = 0;
|
|
34228
|
+
let diagFrameKeysDumped = false;
|
|
34229
|
+
const DIAG_MAX_PKTS = 100;
|
|
34230
|
+
const DIAG_MAX_FRAMES = 300;
|
|
34231
|
+
let videoDecodeMsTotal = 0;
|
|
34232
|
+
let audioDecodeMsTotal = 0;
|
|
34233
|
+
let videoDecodeBatches = 0;
|
|
34234
|
+
let audioDecodeBatches = 0;
|
|
34235
|
+
let readMsTotal = 0;
|
|
34236
|
+
let readBatches = 0;
|
|
34237
|
+
let pumpThrottleMsTotal = 0;
|
|
34238
|
+
let pumpThrottleEntries = 0;
|
|
34239
|
+
let slowestVideoBatchMs = 0;
|
|
34240
|
+
let newestVideoPtsUs = 0;
|
|
34241
|
+
let lastEmittedPtsUs = -1;
|
|
34242
|
+
let ptsRegressions = 0;
|
|
34243
|
+
let worstPtsRegressionMs = 0;
|
|
34048
34244
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
34049
34245
|
const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
|
|
34050
34246
|
const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
|
|
@@ -34053,9 +34249,12 @@ async function startDecoder(opts) {
|
|
|
34053
34249
|
let readErr;
|
|
34054
34250
|
let packets;
|
|
34055
34251
|
try {
|
|
34252
|
+
const _readStart = performance.now();
|
|
34056
34253
|
[readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
|
|
34057
34254
|
limit: 16 * 1024
|
|
34058
34255
|
});
|
|
34256
|
+
readMsTotal += performance.now() - _readStart;
|
|
34257
|
+
readBatches++;
|
|
34059
34258
|
} catch (err) {
|
|
34060
34259
|
console.error("[avbridge] ff_read_frame_multi failed:", err);
|
|
34061
34260
|
return;
|
|
@@ -34067,6 +34266,18 @@ async function startDecoder(opts) {
|
|
|
34067
34266
|
for (const pkt of videoPackets) {
|
|
34068
34267
|
const sec = packetPtsSec(pkt, videoTimeBase);
|
|
34069
34268
|
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
34269
|
+
if (isDebug3() && diagPktsLoggedSinceSeek < DIAG_MAX_PKTS) {
|
|
34270
|
+
const rawHi = pkt.ptshi ?? 0;
|
|
34271
|
+
const rawLo = pkt.pts ?? 0;
|
|
34272
|
+
const isInvalidPts = rawHi === -2147483648 && rawLo === 0;
|
|
34273
|
+
const rawPts64 = isInvalidPts ? null : rawHi * 4294967296 + rawLo;
|
|
34274
|
+
const rawSec = rawPts64 != null && videoTimeBase ? rawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
|
|
34275
|
+
const pktKeys = diagPktsLoggedSinceSeek === 0 ? `[keys: ${Object.keys(pkt).join(",")}]` : "";
|
|
34276
|
+
console.log(
|
|
34277
|
+
`[DIAG-PKT] vidx=${diagPktsLoggedSinceSeek} pts=${isInvalidPts ? "NOPTS" : rawPts64} pts_sec=${rawSec != null ? rawSec.toFixed(3) : "n/a"} ptshi=${rawHi} ptslo=${rawLo} flags=0x${(pkt.flags ?? 0).toString(16)} keyframe=${(pkt.flags ?? 0) & 1 ? "Y" : "N"} stream=${pkt.stream_index} dataLen=${pkt.data?.length ?? 0} seekTarget=${seekTargetSec.toFixed(3)} ` + pktKeys
|
|
34278
|
+
);
|
|
34279
|
+
diagPktsLoggedSinceSeek++;
|
|
34280
|
+
}
|
|
34070
34281
|
}
|
|
34071
34282
|
}
|
|
34072
34283
|
if (audioPackets && audioTimeBase) {
|
|
@@ -34074,9 +34285,25 @@ async function startDecoder(opts) {
|
|
|
34074
34285
|
const sec = packetPtsSec(pkt, audioTimeBase);
|
|
34075
34286
|
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
34076
34287
|
}
|
|
34288
|
+
if (!seenFirstAudioPacketSinceSeek && audioPackets.length > 0) {
|
|
34289
|
+
const firstSec = packetPtsSec(audioPackets[0], audioTimeBase);
|
|
34290
|
+
if (firstSec != null && Number.isFinite(firstSec)) {
|
|
34291
|
+
seenFirstAudioPacketSinceSeek = true;
|
|
34292
|
+
dbg.info(
|
|
34293
|
+
"av-anchor",
|
|
34294
|
+
`seek-target=${seekTargetSec.toFixed(3)}s, first-audio-pkt-pts=${firstSec.toFixed(3)}s (\u0394=${((firstSec - seekTargetSec) * 1e3).toFixed(1)}ms \u2014 pre-target packets will be skipped by AudioOutput)`
|
|
34295
|
+
);
|
|
34296
|
+
}
|
|
34297
|
+
}
|
|
34077
34298
|
}
|
|
34078
34299
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
34079
|
-
await decodeAudioBatch(
|
|
34300
|
+
await decodeAudioBatch(
|
|
34301
|
+
audioPackets,
|
|
34302
|
+
myToken,
|
|
34303
|
+
/*flush*/
|
|
34304
|
+
false,
|
|
34305
|
+
audioTimeBase
|
|
34306
|
+
);
|
|
34080
34307
|
}
|
|
34081
34308
|
if (myToken !== pumpToken || destroyed) return;
|
|
34082
34309
|
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
@@ -34117,8 +34344,17 @@ async function startDecoder(opts) {
|
|
|
34117
34344
|
}
|
|
34118
34345
|
}
|
|
34119
34346
|
}
|
|
34120
|
-
|
|
34121
|
-
|
|
34347
|
+
{
|
|
34348
|
+
const _throttleStart = performance.now();
|
|
34349
|
+
let _throttled = false;
|
|
34350
|
+
while (!destroyed && myToken === pumpToken && opts.audio.bufferAhead() > 2) {
|
|
34351
|
+
_throttled = true;
|
|
34352
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
34353
|
+
}
|
|
34354
|
+
if (_throttled) {
|
|
34355
|
+
pumpThrottleMsTotal += performance.now() - _throttleStart;
|
|
34356
|
+
pumpThrottleEntries++;
|
|
34357
|
+
}
|
|
34122
34358
|
}
|
|
34123
34359
|
if (readErr === libav.AVERROR_EOF) {
|
|
34124
34360
|
if (videoDec) await decodeVideoBatch(
|
|
@@ -34144,6 +34380,7 @@ async function startDecoder(opts) {
|
|
|
34144
34380
|
async function decodeVideoBatch(pkts, myToken, flush = false) {
|
|
34145
34381
|
if (!videoDec || destroyed || myToken !== pumpToken) return;
|
|
34146
34382
|
let frames;
|
|
34383
|
+
const _t0 = performance.now();
|
|
34147
34384
|
try {
|
|
34148
34385
|
frames = await libav.ff_decode_multi(
|
|
34149
34386
|
videoDec.c,
|
|
@@ -34156,32 +34393,133 @@ async function startDecoder(opts) {
|
|
|
34156
34393
|
console.error("[avbridge] video decode batch failed:", err);
|
|
34157
34394
|
return;
|
|
34158
34395
|
}
|
|
34396
|
+
{
|
|
34397
|
+
const _dt = performance.now() - _t0;
|
|
34398
|
+
videoDecodeMsTotal += _dt;
|
|
34399
|
+
videoDecodeBatches++;
|
|
34400
|
+
if (_dt > slowestVideoBatchMs) slowestVideoBatchMs = _dt;
|
|
34401
|
+
}
|
|
34159
34402
|
if (myToken !== pumpToken || destroyed) return;
|
|
34160
34403
|
for (const f of frames) {
|
|
34161
34404
|
if (myToken !== pumpToken || destroyed) return;
|
|
34162
|
-
|
|
34163
|
-
|
|
34164
|
-
|
|
34165
|
-
|
|
34166
|
-
|
|
34167
|
-
|
|
34168
|
-
|
|
34169
|
-
|
|
34170
|
-
|
|
34405
|
+
const _diagShouldLog = isDebug3() && diagFramesLoggedSinceSeek < DIAG_MAX_FRAMES;
|
|
34406
|
+
const _diagRawHi = f.ptshi ?? 0;
|
|
34407
|
+
const _diagRawLo = f.pts ?? 0;
|
|
34408
|
+
const _diagInvalid = _diagRawHi === -2147483648 && _diagRawLo === 0;
|
|
34409
|
+
const _diagRawPts64 = _diagInvalid ? null : _diagRawHi * 4294967296 + _diagRawLo;
|
|
34410
|
+
const _diagRawSec = _diagRawPts64 != null && videoTimeBase ? _diagRawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
|
|
34411
|
+
if (_diagShouldLog && !diagFrameKeysDumped) {
|
|
34412
|
+
diagFrameKeysDumped = true;
|
|
34413
|
+
const allKeys = Object.keys(f);
|
|
34414
|
+
const fieldDump = {};
|
|
34415
|
+
for (const k of allKeys) {
|
|
34416
|
+
const v = f[k];
|
|
34417
|
+
if (k === "data") continue;
|
|
34418
|
+
if (typeof v === "object" && v !== null && "length" in v) continue;
|
|
34419
|
+
fieldDump[k] = v;
|
|
34420
|
+
}
|
|
34421
|
+
console.log(`[DIAG-FRAME] FIRST FRAME post-seek \u2014 all keys: ${allKeys.join(",")}`);
|
|
34422
|
+
console.log(`[DIAG-FRAME] FIRST FRAME field dump:`, fieldDump);
|
|
34423
|
+
}
|
|
34424
|
+
let rawUs = null;
|
|
34425
|
+
if (!_diagInvalid && _diagRawPts64 != null) {
|
|
34426
|
+
const tb = videoTimeBase ?? [1, 1e6];
|
|
34427
|
+
const us = Math.round(_diagRawPts64 * 1e6 * tb[0] / tb[1]);
|
|
34428
|
+
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
34429
|
+
rawUs = us;
|
|
34430
|
+
}
|
|
34431
|
+
}
|
|
34432
|
+
const _diagLog = (decision, finalPtsUs, sanFallback) => {
|
|
34433
|
+
if (!_diagShouldLog) return;
|
|
34434
|
+
const ptsSrc = sanFallback ? `SYNTHETIC(${_diagInvalid ? "NOPTS" : "invalid-range"})` : "LIBAV";
|
|
34435
|
+
console.log(
|
|
34436
|
+
`[DIAG-FRAME] vidx=${diagFramesLoggedSinceSeek} raw_pts=${_diagInvalid ? "NOPTS" : _diagRawPts64} raw_pts_sec=${_diagRawSec != null ? _diagRawSec.toFixed(3) : "n/a"} pts_src=${ptsSrc} final_pts_us=${finalPtsUs} final_pts_sec=${(finalPtsUs / 1e6).toFixed(3)} seekTarget=${seekTargetSec.toFixed(3)} offset_to_target_ms=${(finalPtsUs / 1e3 - seekTargetSec * 1e3).toFixed(1)} lastEmittedPts_us=${lastEmittedPtsUs} decision=${decision}`
|
|
34437
|
+
);
|
|
34438
|
+
diagFramesLoggedSinceSeek++;
|
|
34439
|
+
};
|
|
34440
|
+
let _diagSanFallbackFired = false;
|
|
34441
|
+
const seekTargetUs = Math.round(seekTargetSec * 1e6);
|
|
34442
|
+
if (lastContentUs < 0) {
|
|
34443
|
+
if (rawUs == null) {
|
|
34444
|
+
const isColdStartKeyframe = seekTargetSec === 0 && f.key_frame === 1;
|
|
34445
|
+
if (isColdStartKeyframe) {
|
|
34446
|
+
lastContentUs = 0;
|
|
34447
|
+
_diagSanFallbackFired = true;
|
|
34448
|
+
} else {
|
|
34449
|
+
_diagLog("PRE-ANCHOR-DROP", 0, true);
|
|
34450
|
+
continue;
|
|
34451
|
+
}
|
|
34452
|
+
} else {
|
|
34453
|
+
lastContentUs = rawUs;
|
|
34454
|
+
if (!firstValidPtsLoggedSinceSeek) {
|
|
34455
|
+
firstValidPtsLoggedSinceSeek = true;
|
|
34456
|
+
if (isDebug3()) {
|
|
34457
|
+
console.log(
|
|
34458
|
+
`[avbridge:decoder] post-seek anchor established: first valid raw pts = ${(rawUs / 1e3).toFixed(1)}ms (seekTarget = ${(seekTargetSec * 1e3).toFixed(1)}ms, \u0394 = ${((rawUs - seekTargetUs) / 1e3).toFixed(1)}ms)`
|
|
34459
|
+
);
|
|
34460
|
+
}
|
|
34461
|
+
if (rawUs >= seekTargetUs) {
|
|
34462
|
+
console.warn(
|
|
34463
|
+
`[avbridge:decoder] first valid raw pts \u2265 seek target \u2014 pre-anchor NOPTS frames may have straddled the target and been mis-discarded. First painted frame may be late by up to one keyframe interval.`
|
|
34464
|
+
);
|
|
34465
|
+
}
|
|
34466
|
+
}
|
|
34467
|
+
}
|
|
34468
|
+
} else {
|
|
34469
|
+
if (rawUs != null) {
|
|
34470
|
+
lastContentUs = rawUs;
|
|
34471
|
+
} else {
|
|
34472
|
+
lastContentUs += videoFrameStepUs;
|
|
34473
|
+
_diagSanFallbackFired = true;
|
|
34474
|
+
}
|
|
34475
|
+
}
|
|
34476
|
+
f.pts = lastContentUs;
|
|
34477
|
+
f.ptshi = lastContentUs < 0 ? -1 : 0;
|
|
34478
|
+
const _fPts = lastContentUs;
|
|
34479
|
+
if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
|
|
34480
|
+
if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
|
|
34481
|
+
_diagLog("REGRESSED-DROP", _fPts, _diagSanFallbackFired);
|
|
34482
|
+
ptsRegressions++;
|
|
34483
|
+
const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
|
|
34484
|
+
if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
|
|
34485
|
+
if (ptsRegressions <= 10) {
|
|
34486
|
+
console.warn(
|
|
34487
|
+
`[avbridge:decoder] dropped out-of-order frame #${ptsRegressions}: pts=${(_fPts / 1e3).toFixed(1)}ms < previous=${(lastEmittedPtsUs / 1e3).toFixed(1)}ms (regression=${regressMs.toFixed(1)}ms). Typically a post-seek B-frame reorder tail.`
|
|
34488
|
+
);
|
|
34489
|
+
}
|
|
34490
|
+
continue;
|
|
34491
|
+
}
|
|
34492
|
+
lastEmittedPtsUs = _fPts;
|
|
34493
|
+
const targetUs = Math.round(seekTargetSec * 1e6);
|
|
34494
|
+
if (_fPts < targetUs - videoFrameStepUs) {
|
|
34495
|
+
_diagLog("PRE-TARGET-DROP", _fPts, _diagSanFallbackFired);
|
|
34496
|
+
continue;
|
|
34497
|
+
}
|
|
34171
34498
|
try {
|
|
34172
34499
|
const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
|
|
34173
|
-
opts.renderer.
|
|
34500
|
+
if (opts.renderer.queueDepth() >= opts.renderer.queueHighWater) {
|
|
34501
|
+
vf.close();
|
|
34502
|
+
_diagLog("OVERFLOW-DROP", _fPts, _diagSanFallbackFired);
|
|
34503
|
+
} else {
|
|
34504
|
+
opts.renderer.enqueue(vf);
|
|
34505
|
+
_diagLog("ENQUEUED", _fPts, _diagSanFallbackFired);
|
|
34506
|
+
}
|
|
34174
34507
|
videoFramesDecoded++;
|
|
34175
34508
|
} catch (err) {
|
|
34176
34509
|
if (videoFramesDecoded === 0) {
|
|
34177
34510
|
console.warn("[avbridge] laFrameToVideoFrame failed:", err);
|
|
34178
34511
|
}
|
|
34512
|
+
_diagLog("BRIDGE-ERROR", _fPts, _diagSanFallbackFired);
|
|
34179
34513
|
}
|
|
34180
34514
|
}
|
|
34181
34515
|
}
|
|
34182
|
-
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
34516
|
+
async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
|
|
34183
34517
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
34518
|
+
const pktPtsSec = pkts.map(
|
|
34519
|
+
(p) => tb ? packetPtsSec(p, tb) : null
|
|
34520
|
+
);
|
|
34184
34521
|
let frames;
|
|
34522
|
+
const _t0 = performance.now();
|
|
34185
34523
|
try {
|
|
34186
34524
|
frames = await libav.ff_decode_multi(
|
|
34187
34525
|
audioDec.c,
|
|
@@ -34194,23 +34532,20 @@ async function startDecoder(opts) {
|
|
|
34194
34532
|
console.error("[avbridge] audio decode batch failed:", err);
|
|
34195
34533
|
return;
|
|
34196
34534
|
}
|
|
34535
|
+
audioDecodeMsTotal += performance.now() - _t0;
|
|
34536
|
+
audioDecodeBatches++;
|
|
34197
34537
|
if (myToken !== pumpToken || destroyed) return;
|
|
34198
|
-
for (
|
|
34538
|
+
for (let i = 0; i < frames.length; i++) {
|
|
34199
34539
|
if (myToken !== pumpToken || destroyed) return;
|
|
34200
|
-
|
|
34201
|
-
f,
|
|
34202
|
-
() => {
|
|
34203
|
-
const ts = syntheticAudioUs;
|
|
34204
|
-
const samples2 = f.nb_samples ?? 1024;
|
|
34205
|
-
const sampleRate = f.sample_rate ?? 44100;
|
|
34206
|
-
syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
|
|
34207
|
-
return ts;
|
|
34208
|
-
},
|
|
34209
|
-
audioTimeBase
|
|
34210
|
-
);
|
|
34540
|
+
const f = frames[i];
|
|
34211
34541
|
const samples = libavFrameToInterleavedFloat32(f);
|
|
34212
34542
|
if (samples) {
|
|
34213
|
-
|
|
34543
|
+
const pts = pktPtsSec[i] ?? null;
|
|
34544
|
+
if (isDebug3()) {
|
|
34545
|
+
const dur = samples.data.length / samples.channels / samples.sampleRate;
|
|
34546
|
+
console.log(`[TRACE-DEC] audio frame #${audioFramesDecoded} pts=${pts != null ? pts.toFixed(4) : "NULL"} dur=${dur.toFixed(4)} samples=${samples.data.length / samples.channels} sr=${samples.sampleRate} ch=${samples.channels} pktsIn=${pkts.length} framesOut=${frames.length}`);
|
|
34547
|
+
}
|
|
34548
|
+
opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
|
|
34214
34549
|
audioFramesDecoded++;
|
|
34215
34550
|
}
|
|
34216
34551
|
}
|
|
@@ -34313,13 +34648,17 @@ async function startDecoder(opts) {
|
|
|
34313
34648
|
} catch {
|
|
34314
34649
|
}
|
|
34315
34650
|
await flushBSF();
|
|
34316
|
-
|
|
34317
|
-
|
|
34651
|
+
lastContentUs = -1;
|
|
34652
|
+
lastEmittedPtsUs = -1;
|
|
34653
|
+
firstValidPtsLoggedSinceSeek = false;
|
|
34318
34654
|
pumpRunning = pumpLoop(newToken).catch(
|
|
34319
34655
|
(err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
|
|
34320
34656
|
);
|
|
34321
34657
|
},
|
|
34322
34658
|
async seek(timeSec) {
|
|
34659
|
+
if (isDebug3()) {
|
|
34660
|
+
console.log(`[SEEK] target=${timeSec.toFixed(3)}s (${(timeSec * 1e3).toFixed(0)}ms) wall=${performance.now().toFixed(0)}`);
|
|
34661
|
+
}
|
|
34323
34662
|
const newToken = ++pumpToken;
|
|
34324
34663
|
if (pumpRunning) {
|
|
34325
34664
|
try {
|
|
@@ -34350,8 +34689,14 @@ async function startDecoder(opts) {
|
|
|
34350
34689
|
} catch {
|
|
34351
34690
|
}
|
|
34352
34691
|
await flushBSF();
|
|
34353
|
-
|
|
34354
|
-
|
|
34692
|
+
lastContentUs = -1;
|
|
34693
|
+
lastEmittedPtsUs = -1;
|
|
34694
|
+
firstValidPtsLoggedSinceSeek = false;
|
|
34695
|
+
seenFirstAudioPacketSinceSeek = false;
|
|
34696
|
+
seekTargetSec = timeSec;
|
|
34697
|
+
diagPktsLoggedSinceSeek = 0;
|
|
34698
|
+
diagFramesLoggedSinceSeek = 0;
|
|
34699
|
+
diagFrameKeysDumped = false;
|
|
34355
34700
|
pumpRunning = pumpLoop(newToken).catch(
|
|
34356
34701
|
(err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
|
|
34357
34702
|
);
|
|
@@ -34365,7 +34710,24 @@ async function startDecoder(opts) {
|
|
|
34365
34710
|
packetsRead,
|
|
34366
34711
|
videoFramesDecoded,
|
|
34367
34712
|
audioFramesDecoded,
|
|
34713
|
+
// Throughput instrumentation — the stats panel turns these into
|
|
34714
|
+
// "decode fps actual / realtime target" and shows slowest batch
|
|
34715
|
+
// + producer throttle share.
|
|
34716
|
+
videoDecodeMsTotal,
|
|
34717
|
+
videoDecodeBatches,
|
|
34718
|
+
audioDecodeMsTotal,
|
|
34719
|
+
audioDecodeBatches,
|
|
34720
|
+
readMsTotal,
|
|
34721
|
+
readBatches,
|
|
34722
|
+
pumpThrottleMsTotal,
|
|
34723
|
+
pumpThrottleEntries,
|
|
34724
|
+
slowestVideoBatchMs,
|
|
34725
|
+
newestVideoPtsMs: Math.round(newestVideoPtsUs / 1e3),
|
|
34726
|
+
ptsRegressions,
|
|
34727
|
+
worstPtsRegressionMs,
|
|
34728
|
+
sourceFps: videoFps,
|
|
34368
34729
|
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
34730
|
+
bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
|
|
34369
34731
|
// Confirmed transport info: once prepareLibavInput returns
|
|
34370
34732
|
// successfully, we *know* whether the source is http-range (probe
|
|
34371
34733
|
// succeeded and returned 206) or in-memory blob. Diagnostics hoists
|