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