avbridge 2.12.1 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/README.md +33 -0
- package/dist/{chunk-UM6WCSGL.cjs → chunk-OFJYEITB.cjs} +356 -91
- package/dist/chunk-OFJYEITB.cjs.map +1 -0
- package/dist/{chunk-BN7BRTLY.js → chunk-VOC24LYF.js} +357 -92
- package/dist/chunk-VOC24LYF.js.map +1 -0
- package/dist/element-browser.js +354 -111
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +2 -2
- package/dist/element.js +1 -1
- package/dist/index.cjs +8 -8
- package/dist/index.js +1 -1
- package/dist/player.cjs +457 -135
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +35 -4
- package/dist/player.d.ts +35 -4
- package/dist/player.js +457 -135
- package/dist/player.js.map +1 -1
- package/package.json +1 -1
- package/src/element/avbridge-player.ts +136 -28
- package/src/strategies/fallback/audio-output.ts +164 -35
- package/src/strategies/fallback/decoder.ts +336 -58
- package/src/strategies/fallback/video-renderer.ts +176 -34
- package/src/strategies/hybrid/decoder.ts +22 -19
- package/src/strategies/remux/pipeline.ts +12 -3
- package/dist/chunk-BN7BRTLY.js.map +0 -1
- package/dist/chunk-UM6WCSGL.cjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SubtitleResourceBag, discoverSidecars, attachSubtitleTracks, SubtitleOverlay } from './chunk-EDDWAN2L.js';
|
|
2
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. */
|
|
@@ -1121,13 +1138,17 @@ var VideoRenderer = class {
|
|
|
1121
1138
|
return this.queue.length;
|
|
1122
1139
|
}
|
|
1123
1140
|
/**
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
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.
|
|
1129
1150
|
*/
|
|
1130
|
-
queueHighWater =
|
|
1151
|
+
queueHighWater = 256;
|
|
1131
1152
|
enqueue(frame) {
|
|
1132
1153
|
if (this.destroyed) {
|
|
1133
1154
|
frame.close();
|
|
@@ -1138,7 +1159,7 @@ var VideoRenderer = class {
|
|
|
1138
1159
|
if (this.queue.length === 1 && this.framesPainted === 0) {
|
|
1139
1160
|
this.resolveFirstFrame();
|
|
1140
1161
|
}
|
|
1141
|
-
while (this.queue.length >
|
|
1162
|
+
while (this.queue.length > this.queueHighWater + 8) {
|
|
1142
1163
|
this.queue.shift()?.close();
|
|
1143
1164
|
this.framesDroppedOverflow++;
|
|
1144
1165
|
}
|
|
@@ -1220,12 +1241,9 @@ var VideoRenderer = class {
|
|
|
1220
1241
|
if (this.queue.length === 0) return;
|
|
1221
1242
|
const playing = this.clock.isPlaying();
|
|
1222
1243
|
if (!playing) {
|
|
1223
|
-
if (!this.prerolled) {
|
|
1224
|
-
const head = this.queue.shift();
|
|
1225
|
-
this.paint(head);
|
|
1226
|
-
head.close();
|
|
1244
|
+
if (!this.prerolled && this.queue.length > 0) {
|
|
1227
1245
|
this.prerolled = true;
|
|
1228
|
-
this.
|
|
1246
|
+
this.paint(this.queue[0]);
|
|
1229
1247
|
}
|
|
1230
1248
|
return;
|
|
1231
1249
|
}
|
|
@@ -1234,14 +1252,29 @@ var VideoRenderer = class {
|
|
|
1234
1252
|
const hasPts = headTs > 0 || this.queue.length > 1;
|
|
1235
1253
|
if (hasPts) {
|
|
1236
1254
|
const wallNow2 = performance.now();
|
|
1237
|
-
if (!this.ptsCalibrated
|
|
1238
|
-
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;
|
|
1239
1259
|
this.ptsCalibrated = true;
|
|
1240
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
|
+
}
|
|
1241
1275
|
}
|
|
1242
1276
|
const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
|
|
1243
|
-
const
|
|
1244
|
-
const deadlineUs = audioNowUs + frameDurationUs;
|
|
1277
|
+
const deadlineUs = audioNowUs;
|
|
1245
1278
|
let bestIdx = -1;
|
|
1246
1279
|
for (let i = 0; i < this.queue.length; i++) {
|
|
1247
1280
|
const ts = this.queue[i].timestamp ?? 0;
|
|
@@ -1269,19 +1302,20 @@ var VideoRenderer = class {
|
|
|
1269
1302
|
return;
|
|
1270
1303
|
}
|
|
1271
1304
|
const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
|
|
1272
|
-
const dropThresholdUs = _relaxDrop ? audioNowUs - 60 * 1e6 : audioNowUs - frameDurationUs * 2;
|
|
1273
1305
|
let dropped = 0;
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1306
|
+
const initialBestIdx = bestIdx;
|
|
1307
|
+
if (!_relaxDrop) {
|
|
1308
|
+
while (bestIdx > 0) {
|
|
1277
1309
|
this.queue.shift()?.close();
|
|
1278
1310
|
this.framesDroppedLate++;
|
|
1279
1311
|
bestIdx--;
|
|
1280
1312
|
dropped++;
|
|
1281
|
-
} else {
|
|
1282
|
-
break;
|
|
1283
1313
|
}
|
|
1284
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
|
+
}
|
|
1285
1319
|
this.ticksPainted++;
|
|
1286
1320
|
if (isDebug()) {
|
|
1287
1321
|
const now = performance.now();
|
|
@@ -1317,6 +1351,33 @@ var VideoRenderer = class {
|
|
|
1317
1351
|
}
|
|
1318
1352
|
try {
|
|
1319
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;
|
|
1320
1381
|
this.framesPainted++;
|
|
1321
1382
|
} catch (err) {
|
|
1322
1383
|
if (this.framesPainted === 0 && this.framesDroppedLate === 0) {
|
|
@@ -1329,6 +1390,7 @@ var VideoRenderer = class {
|
|
|
1329
1390
|
const count = this.queue.length;
|
|
1330
1391
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
1331
1392
|
this.prerolled = false;
|
|
1393
|
+
this.hasLastPaintedPts = false;
|
|
1332
1394
|
this.ptsCalibrated = false;
|
|
1333
1395
|
this.hasEverEnqueuedSinceFlush = false;
|
|
1334
1396
|
if (isDebug() && count > 0) {
|
|
@@ -1369,6 +1431,9 @@ var VideoRenderer = class {
|
|
|
1369
1431
|
};
|
|
1370
1432
|
|
|
1371
1433
|
// src/strategies/fallback/audio-output.ts
|
|
1434
|
+
function isDebug2() {
|
|
1435
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
1436
|
+
}
|
|
1372
1437
|
var AudioOutput = class {
|
|
1373
1438
|
ctx;
|
|
1374
1439
|
gain;
|
|
@@ -1390,6 +1455,16 @@ var AudioOutput = class {
|
|
|
1390
1455
|
mediaTimeOfNext = 0;
|
|
1391
1456
|
/** Anchor: media time `mediaTimeOfAnchor` corresponds to ctx time `ctxTimeAtAnchor`. */
|
|
1392
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;
|
|
1393
1468
|
ctxTimeAtAnchor = 0;
|
|
1394
1469
|
pendingQueue = [];
|
|
1395
1470
|
framesScheduled = 0;
|
|
@@ -1462,10 +1537,16 @@ var AudioOutput = class {
|
|
|
1462
1537
|
return this.mediaTimeOfAnchor;
|
|
1463
1538
|
}
|
|
1464
1539
|
if (this.state === "playing") {
|
|
1540
|
+
if (this.firstAudibleCtxStart < 0) {
|
|
1541
|
+
return this.mediaTimeOfAnchor;
|
|
1542
|
+
}
|
|
1465
1543
|
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
|
|
1466
1544
|
}
|
|
1467
1545
|
return this.mediaTimeOfAnchor;
|
|
1468
1546
|
}
|
|
1547
|
+
anchorTime() {
|
|
1548
|
+
return this.mediaTimeOfAnchor;
|
|
1549
|
+
}
|
|
1469
1550
|
isPlaying() {
|
|
1470
1551
|
return this.state === "playing";
|
|
1471
1552
|
}
|
|
@@ -1492,18 +1573,81 @@ var AudioOutput = class {
|
|
|
1492
1573
|
* Schedule a chunk of decoded samples. Queues internally while idle (cold
|
|
1493
1574
|
* start or post-seek), schedules directly to the audio graph while playing.
|
|
1494
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.
|
|
1495
1586
|
*/
|
|
1496
|
-
schedule(samples, channels, sampleRate) {
|
|
1587
|
+
schedule(samples, channels, sampleRate, ptsSec) {
|
|
1497
1588
|
if (this.destroyed || this.noAudio) return;
|
|
1498
1589
|
const frameCount = samples.length / channels;
|
|
1499
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
|
+
}
|
|
1500
1595
|
if (this.state === "idle" || this.state === "paused") {
|
|
1501
|
-
this.pendingQueue.push({
|
|
1596
|
+
this.pendingQueue.push({
|
|
1597
|
+
samples,
|
|
1598
|
+
channels,
|
|
1599
|
+
sampleRate,
|
|
1600
|
+
frameCount,
|
|
1601
|
+
durationSec,
|
|
1602
|
+
ptsSec: hasPts ? ptsSec : null
|
|
1603
|
+
});
|
|
1502
1604
|
return;
|
|
1503
1605
|
}
|
|
1504
|
-
this.scheduleNow(
|
|
1606
|
+
this.scheduleNow(
|
|
1607
|
+
samples,
|
|
1608
|
+
channels,
|
|
1609
|
+
sampleRate,
|
|
1610
|
+
frameCount,
|
|
1611
|
+
hasPts ? ptsSec : null
|
|
1612
|
+
);
|
|
1505
1613
|
}
|
|
1506
|
-
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
|
+
}
|
|
1507
1651
|
const buffer = this.ctx.createBuffer(channels, frameCount, sampleRate);
|
|
1508
1652
|
for (let ch = 0; ch < channels; ch++) {
|
|
1509
1653
|
const channelData = buffer.getChannelData(ch);
|
|
@@ -1515,14 +1659,7 @@ var AudioOutput = class {
|
|
|
1515
1659
|
node.buffer = buffer;
|
|
1516
1660
|
node.connect(this.gain);
|
|
1517
1661
|
if (this._rate !== 1) node.playbackRate.value = this._rate;
|
|
1518
|
-
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
1519
|
-
if (ctxStart < this.ctx.currentTime) {
|
|
1520
|
-
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1521
|
-
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
1522
|
-
ctxStart = this.ctx.currentTime;
|
|
1523
|
-
}
|
|
1524
1662
|
node.start(ctxStart);
|
|
1525
|
-
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
1526
1663
|
this.framesScheduled++;
|
|
1527
1664
|
}
|
|
1528
1665
|
// ── Lifecycle ─────────────────────────────────────────────────────────
|
|
@@ -1547,12 +1684,15 @@ var AudioOutput = class {
|
|
|
1547
1684
|
} catch {
|
|
1548
1685
|
}
|
|
1549
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
|
+
}
|
|
1550
1690
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1551
1691
|
this.state = "playing";
|
|
1552
1692
|
const drain2 = this.pendingQueue;
|
|
1553
1693
|
this.pendingQueue = [];
|
|
1554
1694
|
for (const c of drain2) {
|
|
1555
|
-
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
1695
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
|
|
1556
1696
|
}
|
|
1557
1697
|
return;
|
|
1558
1698
|
}
|
|
@@ -1560,10 +1700,13 @@ var AudioOutput = class {
|
|
|
1560
1700
|
this.ctxTimeAtAnchor = this.ctx.currentTime + STARTUP_DELAY;
|
|
1561
1701
|
this.mediaTimeOfNext = this.mediaTimeOfAnchor;
|
|
1562
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
|
+
}
|
|
1563
1706
|
const drain = this.pendingQueue;
|
|
1564
1707
|
this.pendingQueue = [];
|
|
1565
1708
|
for (const c of drain) {
|
|
1566
|
-
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
1709
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
|
|
1567
1710
|
}
|
|
1568
1711
|
}
|
|
1569
1712
|
/** Pause playback. Suspends the audio context. */
|
|
@@ -1589,6 +1732,9 @@ var AudioOutput = class {
|
|
|
1589
1732
|
* supplying new samples) and then call `start()` to resume playback.
|
|
1590
1733
|
*/
|
|
1591
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
|
+
}
|
|
1592
1738
|
if (this.noAudio) {
|
|
1593
1739
|
this.pendingQueue = [];
|
|
1594
1740
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
@@ -1607,6 +1753,7 @@ var AudioOutput = class {
|
|
|
1607
1753
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
1608
1754
|
this.mediaTimeOfNext = newMediaTime;
|
|
1609
1755
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1756
|
+
this.firstAudibleCtxStart = -1;
|
|
1610
1757
|
this.state = "idle";
|
|
1611
1758
|
if (this.ctx.state === "running") {
|
|
1612
1759
|
await this.ctx.suspend();
|
|
@@ -1775,7 +1922,6 @@ async function startHybridDecoder(opts) {
|
|
|
1775
1922
|
let videoChunksFed = 0;
|
|
1776
1923
|
let bufferedUntilSec = 0;
|
|
1777
1924
|
let syntheticVideoUs = 0;
|
|
1778
|
-
let syntheticAudioUs = 0;
|
|
1779
1925
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
1780
1926
|
const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
|
|
1781
1927
|
const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
|
|
@@ -1807,7 +1953,13 @@ async function startHybridDecoder(opts) {
|
|
|
1807
1953
|
}
|
|
1808
1954
|
}
|
|
1809
1955
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
1810
|
-
await decodeAudioBatch(
|
|
1956
|
+
await decodeAudioBatch(
|
|
1957
|
+
audioPackets,
|
|
1958
|
+
myToken,
|
|
1959
|
+
/*flush*/
|
|
1960
|
+
false,
|
|
1961
|
+
audioTimeBase
|
|
1962
|
+
);
|
|
1811
1963
|
}
|
|
1812
1964
|
if (myToken !== pumpToken || destroyed) return;
|
|
1813
1965
|
await new Promise((r) => setTimeout(r, 0));
|
|
@@ -1854,8 +2006,11 @@ async function startHybridDecoder(opts) {
|
|
|
1854
2006
|
}
|
|
1855
2007
|
}
|
|
1856
2008
|
}
|
|
1857
|
-
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
2009
|
+
async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
|
|
1858
2010
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
2011
|
+
const pktPtsSec = pkts.map(
|
|
2012
|
+
(p) => tb ? packetPtsSec(p, tb) : null
|
|
2013
|
+
);
|
|
1859
2014
|
const AUDIO_SUB_BATCH = 4;
|
|
1860
2015
|
let allFrames = [];
|
|
1861
2016
|
for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
|
|
@@ -1893,22 +2048,13 @@ async function startHybridDecoder(opts) {
|
|
|
1893
2048
|
}
|
|
1894
2049
|
if (myToken !== pumpToken || destroyed) return;
|
|
1895
2050
|
const frames = allFrames;
|
|
1896
|
-
for (
|
|
2051
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1897
2052
|
if (myToken !== pumpToken || destroyed) return;
|
|
1898
|
-
|
|
1899
|
-
f,
|
|
1900
|
-
() => {
|
|
1901
|
-
const ts = syntheticAudioUs;
|
|
1902
|
-
const samples2 = f.nb_samples ?? 1024;
|
|
1903
|
-
const sampleRate = f.sample_rate ?? 44100;
|
|
1904
|
-
syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
|
|
1905
|
-
return ts;
|
|
1906
|
-
},
|
|
1907
|
-
audioTimeBase
|
|
1908
|
-
);
|
|
2053
|
+
const f = frames[i];
|
|
1909
2054
|
const samples = libavFrameToInterleavedFloat32(f);
|
|
1910
2055
|
if (samples) {
|
|
1911
|
-
|
|
2056
|
+
const pts = pktPtsSec[i] ?? null;
|
|
2057
|
+
opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
|
|
1912
2058
|
audioFramesDecoded++;
|
|
1913
2059
|
}
|
|
1914
2060
|
}
|
|
@@ -2018,7 +2164,6 @@ async function startHybridDecoder(opts) {
|
|
|
2018
2164
|
}
|
|
2019
2165
|
await flushBSF();
|
|
2020
2166
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
2021
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
2022
2167
|
pumpRunning = pumpLoop(newToken).catch(
|
|
2023
2168
|
(err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
|
|
2024
2169
|
);
|
|
@@ -2057,7 +2202,6 @@ async function startHybridDecoder(opts) {
|
|
|
2057
2202
|
}
|
|
2058
2203
|
await flushBSF();
|
|
2059
2204
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
2060
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
2061
2205
|
pumpRunning = pumpLoop(newToken).catch(
|
|
2062
2206
|
(err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
|
|
2063
2207
|
);
|
|
@@ -2310,6 +2454,9 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2310
2454
|
}
|
|
2311
2455
|
|
|
2312
2456
|
// src/strategies/fallback/decoder.ts
|
|
2457
|
+
function isDebug3() {
|
|
2458
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
2459
|
+
}
|
|
2313
2460
|
async function startDecoder(opts) {
|
|
2314
2461
|
const variant = "avbridge";
|
|
2315
2462
|
const libav = await loadLibav(variant);
|
|
@@ -2441,8 +2588,15 @@ async function startDecoder(opts) {
|
|
|
2441
2588
|
let watchdogSlowSinceMs = 0;
|
|
2442
2589
|
let watchdogSlowWarned = false;
|
|
2443
2590
|
let watchdogOverflowWarned = false;
|
|
2444
|
-
let
|
|
2445
|
-
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;
|
|
2446
2600
|
let videoDecodeMsTotal = 0;
|
|
2447
2601
|
let audioDecodeMsTotal = 0;
|
|
2448
2602
|
let videoDecodeBatches = 0;
|
|
@@ -2481,6 +2635,18 @@ async function startDecoder(opts) {
|
|
|
2481
2635
|
for (const pkt of videoPackets) {
|
|
2482
2636
|
const sec = packetPtsSec(pkt, videoTimeBase);
|
|
2483
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
|
+
}
|
|
2484
2650
|
}
|
|
2485
2651
|
}
|
|
2486
2652
|
if (audioPackets && audioTimeBase) {
|
|
@@ -2488,9 +2654,25 @@ async function startDecoder(opts) {
|
|
|
2488
2654
|
const sec = packetPtsSec(pkt, audioTimeBase);
|
|
2489
2655
|
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2490
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
|
+
}
|
|
2491
2667
|
}
|
|
2492
2668
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2493
|
-
await decodeAudioBatch(
|
|
2669
|
+
await decodeAudioBatch(
|
|
2670
|
+
audioPackets,
|
|
2671
|
+
myToken,
|
|
2672
|
+
/*flush*/
|
|
2673
|
+
false,
|
|
2674
|
+
audioTimeBase
|
|
2675
|
+
);
|
|
2494
2676
|
}
|
|
2495
2677
|
if (myToken !== pumpToken || destroyed) return;
|
|
2496
2678
|
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
@@ -2534,7 +2716,7 @@ async function startDecoder(opts) {
|
|
|
2534
2716
|
{
|
|
2535
2717
|
const _throttleStart = performance.now();
|
|
2536
2718
|
let _throttled = false;
|
|
2537
|
-
while (!destroyed && myToken === pumpToken &&
|
|
2719
|
+
while (!destroyed && myToken === pumpToken && opts.audio.bufferAhead() > 2) {
|
|
2538
2720
|
_throttled = true;
|
|
2539
2721
|
await new Promise((r) => setTimeout(r, 50));
|
|
2540
2722
|
}
|
|
@@ -2589,18 +2771,83 @@ async function startDecoder(opts) {
|
|
|
2589
2771
|
if (myToken !== pumpToken || destroyed) return;
|
|
2590
2772
|
for (const f of frames) {
|
|
2591
2773
|
if (myToken !== pumpToken || destroyed) return;
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
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;
|
|
2602
2848
|
if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
|
|
2603
2849
|
if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
|
|
2850
|
+
_diagLog("REGRESSED-DROP", _fPts, _diagSanFallbackFired);
|
|
2604
2851
|
ptsRegressions++;
|
|
2605
2852
|
const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
|
|
2606
2853
|
if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
|
|
@@ -2612,19 +2859,34 @@ async function startDecoder(opts) {
|
|
|
2612
2859
|
continue;
|
|
2613
2860
|
}
|
|
2614
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
|
+
}
|
|
2615
2867
|
try {
|
|
2616
2868
|
const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
|
|
2617
|
-
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
|
+
}
|
|
2618
2876
|
videoFramesDecoded++;
|
|
2619
2877
|
} catch (err) {
|
|
2620
2878
|
if (videoFramesDecoded === 0) {
|
|
2621
2879
|
console.warn("[avbridge] laFrameToVideoFrame failed:", err);
|
|
2622
2880
|
}
|
|
2881
|
+
_diagLog("BRIDGE-ERROR", _fPts, _diagSanFallbackFired);
|
|
2623
2882
|
}
|
|
2624
2883
|
}
|
|
2625
2884
|
}
|
|
2626
|
-
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
2885
|
+
async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
|
|
2627
2886
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
2887
|
+
const pktPtsSec = pkts.map(
|
|
2888
|
+
(p) => tb ? packetPtsSec(p, tb) : null
|
|
2889
|
+
);
|
|
2628
2890
|
let frames;
|
|
2629
2891
|
const _t0 = performance.now();
|
|
2630
2892
|
try {
|
|
@@ -2642,22 +2904,17 @@ async function startDecoder(opts) {
|
|
|
2642
2904
|
audioDecodeMsTotal += performance.now() - _t0;
|
|
2643
2905
|
audioDecodeBatches++;
|
|
2644
2906
|
if (myToken !== pumpToken || destroyed) return;
|
|
2645
|
-
for (
|
|
2907
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2646
2908
|
if (myToken !== pumpToken || destroyed) return;
|
|
2647
|
-
|
|
2648
|
-
f,
|
|
2649
|
-
() => {
|
|
2650
|
-
const ts = syntheticAudioUs;
|
|
2651
|
-
const samples2 = f.nb_samples ?? 1024;
|
|
2652
|
-
const sampleRate = f.sample_rate ?? 44100;
|
|
2653
|
-
syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
|
|
2654
|
-
return ts;
|
|
2655
|
-
},
|
|
2656
|
-
audioTimeBase
|
|
2657
|
-
);
|
|
2909
|
+
const f = frames[i];
|
|
2658
2910
|
const samples = libavFrameToInterleavedFloat32(f);
|
|
2659
2911
|
if (samples) {
|
|
2660
|
-
|
|
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);
|
|
2661
2918
|
audioFramesDecoded++;
|
|
2662
2919
|
}
|
|
2663
2920
|
}
|
|
@@ -2760,14 +3017,17 @@ async function startDecoder(opts) {
|
|
|
2760
3017
|
} catch {
|
|
2761
3018
|
}
|
|
2762
3019
|
await flushBSF();
|
|
2763
|
-
|
|
2764
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
3020
|
+
lastContentUs = -1;
|
|
2765
3021
|
lastEmittedPtsUs = -1;
|
|
3022
|
+
firstValidPtsLoggedSinceSeek = false;
|
|
2766
3023
|
pumpRunning = pumpLoop(newToken).catch(
|
|
2767
3024
|
(err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
|
|
2768
3025
|
);
|
|
2769
3026
|
},
|
|
2770
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
|
+
}
|
|
2771
3031
|
const newToken = ++pumpToken;
|
|
2772
3032
|
if (pumpRunning) {
|
|
2773
3033
|
try {
|
|
@@ -2798,9 +3058,14 @@ async function startDecoder(opts) {
|
|
|
2798
3058
|
} catch {
|
|
2799
3059
|
}
|
|
2800
3060
|
await flushBSF();
|
|
2801
|
-
|
|
2802
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
3061
|
+
lastContentUs = -1;
|
|
2803
3062
|
lastEmittedPtsUs = -1;
|
|
3063
|
+
firstValidPtsLoggedSinceSeek = false;
|
|
3064
|
+
seenFirstAudioPacketSinceSeek = false;
|
|
3065
|
+
seekTargetSec = timeSec;
|
|
3066
|
+
diagPktsLoggedSinceSeek = 0;
|
|
3067
|
+
diagFramesLoggedSinceSeek = 0;
|
|
3068
|
+
diagFrameKeysDumped = false;
|
|
2804
3069
|
pumpRunning = pumpLoop(newToken).catch(
|
|
2805
3070
|
(err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
|
|
2806
3071
|
);
|
|
@@ -3654,5 +3919,5 @@ function defaultFallbackChain(strategy) {
|
|
|
3654
3919
|
}
|
|
3655
3920
|
|
|
3656
3921
|
export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext, createPlayer };
|
|
3657
|
-
//# sourceMappingURL=chunk-
|
|
3658
|
-
//# sourceMappingURL=chunk-
|
|
3922
|
+
//# sourceMappingURL=chunk-VOC24LYF.js.map
|
|
3923
|
+
//# sourceMappingURL=chunk-VOC24LYF.js.map
|