avbridge 2.12.1 → 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 +101 -0
- package/README.md +33 -0
- package/dist/{chunk-UM6WCSGL.cjs → chunk-OFJYEITB.cjs} +356 -91
- package/dist/chunk-OFJYEITB.cjs.map +1 -0
- package/dist/{chunk-BN7BRTLY.js → chunk-VOC24LYF.js} +357 -92
- package/dist/chunk-VOC24LYF.js.map +1 -0
- package/dist/element-browser.js +354 -111
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +2 -2
- package/dist/element.js +1 -1
- package/dist/index.cjs +8 -8
- package/dist/index.js +1 -1
- package/dist/player.cjs +457 -135
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +35 -4
- package/dist/player.d.ts +35 -4
- package/dist/player.js +457 -135
- package/dist/player.js.map +1 -1
- package/package.json +1 -1
- package/src/element/avbridge-player.ts +136 -28
- package/src/strategies/fallback/audio-output.ts +164 -35
- package/src/strategies/fallback/decoder.ts +336 -58
- package/src/strategies/fallback/video-renderer.ts +176 -34
- package/src/strategies/hybrid/decoder.ts +22 -19
- package/src/strategies/remux/pipeline.ts +12 -3
- package/dist/chunk-BN7BRTLY.js.map +0 -1
- package/dist/chunk-UM6WCSGL.cjs.map +0 -1
package/dist/element-browser.js
CHANGED
|
@@ -32255,22 +32255,25 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
32255
32255
|
}
|
|
32256
32256
|
}
|
|
32257
32257
|
let mimePromise = null;
|
|
32258
|
+
const myToken = pumpToken;
|
|
32258
32259
|
const writable = new WritableStream({
|
|
32259
32260
|
write: async (chunk) => {
|
|
32260
|
-
if (destroyed) return;
|
|
32261
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
32261
32262
|
if (!sink) {
|
|
32262
32263
|
const mime = await (mimePromise ??= output.getMimeType());
|
|
32264
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
32263
32265
|
sink = new MseSink({ mime, video });
|
|
32264
32266
|
await sink.ready();
|
|
32267
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
32265
32268
|
if (pendingStartTime > 0) {
|
|
32266
32269
|
sink.invalidate(pendingStartTime);
|
|
32267
32270
|
}
|
|
32268
32271
|
sink.setPlayOnSeek(pendingAutoPlay);
|
|
32269
32272
|
}
|
|
32270
|
-
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)) {
|
|
32271
32274
|
await new Promise((r) => setTimeout(r, 500));
|
|
32272
32275
|
}
|
|
32273
|
-
if (destroyed) return;
|
|
32276
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
32274
32277
|
sink.append(chunk.data);
|
|
32275
32278
|
stats.bytesWritten += chunk.data.byteLength;
|
|
32276
32279
|
stats.fragments++;
|
|
@@ -32538,7 +32541,21 @@ var VideoRenderer = class {
|
|
|
32538
32541
|
framesPainted = 0;
|
|
32539
32542
|
framesDroppedLate = 0;
|
|
32540
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. */
|
|
32541
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;
|
|
32542
32559
|
/** Wall-clock time of the last paint, in ms (performance.now()). */
|
|
32543
32560
|
lastPaintWall = 0;
|
|
32544
32561
|
/** Minimum ms between paints — paces video at roughly source fps. */
|
|
@@ -32588,13 +32605,17 @@ var VideoRenderer = class {
|
|
|
32588
32605
|
return this.queue.length;
|
|
32589
32606
|
}
|
|
32590
32607
|
/**
|
|
32591
|
-
*
|
|
32592
|
-
*
|
|
32593
|
-
*
|
|
32594
|
-
*
|
|
32595
|
-
*
|
|
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.
|
|
32596
32617
|
*/
|
|
32597
|
-
queueHighWater =
|
|
32618
|
+
queueHighWater = 256;
|
|
32598
32619
|
enqueue(frame) {
|
|
32599
32620
|
if (this.destroyed) {
|
|
32600
32621
|
frame.close();
|
|
@@ -32605,7 +32626,7 @@ var VideoRenderer = class {
|
|
|
32605
32626
|
if (this.queue.length === 1 && this.framesPainted === 0) {
|
|
32606
32627
|
this.resolveFirstFrame();
|
|
32607
32628
|
}
|
|
32608
|
-
while (this.queue.length >
|
|
32629
|
+
while (this.queue.length > this.queueHighWater + 8) {
|
|
32609
32630
|
this.queue.shift()?.close();
|
|
32610
32631
|
this.framesDroppedOverflow++;
|
|
32611
32632
|
}
|
|
@@ -32687,12 +32708,9 @@ var VideoRenderer = class {
|
|
|
32687
32708
|
if (this.queue.length === 0) return;
|
|
32688
32709
|
const playing = this.clock.isPlaying();
|
|
32689
32710
|
if (!playing) {
|
|
32690
|
-
if (!this.prerolled) {
|
|
32691
|
-
const head = this.queue.shift();
|
|
32692
|
-
this.paint(head);
|
|
32693
|
-
head.close();
|
|
32711
|
+
if (!this.prerolled && this.queue.length > 0) {
|
|
32694
32712
|
this.prerolled = true;
|
|
32695
|
-
this.
|
|
32713
|
+
this.paint(this.queue[0]);
|
|
32696
32714
|
}
|
|
32697
32715
|
return;
|
|
32698
32716
|
}
|
|
@@ -32701,14 +32719,29 @@ var VideoRenderer = class {
|
|
|
32701
32719
|
const hasPts = headTs > 0 || this.queue.length > 1;
|
|
32702
32720
|
if (hasPts) {
|
|
32703
32721
|
const wallNow2 = performance.now();
|
|
32704
|
-
if (!this.ptsCalibrated
|
|
32705
|
-
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;
|
|
32706
32726
|
this.ptsCalibrated = true;
|
|
32707
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
|
+
}
|
|
32708
32742
|
}
|
|
32709
32743
|
const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
|
|
32710
|
-
const
|
|
32711
|
-
const deadlineUs = audioNowUs + frameDurationUs;
|
|
32744
|
+
const deadlineUs = audioNowUs;
|
|
32712
32745
|
let bestIdx = -1;
|
|
32713
32746
|
for (let i = 0; i < this.queue.length; i++) {
|
|
32714
32747
|
const ts = this.queue[i].timestamp ?? 0;
|
|
@@ -32736,19 +32769,20 @@ var VideoRenderer = class {
|
|
|
32736
32769
|
return;
|
|
32737
32770
|
}
|
|
32738
32771
|
const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
|
|
32739
|
-
const dropThresholdUs = _relaxDrop ? audioNowUs - 60 * 1e6 : audioNowUs - frameDurationUs * 2;
|
|
32740
32772
|
let dropped = 0;
|
|
32741
|
-
|
|
32742
|
-
|
|
32743
|
-
|
|
32773
|
+
const initialBestIdx = bestIdx;
|
|
32774
|
+
if (!_relaxDrop) {
|
|
32775
|
+
while (bestIdx > 0) {
|
|
32744
32776
|
this.queue.shift()?.close();
|
|
32745
32777
|
this.framesDroppedLate++;
|
|
32746
32778
|
bestIdx--;
|
|
32747
32779
|
dropped++;
|
|
32748
|
-
} else {
|
|
32749
|
-
break;
|
|
32750
32780
|
}
|
|
32751
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
|
+
}
|
|
32752
32786
|
this.ticksPainted++;
|
|
32753
32787
|
if (isDebug()) {
|
|
32754
32788
|
const now = performance.now();
|
|
@@ -32784,6 +32818,33 @@ var VideoRenderer = class {
|
|
|
32784
32818
|
}
|
|
32785
32819
|
try {
|
|
32786
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;
|
|
32787
32848
|
this.framesPainted++;
|
|
32788
32849
|
} catch (err) {
|
|
32789
32850
|
if (this.framesPainted === 0 && this.framesDroppedLate === 0) {
|
|
@@ -32796,6 +32857,7 @@ var VideoRenderer = class {
|
|
|
32796
32857
|
const count = this.queue.length;
|
|
32797
32858
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
32798
32859
|
this.prerolled = false;
|
|
32860
|
+
this.hasLastPaintedPts = false;
|
|
32799
32861
|
this.ptsCalibrated = false;
|
|
32800
32862
|
this.hasEverEnqueuedSinceFlush = false;
|
|
32801
32863
|
if (isDebug() && count > 0) {
|
|
@@ -32836,6 +32898,9 @@ var VideoRenderer = class {
|
|
|
32836
32898
|
};
|
|
32837
32899
|
|
|
32838
32900
|
// src/strategies/fallback/audio-output.ts
|
|
32901
|
+
function isDebug2() {
|
|
32902
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
32903
|
+
}
|
|
32839
32904
|
var AudioOutput = class {
|
|
32840
32905
|
ctx;
|
|
32841
32906
|
gain;
|
|
@@ -32857,6 +32922,16 @@ var AudioOutput = class {
|
|
|
32857
32922
|
mediaTimeOfNext = 0;
|
|
32858
32923
|
/** Anchor: media time `mediaTimeOfAnchor` corresponds to ctx time `ctxTimeAtAnchor`. */
|
|
32859
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;
|
|
32860
32935
|
ctxTimeAtAnchor = 0;
|
|
32861
32936
|
pendingQueue = [];
|
|
32862
32937
|
framesScheduled = 0;
|
|
@@ -32929,10 +33004,16 @@ var AudioOutput = class {
|
|
|
32929
33004
|
return this.mediaTimeOfAnchor;
|
|
32930
33005
|
}
|
|
32931
33006
|
if (this.state === "playing") {
|
|
33007
|
+
if (this.firstAudibleCtxStart < 0) {
|
|
33008
|
+
return this.mediaTimeOfAnchor;
|
|
33009
|
+
}
|
|
32932
33010
|
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
|
|
32933
33011
|
}
|
|
32934
33012
|
return this.mediaTimeOfAnchor;
|
|
32935
33013
|
}
|
|
33014
|
+
anchorTime() {
|
|
33015
|
+
return this.mediaTimeOfAnchor;
|
|
33016
|
+
}
|
|
32936
33017
|
isPlaying() {
|
|
32937
33018
|
return this.state === "playing";
|
|
32938
33019
|
}
|
|
@@ -32959,18 +33040,81 @@ var AudioOutput = class {
|
|
|
32959
33040
|
* Schedule a chunk of decoded samples. Queues internally while idle (cold
|
|
32960
33041
|
* start or post-seek), schedules directly to the audio graph while playing.
|
|
32961
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.
|
|
32962
33053
|
*/
|
|
32963
|
-
schedule(samples, channels, sampleRate) {
|
|
33054
|
+
schedule(samples, channels, sampleRate, ptsSec) {
|
|
32964
33055
|
if (this.destroyed || this.noAudio) return;
|
|
32965
33056
|
const frameCount = samples.length / channels;
|
|
32966
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
|
+
}
|
|
32967
33062
|
if (this.state === "idle" || this.state === "paused") {
|
|
32968
|
-
this.pendingQueue.push({
|
|
33063
|
+
this.pendingQueue.push({
|
|
33064
|
+
samples,
|
|
33065
|
+
channels,
|
|
33066
|
+
sampleRate,
|
|
33067
|
+
frameCount,
|
|
33068
|
+
durationSec,
|
|
33069
|
+
ptsSec: hasPts ? ptsSec : null
|
|
33070
|
+
});
|
|
32969
33071
|
return;
|
|
32970
33072
|
}
|
|
32971
|
-
this.scheduleNow(
|
|
33073
|
+
this.scheduleNow(
|
|
33074
|
+
samples,
|
|
33075
|
+
channels,
|
|
33076
|
+
sampleRate,
|
|
33077
|
+
frameCount,
|
|
33078
|
+
hasPts ? ptsSec : null
|
|
33079
|
+
);
|
|
32972
33080
|
}
|
|
32973
|
-
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
|
+
}
|
|
32974
33118
|
const buffer = this.ctx.createBuffer(channels, frameCount, sampleRate);
|
|
32975
33119
|
for (let ch = 0; ch < channels; ch++) {
|
|
32976
33120
|
const channelData = buffer.getChannelData(ch);
|
|
@@ -32982,14 +33126,7 @@ var AudioOutput = class {
|
|
|
32982
33126
|
node3.buffer = buffer;
|
|
32983
33127
|
node3.connect(this.gain);
|
|
32984
33128
|
if (this._rate !== 1) node3.playbackRate.value = this._rate;
|
|
32985
|
-
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
32986
|
-
if (ctxStart < this.ctx.currentTime) {
|
|
32987
|
-
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
32988
|
-
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
32989
|
-
ctxStart = this.ctx.currentTime;
|
|
32990
|
-
}
|
|
32991
33129
|
node3.start(ctxStart);
|
|
32992
|
-
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
32993
33130
|
this.framesScheduled++;
|
|
32994
33131
|
}
|
|
32995
33132
|
// ── Lifecycle ─────────────────────────────────────────────────────────
|
|
@@ -33014,12 +33151,15 @@ var AudioOutput = class {
|
|
|
33014
33151
|
} catch {
|
|
33015
33152
|
}
|
|
33016
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
|
+
}
|
|
33017
33157
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
33018
33158
|
this.state = "playing";
|
|
33019
33159
|
const drain2 = this.pendingQueue;
|
|
33020
33160
|
this.pendingQueue = [];
|
|
33021
33161
|
for (const c of drain2) {
|
|
33022
|
-
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
33162
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
|
|
33023
33163
|
}
|
|
33024
33164
|
return;
|
|
33025
33165
|
}
|
|
@@ -33027,10 +33167,13 @@ var AudioOutput = class {
|
|
|
33027
33167
|
this.ctxTimeAtAnchor = this.ctx.currentTime + STARTUP_DELAY;
|
|
33028
33168
|
this.mediaTimeOfNext = this.mediaTimeOfAnchor;
|
|
33029
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
|
+
}
|
|
33030
33173
|
const drain = this.pendingQueue;
|
|
33031
33174
|
this.pendingQueue = [];
|
|
33032
33175
|
for (const c of drain) {
|
|
33033
|
-
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
33176
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
|
|
33034
33177
|
}
|
|
33035
33178
|
}
|
|
33036
33179
|
/** Pause playback. Suspends the audio context. */
|
|
@@ -33056,6 +33199,9 @@ var AudioOutput = class {
|
|
|
33056
33199
|
* supplying new samples) and then call `start()` to resume playback.
|
|
33057
33200
|
*/
|
|
33058
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
|
+
}
|
|
33059
33205
|
if (this.noAudio) {
|
|
33060
33206
|
this.pendingQueue = [];
|
|
33061
33207
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
@@ -33074,6 +33220,7 @@ var AudioOutput = class {
|
|
|
33074
33220
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
33075
33221
|
this.mediaTimeOfNext = newMediaTime;
|
|
33076
33222
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
33223
|
+
this.firstAudibleCtxStart = -1;
|
|
33077
33224
|
this.state = "idle";
|
|
33078
33225
|
if (this.ctx.state === "running") {
|
|
33079
33226
|
await this.ctx.suspend();
|
|
@@ -33257,28 +33404,6 @@ function asUint8(x) {
|
|
|
33257
33404
|
const ta = x;
|
|
33258
33405
|
return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
|
|
33259
33406
|
}
|
|
33260
|
-
function sanitizeFrameTimestamp(frame, nextUs, fallbackTimeBase) {
|
|
33261
|
-
const lo = frame.pts ?? 0;
|
|
33262
|
-
const hi = frame.ptshi ?? 0;
|
|
33263
|
-
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
33264
|
-
if (isInvalid) {
|
|
33265
|
-
const us2 = nextUs();
|
|
33266
|
-
frame.pts = us2;
|
|
33267
|
-
frame.ptshi = 0;
|
|
33268
|
-
return;
|
|
33269
|
-
}
|
|
33270
|
-
const tb = fallbackTimeBase ?? [1, 1e6];
|
|
33271
|
-
const pts64 = hi * 4294967296 + lo;
|
|
33272
|
-
const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
|
|
33273
|
-
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
33274
|
-
frame.pts = us;
|
|
33275
|
-
frame.ptshi = us < 0 ? -1 : 0;
|
|
33276
|
-
return;
|
|
33277
|
-
}
|
|
33278
|
-
const fallback = nextUs();
|
|
33279
|
-
frame.pts = fallback;
|
|
33280
|
-
frame.ptshi = 0;
|
|
33281
|
-
}
|
|
33282
33407
|
|
|
33283
33408
|
// src/strategies/hybrid/decoder.ts
|
|
33284
33409
|
async function startHybridDecoder(opts) {
|
|
@@ -33426,7 +33551,6 @@ async function startHybridDecoder(opts) {
|
|
|
33426
33551
|
let videoChunksFed = 0;
|
|
33427
33552
|
let bufferedUntilSec = 0;
|
|
33428
33553
|
let syntheticVideoUs = 0;
|
|
33429
|
-
let syntheticAudioUs = 0;
|
|
33430
33554
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
33431
33555
|
const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
|
|
33432
33556
|
const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
|
|
@@ -33458,7 +33582,13 @@ async function startHybridDecoder(opts) {
|
|
|
33458
33582
|
}
|
|
33459
33583
|
}
|
|
33460
33584
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
33461
|
-
await decodeAudioBatch(
|
|
33585
|
+
await decodeAudioBatch(
|
|
33586
|
+
audioPackets,
|
|
33587
|
+
myToken,
|
|
33588
|
+
/*flush*/
|
|
33589
|
+
false,
|
|
33590
|
+
audioTimeBase
|
|
33591
|
+
);
|
|
33462
33592
|
}
|
|
33463
33593
|
if (myToken !== pumpToken || destroyed) return;
|
|
33464
33594
|
await new Promise((r) => setTimeout(r, 0));
|
|
@@ -33505,8 +33635,11 @@ async function startHybridDecoder(opts) {
|
|
|
33505
33635
|
}
|
|
33506
33636
|
}
|
|
33507
33637
|
}
|
|
33508
|
-
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
33638
|
+
async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
|
|
33509
33639
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
33640
|
+
const pktPtsSec = pkts.map(
|
|
33641
|
+
(p) => tb ? packetPtsSec(p, tb) : null
|
|
33642
|
+
);
|
|
33510
33643
|
const AUDIO_SUB_BATCH = 4;
|
|
33511
33644
|
let allFrames = [];
|
|
33512
33645
|
for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
|
|
@@ -33544,22 +33677,13 @@ async function startHybridDecoder(opts) {
|
|
|
33544
33677
|
}
|
|
33545
33678
|
if (myToken !== pumpToken || destroyed) return;
|
|
33546
33679
|
const frames = allFrames;
|
|
33547
|
-
for (
|
|
33680
|
+
for (let i = 0; i < frames.length; i++) {
|
|
33548
33681
|
if (myToken !== pumpToken || destroyed) return;
|
|
33549
|
-
|
|
33550
|
-
f,
|
|
33551
|
-
() => {
|
|
33552
|
-
const ts = syntheticAudioUs;
|
|
33553
|
-
const samples2 = f.nb_samples ?? 1024;
|
|
33554
|
-
const sampleRate = f.sample_rate ?? 44100;
|
|
33555
|
-
syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
|
|
33556
|
-
return ts;
|
|
33557
|
-
},
|
|
33558
|
-
audioTimeBase
|
|
33559
|
-
);
|
|
33682
|
+
const f = frames[i];
|
|
33560
33683
|
const samples = libavFrameToInterleavedFloat32(f);
|
|
33561
33684
|
if (samples) {
|
|
33562
|
-
|
|
33685
|
+
const pts = pktPtsSec[i] ?? null;
|
|
33686
|
+
opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
|
|
33563
33687
|
audioFramesDecoded++;
|
|
33564
33688
|
}
|
|
33565
33689
|
}
|
|
@@ -33669,7 +33793,6 @@ async function startHybridDecoder(opts) {
|
|
|
33669
33793
|
}
|
|
33670
33794
|
await flushBSF();
|
|
33671
33795
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
33672
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
33673
33796
|
pumpRunning = pumpLoop(newToken).catch(
|
|
33674
33797
|
(err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
|
|
33675
33798
|
);
|
|
@@ -33708,7 +33831,6 @@ async function startHybridDecoder(opts) {
|
|
|
33708
33831
|
}
|
|
33709
33832
|
await flushBSF();
|
|
33710
33833
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
33711
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
33712
33834
|
pumpRunning = pumpLoop(newToken).catch(
|
|
33713
33835
|
(err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
|
|
33714
33836
|
);
|
|
@@ -33963,6 +34085,9 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
33963
34085
|
// src/strategies/fallback/decoder.ts
|
|
33964
34086
|
init_libav_loader();
|
|
33965
34087
|
init_debug();
|
|
34088
|
+
function isDebug3() {
|
|
34089
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
34090
|
+
}
|
|
33966
34091
|
async function startDecoder(opts) {
|
|
33967
34092
|
const variant = "avbridge";
|
|
33968
34093
|
const libav = await loadLibav(variant);
|
|
@@ -34094,8 +34219,15 @@ async function startDecoder(opts) {
|
|
|
34094
34219
|
let watchdogSlowSinceMs = 0;
|
|
34095
34220
|
let watchdogSlowWarned = false;
|
|
34096
34221
|
let watchdogOverflowWarned = false;
|
|
34097
|
-
let
|
|
34098
|
-
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;
|
|
34099
34231
|
let videoDecodeMsTotal = 0;
|
|
34100
34232
|
let audioDecodeMsTotal = 0;
|
|
34101
34233
|
let videoDecodeBatches = 0;
|
|
@@ -34134,6 +34266,18 @@ async function startDecoder(opts) {
|
|
|
34134
34266
|
for (const pkt of videoPackets) {
|
|
34135
34267
|
const sec = packetPtsSec(pkt, videoTimeBase);
|
|
34136
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
|
+
}
|
|
34137
34281
|
}
|
|
34138
34282
|
}
|
|
34139
34283
|
if (audioPackets && audioTimeBase) {
|
|
@@ -34141,9 +34285,25 @@ async function startDecoder(opts) {
|
|
|
34141
34285
|
const sec = packetPtsSec(pkt, audioTimeBase);
|
|
34142
34286
|
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
34143
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
|
+
}
|
|
34144
34298
|
}
|
|
34145
34299
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
34146
|
-
await decodeAudioBatch(
|
|
34300
|
+
await decodeAudioBatch(
|
|
34301
|
+
audioPackets,
|
|
34302
|
+
myToken,
|
|
34303
|
+
/*flush*/
|
|
34304
|
+
false,
|
|
34305
|
+
audioTimeBase
|
|
34306
|
+
);
|
|
34147
34307
|
}
|
|
34148
34308
|
if (myToken !== pumpToken || destroyed) return;
|
|
34149
34309
|
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
@@ -34187,7 +34347,7 @@ async function startDecoder(opts) {
|
|
|
34187
34347
|
{
|
|
34188
34348
|
const _throttleStart = performance.now();
|
|
34189
34349
|
let _throttled = false;
|
|
34190
|
-
while (!destroyed && myToken === pumpToken &&
|
|
34350
|
+
while (!destroyed && myToken === pumpToken && opts.audio.bufferAhead() > 2) {
|
|
34191
34351
|
_throttled = true;
|
|
34192
34352
|
await new Promise((r) => setTimeout(r, 50));
|
|
34193
34353
|
}
|
|
@@ -34242,18 +34402,83 @@ async function startDecoder(opts) {
|
|
|
34242
34402
|
if (myToken !== pumpToken || destroyed) return;
|
|
34243
34403
|
for (const f of frames) {
|
|
34244
34404
|
if (myToken !== pumpToken || destroyed) return;
|
|
34245
|
-
|
|
34246
|
-
|
|
34247
|
-
|
|
34248
|
-
|
|
34249
|
-
|
|
34250
|
-
|
|
34251
|
-
|
|
34252
|
-
|
|
34253
|
-
|
|
34254
|
-
|
|
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;
|
|
34255
34479
|
if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
|
|
34256
34480
|
if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
|
|
34481
|
+
_diagLog("REGRESSED-DROP", _fPts, _diagSanFallbackFired);
|
|
34257
34482
|
ptsRegressions++;
|
|
34258
34483
|
const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
|
|
34259
34484
|
if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
|
|
@@ -34265,19 +34490,34 @@ async function startDecoder(opts) {
|
|
|
34265
34490
|
continue;
|
|
34266
34491
|
}
|
|
34267
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
|
+
}
|
|
34268
34498
|
try {
|
|
34269
34499
|
const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
|
|
34270
|
-
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
|
+
}
|
|
34271
34507
|
videoFramesDecoded++;
|
|
34272
34508
|
} catch (err) {
|
|
34273
34509
|
if (videoFramesDecoded === 0) {
|
|
34274
34510
|
console.warn("[avbridge] laFrameToVideoFrame failed:", err);
|
|
34275
34511
|
}
|
|
34512
|
+
_diagLog("BRIDGE-ERROR", _fPts, _diagSanFallbackFired);
|
|
34276
34513
|
}
|
|
34277
34514
|
}
|
|
34278
34515
|
}
|
|
34279
|
-
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
34516
|
+
async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
|
|
34280
34517
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
34518
|
+
const pktPtsSec = pkts.map(
|
|
34519
|
+
(p) => tb ? packetPtsSec(p, tb) : null
|
|
34520
|
+
);
|
|
34281
34521
|
let frames;
|
|
34282
34522
|
const _t0 = performance.now();
|
|
34283
34523
|
try {
|
|
@@ -34295,22 +34535,17 @@ async function startDecoder(opts) {
|
|
|
34295
34535
|
audioDecodeMsTotal += performance.now() - _t0;
|
|
34296
34536
|
audioDecodeBatches++;
|
|
34297
34537
|
if (myToken !== pumpToken || destroyed) return;
|
|
34298
|
-
for (
|
|
34538
|
+
for (let i = 0; i < frames.length; i++) {
|
|
34299
34539
|
if (myToken !== pumpToken || destroyed) return;
|
|
34300
|
-
|
|
34301
|
-
f,
|
|
34302
|
-
() => {
|
|
34303
|
-
const ts = syntheticAudioUs;
|
|
34304
|
-
const samples2 = f.nb_samples ?? 1024;
|
|
34305
|
-
const sampleRate = f.sample_rate ?? 44100;
|
|
34306
|
-
syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
|
|
34307
|
-
return ts;
|
|
34308
|
-
},
|
|
34309
|
-
audioTimeBase
|
|
34310
|
-
);
|
|
34540
|
+
const f = frames[i];
|
|
34311
34541
|
const samples = libavFrameToInterleavedFloat32(f);
|
|
34312
34542
|
if (samples) {
|
|
34313
|
-
|
|
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);
|
|
34314
34549
|
audioFramesDecoded++;
|
|
34315
34550
|
}
|
|
34316
34551
|
}
|
|
@@ -34413,14 +34648,17 @@ async function startDecoder(opts) {
|
|
|
34413
34648
|
} catch {
|
|
34414
34649
|
}
|
|
34415
34650
|
await flushBSF();
|
|
34416
|
-
|
|
34417
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
34651
|
+
lastContentUs = -1;
|
|
34418
34652
|
lastEmittedPtsUs = -1;
|
|
34653
|
+
firstValidPtsLoggedSinceSeek = false;
|
|
34419
34654
|
pumpRunning = pumpLoop(newToken).catch(
|
|
34420
34655
|
(err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
|
|
34421
34656
|
);
|
|
34422
34657
|
},
|
|
34423
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
|
+
}
|
|
34424
34662
|
const newToken = ++pumpToken;
|
|
34425
34663
|
if (pumpRunning) {
|
|
34426
34664
|
try {
|
|
@@ -34451,9 +34689,14 @@ async function startDecoder(opts) {
|
|
|
34451
34689
|
} catch {
|
|
34452
34690
|
}
|
|
34453
34691
|
await flushBSF();
|
|
34454
|
-
|
|
34455
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
34692
|
+
lastContentUs = -1;
|
|
34456
34693
|
lastEmittedPtsUs = -1;
|
|
34694
|
+
firstValidPtsLoggedSinceSeek = false;
|
|
34695
|
+
seenFirstAudioPacketSinceSeek = false;
|
|
34696
|
+
seekTargetSec = timeSec;
|
|
34697
|
+
diagPktsLoggedSinceSeek = 0;
|
|
34698
|
+
diagFramesLoggedSinceSeek = 0;
|
|
34699
|
+
diagFrameKeysDumped = false;
|
|
34457
34700
|
pumpRunning = pumpLoop(newToken).catch(
|
|
34458
34701
|
(err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
|
|
34459
34702
|
);
|