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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chunkWRKO6Q42_cjs = require('./chunk-WRKO6Q42.cjs');
|
|
4
|
-
var
|
|
4
|
+
var chunkVLI3Y6IJ_cjs = require('./chunk-VLI3Y6IJ.cjs');
|
|
5
5
|
var chunk2IJ66NTD_cjs = require('./chunk-2IJ66NTD.cjs');
|
|
6
6
|
var chunkHZUVMXBN_cjs = require('./chunk-HZUVMXBN.cjs');
|
|
7
7
|
var chunkG4APZMCP_cjs = require('./chunk-G4APZMCP.cjs');
|
|
@@ -734,12 +734,12 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
734
734
|
const mb = await import('mediabunny');
|
|
735
735
|
const videoTrackInfo = ctx.videoTracks[0];
|
|
736
736
|
if (!videoTrackInfo) throw new Error("remux: source has no video track");
|
|
737
|
-
const mbVideoCodec =
|
|
737
|
+
const mbVideoCodec = chunkVLI3Y6IJ_cjs.avbridgeVideoToMediabunny(videoTrackInfo.codec);
|
|
738
738
|
if (!mbVideoCodec) {
|
|
739
739
|
throw new Error(`remux: video codec "${videoTrackInfo.codec}" is not supported by mediabunny output`);
|
|
740
740
|
}
|
|
741
741
|
const input = new mb.Input({
|
|
742
|
-
source: await
|
|
742
|
+
source: await chunkVLI3Y6IJ_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
|
|
743
743
|
formats: mb.ALL_FORMATS
|
|
744
744
|
});
|
|
745
745
|
const allTracks = await input.getTracks();
|
|
@@ -771,7 +771,7 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
771
771
|
throw new Error("remux: audio track not found in input");
|
|
772
772
|
}
|
|
773
773
|
inputAudio = newInput;
|
|
774
|
-
mbAudioCodec =
|
|
774
|
+
mbAudioCodec = chunkVLI3Y6IJ_cjs.avbridgeAudioToMediabunny(trackInfo.codec);
|
|
775
775
|
audioSink = new mb.EncodedPacketSink(newInput);
|
|
776
776
|
audioConfig = await newInput.getDecoderConfig();
|
|
777
777
|
}
|
|
@@ -791,22 +791,25 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
791
791
|
}
|
|
792
792
|
}
|
|
793
793
|
let mimePromise = null;
|
|
794
|
+
const myToken = pumpToken;
|
|
794
795
|
const writable = new WritableStream({
|
|
795
796
|
write: async (chunk) => {
|
|
796
|
-
if (destroyed) return;
|
|
797
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
797
798
|
if (!sink) {
|
|
798
799
|
const mime = await (mimePromise ??= output.getMimeType());
|
|
800
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
799
801
|
sink = new MseSink({ mime, video });
|
|
800
802
|
await sink.ready();
|
|
803
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
801
804
|
if (pendingStartTime > 0) {
|
|
802
805
|
sink.invalidate(pendingStartTime);
|
|
803
806
|
}
|
|
804
807
|
sink.setPlayOnSeek(pendingAutoPlay);
|
|
805
808
|
}
|
|
806
|
-
while (sink && !destroyed && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
|
|
809
|
+
while (sink && !destroyed && pumpToken === myToken && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
|
|
807
810
|
await new Promise((r) => setTimeout(r, 500));
|
|
808
811
|
}
|
|
809
|
-
if (destroyed) return;
|
|
812
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
810
813
|
sink.append(chunk.data);
|
|
811
814
|
stats.bytesWritten += chunk.data.byteLength;
|
|
812
815
|
stats.fragments++;
|
|
@@ -1073,7 +1076,21 @@ var VideoRenderer = class {
|
|
|
1073
1076
|
framesPainted = 0;
|
|
1074
1077
|
framesDroppedLate = 0;
|
|
1075
1078
|
framesDroppedOverflow = 0;
|
|
1079
|
+
/** True once the head frame has been painted as a pre-roll poster
|
|
1080
|
+
* since the last flush. Used to ensure pre-roll paints exactly one
|
|
1081
|
+
* frame (held static) during the post-seek discard window. */
|
|
1076
1082
|
prerolled = false;
|
|
1083
|
+
/** PTS (µs) of the most recently painted frame. Used as the calibration
|
|
1084
|
+
* reference on the first post-flush snap: the pre-roll path paints one
|
|
1085
|
+
* frame *before* PTS-based playback starts, so the queue head's PTS at
|
|
1086
|
+
* first PTS-based paint is the *next* frame, off by one frameDur from
|
|
1087
|
+
* the actually-displayed frame. Calibrating against the painted frame
|
|
1088
|
+
* instead of the queue head removes that one-frame offset and yields
|
|
1089
|
+
* calib ≈ 0 instead of +frameDur. */
|
|
1090
|
+
lastPaintedPtsUs = 0;
|
|
1091
|
+
hasLastPaintedPts = false;
|
|
1092
|
+
/** Audio-clock reading (ms) at the previous paint, for overlay Δaud. */
|
|
1093
|
+
lastPaintAudMs = 0;
|
|
1077
1094
|
/** Wall-clock time of the last paint, in ms (performance.now()). */
|
|
1078
1095
|
lastPaintWall = 0;
|
|
1079
1096
|
/** Minimum ms between paints — paces video at roughly source fps. */
|
|
@@ -1104,32 +1121,47 @@ var VideoRenderer = class {
|
|
|
1104
1121
|
/** Resolves once the first decoded frame has been enqueued. */
|
|
1105
1122
|
firstFrameReady;
|
|
1106
1123
|
resolveFirstFrame;
|
|
1107
|
-
/**
|
|
1124
|
+
/**
|
|
1125
|
+
* True once at least one frame has been enqueued *since the last flush*.
|
|
1126
|
+
* Used by `readyState` — initial cold-start reports HAVE_NOTHING until
|
|
1127
|
+
* any frame has arrived, and after a seek we want the same semantics
|
|
1128
|
+
* (HAVE_NOTHING until post-seek frames arrive), so the cumulative
|
|
1129
|
+
* `framesPainted > 0` that used to live here was wrong: it kept the
|
|
1130
|
+
* state "true forever" after the first frame ever, so post-seek
|
|
1131
|
+
* `waitForBuffer()` would exit immediately with an empty queue and
|
|
1132
|
+
* leave video frozen while audio kept going.
|
|
1133
|
+
*/
|
|
1108
1134
|
hasFrames() {
|
|
1109
|
-
return this.queue.length > 0 || this.
|
|
1135
|
+
return this.queue.length > 0 || this.hasEverEnqueuedSinceFlush;
|
|
1110
1136
|
}
|
|
1137
|
+
hasEverEnqueuedSinceFlush = false;
|
|
1111
1138
|
/** Current depth of the frame queue. Used by the decoder for backpressure. */
|
|
1112
1139
|
queueDepth() {
|
|
1113
1140
|
return this.queue.length;
|
|
1114
1141
|
}
|
|
1115
1142
|
/**
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1143
|
+
* Cap the decoder may fill the queue up to. Used by the decoder's
|
|
1144
|
+
* enqueue-side discard logic (it closes new frames instead of pushing
|
|
1145
|
+
* them when this is reached). Sized so a long post-seek catch-up
|
|
1146
|
+
* fits — the decoder produces frames at PTS T_kf onwards rapidly
|
|
1147
|
+
* while the demuxer is chewing through pre-target audio; if the
|
|
1148
|
+
* queue can hold the whole post-seek burst, the renderer plays
|
|
1149
|
+
* smoothly from pre-roll without a frozen-video gap when audio.start
|
|
1150
|
+
* fires. At ~340 KB per SD frame the cap is ~85 MB peak; at HD it's
|
|
1151
|
+
* larger but still bounded.
|
|
1121
1152
|
*/
|
|
1122
|
-
queueHighWater =
|
|
1153
|
+
queueHighWater = 256;
|
|
1123
1154
|
enqueue(frame) {
|
|
1124
1155
|
if (this.destroyed) {
|
|
1125
1156
|
frame.close();
|
|
1126
1157
|
return;
|
|
1127
1158
|
}
|
|
1128
1159
|
this.queue.push(frame);
|
|
1160
|
+
this.hasEverEnqueuedSinceFlush = true;
|
|
1129
1161
|
if (this.queue.length === 1 && this.framesPainted === 0) {
|
|
1130
1162
|
this.resolveFirstFrame();
|
|
1131
1163
|
}
|
|
1132
|
-
while (this.queue.length >
|
|
1164
|
+
while (this.queue.length > this.queueHighWater + 8) {
|
|
1133
1165
|
this.queue.shift()?.close();
|
|
1134
1166
|
this.framesDroppedOverflow++;
|
|
1135
1167
|
}
|
|
@@ -1211,12 +1243,9 @@ var VideoRenderer = class {
|
|
|
1211
1243
|
if (this.queue.length === 0) return;
|
|
1212
1244
|
const playing = this.clock.isPlaying();
|
|
1213
1245
|
if (!playing) {
|
|
1214
|
-
if (!this.prerolled) {
|
|
1215
|
-
const head = this.queue.shift();
|
|
1216
|
-
this.paint(head);
|
|
1217
|
-
head.close();
|
|
1246
|
+
if (!this.prerolled && this.queue.length > 0) {
|
|
1218
1247
|
this.prerolled = true;
|
|
1219
|
-
this.
|
|
1248
|
+
this.paint(this.queue[0]);
|
|
1220
1249
|
}
|
|
1221
1250
|
return;
|
|
1222
1251
|
}
|
|
@@ -1225,14 +1254,29 @@ var VideoRenderer = class {
|
|
|
1225
1254
|
const hasPts = headTs > 0 || this.queue.length > 1;
|
|
1226
1255
|
if (hasPts) {
|
|
1227
1256
|
const wallNow2 = performance.now();
|
|
1228
|
-
if (!this.ptsCalibrated
|
|
1229
|
-
this.
|
|
1257
|
+
if (!this.ptsCalibrated) {
|
|
1258
|
+
const anchorUs = (this.clock.anchorTime?.() ?? this.clock.now()) * 1e6;
|
|
1259
|
+
const referencePtsUs = this.hasLastPaintedPts ? this.lastPaintedPtsUs : headTs;
|
|
1260
|
+
this.ptsCalibrationUs = referencePtsUs - anchorUs;
|
|
1230
1261
|
this.ptsCalibrated = true;
|
|
1231
1262
|
this.lastCalibrationWall = wallNow2;
|
|
1263
|
+
if (isDebug()) {
|
|
1264
|
+
console.log(
|
|
1265
|
+
`[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`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
} else if (wallNow2 - this.lastCalibrationWall > 1e4) {
|
|
1269
|
+
const oldCalib = this.ptsCalibrationUs;
|
|
1270
|
+
this.ptsCalibrationUs = headTs - rawAudioNowUs;
|
|
1271
|
+
this.lastCalibrationWall = wallNow2;
|
|
1272
|
+
if (isDebug()) {
|
|
1273
|
+
console.log(
|
|
1274
|
+
`[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)`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1232
1277
|
}
|
|
1233
1278
|
const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
|
|
1234
|
-
const
|
|
1235
|
-
const deadlineUs = audioNowUs + frameDurationUs;
|
|
1279
|
+
const deadlineUs = audioNowUs;
|
|
1236
1280
|
let bestIdx = -1;
|
|
1237
1281
|
for (let i = 0; i < this.queue.length; i++) {
|
|
1238
1282
|
const ts = this.queue[i].timestamp ?? 0;
|
|
@@ -1259,19 +1303,21 @@ var VideoRenderer = class {
|
|
|
1259
1303
|
}
|
|
1260
1304
|
return;
|
|
1261
1305
|
}
|
|
1262
|
-
const
|
|
1306
|
+
const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
|
|
1263
1307
|
let dropped = 0;
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1308
|
+
const initialBestIdx = bestIdx;
|
|
1309
|
+
if (!_relaxDrop) {
|
|
1310
|
+
while (bestIdx > 0) {
|
|
1267
1311
|
this.queue.shift()?.close();
|
|
1268
1312
|
this.framesDroppedLate++;
|
|
1269
1313
|
bestIdx--;
|
|
1270
1314
|
dropped++;
|
|
1271
|
-
} else {
|
|
1272
|
-
break;
|
|
1273
1315
|
}
|
|
1274
1316
|
}
|
|
1317
|
+
const paintTs = this.queue[0]?.timestamp ?? 0;
|
|
1318
|
+
if (isDebug()) {
|
|
1319
|
+
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)}`);
|
|
1320
|
+
}
|
|
1275
1321
|
this.ticksPainted++;
|
|
1276
1322
|
if (isDebug()) {
|
|
1277
1323
|
const now = performance.now();
|
|
@@ -1307,6 +1353,33 @@ var VideoRenderer = class {
|
|
|
1307
1353
|
}
|
|
1308
1354
|
try {
|
|
1309
1355
|
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
1356
|
+
if (isDebug()) {
|
|
1357
|
+
const wallNow = performance.now();
|
|
1358
|
+
const audNowMs = this.clock.now() * 1e3;
|
|
1359
|
+
const ptsMs = (frame.timestamp ?? 0) / 1e3;
|
|
1360
|
+
const dWall = this.lastPaintWall > 0 ? wallNow - this.lastPaintWall : 0;
|
|
1361
|
+
const dAud = this.lastPaintAudMs > 0 ? audNowMs - this.lastPaintAudMs : 0;
|
|
1362
|
+
const dPts = this.hasLastPaintedPts ? ptsMs - this.lastPaintedPtsUs / 1e3 : 0;
|
|
1363
|
+
this.ctx.save();
|
|
1364
|
+
this.ctx.font = "bold 18px monospace";
|
|
1365
|
+
const lines = [
|
|
1366
|
+
`#${this.framesPainted + 1} pts=${ptsMs.toFixed(0)} aud=${audNowMs.toFixed(0)} wall=${wallNow.toFixed(0)}`,
|
|
1367
|
+
`\u0394pts=${dPts.toFixed(0)} \u0394aud=${dAud.toFixed(0)} \u0394wall=${dWall.toFixed(0)}`
|
|
1368
|
+
];
|
|
1369
|
+
const lineHeight = 22;
|
|
1370
|
+
const padTop = 6;
|
|
1371
|
+
const stripH = padTop + lineHeight * lines.length;
|
|
1372
|
+
this.ctx.fillStyle = "rgba(0,0,0,0.7)";
|
|
1373
|
+
this.ctx.fillRect(0, 0, this.canvas.width, stripH);
|
|
1374
|
+
this.ctx.fillStyle = "#0f0";
|
|
1375
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1376
|
+
this.ctx.fillText(lines[i], 8, padTop + lineHeight * (i + 1) - 4);
|
|
1377
|
+
}
|
|
1378
|
+
this.ctx.restore();
|
|
1379
|
+
}
|
|
1380
|
+
this.lastPaintedPtsUs = frame.timestamp ?? 0;
|
|
1381
|
+
this.hasLastPaintedPts = true;
|
|
1382
|
+
this.lastPaintAudMs = this.clock.now() * 1e3;
|
|
1310
1383
|
this.framesPainted++;
|
|
1311
1384
|
} catch (err) {
|
|
1312
1385
|
if (this.framesPainted === 0 && this.framesDroppedLate === 0) {
|
|
@@ -1319,17 +1392,30 @@ var VideoRenderer = class {
|
|
|
1319
1392
|
const count = this.queue.length;
|
|
1320
1393
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
1321
1394
|
this.prerolled = false;
|
|
1395
|
+
this.hasLastPaintedPts = false;
|
|
1322
1396
|
this.ptsCalibrated = false;
|
|
1397
|
+
this.hasEverEnqueuedSinceFlush = false;
|
|
1323
1398
|
if (isDebug() && count > 0) {
|
|
1324
1399
|
console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
|
|
1325
1400
|
}
|
|
1326
1401
|
}
|
|
1327
1402
|
stats() {
|
|
1403
|
+
let queueSpanMs = 0;
|
|
1404
|
+
let queueHeadMs = 0;
|
|
1405
|
+
let queueTailMs = 0;
|
|
1406
|
+
if (this.queue.length > 0) {
|
|
1407
|
+
queueHeadMs = Math.round((this.queue[0].timestamp ?? 0) / 1e3);
|
|
1408
|
+
queueTailMs = Math.round((this.queue[this.queue.length - 1].timestamp ?? 0) / 1e3);
|
|
1409
|
+
queueSpanMs = Math.max(0, queueTailMs - queueHeadMs);
|
|
1410
|
+
}
|
|
1328
1411
|
return {
|
|
1329
1412
|
framesPainted: this.framesPainted,
|
|
1330
1413
|
framesDroppedLate: this.framesDroppedLate,
|
|
1331
1414
|
framesDroppedOverflow: this.framesDroppedOverflow,
|
|
1332
|
-
queueDepth: this.queue.length
|
|
1415
|
+
queueDepth: this.queue.length,
|
|
1416
|
+
queueHeadMs,
|
|
1417
|
+
queueTailMs,
|
|
1418
|
+
queueSpanMs
|
|
1333
1419
|
};
|
|
1334
1420
|
}
|
|
1335
1421
|
destroy() {
|
|
@@ -1347,6 +1433,9 @@ var VideoRenderer = class {
|
|
|
1347
1433
|
};
|
|
1348
1434
|
|
|
1349
1435
|
// src/strategies/fallback/audio-output.ts
|
|
1436
|
+
function isDebug2() {
|
|
1437
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
1438
|
+
}
|
|
1350
1439
|
var AudioOutput = class {
|
|
1351
1440
|
ctx;
|
|
1352
1441
|
gain;
|
|
@@ -1368,6 +1457,16 @@ var AudioOutput = class {
|
|
|
1368
1457
|
mediaTimeOfNext = 0;
|
|
1369
1458
|
/** Anchor: media time `mediaTimeOfAnchor` corresponds to ctx time `ctxTimeAtAnchor`. */
|
|
1370
1459
|
mediaTimeOfAnchor = 0;
|
|
1460
|
+
/**
|
|
1461
|
+
* Ctx time at which the first audible chunk will start playing. `-1`
|
|
1462
|
+
* before any chunk has been scheduled successfully (clock is frozen);
|
|
1463
|
+
* the actual ctx time once one has. The renderer's `clock.now()` uses
|
|
1464
|
+
* this to avoid advancing during the silent-gap window between
|
|
1465
|
+
* `audio.start()` and the first chunk that schedules without being
|
|
1466
|
+
* dropped — that gap is what produces the "audio-less fast-forward"
|
|
1467
|
+
* the user sees post-seek when the gate releases on video-only grace.
|
|
1468
|
+
*/
|
|
1469
|
+
firstAudibleCtxStart = -1;
|
|
1371
1470
|
ctxTimeAtAnchor = 0;
|
|
1372
1471
|
pendingQueue = [];
|
|
1373
1472
|
framesScheduled = 0;
|
|
@@ -1440,10 +1539,16 @@ var AudioOutput = class {
|
|
|
1440
1539
|
return this.mediaTimeOfAnchor;
|
|
1441
1540
|
}
|
|
1442
1541
|
if (this.state === "playing") {
|
|
1542
|
+
if (this.firstAudibleCtxStart < 0) {
|
|
1543
|
+
return this.mediaTimeOfAnchor;
|
|
1544
|
+
}
|
|
1443
1545
|
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
|
|
1444
1546
|
}
|
|
1445
1547
|
return this.mediaTimeOfAnchor;
|
|
1446
1548
|
}
|
|
1549
|
+
anchorTime() {
|
|
1550
|
+
return this.mediaTimeOfAnchor;
|
|
1551
|
+
}
|
|
1447
1552
|
isPlaying() {
|
|
1448
1553
|
return this.state === "playing";
|
|
1449
1554
|
}
|
|
@@ -1470,18 +1575,81 @@ var AudioOutput = class {
|
|
|
1470
1575
|
* Schedule a chunk of decoded samples. Queues internally while idle (cold
|
|
1471
1576
|
* start or post-seek), schedules directly to the audio graph while playing.
|
|
1472
1577
|
* In wall-clock mode, samples are silently discarded.
|
|
1578
|
+
*
|
|
1579
|
+
* `ptsSec` is the chunk's source-domain content PTS in seconds, from
|
|
1580
|
+
* the demuxer. When provided, the chunk plays at the ctx-time
|
|
1581
|
+
* corresponding to that PTS — so pre-target audio after a seek
|
|
1582
|
+
* naturally drops (its computed `ctxStart` falls in the past) and
|
|
1583
|
+
* post-target audio plays at its true content time, without any
|
|
1584
|
+
* external trim or anchor rebase. When `ptsSec` is null (cold start
|
|
1585
|
+
* with no PTS yet, or codecs whose packet→frame mapping isn't 1:1),
|
|
1586
|
+
* the chunk is scheduled sequentially after `mediaTimeOfNext` — the
|
|
1587
|
+
* pre-refactor behavior.
|
|
1473
1588
|
*/
|
|
1474
|
-
schedule(samples, channels, sampleRate) {
|
|
1589
|
+
schedule(samples, channels, sampleRate, ptsSec) {
|
|
1475
1590
|
if (this.destroyed || this.noAudio) return;
|
|
1476
1591
|
const frameCount = samples.length / channels;
|
|
1477
1592
|
const durationSec = frameCount / sampleRate;
|
|
1593
|
+
const hasPts = ptsSec != null && Number.isFinite(ptsSec);
|
|
1594
|
+
if (hasPts && ptsSec + durationSec / this._rate < this.mediaTimeOfAnchor) {
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1478
1597
|
if (this.state === "idle" || this.state === "paused") {
|
|
1479
|
-
this.pendingQueue.push({
|
|
1598
|
+
this.pendingQueue.push({
|
|
1599
|
+
samples,
|
|
1600
|
+
channels,
|
|
1601
|
+
sampleRate,
|
|
1602
|
+
frameCount,
|
|
1603
|
+
durationSec,
|
|
1604
|
+
ptsSec: hasPts ? ptsSec : null
|
|
1605
|
+
});
|
|
1480
1606
|
return;
|
|
1481
1607
|
}
|
|
1482
|
-
this.scheduleNow(
|
|
1608
|
+
this.scheduleNow(
|
|
1609
|
+
samples,
|
|
1610
|
+
channels,
|
|
1611
|
+
sampleRate,
|
|
1612
|
+
frameCount,
|
|
1613
|
+
hasPts ? ptsSec : null
|
|
1614
|
+
);
|
|
1483
1615
|
}
|
|
1484
|
-
scheduleNow(samples, channels, sampleRate, frameCount) {
|
|
1616
|
+
scheduleNow(samples, channels, sampleRate, frameCount, ptsSec) {
|
|
1617
|
+
const durationSec = frameCount / sampleRate;
|
|
1618
|
+
let ctxStart;
|
|
1619
|
+
if (ptsSec != null) {
|
|
1620
|
+
ctxStart = this.ctxTimeAtAnchor + (ptsSec - this.mediaTimeOfAnchor) / this._rate;
|
|
1621
|
+
if (isDebug2()) {
|
|
1622
|
+
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}`);
|
|
1623
|
+
}
|
|
1624
|
+
if (ctxStart < this.ctx.currentTime - 1e-3) {
|
|
1625
|
+
if (isDebug2()) {
|
|
1626
|
+
console.log(`[TRACE-AUD] DROP late chunk pts=${ptsSec.toFixed(3)} ctxStart=${ctxStart.toFixed(4)} < ctxNow=${this.ctx.currentTime.toFixed(4)}`);
|
|
1627
|
+
}
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
if (this.firstAudibleCtxStart < 0) {
|
|
1631
|
+
this.firstAudibleCtxStart = ctxStart;
|
|
1632
|
+
this.mediaTimeOfAnchor = ptsSec;
|
|
1633
|
+
this.ctxTimeAtAnchor = ctxStart;
|
|
1634
|
+
if (isDebug2()) {
|
|
1635
|
+
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)}`);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const endMediaTime = ptsSec + durationSec / this._rate;
|
|
1639
|
+
if (endMediaTime > this.mediaTimeOfNext) {
|
|
1640
|
+
this.mediaTimeOfNext = endMediaTime;
|
|
1641
|
+
}
|
|
1642
|
+
} else {
|
|
1643
|
+
ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
1644
|
+
console.warn(`[TRACE-AUD] LEGACY (no PTS) sched dur=${durationSec.toFixed(4)} ctxStart=${ctxStart.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)}`);
|
|
1645
|
+
if (ctxStart < this.ctx.currentTime) {
|
|
1646
|
+
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)}`);
|
|
1647
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1648
|
+
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
1649
|
+
ctxStart = this.ctx.currentTime;
|
|
1650
|
+
}
|
|
1651
|
+
this.mediaTimeOfNext += durationSec;
|
|
1652
|
+
}
|
|
1485
1653
|
const buffer = this.ctx.createBuffer(channels, frameCount, sampleRate);
|
|
1486
1654
|
for (let ch = 0; ch < channels; ch++) {
|
|
1487
1655
|
const channelData = buffer.getChannelData(ch);
|
|
@@ -1493,14 +1661,7 @@ var AudioOutput = class {
|
|
|
1493
1661
|
node.buffer = buffer;
|
|
1494
1662
|
node.connect(this.gain);
|
|
1495
1663
|
if (this._rate !== 1) node.playbackRate.value = this._rate;
|
|
1496
|
-
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
1497
|
-
if (ctxStart < this.ctx.currentTime) {
|
|
1498
|
-
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1499
|
-
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
1500
|
-
ctxStart = this.ctx.currentTime;
|
|
1501
|
-
}
|
|
1502
1664
|
node.start(ctxStart);
|
|
1503
|
-
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
1504
1665
|
this.framesScheduled++;
|
|
1505
1666
|
}
|
|
1506
1667
|
// ── Lifecycle ─────────────────────────────────────────────────────────
|
|
@@ -1525,12 +1686,15 @@ var AudioOutput = class {
|
|
|
1525
1686
|
} catch {
|
|
1526
1687
|
}
|
|
1527
1688
|
if (this.state === "paused") {
|
|
1689
|
+
if (isDebug2()) {
|
|
1690
|
+
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}`);
|
|
1691
|
+
}
|
|
1528
1692
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1529
1693
|
this.state = "playing";
|
|
1530
1694
|
const drain2 = this.pendingQueue;
|
|
1531
1695
|
this.pendingQueue = [];
|
|
1532
1696
|
for (const c of drain2) {
|
|
1533
|
-
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
1697
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
|
|
1534
1698
|
}
|
|
1535
1699
|
return;
|
|
1536
1700
|
}
|
|
@@ -1538,10 +1702,13 @@ var AudioOutput = class {
|
|
|
1538
1702
|
this.ctxTimeAtAnchor = this.ctx.currentTime + STARTUP_DELAY;
|
|
1539
1703
|
this.mediaTimeOfNext = this.mediaTimeOfAnchor;
|
|
1540
1704
|
this.state = "playing";
|
|
1705
|
+
if (isDebug2()) {
|
|
1706
|
+
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}`);
|
|
1707
|
+
}
|
|
1541
1708
|
const drain = this.pendingQueue;
|
|
1542
1709
|
this.pendingQueue = [];
|
|
1543
1710
|
for (const c of drain) {
|
|
1544
|
-
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
1711
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
|
|
1545
1712
|
}
|
|
1546
1713
|
}
|
|
1547
1714
|
/** Pause playback. Suspends the audio context. */
|
|
@@ -1567,6 +1734,9 @@ var AudioOutput = class {
|
|
|
1567
1734
|
* supplying new samples) and then call `start()` to resume playback.
|
|
1568
1735
|
*/
|
|
1569
1736
|
async reset(newMediaTime) {
|
|
1737
|
+
if (isDebug2()) {
|
|
1738
|
+
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}`);
|
|
1739
|
+
}
|
|
1570
1740
|
if (this.noAudio) {
|
|
1571
1741
|
this.pendingQueue = [];
|
|
1572
1742
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
@@ -1585,6 +1755,7 @@ var AudioOutput = class {
|
|
|
1585
1755
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
1586
1756
|
this.mediaTimeOfNext = newMediaTime;
|
|
1587
1757
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1758
|
+
this.firstAudibleCtxStart = -1;
|
|
1588
1759
|
this.state = "idle";
|
|
1589
1760
|
if (this.ctx.state === "running") {
|
|
1590
1761
|
await this.ctx.suspend();
|
|
@@ -1687,6 +1858,7 @@ async function startHybridDecoder(opts) {
|
|
|
1687
1858
|
}
|
|
1688
1859
|
let bsfCtx = null;
|
|
1689
1860
|
let bsfPkt = null;
|
|
1861
|
+
let bsfRequiredButMissing = false;
|
|
1690
1862
|
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
1691
1863
|
try {
|
|
1692
1864
|
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
@@ -1697,13 +1869,19 @@ async function startHybridDecoder(opts) {
|
|
|
1697
1869
|
bsfPkt = await libav.av_packet_alloc();
|
|
1698
1870
|
chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
|
|
1699
1871
|
} else {
|
|
1700
|
-
|
|
1872
|
+
bsfRequiredButMissing = true;
|
|
1701
1873
|
bsfCtx = null;
|
|
1702
1874
|
}
|
|
1703
1875
|
} catch (err) {
|
|
1704
|
-
|
|
1876
|
+
bsfRequiredButMissing = true;
|
|
1705
1877
|
bsfCtx = null;
|
|
1706
1878
|
bsfPkt = null;
|
|
1879
|
+
chunkG4APZMCP_cjs.dbg.warn("bsf", `hybrid: mpeg4_unpack_bframes BSF init failed: ${err.message}`);
|
|
1880
|
+
}
|
|
1881
|
+
if (bsfRequiredButMissing) {
|
|
1882
|
+
console.error(
|
|
1883
|
+
"[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."
|
|
1884
|
+
);
|
|
1707
1885
|
}
|
|
1708
1886
|
}
|
|
1709
1887
|
async function applyBSF(packets) {
|
|
@@ -1713,7 +1891,6 @@ async function startHybridDecoder(opts) {
|
|
|
1713
1891
|
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
1714
1892
|
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
1715
1893
|
if (sendErr < 0) {
|
|
1716
|
-
out.push(pkt);
|
|
1717
1894
|
continue;
|
|
1718
1895
|
}
|
|
1719
1896
|
while (true) {
|
|
@@ -1727,10 +1904,13 @@ async function startHybridDecoder(opts) {
|
|
|
1727
1904
|
async function flushBSF() {
|
|
1728
1905
|
if (!bsfCtx || !bsfPkt) return;
|
|
1729
1906
|
try {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1907
|
+
if (libav.av_bsf_flush) {
|
|
1908
|
+
await libav.av_bsf_flush(bsfCtx);
|
|
1909
|
+
} else {
|
|
1910
|
+
while (true) {
|
|
1911
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
1912
|
+
if (err < 0) break;
|
|
1913
|
+
}
|
|
1734
1914
|
}
|
|
1735
1915
|
} catch {
|
|
1736
1916
|
}
|
|
@@ -1744,7 +1924,6 @@ async function startHybridDecoder(opts) {
|
|
|
1744
1924
|
let videoChunksFed = 0;
|
|
1745
1925
|
let bufferedUntilSec = 0;
|
|
1746
1926
|
let syntheticVideoUs = 0;
|
|
1747
|
-
let syntheticAudioUs = 0;
|
|
1748
1927
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
1749
1928
|
const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
|
|
1750
1929
|
const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
|
|
@@ -1776,7 +1955,13 @@ async function startHybridDecoder(opts) {
|
|
|
1776
1955
|
}
|
|
1777
1956
|
}
|
|
1778
1957
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
1779
|
-
await decodeAudioBatch(
|
|
1958
|
+
await decodeAudioBatch(
|
|
1959
|
+
audioPackets,
|
|
1960
|
+
myToken,
|
|
1961
|
+
/*flush*/
|
|
1962
|
+
false,
|
|
1963
|
+
audioTimeBase
|
|
1964
|
+
);
|
|
1780
1965
|
}
|
|
1781
1966
|
if (myToken !== pumpToken || destroyed) return;
|
|
1782
1967
|
await new Promise((r) => setTimeout(r, 0));
|
|
@@ -1823,8 +2008,11 @@ async function startHybridDecoder(opts) {
|
|
|
1823
2008
|
}
|
|
1824
2009
|
}
|
|
1825
2010
|
}
|
|
1826
|
-
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
2011
|
+
async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
|
|
1827
2012
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
2013
|
+
const pktPtsSec = pkts.map(
|
|
2014
|
+
(p) => tb ? chunkHZUVMXBN_cjs.packetPtsSec(p, tb) : null
|
|
2015
|
+
);
|
|
1828
2016
|
const AUDIO_SUB_BATCH = 4;
|
|
1829
2017
|
let allFrames = [];
|
|
1830
2018
|
for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
|
|
@@ -1862,22 +2050,13 @@ async function startHybridDecoder(opts) {
|
|
|
1862
2050
|
}
|
|
1863
2051
|
if (myToken !== pumpToken || destroyed) return;
|
|
1864
2052
|
const frames = allFrames;
|
|
1865
|
-
for (
|
|
2053
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1866
2054
|
if (myToken !== pumpToken || destroyed) return;
|
|
1867
|
-
|
|
1868
|
-
f,
|
|
1869
|
-
() => {
|
|
1870
|
-
const ts = syntheticAudioUs;
|
|
1871
|
-
const samples2 = f.nb_samples ?? 1024;
|
|
1872
|
-
const sampleRate = f.sample_rate ?? 44100;
|
|
1873
|
-
syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
|
|
1874
|
-
return ts;
|
|
1875
|
-
},
|
|
1876
|
-
audioTimeBase
|
|
1877
|
-
);
|
|
2055
|
+
const f = frames[i];
|
|
1878
2056
|
const samples = chunkHZUVMXBN_cjs.libavFrameToInterleavedFloat32(f);
|
|
1879
2057
|
if (samples) {
|
|
1880
|
-
|
|
2058
|
+
const pts = pktPtsSec[i] ?? null;
|
|
2059
|
+
opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
|
|
1881
2060
|
audioFramesDecoded++;
|
|
1882
2061
|
}
|
|
1883
2062
|
}
|
|
@@ -1987,7 +2166,6 @@ async function startHybridDecoder(opts) {
|
|
|
1987
2166
|
}
|
|
1988
2167
|
await flushBSF();
|
|
1989
2168
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
1990
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
1991
2169
|
pumpRunning = pumpLoop(newToken).catch(
|
|
1992
2170
|
(err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
|
|
1993
2171
|
);
|
|
@@ -2026,7 +2204,6 @@ async function startHybridDecoder(opts) {
|
|
|
2026
2204
|
}
|
|
2027
2205
|
await flushBSF();
|
|
2028
2206
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
2029
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
2030
2207
|
pumpRunning = pumpLoop(newToken).catch(
|
|
2031
2208
|
(err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
|
|
2032
2209
|
);
|
|
@@ -2042,6 +2219,7 @@ async function startHybridDecoder(opts) {
|
|
|
2042
2219
|
videoChunksFed,
|
|
2043
2220
|
audioFramesDecoded,
|
|
2044
2221
|
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
2222
|
+
bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
|
|
2045
2223
|
videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
|
|
2046
2224
|
// Confirmed transport info — see fallback decoder for the pattern.
|
|
2047
2225
|
_transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
|
|
@@ -2278,6 +2456,9 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2278
2456
|
}
|
|
2279
2457
|
|
|
2280
2458
|
// src/strategies/fallback/decoder.ts
|
|
2459
|
+
function isDebug3() {
|
|
2460
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
2461
|
+
}
|
|
2281
2462
|
async function startDecoder(opts) {
|
|
2282
2463
|
const variant = "avbridge";
|
|
2283
2464
|
const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
|
|
@@ -2341,6 +2522,7 @@ async function startDecoder(opts) {
|
|
|
2341
2522
|
}
|
|
2342
2523
|
let bsfCtx = null;
|
|
2343
2524
|
let bsfPkt = null;
|
|
2525
|
+
let bsfRequiredButMissing = false;
|
|
2344
2526
|
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
2345
2527
|
try {
|
|
2346
2528
|
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
@@ -2351,13 +2533,19 @@ async function startDecoder(opts) {
|
|
|
2351
2533
|
bsfPkt = await libav.av_packet_alloc();
|
|
2352
2534
|
chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
|
|
2353
2535
|
} else {
|
|
2354
|
-
|
|
2536
|
+
bsfRequiredButMissing = true;
|
|
2355
2537
|
bsfCtx = null;
|
|
2356
2538
|
}
|
|
2357
2539
|
} catch (err) {
|
|
2358
|
-
|
|
2540
|
+
bsfRequiredButMissing = true;
|
|
2359
2541
|
bsfCtx = null;
|
|
2360
2542
|
bsfPkt = null;
|
|
2543
|
+
chunkG4APZMCP_cjs.dbg.warn("bsf", `mpeg4_unpack_bframes BSF init failed: ${err.message}`);
|
|
2544
|
+
}
|
|
2545
|
+
if (bsfRequiredButMissing) {
|
|
2546
|
+
console.error(
|
|
2547
|
+
"[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."
|
|
2548
|
+
);
|
|
2361
2549
|
}
|
|
2362
2550
|
}
|
|
2363
2551
|
async function applyBSF(packets) {
|
|
@@ -2367,7 +2555,6 @@ async function startDecoder(opts) {
|
|
|
2367
2555
|
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
2368
2556
|
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
2369
2557
|
if (sendErr < 0) {
|
|
2370
|
-
out.push(pkt);
|
|
2371
2558
|
continue;
|
|
2372
2559
|
}
|
|
2373
2560
|
while (true) {
|
|
@@ -2381,10 +2568,13 @@ async function startDecoder(opts) {
|
|
|
2381
2568
|
async function flushBSF() {
|
|
2382
2569
|
if (!bsfCtx || !bsfPkt) return;
|
|
2383
2570
|
try {
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2571
|
+
if (libav.av_bsf_flush) {
|
|
2572
|
+
await libav.av_bsf_flush(bsfCtx);
|
|
2573
|
+
} else {
|
|
2574
|
+
while (true) {
|
|
2575
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
2576
|
+
if (err < 0) break;
|
|
2577
|
+
}
|
|
2388
2578
|
}
|
|
2389
2579
|
} catch {
|
|
2390
2580
|
}
|
|
@@ -2400,8 +2590,28 @@ async function startDecoder(opts) {
|
|
|
2400
2590
|
let watchdogSlowSinceMs = 0;
|
|
2401
2591
|
let watchdogSlowWarned = false;
|
|
2402
2592
|
let watchdogOverflowWarned = false;
|
|
2403
|
-
let
|
|
2404
|
-
let
|
|
2593
|
+
let lastContentUs = -1;
|
|
2594
|
+
let firstValidPtsLoggedSinceSeek = false;
|
|
2595
|
+
let seenFirstAudioPacketSinceSeek = false;
|
|
2596
|
+
let seekTargetSec = 0;
|
|
2597
|
+
let diagPktsLoggedSinceSeek = 0;
|
|
2598
|
+
let diagFramesLoggedSinceSeek = 0;
|
|
2599
|
+
let diagFrameKeysDumped = false;
|
|
2600
|
+
const DIAG_MAX_PKTS = 100;
|
|
2601
|
+
const DIAG_MAX_FRAMES = 300;
|
|
2602
|
+
let videoDecodeMsTotal = 0;
|
|
2603
|
+
let audioDecodeMsTotal = 0;
|
|
2604
|
+
let videoDecodeBatches = 0;
|
|
2605
|
+
let audioDecodeBatches = 0;
|
|
2606
|
+
let readMsTotal = 0;
|
|
2607
|
+
let readBatches = 0;
|
|
2608
|
+
let pumpThrottleMsTotal = 0;
|
|
2609
|
+
let pumpThrottleEntries = 0;
|
|
2610
|
+
let slowestVideoBatchMs = 0;
|
|
2611
|
+
let newestVideoPtsUs = 0;
|
|
2612
|
+
let lastEmittedPtsUs = -1;
|
|
2613
|
+
let ptsRegressions = 0;
|
|
2614
|
+
let worstPtsRegressionMs = 0;
|
|
2405
2615
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
2406
2616
|
const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
|
|
2407
2617
|
const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
|
|
@@ -2410,9 +2620,12 @@ async function startDecoder(opts) {
|
|
|
2410
2620
|
let readErr;
|
|
2411
2621
|
let packets;
|
|
2412
2622
|
try {
|
|
2623
|
+
const _readStart = performance.now();
|
|
2413
2624
|
[readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
|
|
2414
2625
|
limit: 16 * 1024
|
|
2415
2626
|
});
|
|
2627
|
+
readMsTotal += performance.now() - _readStart;
|
|
2628
|
+
readBatches++;
|
|
2416
2629
|
} catch (err) {
|
|
2417
2630
|
console.error("[avbridge] ff_read_frame_multi failed:", err);
|
|
2418
2631
|
return;
|
|
@@ -2424,6 +2637,18 @@ async function startDecoder(opts) {
|
|
|
2424
2637
|
for (const pkt of videoPackets) {
|
|
2425
2638
|
const sec = chunkHZUVMXBN_cjs.packetPtsSec(pkt, videoTimeBase);
|
|
2426
2639
|
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2640
|
+
if (isDebug3() && diagPktsLoggedSinceSeek < DIAG_MAX_PKTS) {
|
|
2641
|
+
const rawHi = pkt.ptshi ?? 0;
|
|
2642
|
+
const rawLo = pkt.pts ?? 0;
|
|
2643
|
+
const isInvalidPts = rawHi === -2147483648 && rawLo === 0;
|
|
2644
|
+
const rawPts64 = isInvalidPts ? null : rawHi * 4294967296 + rawLo;
|
|
2645
|
+
const rawSec = rawPts64 != null && videoTimeBase ? rawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
|
|
2646
|
+
const pktKeys = diagPktsLoggedSinceSeek === 0 ? `[keys: ${Object.keys(pkt).join(",")}]` : "";
|
|
2647
|
+
console.log(
|
|
2648
|
+
`[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
|
|
2649
|
+
);
|
|
2650
|
+
diagPktsLoggedSinceSeek++;
|
|
2651
|
+
}
|
|
2427
2652
|
}
|
|
2428
2653
|
}
|
|
2429
2654
|
if (audioPackets && audioTimeBase) {
|
|
@@ -2431,9 +2656,25 @@ async function startDecoder(opts) {
|
|
|
2431
2656
|
const sec = chunkHZUVMXBN_cjs.packetPtsSec(pkt, audioTimeBase);
|
|
2432
2657
|
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2433
2658
|
}
|
|
2659
|
+
if (!seenFirstAudioPacketSinceSeek && audioPackets.length > 0) {
|
|
2660
|
+
const firstSec = chunkHZUVMXBN_cjs.packetPtsSec(audioPackets[0], audioTimeBase);
|
|
2661
|
+
if (firstSec != null && Number.isFinite(firstSec)) {
|
|
2662
|
+
seenFirstAudioPacketSinceSeek = true;
|
|
2663
|
+
chunkG4APZMCP_cjs.dbg.info(
|
|
2664
|
+
"av-anchor",
|
|
2665
|
+
`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)`
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2434
2669
|
}
|
|
2435
2670
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2436
|
-
await decodeAudioBatch(
|
|
2671
|
+
await decodeAudioBatch(
|
|
2672
|
+
audioPackets,
|
|
2673
|
+
myToken,
|
|
2674
|
+
/*flush*/
|
|
2675
|
+
false,
|
|
2676
|
+
audioTimeBase
|
|
2677
|
+
);
|
|
2437
2678
|
}
|
|
2438
2679
|
if (myToken !== pumpToken || destroyed) return;
|
|
2439
2680
|
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
@@ -2474,8 +2715,17 @@ async function startDecoder(opts) {
|
|
|
2474
2715
|
}
|
|
2475
2716
|
}
|
|
2476
2717
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2718
|
+
{
|
|
2719
|
+
const _throttleStart = performance.now();
|
|
2720
|
+
let _throttled = false;
|
|
2721
|
+
while (!destroyed && myToken === pumpToken && opts.audio.bufferAhead() > 2) {
|
|
2722
|
+
_throttled = true;
|
|
2723
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2724
|
+
}
|
|
2725
|
+
if (_throttled) {
|
|
2726
|
+
pumpThrottleMsTotal += performance.now() - _throttleStart;
|
|
2727
|
+
pumpThrottleEntries++;
|
|
2728
|
+
}
|
|
2479
2729
|
}
|
|
2480
2730
|
if (readErr === libav.AVERROR_EOF) {
|
|
2481
2731
|
if (videoDec) await decodeVideoBatch(
|
|
@@ -2501,6 +2751,7 @@ async function startDecoder(opts) {
|
|
|
2501
2751
|
async function decodeVideoBatch(pkts, myToken, flush = false) {
|
|
2502
2752
|
if (!videoDec || destroyed || myToken !== pumpToken) return;
|
|
2503
2753
|
let frames;
|
|
2754
|
+
const _t0 = performance.now();
|
|
2504
2755
|
try {
|
|
2505
2756
|
frames = await libav.ff_decode_multi(
|
|
2506
2757
|
videoDec.c,
|
|
@@ -2513,32 +2764,133 @@ async function startDecoder(opts) {
|
|
|
2513
2764
|
console.error("[avbridge] video decode batch failed:", err);
|
|
2514
2765
|
return;
|
|
2515
2766
|
}
|
|
2767
|
+
{
|
|
2768
|
+
const _dt = performance.now() - _t0;
|
|
2769
|
+
videoDecodeMsTotal += _dt;
|
|
2770
|
+
videoDecodeBatches++;
|
|
2771
|
+
if (_dt > slowestVideoBatchMs) slowestVideoBatchMs = _dt;
|
|
2772
|
+
}
|
|
2516
2773
|
if (myToken !== pumpToken || destroyed) return;
|
|
2517
2774
|
for (const f of frames) {
|
|
2518
2775
|
if (myToken !== pumpToken || destroyed) return;
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2776
|
+
const _diagShouldLog = isDebug3() && diagFramesLoggedSinceSeek < DIAG_MAX_FRAMES;
|
|
2777
|
+
const _diagRawHi = f.ptshi ?? 0;
|
|
2778
|
+
const _diagRawLo = f.pts ?? 0;
|
|
2779
|
+
const _diagInvalid = _diagRawHi === -2147483648 && _diagRawLo === 0;
|
|
2780
|
+
const _diagRawPts64 = _diagInvalid ? null : _diagRawHi * 4294967296 + _diagRawLo;
|
|
2781
|
+
const _diagRawSec = _diagRawPts64 != null && videoTimeBase ? _diagRawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
|
|
2782
|
+
if (_diagShouldLog && !diagFrameKeysDumped) {
|
|
2783
|
+
diagFrameKeysDumped = true;
|
|
2784
|
+
const allKeys = Object.keys(f);
|
|
2785
|
+
const fieldDump = {};
|
|
2786
|
+
for (const k of allKeys) {
|
|
2787
|
+
const v = f[k];
|
|
2788
|
+
if (k === "data") continue;
|
|
2789
|
+
if (typeof v === "object" && v !== null && "length" in v) continue;
|
|
2790
|
+
fieldDump[k] = v;
|
|
2791
|
+
}
|
|
2792
|
+
console.log(`[DIAG-FRAME] FIRST FRAME post-seek \u2014 all keys: ${allKeys.join(",")}`);
|
|
2793
|
+
console.log(`[DIAG-FRAME] FIRST FRAME field dump:`, fieldDump);
|
|
2794
|
+
}
|
|
2795
|
+
let rawUs = null;
|
|
2796
|
+
if (!_diagInvalid && _diagRawPts64 != null) {
|
|
2797
|
+
const tb = videoTimeBase ?? [1, 1e6];
|
|
2798
|
+
const us = Math.round(_diagRawPts64 * 1e6 * tb[0] / tb[1]);
|
|
2799
|
+
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
2800
|
+
rawUs = us;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
const _diagLog = (decision, finalPtsUs, sanFallback) => {
|
|
2804
|
+
if (!_diagShouldLog) return;
|
|
2805
|
+
const ptsSrc = sanFallback ? `SYNTHETIC(${_diagInvalid ? "NOPTS" : "invalid-range"})` : "LIBAV";
|
|
2806
|
+
console.log(
|
|
2807
|
+
`[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}`
|
|
2808
|
+
);
|
|
2809
|
+
diagFramesLoggedSinceSeek++;
|
|
2810
|
+
};
|
|
2811
|
+
let _diagSanFallbackFired = false;
|
|
2812
|
+
const seekTargetUs = Math.round(seekTargetSec * 1e6);
|
|
2813
|
+
if (lastContentUs < 0) {
|
|
2814
|
+
if (rawUs == null) {
|
|
2815
|
+
const isColdStartKeyframe = seekTargetSec === 0 && f.key_frame === 1;
|
|
2816
|
+
if (isColdStartKeyframe) {
|
|
2817
|
+
lastContentUs = 0;
|
|
2818
|
+
_diagSanFallbackFired = true;
|
|
2819
|
+
} else {
|
|
2820
|
+
_diagLog("PRE-ANCHOR-DROP", 0, true);
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
} else {
|
|
2824
|
+
lastContentUs = rawUs;
|
|
2825
|
+
if (!firstValidPtsLoggedSinceSeek) {
|
|
2826
|
+
firstValidPtsLoggedSinceSeek = true;
|
|
2827
|
+
if (isDebug3()) {
|
|
2828
|
+
console.log(
|
|
2829
|
+
`[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)`
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
if (rawUs >= seekTargetUs) {
|
|
2833
|
+
console.warn(
|
|
2834
|
+
`[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.`
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
} else {
|
|
2840
|
+
if (rawUs != null) {
|
|
2841
|
+
lastContentUs = rawUs;
|
|
2842
|
+
} else {
|
|
2843
|
+
lastContentUs += videoFrameStepUs;
|
|
2844
|
+
_diagSanFallbackFired = true;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
f.pts = lastContentUs;
|
|
2848
|
+
f.ptshi = lastContentUs < 0 ? -1 : 0;
|
|
2849
|
+
const _fPts = lastContentUs;
|
|
2850
|
+
if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
|
|
2851
|
+
if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
|
|
2852
|
+
_diagLog("REGRESSED-DROP", _fPts, _diagSanFallbackFired);
|
|
2853
|
+
ptsRegressions++;
|
|
2854
|
+
const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
|
|
2855
|
+
if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
|
|
2856
|
+
if (ptsRegressions <= 10) {
|
|
2857
|
+
console.warn(
|
|
2858
|
+
`[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.`
|
|
2859
|
+
);
|
|
2860
|
+
}
|
|
2861
|
+
continue;
|
|
2862
|
+
}
|
|
2863
|
+
lastEmittedPtsUs = _fPts;
|
|
2864
|
+
const targetUs = Math.round(seekTargetSec * 1e6);
|
|
2865
|
+
if (_fPts < targetUs - videoFrameStepUs) {
|
|
2866
|
+
_diagLog("PRE-TARGET-DROP", _fPts, _diagSanFallbackFired);
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2528
2869
|
try {
|
|
2529
2870
|
const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
|
|
2530
|
-
opts.renderer.
|
|
2871
|
+
if (opts.renderer.queueDepth() >= opts.renderer.queueHighWater) {
|
|
2872
|
+
vf.close();
|
|
2873
|
+
_diagLog("OVERFLOW-DROP", _fPts, _diagSanFallbackFired);
|
|
2874
|
+
} else {
|
|
2875
|
+
opts.renderer.enqueue(vf);
|
|
2876
|
+
_diagLog("ENQUEUED", _fPts, _diagSanFallbackFired);
|
|
2877
|
+
}
|
|
2531
2878
|
videoFramesDecoded++;
|
|
2532
2879
|
} catch (err) {
|
|
2533
2880
|
if (videoFramesDecoded === 0) {
|
|
2534
2881
|
console.warn("[avbridge] laFrameToVideoFrame failed:", err);
|
|
2535
2882
|
}
|
|
2883
|
+
_diagLog("BRIDGE-ERROR", _fPts, _diagSanFallbackFired);
|
|
2536
2884
|
}
|
|
2537
2885
|
}
|
|
2538
2886
|
}
|
|
2539
|
-
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
2887
|
+
async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
|
|
2540
2888
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
2889
|
+
const pktPtsSec = pkts.map(
|
|
2890
|
+
(p) => tb ? chunkHZUVMXBN_cjs.packetPtsSec(p, tb) : null
|
|
2891
|
+
);
|
|
2541
2892
|
let frames;
|
|
2893
|
+
const _t0 = performance.now();
|
|
2542
2894
|
try {
|
|
2543
2895
|
frames = await libav.ff_decode_multi(
|
|
2544
2896
|
audioDec.c,
|
|
@@ -2551,23 +2903,20 @@ async function startDecoder(opts) {
|
|
|
2551
2903
|
console.error("[avbridge] audio decode batch failed:", err);
|
|
2552
2904
|
return;
|
|
2553
2905
|
}
|
|
2906
|
+
audioDecodeMsTotal += performance.now() - _t0;
|
|
2907
|
+
audioDecodeBatches++;
|
|
2554
2908
|
if (myToken !== pumpToken || destroyed) return;
|
|
2555
|
-
for (
|
|
2909
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2556
2910
|
if (myToken !== pumpToken || destroyed) return;
|
|
2557
|
-
|
|
2558
|
-
f,
|
|
2559
|
-
() => {
|
|
2560
|
-
const ts = syntheticAudioUs;
|
|
2561
|
-
const samples2 = f.nb_samples ?? 1024;
|
|
2562
|
-
const sampleRate = f.sample_rate ?? 44100;
|
|
2563
|
-
syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
|
|
2564
|
-
return ts;
|
|
2565
|
-
},
|
|
2566
|
-
audioTimeBase
|
|
2567
|
-
);
|
|
2911
|
+
const f = frames[i];
|
|
2568
2912
|
const samples = chunkHZUVMXBN_cjs.libavFrameToInterleavedFloat32(f);
|
|
2569
2913
|
if (samples) {
|
|
2570
|
-
|
|
2914
|
+
const pts = pktPtsSec[i] ?? null;
|
|
2915
|
+
if (isDebug3()) {
|
|
2916
|
+
const dur = samples.data.length / samples.channels / samples.sampleRate;
|
|
2917
|
+
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}`);
|
|
2918
|
+
}
|
|
2919
|
+
opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
|
|
2571
2920
|
audioFramesDecoded++;
|
|
2572
2921
|
}
|
|
2573
2922
|
}
|
|
@@ -2670,13 +3019,17 @@ async function startDecoder(opts) {
|
|
|
2670
3019
|
} catch {
|
|
2671
3020
|
}
|
|
2672
3021
|
await flushBSF();
|
|
2673
|
-
|
|
2674
|
-
|
|
3022
|
+
lastContentUs = -1;
|
|
3023
|
+
lastEmittedPtsUs = -1;
|
|
3024
|
+
firstValidPtsLoggedSinceSeek = false;
|
|
2675
3025
|
pumpRunning = pumpLoop(newToken).catch(
|
|
2676
3026
|
(err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
|
|
2677
3027
|
);
|
|
2678
3028
|
},
|
|
2679
3029
|
async seek(timeSec) {
|
|
3030
|
+
if (isDebug3()) {
|
|
3031
|
+
console.log(`[SEEK] target=${timeSec.toFixed(3)}s (${(timeSec * 1e3).toFixed(0)}ms) wall=${performance.now().toFixed(0)}`);
|
|
3032
|
+
}
|
|
2680
3033
|
const newToken = ++pumpToken;
|
|
2681
3034
|
if (pumpRunning) {
|
|
2682
3035
|
try {
|
|
@@ -2707,8 +3060,14 @@ async function startDecoder(opts) {
|
|
|
2707
3060
|
} catch {
|
|
2708
3061
|
}
|
|
2709
3062
|
await flushBSF();
|
|
2710
|
-
|
|
2711
|
-
|
|
3063
|
+
lastContentUs = -1;
|
|
3064
|
+
lastEmittedPtsUs = -1;
|
|
3065
|
+
firstValidPtsLoggedSinceSeek = false;
|
|
3066
|
+
seenFirstAudioPacketSinceSeek = false;
|
|
3067
|
+
seekTargetSec = timeSec;
|
|
3068
|
+
diagPktsLoggedSinceSeek = 0;
|
|
3069
|
+
diagFramesLoggedSinceSeek = 0;
|
|
3070
|
+
diagFrameKeysDumped = false;
|
|
2712
3071
|
pumpRunning = pumpLoop(newToken).catch(
|
|
2713
3072
|
(err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
|
|
2714
3073
|
);
|
|
@@ -2722,7 +3081,24 @@ async function startDecoder(opts) {
|
|
|
2722
3081
|
packetsRead,
|
|
2723
3082
|
videoFramesDecoded,
|
|
2724
3083
|
audioFramesDecoded,
|
|
3084
|
+
// Throughput instrumentation — the stats panel turns these into
|
|
3085
|
+
// "decode fps actual / realtime target" and shows slowest batch
|
|
3086
|
+
// + producer throttle share.
|
|
3087
|
+
videoDecodeMsTotal,
|
|
3088
|
+
videoDecodeBatches,
|
|
3089
|
+
audioDecodeMsTotal,
|
|
3090
|
+
audioDecodeBatches,
|
|
3091
|
+
readMsTotal,
|
|
3092
|
+
readBatches,
|
|
3093
|
+
pumpThrottleMsTotal,
|
|
3094
|
+
pumpThrottleEntries,
|
|
3095
|
+
slowestVideoBatchMs,
|
|
3096
|
+
newestVideoPtsMs: Math.round(newestVideoPtsUs / 1e3),
|
|
3097
|
+
ptsRegressions,
|
|
3098
|
+
worstPtsRegressionMs,
|
|
3099
|
+
sourceFps: videoFps,
|
|
2725
3100
|
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
3101
|
+
bsfMissing: bsfRequiredButMissing ? ["mpeg4_unpack_bframes"] : [],
|
|
2726
3102
|
// Confirmed transport info: once prepareLibavInput returns
|
|
2727
3103
|
// successfully, we *know* whether the source is http-range (probe
|
|
2728
3104
|
// succeeded and returned 206) or in-memory blob. Diagnostics hoists
|
|
@@ -3081,7 +3457,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3081
3457
|
const bootstrapStart = performance.now();
|
|
3082
3458
|
try {
|
|
3083
3459
|
chunkG4APZMCP_cjs.dbg.info("bootstrap", "start");
|
|
3084
|
-
const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () =>
|
|
3460
|
+
const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => chunkVLI3Y6IJ_cjs.probe(this.options.source, this.transport));
|
|
3085
3461
|
chunkG4APZMCP_cjs.dbg.info(
|
|
3086
3462
|
"probe",
|
|
3087
3463
|
`container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
|
|
@@ -3551,5 +3927,5 @@ exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
|
|
|
3551
3927
|
exports.UnifiedPlayer = UnifiedPlayer;
|
|
3552
3928
|
exports.classifyContext = classifyContext;
|
|
3553
3929
|
exports.createPlayer = createPlayer;
|
|
3554
|
-
//# sourceMappingURL=chunk-
|
|
3555
|
-
//# sourceMappingURL=chunk-
|
|
3930
|
+
//# sourceMappingURL=chunk-OFJYEITB.cjs.map
|
|
3931
|
+
//# sourceMappingURL=chunk-OFJYEITB.cjs.map
|