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
|
@@ -791,22 +791,25 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
791
791
|
}
|
|
792
792
|
}
|
|
793
793
|
let mimePromise = null;
|
|
794
|
+
const myToken = pumpToken;
|
|
794
795
|
const writable = new WritableStream({
|
|
795
796
|
write: async (chunk) => {
|
|
796
|
-
if (destroyed) return;
|
|
797
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
797
798
|
if (!sink) {
|
|
798
799
|
const mime = await (mimePromise ??= output.getMimeType());
|
|
800
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
799
801
|
sink = new MseSink({ mime, video });
|
|
800
802
|
await sink.ready();
|
|
803
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
801
804
|
if (pendingStartTime > 0) {
|
|
802
805
|
sink.invalidate(pendingStartTime);
|
|
803
806
|
}
|
|
804
807
|
sink.setPlayOnSeek(pendingAutoPlay);
|
|
805
808
|
}
|
|
806
|
-
while (sink && !destroyed && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
|
|
809
|
+
while (sink && !destroyed && pumpToken === myToken && (sink.queueLength() > 10 || sink.bufferedAhead() > 60 || sink.totalBuffered() > 120)) {
|
|
807
810
|
await new Promise((r) => setTimeout(r, 500));
|
|
808
811
|
}
|
|
809
|
-
if (destroyed) return;
|
|
812
|
+
if (destroyed || pumpToken !== myToken) return;
|
|
810
813
|
sink.append(chunk.data);
|
|
811
814
|
stats.bytesWritten += chunk.data.byteLength;
|
|
812
815
|
stats.fragments++;
|
|
@@ -1073,7 +1076,21 @@ var VideoRenderer = class {
|
|
|
1073
1076
|
framesPainted = 0;
|
|
1074
1077
|
framesDroppedLate = 0;
|
|
1075
1078
|
framesDroppedOverflow = 0;
|
|
1079
|
+
/** True once the head frame has been painted as a pre-roll poster
|
|
1080
|
+
* since the last flush. Used to ensure pre-roll paints exactly one
|
|
1081
|
+
* frame (held static) during the post-seek discard window. */
|
|
1076
1082
|
prerolled = false;
|
|
1083
|
+
/** PTS (µs) of the most recently painted frame. Used as the calibration
|
|
1084
|
+
* reference on the first post-flush snap: the pre-roll path paints one
|
|
1085
|
+
* frame *before* PTS-based playback starts, so the queue head's PTS at
|
|
1086
|
+
* first PTS-based paint is the *next* frame, off by one frameDur from
|
|
1087
|
+
* the actually-displayed frame. Calibrating against the painted frame
|
|
1088
|
+
* instead of the queue head removes that one-frame offset and yields
|
|
1089
|
+
* calib ≈ 0 instead of +frameDur. */
|
|
1090
|
+
lastPaintedPtsUs = 0;
|
|
1091
|
+
hasLastPaintedPts = false;
|
|
1092
|
+
/** Audio-clock reading (ms) at the previous paint, for overlay Δaud. */
|
|
1093
|
+
lastPaintAudMs = 0;
|
|
1077
1094
|
/** Wall-clock time of the last paint, in ms (performance.now()). */
|
|
1078
1095
|
lastPaintWall = 0;
|
|
1079
1096
|
/** Minimum ms between paints — paces video at roughly source fps. */
|
|
@@ -1123,13 +1140,17 @@ var VideoRenderer = class {
|
|
|
1123
1140
|
return this.queue.length;
|
|
1124
1141
|
}
|
|
1125
1142
|
/**
|
|
1126
|
-
*
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1143
|
+
* Cap the decoder may fill the queue up to. Used by the decoder's
|
|
1144
|
+
* enqueue-side discard logic (it closes new frames instead of pushing
|
|
1145
|
+
* them when this is reached). Sized so a long post-seek catch-up
|
|
1146
|
+
* fits — the decoder produces frames at PTS T_kf onwards rapidly
|
|
1147
|
+
* while the demuxer is chewing through pre-target audio; if the
|
|
1148
|
+
* queue can hold the whole post-seek burst, the renderer plays
|
|
1149
|
+
* smoothly from pre-roll without a frozen-video gap when audio.start
|
|
1150
|
+
* fires. At ~340 KB per SD frame the cap is ~85 MB peak; at HD it's
|
|
1151
|
+
* larger but still bounded.
|
|
1131
1152
|
*/
|
|
1132
|
-
queueHighWater =
|
|
1153
|
+
queueHighWater = 256;
|
|
1133
1154
|
enqueue(frame) {
|
|
1134
1155
|
if (this.destroyed) {
|
|
1135
1156
|
frame.close();
|
|
@@ -1140,7 +1161,7 @@ var VideoRenderer = class {
|
|
|
1140
1161
|
if (this.queue.length === 1 && this.framesPainted === 0) {
|
|
1141
1162
|
this.resolveFirstFrame();
|
|
1142
1163
|
}
|
|
1143
|
-
while (this.queue.length >
|
|
1164
|
+
while (this.queue.length > this.queueHighWater + 8) {
|
|
1144
1165
|
this.queue.shift()?.close();
|
|
1145
1166
|
this.framesDroppedOverflow++;
|
|
1146
1167
|
}
|
|
@@ -1222,12 +1243,9 @@ var VideoRenderer = class {
|
|
|
1222
1243
|
if (this.queue.length === 0) return;
|
|
1223
1244
|
const playing = this.clock.isPlaying();
|
|
1224
1245
|
if (!playing) {
|
|
1225
|
-
if (!this.prerolled) {
|
|
1226
|
-
const head = this.queue.shift();
|
|
1227
|
-
this.paint(head);
|
|
1228
|
-
head.close();
|
|
1246
|
+
if (!this.prerolled && this.queue.length > 0) {
|
|
1229
1247
|
this.prerolled = true;
|
|
1230
|
-
this.
|
|
1248
|
+
this.paint(this.queue[0]);
|
|
1231
1249
|
}
|
|
1232
1250
|
return;
|
|
1233
1251
|
}
|
|
@@ -1236,14 +1254,29 @@ var VideoRenderer = class {
|
|
|
1236
1254
|
const hasPts = headTs > 0 || this.queue.length > 1;
|
|
1237
1255
|
if (hasPts) {
|
|
1238
1256
|
const wallNow2 = performance.now();
|
|
1239
|
-
if (!this.ptsCalibrated
|
|
1240
|
-
this.
|
|
1257
|
+
if (!this.ptsCalibrated) {
|
|
1258
|
+
const anchorUs = (this.clock.anchorTime?.() ?? this.clock.now()) * 1e6;
|
|
1259
|
+
const referencePtsUs = this.hasLastPaintedPts ? this.lastPaintedPtsUs : headTs;
|
|
1260
|
+
this.ptsCalibrationUs = referencePtsUs - anchorUs;
|
|
1241
1261
|
this.ptsCalibrated = true;
|
|
1242
1262
|
this.lastCalibrationWall = wallNow2;
|
|
1263
|
+
if (isDebug()) {
|
|
1264
|
+
console.log(
|
|
1265
|
+
`[avbridge:renderer] CALIB-FIRST audioAnchor=${(anchorUs / 1e3).toFixed(1)}ms prerolledPTS=${this.hasLastPaintedPts ? (this.lastPaintedPtsUs / 1e3).toFixed(1) : "n/a"}ms queueHeadPTS=${(headTs / 1e3).toFixed(1)}ms rawAudioNow=${(rawAudioNowUs / 1e3).toFixed(1)}ms \u2192 calib=${(this.ptsCalibrationUs / 1e3).toFixed(1)}ms`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
} else if (wallNow2 - this.lastCalibrationWall > 1e4) {
|
|
1269
|
+
const oldCalib = this.ptsCalibrationUs;
|
|
1270
|
+
this.ptsCalibrationUs = headTs - rawAudioNowUs;
|
|
1271
|
+
this.lastCalibrationWall = wallNow2;
|
|
1272
|
+
if (isDebug()) {
|
|
1273
|
+
console.log(
|
|
1274
|
+
`[avbridge:renderer] CALIB-RESNAP headPTS=${(headTs / 1e3).toFixed(1)}ms rawAudioNow=${(rawAudioNowUs / 1e3).toFixed(1)}ms calib ${(oldCalib / 1e3).toFixed(1)}ms \u2192 ${(this.ptsCalibrationUs / 1e3).toFixed(1)}ms (\u0394=${((this.ptsCalibrationUs - oldCalib) / 1e3).toFixed(1)}ms after 10s)`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1243
1277
|
}
|
|
1244
1278
|
const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
|
|
1245
|
-
const
|
|
1246
|
-
const deadlineUs = audioNowUs + frameDurationUs;
|
|
1279
|
+
const deadlineUs = audioNowUs;
|
|
1247
1280
|
let bestIdx = -1;
|
|
1248
1281
|
for (let i = 0; i < this.queue.length; i++) {
|
|
1249
1282
|
const ts = this.queue[i].timestamp ?? 0;
|
|
@@ -1271,19 +1304,20 @@ var VideoRenderer = class {
|
|
|
1271
1304
|
return;
|
|
1272
1305
|
}
|
|
1273
1306
|
const _relaxDrop = globalThis.AVBRIDGE_RELAX_DROP === true;
|
|
1274
|
-
const dropThresholdUs = _relaxDrop ? audioNowUs - 60 * 1e6 : audioNowUs - frameDurationUs * 2;
|
|
1275
1307
|
let dropped = 0;
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1308
|
+
const initialBestIdx = bestIdx;
|
|
1309
|
+
if (!_relaxDrop) {
|
|
1310
|
+
while (bestIdx > 0) {
|
|
1279
1311
|
this.queue.shift()?.close();
|
|
1280
1312
|
this.framesDroppedLate++;
|
|
1281
1313
|
bestIdx--;
|
|
1282
1314
|
dropped++;
|
|
1283
|
-
} else {
|
|
1284
|
-
break;
|
|
1285
1315
|
}
|
|
1286
1316
|
}
|
|
1317
|
+
const paintTs = this.queue[0]?.timestamp ?? 0;
|
|
1318
|
+
if (isDebug()) {
|
|
1319
|
+
console.log(`[TRACE] PAINT bestIdx_initial=${initialBestIdx} dropped=${dropped} paintPts=${(paintTs / 1e3).toFixed(1)}ms audioNow=${(audioNowUs / 1e3).toFixed(1)}ms deadline=${(deadlineUs / 1e3).toFixed(1)}ms queueLen=${this.queue.length} wall=${performance.now().toFixed(0)}`);
|
|
1320
|
+
}
|
|
1287
1321
|
this.ticksPainted++;
|
|
1288
1322
|
if (isDebug()) {
|
|
1289
1323
|
const now = performance.now();
|
|
@@ -1319,6 +1353,33 @@ var VideoRenderer = class {
|
|
|
1319
1353
|
}
|
|
1320
1354
|
try {
|
|
1321
1355
|
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
1356
|
+
if (isDebug()) {
|
|
1357
|
+
const wallNow = performance.now();
|
|
1358
|
+
const audNowMs = this.clock.now() * 1e3;
|
|
1359
|
+
const ptsMs = (frame.timestamp ?? 0) / 1e3;
|
|
1360
|
+
const dWall = this.lastPaintWall > 0 ? wallNow - this.lastPaintWall : 0;
|
|
1361
|
+
const dAud = this.lastPaintAudMs > 0 ? audNowMs - this.lastPaintAudMs : 0;
|
|
1362
|
+
const dPts = this.hasLastPaintedPts ? ptsMs - this.lastPaintedPtsUs / 1e3 : 0;
|
|
1363
|
+
this.ctx.save();
|
|
1364
|
+
this.ctx.font = "bold 18px monospace";
|
|
1365
|
+
const lines = [
|
|
1366
|
+
`#${this.framesPainted + 1} pts=${ptsMs.toFixed(0)} aud=${audNowMs.toFixed(0)} wall=${wallNow.toFixed(0)}`,
|
|
1367
|
+
`\u0394pts=${dPts.toFixed(0)} \u0394aud=${dAud.toFixed(0)} \u0394wall=${dWall.toFixed(0)}`
|
|
1368
|
+
];
|
|
1369
|
+
const lineHeight = 22;
|
|
1370
|
+
const padTop = 6;
|
|
1371
|
+
const stripH = padTop + lineHeight * lines.length;
|
|
1372
|
+
this.ctx.fillStyle = "rgba(0,0,0,0.7)";
|
|
1373
|
+
this.ctx.fillRect(0, 0, this.canvas.width, stripH);
|
|
1374
|
+
this.ctx.fillStyle = "#0f0";
|
|
1375
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1376
|
+
this.ctx.fillText(lines[i], 8, padTop + lineHeight * (i + 1) - 4);
|
|
1377
|
+
}
|
|
1378
|
+
this.ctx.restore();
|
|
1379
|
+
}
|
|
1380
|
+
this.lastPaintedPtsUs = frame.timestamp ?? 0;
|
|
1381
|
+
this.hasLastPaintedPts = true;
|
|
1382
|
+
this.lastPaintAudMs = this.clock.now() * 1e3;
|
|
1322
1383
|
this.framesPainted++;
|
|
1323
1384
|
} catch (err) {
|
|
1324
1385
|
if (this.framesPainted === 0 && this.framesDroppedLate === 0) {
|
|
@@ -1331,6 +1392,7 @@ var VideoRenderer = class {
|
|
|
1331
1392
|
const count = this.queue.length;
|
|
1332
1393
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
1333
1394
|
this.prerolled = false;
|
|
1395
|
+
this.hasLastPaintedPts = false;
|
|
1334
1396
|
this.ptsCalibrated = false;
|
|
1335
1397
|
this.hasEverEnqueuedSinceFlush = false;
|
|
1336
1398
|
if (isDebug() && count > 0) {
|
|
@@ -1371,6 +1433,9 @@ var VideoRenderer = class {
|
|
|
1371
1433
|
};
|
|
1372
1434
|
|
|
1373
1435
|
// src/strategies/fallback/audio-output.ts
|
|
1436
|
+
function isDebug2() {
|
|
1437
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
1438
|
+
}
|
|
1374
1439
|
var AudioOutput = class {
|
|
1375
1440
|
ctx;
|
|
1376
1441
|
gain;
|
|
@@ -1392,6 +1457,16 @@ var AudioOutput = class {
|
|
|
1392
1457
|
mediaTimeOfNext = 0;
|
|
1393
1458
|
/** Anchor: media time `mediaTimeOfAnchor` corresponds to ctx time `ctxTimeAtAnchor`. */
|
|
1394
1459
|
mediaTimeOfAnchor = 0;
|
|
1460
|
+
/**
|
|
1461
|
+
* Ctx time at which the first audible chunk will start playing. `-1`
|
|
1462
|
+
* before any chunk has been scheduled successfully (clock is frozen);
|
|
1463
|
+
* the actual ctx time once one has. The renderer's `clock.now()` uses
|
|
1464
|
+
* this to avoid advancing during the silent-gap window between
|
|
1465
|
+
* `audio.start()` and the first chunk that schedules without being
|
|
1466
|
+
* dropped — that gap is what produces the "audio-less fast-forward"
|
|
1467
|
+
* the user sees post-seek when the gate releases on video-only grace.
|
|
1468
|
+
*/
|
|
1469
|
+
firstAudibleCtxStart = -1;
|
|
1395
1470
|
ctxTimeAtAnchor = 0;
|
|
1396
1471
|
pendingQueue = [];
|
|
1397
1472
|
framesScheduled = 0;
|
|
@@ -1464,10 +1539,16 @@ var AudioOutput = class {
|
|
|
1464
1539
|
return this.mediaTimeOfAnchor;
|
|
1465
1540
|
}
|
|
1466
1541
|
if (this.state === "playing") {
|
|
1542
|
+
if (this.firstAudibleCtxStart < 0) {
|
|
1543
|
+
return this.mediaTimeOfAnchor;
|
|
1544
|
+
}
|
|
1467
1545
|
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
|
|
1468
1546
|
}
|
|
1469
1547
|
return this.mediaTimeOfAnchor;
|
|
1470
1548
|
}
|
|
1549
|
+
anchorTime() {
|
|
1550
|
+
return this.mediaTimeOfAnchor;
|
|
1551
|
+
}
|
|
1471
1552
|
isPlaying() {
|
|
1472
1553
|
return this.state === "playing";
|
|
1473
1554
|
}
|
|
@@ -1494,18 +1575,81 @@ var AudioOutput = class {
|
|
|
1494
1575
|
* Schedule a chunk of decoded samples. Queues internally while idle (cold
|
|
1495
1576
|
* start or post-seek), schedules directly to the audio graph while playing.
|
|
1496
1577
|
* In wall-clock mode, samples are silently discarded.
|
|
1578
|
+
*
|
|
1579
|
+
* `ptsSec` is the chunk's source-domain content PTS in seconds, from
|
|
1580
|
+
* the demuxer. When provided, the chunk plays at the ctx-time
|
|
1581
|
+
* corresponding to that PTS — so pre-target audio after a seek
|
|
1582
|
+
* naturally drops (its computed `ctxStart` falls in the past) and
|
|
1583
|
+
* post-target audio plays at its true content time, without any
|
|
1584
|
+
* external trim or anchor rebase. When `ptsSec` is null (cold start
|
|
1585
|
+
* with no PTS yet, or codecs whose packet→frame mapping isn't 1:1),
|
|
1586
|
+
* the chunk is scheduled sequentially after `mediaTimeOfNext` — the
|
|
1587
|
+
* pre-refactor behavior.
|
|
1497
1588
|
*/
|
|
1498
|
-
schedule(samples, channels, sampleRate) {
|
|
1589
|
+
schedule(samples, channels, sampleRate, ptsSec) {
|
|
1499
1590
|
if (this.destroyed || this.noAudio) return;
|
|
1500
1591
|
const frameCount = samples.length / channels;
|
|
1501
1592
|
const durationSec = frameCount / sampleRate;
|
|
1593
|
+
const hasPts = ptsSec != null && Number.isFinite(ptsSec);
|
|
1594
|
+
if (hasPts && ptsSec + durationSec / this._rate < this.mediaTimeOfAnchor) {
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1502
1597
|
if (this.state === "idle" || this.state === "paused") {
|
|
1503
|
-
this.pendingQueue.push({
|
|
1598
|
+
this.pendingQueue.push({
|
|
1599
|
+
samples,
|
|
1600
|
+
channels,
|
|
1601
|
+
sampleRate,
|
|
1602
|
+
frameCount,
|
|
1603
|
+
durationSec,
|
|
1604
|
+
ptsSec: hasPts ? ptsSec : null
|
|
1605
|
+
});
|
|
1504
1606
|
return;
|
|
1505
1607
|
}
|
|
1506
|
-
this.scheduleNow(
|
|
1608
|
+
this.scheduleNow(
|
|
1609
|
+
samples,
|
|
1610
|
+
channels,
|
|
1611
|
+
sampleRate,
|
|
1612
|
+
frameCount,
|
|
1613
|
+
hasPts ? ptsSec : null
|
|
1614
|
+
);
|
|
1507
1615
|
}
|
|
1508
|
-
scheduleNow(samples, channels, sampleRate, frameCount) {
|
|
1616
|
+
scheduleNow(samples, channels, sampleRate, frameCount, ptsSec) {
|
|
1617
|
+
const durationSec = frameCount / sampleRate;
|
|
1618
|
+
let ctxStart;
|
|
1619
|
+
if (ptsSec != null) {
|
|
1620
|
+
ctxStart = this.ctxTimeAtAnchor + (ptsSec - this.mediaTimeOfAnchor) / this._rate;
|
|
1621
|
+
if (isDebug2()) {
|
|
1622
|
+
console.log(`[TRACE-AUD] PTS sched #${this.framesScheduled} pts=${ptsSec.toFixed(3)} dur=${durationSec.toFixed(4)} ctxStart=${ctxStart.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)} anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} mtNext=${this.mediaTimeOfNext.toFixed(3)} rate=${this._rate}`);
|
|
1623
|
+
}
|
|
1624
|
+
if (ctxStart < this.ctx.currentTime - 1e-3) {
|
|
1625
|
+
if (isDebug2()) {
|
|
1626
|
+
console.log(`[TRACE-AUD] DROP late chunk pts=${ptsSec.toFixed(3)} ctxStart=${ctxStart.toFixed(4)} < ctxNow=${this.ctx.currentTime.toFixed(4)}`);
|
|
1627
|
+
}
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
if (this.firstAudibleCtxStart < 0) {
|
|
1631
|
+
this.firstAudibleCtxStart = ctxStart;
|
|
1632
|
+
this.mediaTimeOfAnchor = ptsSec;
|
|
1633
|
+
this.ctxTimeAtAnchor = ctxStart;
|
|
1634
|
+
if (isDebug2()) {
|
|
1635
|
+
console.log(`[TRACE-AUD] UNFREEZE clock \u2014 first audible chunk pts=${ptsSec.toFixed(3)} ctxStart=${ctxStart.toFixed(4)} \u2192 anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)}`);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const endMediaTime = ptsSec + durationSec / this._rate;
|
|
1639
|
+
if (endMediaTime > this.mediaTimeOfNext) {
|
|
1640
|
+
this.mediaTimeOfNext = endMediaTime;
|
|
1641
|
+
}
|
|
1642
|
+
} else {
|
|
1643
|
+
ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
1644
|
+
console.warn(`[TRACE-AUD] LEGACY (no PTS) sched dur=${durationSec.toFixed(4)} ctxStart=${ctxStart.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)}`);
|
|
1645
|
+
if (ctxStart < this.ctx.currentTime) {
|
|
1646
|
+
console.warn(`[TRACE-AUD] REBASE anchor was=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor was=${this.ctxTimeAtAnchor.toFixed(4)} \u2192 anchor=${this.mediaTimeOfNext.toFixed(3)} ctxAnchor=${this.ctx.currentTime.toFixed(4)}`);
|
|
1647
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1648
|
+
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
1649
|
+
ctxStart = this.ctx.currentTime;
|
|
1650
|
+
}
|
|
1651
|
+
this.mediaTimeOfNext += durationSec;
|
|
1652
|
+
}
|
|
1509
1653
|
const buffer = this.ctx.createBuffer(channels, frameCount, sampleRate);
|
|
1510
1654
|
for (let ch = 0; ch < channels; ch++) {
|
|
1511
1655
|
const channelData = buffer.getChannelData(ch);
|
|
@@ -1517,14 +1661,7 @@ var AudioOutput = class {
|
|
|
1517
1661
|
node.buffer = buffer;
|
|
1518
1662
|
node.connect(this.gain);
|
|
1519
1663
|
if (this._rate !== 1) node.playbackRate.value = this._rate;
|
|
1520
|
-
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
1521
|
-
if (ctxStart < this.ctx.currentTime) {
|
|
1522
|
-
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1523
|
-
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
1524
|
-
ctxStart = this.ctx.currentTime;
|
|
1525
|
-
}
|
|
1526
1664
|
node.start(ctxStart);
|
|
1527
|
-
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
1528
1665
|
this.framesScheduled++;
|
|
1529
1666
|
}
|
|
1530
1667
|
// ── Lifecycle ─────────────────────────────────────────────────────────
|
|
@@ -1549,12 +1686,15 @@ var AudioOutput = class {
|
|
|
1549
1686
|
} catch {
|
|
1550
1687
|
}
|
|
1551
1688
|
if (this.state === "paused") {
|
|
1689
|
+
if (isDebug2()) {
|
|
1690
|
+
console.log(`[TRACE-AUD] START(resume) anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} \u2192 ctxAnchor=${this.ctx.currentTime.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)} pendingCount=${this.pendingQueue.length}`);
|
|
1691
|
+
}
|
|
1552
1692
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1553
1693
|
this.state = "playing";
|
|
1554
1694
|
const drain2 = this.pendingQueue;
|
|
1555
1695
|
this.pendingQueue = [];
|
|
1556
1696
|
for (const c of drain2) {
|
|
1557
|
-
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
1697
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
|
|
1558
1698
|
}
|
|
1559
1699
|
return;
|
|
1560
1700
|
}
|
|
@@ -1562,10 +1702,13 @@ var AudioOutput = class {
|
|
|
1562
1702
|
this.ctxTimeAtAnchor = this.ctx.currentTime + STARTUP_DELAY;
|
|
1563
1703
|
this.mediaTimeOfNext = this.mediaTimeOfAnchor;
|
|
1564
1704
|
this.state = "playing";
|
|
1705
|
+
if (isDebug2()) {
|
|
1706
|
+
console.log(`[TRACE-AUD] START(cold) anchor=${this.mediaTimeOfAnchor.toFixed(3)} ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} mtNext=${this.mediaTimeOfNext.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(4)} pendingCount=${this.pendingQueue.length}`);
|
|
1707
|
+
}
|
|
1565
1708
|
const drain = this.pendingQueue;
|
|
1566
1709
|
this.pendingQueue = [];
|
|
1567
1710
|
for (const c of drain) {
|
|
1568
|
-
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount);
|
|
1711
|
+
this.scheduleNow(c.samples, c.channels, c.sampleRate, c.frameCount, c.ptsSec);
|
|
1569
1712
|
}
|
|
1570
1713
|
}
|
|
1571
1714
|
/** Pause playback. Suspends the audio context. */
|
|
@@ -1591,6 +1734,9 @@ var AudioOutput = class {
|
|
|
1591
1734
|
* supplying new samples) and then call `start()` to resume playback.
|
|
1592
1735
|
*/
|
|
1593
1736
|
async reset(newMediaTime) {
|
|
1737
|
+
if (isDebug2()) {
|
|
1738
|
+
console.log(`[TRACE-AUD] RESET to=${newMediaTime.toFixed(3)} prev_anchor=${this.mediaTimeOfAnchor.toFixed(3)} prev_mtNext=${this.mediaTimeOfNext.toFixed(3)} prev_ctxAnchor=${this.ctxTimeAtAnchor.toFixed(4)} ctxNow=${this.ctx.currentTime.toFixed(4)} state=${this.state}`);
|
|
1739
|
+
}
|
|
1594
1740
|
if (this.noAudio) {
|
|
1595
1741
|
this.pendingQueue = [];
|
|
1596
1742
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
@@ -1609,6 +1755,7 @@ var AudioOutput = class {
|
|
|
1609
1755
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
1610
1756
|
this.mediaTimeOfNext = newMediaTime;
|
|
1611
1757
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1758
|
+
this.firstAudibleCtxStart = -1;
|
|
1612
1759
|
this.state = "idle";
|
|
1613
1760
|
if (this.ctx.state === "running") {
|
|
1614
1761
|
await this.ctx.suspend();
|
|
@@ -1777,7 +1924,6 @@ async function startHybridDecoder(opts) {
|
|
|
1777
1924
|
let videoChunksFed = 0;
|
|
1778
1925
|
let bufferedUntilSec = 0;
|
|
1779
1926
|
let syntheticVideoUs = 0;
|
|
1780
|
-
let syntheticAudioUs = 0;
|
|
1781
1927
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
1782
1928
|
const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
|
|
1783
1929
|
const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
|
|
@@ -1809,7 +1955,13 @@ async function startHybridDecoder(opts) {
|
|
|
1809
1955
|
}
|
|
1810
1956
|
}
|
|
1811
1957
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
1812
|
-
await decodeAudioBatch(
|
|
1958
|
+
await decodeAudioBatch(
|
|
1959
|
+
audioPackets,
|
|
1960
|
+
myToken,
|
|
1961
|
+
/*flush*/
|
|
1962
|
+
false,
|
|
1963
|
+
audioTimeBase
|
|
1964
|
+
);
|
|
1813
1965
|
}
|
|
1814
1966
|
if (myToken !== pumpToken || destroyed) return;
|
|
1815
1967
|
await new Promise((r) => setTimeout(r, 0));
|
|
@@ -1856,8 +2008,11 @@ async function startHybridDecoder(opts) {
|
|
|
1856
2008
|
}
|
|
1857
2009
|
}
|
|
1858
2010
|
}
|
|
1859
|
-
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
2011
|
+
async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
|
|
1860
2012
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
2013
|
+
const pktPtsSec = pkts.map(
|
|
2014
|
+
(p) => tb ? chunkHZUVMXBN_cjs.packetPtsSec(p, tb) : null
|
|
2015
|
+
);
|
|
1861
2016
|
const AUDIO_SUB_BATCH = 4;
|
|
1862
2017
|
let allFrames = [];
|
|
1863
2018
|
for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
|
|
@@ -1895,22 +2050,13 @@ async function startHybridDecoder(opts) {
|
|
|
1895
2050
|
}
|
|
1896
2051
|
if (myToken !== pumpToken || destroyed) return;
|
|
1897
2052
|
const frames = allFrames;
|
|
1898
|
-
for (
|
|
2053
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1899
2054
|
if (myToken !== pumpToken || destroyed) return;
|
|
1900
|
-
|
|
1901
|
-
f,
|
|
1902
|
-
() => {
|
|
1903
|
-
const ts = syntheticAudioUs;
|
|
1904
|
-
const samples2 = f.nb_samples ?? 1024;
|
|
1905
|
-
const sampleRate = f.sample_rate ?? 44100;
|
|
1906
|
-
syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
|
|
1907
|
-
return ts;
|
|
1908
|
-
},
|
|
1909
|
-
audioTimeBase
|
|
1910
|
-
);
|
|
2055
|
+
const f = frames[i];
|
|
1911
2056
|
const samples = chunkHZUVMXBN_cjs.libavFrameToInterleavedFloat32(f);
|
|
1912
2057
|
if (samples) {
|
|
1913
|
-
|
|
2058
|
+
const pts = pktPtsSec[i] ?? null;
|
|
2059
|
+
opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
|
|
1914
2060
|
audioFramesDecoded++;
|
|
1915
2061
|
}
|
|
1916
2062
|
}
|
|
@@ -2020,7 +2166,6 @@ async function startHybridDecoder(opts) {
|
|
|
2020
2166
|
}
|
|
2021
2167
|
await flushBSF();
|
|
2022
2168
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
2023
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
2024
2169
|
pumpRunning = pumpLoop(newToken).catch(
|
|
2025
2170
|
(err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
|
|
2026
2171
|
);
|
|
@@ -2059,7 +2204,6 @@ async function startHybridDecoder(opts) {
|
|
|
2059
2204
|
}
|
|
2060
2205
|
await flushBSF();
|
|
2061
2206
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
2062
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
2063
2207
|
pumpRunning = pumpLoop(newToken).catch(
|
|
2064
2208
|
(err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
|
|
2065
2209
|
);
|
|
@@ -2312,6 +2456,9 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2312
2456
|
}
|
|
2313
2457
|
|
|
2314
2458
|
// src/strategies/fallback/decoder.ts
|
|
2459
|
+
function isDebug3() {
|
|
2460
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
2461
|
+
}
|
|
2315
2462
|
async function startDecoder(opts) {
|
|
2316
2463
|
const variant = "avbridge";
|
|
2317
2464
|
const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
|
|
@@ -2443,8 +2590,15 @@ async function startDecoder(opts) {
|
|
|
2443
2590
|
let watchdogSlowSinceMs = 0;
|
|
2444
2591
|
let watchdogSlowWarned = false;
|
|
2445
2592
|
let watchdogOverflowWarned = false;
|
|
2446
|
-
let
|
|
2447
|
-
let
|
|
2593
|
+
let lastContentUs = -1;
|
|
2594
|
+
let firstValidPtsLoggedSinceSeek = false;
|
|
2595
|
+
let seenFirstAudioPacketSinceSeek = false;
|
|
2596
|
+
let seekTargetSec = 0;
|
|
2597
|
+
let diagPktsLoggedSinceSeek = 0;
|
|
2598
|
+
let diagFramesLoggedSinceSeek = 0;
|
|
2599
|
+
let diagFrameKeysDumped = false;
|
|
2600
|
+
const DIAG_MAX_PKTS = 100;
|
|
2601
|
+
const DIAG_MAX_FRAMES = 300;
|
|
2448
2602
|
let videoDecodeMsTotal = 0;
|
|
2449
2603
|
let audioDecodeMsTotal = 0;
|
|
2450
2604
|
let videoDecodeBatches = 0;
|
|
@@ -2483,6 +2637,18 @@ async function startDecoder(opts) {
|
|
|
2483
2637
|
for (const pkt of videoPackets) {
|
|
2484
2638
|
const sec = chunkHZUVMXBN_cjs.packetPtsSec(pkt, videoTimeBase);
|
|
2485
2639
|
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2640
|
+
if (isDebug3() && diagPktsLoggedSinceSeek < DIAG_MAX_PKTS) {
|
|
2641
|
+
const rawHi = pkt.ptshi ?? 0;
|
|
2642
|
+
const rawLo = pkt.pts ?? 0;
|
|
2643
|
+
const isInvalidPts = rawHi === -2147483648 && rawLo === 0;
|
|
2644
|
+
const rawPts64 = isInvalidPts ? null : rawHi * 4294967296 + rawLo;
|
|
2645
|
+
const rawSec = rawPts64 != null && videoTimeBase ? rawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
|
|
2646
|
+
const pktKeys = diagPktsLoggedSinceSeek === 0 ? `[keys: ${Object.keys(pkt).join(",")}]` : "";
|
|
2647
|
+
console.log(
|
|
2648
|
+
`[DIAG-PKT] vidx=${diagPktsLoggedSinceSeek} pts=${isInvalidPts ? "NOPTS" : rawPts64} pts_sec=${rawSec != null ? rawSec.toFixed(3) : "n/a"} ptshi=${rawHi} ptslo=${rawLo} flags=0x${(pkt.flags ?? 0).toString(16)} keyframe=${(pkt.flags ?? 0) & 1 ? "Y" : "N"} stream=${pkt.stream_index} dataLen=${pkt.data?.length ?? 0} seekTarget=${seekTargetSec.toFixed(3)} ` + pktKeys
|
|
2649
|
+
);
|
|
2650
|
+
diagPktsLoggedSinceSeek++;
|
|
2651
|
+
}
|
|
2486
2652
|
}
|
|
2487
2653
|
}
|
|
2488
2654
|
if (audioPackets && audioTimeBase) {
|
|
@@ -2490,9 +2656,25 @@ async function startDecoder(opts) {
|
|
|
2490
2656
|
const sec = chunkHZUVMXBN_cjs.packetPtsSec(pkt, audioTimeBase);
|
|
2491
2657
|
if (sec != null && sec > bufferedUntilSec) bufferedUntilSec = sec;
|
|
2492
2658
|
}
|
|
2659
|
+
if (!seenFirstAudioPacketSinceSeek && audioPackets.length > 0) {
|
|
2660
|
+
const firstSec = chunkHZUVMXBN_cjs.packetPtsSec(audioPackets[0], audioTimeBase);
|
|
2661
|
+
if (firstSec != null && Number.isFinite(firstSec)) {
|
|
2662
|
+
seenFirstAudioPacketSinceSeek = true;
|
|
2663
|
+
chunkG4APZMCP_cjs.dbg.info(
|
|
2664
|
+
"av-anchor",
|
|
2665
|
+
`seek-target=${seekTargetSec.toFixed(3)}s, first-audio-pkt-pts=${firstSec.toFixed(3)}s (\u0394=${((firstSec - seekTargetSec) * 1e3).toFixed(1)}ms \u2014 pre-target packets will be skipped by AudioOutput)`
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2493
2669
|
}
|
|
2494
2670
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2495
|
-
await decodeAudioBatch(
|
|
2671
|
+
await decodeAudioBatch(
|
|
2672
|
+
audioPackets,
|
|
2673
|
+
myToken,
|
|
2674
|
+
/*flush*/
|
|
2675
|
+
false,
|
|
2676
|
+
audioTimeBase
|
|
2677
|
+
);
|
|
2496
2678
|
}
|
|
2497
2679
|
if (myToken !== pumpToken || destroyed) return;
|
|
2498
2680
|
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
@@ -2536,7 +2718,7 @@ async function startDecoder(opts) {
|
|
|
2536
2718
|
{
|
|
2537
2719
|
const _throttleStart = performance.now();
|
|
2538
2720
|
let _throttled = false;
|
|
2539
|
-
while (!destroyed && myToken === pumpToken &&
|
|
2721
|
+
while (!destroyed && myToken === pumpToken && opts.audio.bufferAhead() > 2) {
|
|
2540
2722
|
_throttled = true;
|
|
2541
2723
|
await new Promise((r) => setTimeout(r, 50));
|
|
2542
2724
|
}
|
|
@@ -2591,18 +2773,83 @@ async function startDecoder(opts) {
|
|
|
2591
2773
|
if (myToken !== pumpToken || destroyed) return;
|
|
2592
2774
|
for (const f of frames) {
|
|
2593
2775
|
if (myToken !== pumpToken || destroyed) return;
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2776
|
+
const _diagShouldLog = isDebug3() && diagFramesLoggedSinceSeek < DIAG_MAX_FRAMES;
|
|
2777
|
+
const _diagRawHi = f.ptshi ?? 0;
|
|
2778
|
+
const _diagRawLo = f.pts ?? 0;
|
|
2779
|
+
const _diagInvalid = _diagRawHi === -2147483648 && _diagRawLo === 0;
|
|
2780
|
+
const _diagRawPts64 = _diagInvalid ? null : _diagRawHi * 4294967296 + _diagRawLo;
|
|
2781
|
+
const _diagRawSec = _diagRawPts64 != null && videoTimeBase ? _diagRawPts64 * videoTimeBase[0] / videoTimeBase[1] : null;
|
|
2782
|
+
if (_diagShouldLog && !diagFrameKeysDumped) {
|
|
2783
|
+
diagFrameKeysDumped = true;
|
|
2784
|
+
const allKeys = Object.keys(f);
|
|
2785
|
+
const fieldDump = {};
|
|
2786
|
+
for (const k of allKeys) {
|
|
2787
|
+
const v = f[k];
|
|
2788
|
+
if (k === "data") continue;
|
|
2789
|
+
if (typeof v === "object" && v !== null && "length" in v) continue;
|
|
2790
|
+
fieldDump[k] = v;
|
|
2791
|
+
}
|
|
2792
|
+
console.log(`[DIAG-FRAME] FIRST FRAME post-seek \u2014 all keys: ${allKeys.join(",")}`);
|
|
2793
|
+
console.log(`[DIAG-FRAME] FIRST FRAME field dump:`, fieldDump);
|
|
2794
|
+
}
|
|
2795
|
+
let rawUs = null;
|
|
2796
|
+
if (!_diagInvalid && _diagRawPts64 != null) {
|
|
2797
|
+
const tb = videoTimeBase ?? [1, 1e6];
|
|
2798
|
+
const us = Math.round(_diagRawPts64 * 1e6 * tb[0] / tb[1]);
|
|
2799
|
+
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
2800
|
+
rawUs = us;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
const _diagLog = (decision, finalPtsUs, sanFallback) => {
|
|
2804
|
+
if (!_diagShouldLog) return;
|
|
2805
|
+
const ptsSrc = sanFallback ? `SYNTHETIC(${_diagInvalid ? "NOPTS" : "invalid-range"})` : "LIBAV";
|
|
2806
|
+
console.log(
|
|
2807
|
+
`[DIAG-FRAME] vidx=${diagFramesLoggedSinceSeek} raw_pts=${_diagInvalid ? "NOPTS" : _diagRawPts64} raw_pts_sec=${_diagRawSec != null ? _diagRawSec.toFixed(3) : "n/a"} pts_src=${ptsSrc} final_pts_us=${finalPtsUs} final_pts_sec=${(finalPtsUs / 1e6).toFixed(3)} seekTarget=${seekTargetSec.toFixed(3)} offset_to_target_ms=${(finalPtsUs / 1e3 - seekTargetSec * 1e3).toFixed(1)} lastEmittedPts_us=${lastEmittedPtsUs} decision=${decision}`
|
|
2808
|
+
);
|
|
2809
|
+
diagFramesLoggedSinceSeek++;
|
|
2810
|
+
};
|
|
2811
|
+
let _diagSanFallbackFired = false;
|
|
2812
|
+
const seekTargetUs = Math.round(seekTargetSec * 1e6);
|
|
2813
|
+
if (lastContentUs < 0) {
|
|
2814
|
+
if (rawUs == null) {
|
|
2815
|
+
const isColdStartKeyframe = seekTargetSec === 0 && f.key_frame === 1;
|
|
2816
|
+
if (isColdStartKeyframe) {
|
|
2817
|
+
lastContentUs = 0;
|
|
2818
|
+
_diagSanFallbackFired = true;
|
|
2819
|
+
} else {
|
|
2820
|
+
_diagLog("PRE-ANCHOR-DROP", 0, true);
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
} else {
|
|
2824
|
+
lastContentUs = rawUs;
|
|
2825
|
+
if (!firstValidPtsLoggedSinceSeek) {
|
|
2826
|
+
firstValidPtsLoggedSinceSeek = true;
|
|
2827
|
+
if (isDebug3()) {
|
|
2828
|
+
console.log(
|
|
2829
|
+
`[avbridge:decoder] post-seek anchor established: first valid raw pts = ${(rawUs / 1e3).toFixed(1)}ms (seekTarget = ${(seekTargetSec * 1e3).toFixed(1)}ms, \u0394 = ${((rawUs - seekTargetUs) / 1e3).toFixed(1)}ms)`
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
if (rawUs >= seekTargetUs) {
|
|
2833
|
+
console.warn(
|
|
2834
|
+
`[avbridge:decoder] first valid raw pts \u2265 seek target \u2014 pre-anchor NOPTS frames may have straddled the target and been mis-discarded. First painted frame may be late by up to one keyframe interval.`
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
} else {
|
|
2840
|
+
if (rawUs != null) {
|
|
2841
|
+
lastContentUs = rawUs;
|
|
2842
|
+
} else {
|
|
2843
|
+
lastContentUs += videoFrameStepUs;
|
|
2844
|
+
_diagSanFallbackFired = true;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
f.pts = lastContentUs;
|
|
2848
|
+
f.ptshi = lastContentUs < 0 ? -1 : 0;
|
|
2849
|
+
const _fPts = lastContentUs;
|
|
2604
2850
|
if (_fPts > newestVideoPtsUs) newestVideoPtsUs = _fPts;
|
|
2605
2851
|
if (lastEmittedPtsUs >= 0 && _fPts < lastEmittedPtsUs) {
|
|
2852
|
+
_diagLog("REGRESSED-DROP", _fPts, _diagSanFallbackFired);
|
|
2606
2853
|
ptsRegressions++;
|
|
2607
2854
|
const regressMs = (lastEmittedPtsUs - _fPts) / 1e3;
|
|
2608
2855
|
if (regressMs > worstPtsRegressionMs) worstPtsRegressionMs = regressMs;
|
|
@@ -2614,19 +2861,34 @@ async function startDecoder(opts) {
|
|
|
2614
2861
|
continue;
|
|
2615
2862
|
}
|
|
2616
2863
|
lastEmittedPtsUs = _fPts;
|
|
2864
|
+
const targetUs = Math.round(seekTargetSec * 1e6);
|
|
2865
|
+
if (_fPts < targetUs - videoFrameStepUs) {
|
|
2866
|
+
_diagLog("PRE-TARGET-DROP", _fPts, _diagSanFallbackFired);
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2617
2869
|
try {
|
|
2618
2870
|
const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
|
|
2619
|
-
opts.renderer.
|
|
2871
|
+
if (opts.renderer.queueDepth() >= opts.renderer.queueHighWater) {
|
|
2872
|
+
vf.close();
|
|
2873
|
+
_diagLog("OVERFLOW-DROP", _fPts, _diagSanFallbackFired);
|
|
2874
|
+
} else {
|
|
2875
|
+
opts.renderer.enqueue(vf);
|
|
2876
|
+
_diagLog("ENQUEUED", _fPts, _diagSanFallbackFired);
|
|
2877
|
+
}
|
|
2620
2878
|
videoFramesDecoded++;
|
|
2621
2879
|
} catch (err) {
|
|
2622
2880
|
if (videoFramesDecoded === 0) {
|
|
2623
2881
|
console.warn("[avbridge] laFrameToVideoFrame failed:", err);
|
|
2624
2882
|
}
|
|
2883
|
+
_diagLog("BRIDGE-ERROR", _fPts, _diagSanFallbackFired);
|
|
2625
2884
|
}
|
|
2626
2885
|
}
|
|
2627
2886
|
}
|
|
2628
|
-
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
2887
|
+
async function decodeAudioBatch(pkts, myToken, flush = false, tb) {
|
|
2629
2888
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
2889
|
+
const pktPtsSec = pkts.map(
|
|
2890
|
+
(p) => tb ? chunkHZUVMXBN_cjs.packetPtsSec(p, tb) : null
|
|
2891
|
+
);
|
|
2630
2892
|
let frames;
|
|
2631
2893
|
const _t0 = performance.now();
|
|
2632
2894
|
try {
|
|
@@ -2644,22 +2906,17 @@ async function startDecoder(opts) {
|
|
|
2644
2906
|
audioDecodeMsTotal += performance.now() - _t0;
|
|
2645
2907
|
audioDecodeBatches++;
|
|
2646
2908
|
if (myToken !== pumpToken || destroyed) return;
|
|
2647
|
-
for (
|
|
2909
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2648
2910
|
if (myToken !== pumpToken || destroyed) return;
|
|
2649
|
-
|
|
2650
|
-
f,
|
|
2651
|
-
() => {
|
|
2652
|
-
const ts = syntheticAudioUs;
|
|
2653
|
-
const samples2 = f.nb_samples ?? 1024;
|
|
2654
|
-
const sampleRate = f.sample_rate ?? 44100;
|
|
2655
|
-
syntheticAudioUs += Math.round(samples2 * 1e6 / sampleRate);
|
|
2656
|
-
return ts;
|
|
2657
|
-
},
|
|
2658
|
-
audioTimeBase
|
|
2659
|
-
);
|
|
2911
|
+
const f = frames[i];
|
|
2660
2912
|
const samples = chunkHZUVMXBN_cjs.libavFrameToInterleavedFloat32(f);
|
|
2661
2913
|
if (samples) {
|
|
2662
|
-
|
|
2914
|
+
const pts = pktPtsSec[i] ?? null;
|
|
2915
|
+
if (isDebug3()) {
|
|
2916
|
+
const dur = samples.data.length / samples.channels / samples.sampleRate;
|
|
2917
|
+
console.log(`[TRACE-DEC] audio frame #${audioFramesDecoded} pts=${pts != null ? pts.toFixed(4) : "NULL"} dur=${dur.toFixed(4)} samples=${samples.data.length / samples.channels} sr=${samples.sampleRate} ch=${samples.channels} pktsIn=${pkts.length} framesOut=${frames.length}`);
|
|
2918
|
+
}
|
|
2919
|
+
opts.audio.schedule(samples.data, samples.channels, samples.sampleRate, pts);
|
|
2663
2920
|
audioFramesDecoded++;
|
|
2664
2921
|
}
|
|
2665
2922
|
}
|
|
@@ -2762,14 +3019,17 @@ async function startDecoder(opts) {
|
|
|
2762
3019
|
} catch {
|
|
2763
3020
|
}
|
|
2764
3021
|
await flushBSF();
|
|
2765
|
-
|
|
2766
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
3022
|
+
lastContentUs = -1;
|
|
2767
3023
|
lastEmittedPtsUs = -1;
|
|
3024
|
+
firstValidPtsLoggedSinceSeek = false;
|
|
2768
3025
|
pumpRunning = pumpLoop(newToken).catch(
|
|
2769
3026
|
(err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
|
|
2770
3027
|
);
|
|
2771
3028
|
},
|
|
2772
3029
|
async seek(timeSec) {
|
|
3030
|
+
if (isDebug3()) {
|
|
3031
|
+
console.log(`[SEEK] target=${timeSec.toFixed(3)}s (${(timeSec * 1e3).toFixed(0)}ms) wall=${performance.now().toFixed(0)}`);
|
|
3032
|
+
}
|
|
2773
3033
|
const newToken = ++pumpToken;
|
|
2774
3034
|
if (pumpRunning) {
|
|
2775
3035
|
try {
|
|
@@ -2800,9 +3060,14 @@ async function startDecoder(opts) {
|
|
|
2800
3060
|
} catch {
|
|
2801
3061
|
}
|
|
2802
3062
|
await flushBSF();
|
|
2803
|
-
|
|
2804
|
-
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
3063
|
+
lastContentUs = -1;
|
|
2805
3064
|
lastEmittedPtsUs = -1;
|
|
3065
|
+
firstValidPtsLoggedSinceSeek = false;
|
|
3066
|
+
seenFirstAudioPacketSinceSeek = false;
|
|
3067
|
+
seekTargetSec = timeSec;
|
|
3068
|
+
diagPktsLoggedSinceSeek = 0;
|
|
3069
|
+
diagFramesLoggedSinceSeek = 0;
|
|
3070
|
+
diagFrameKeysDumped = false;
|
|
2806
3071
|
pumpRunning = pumpLoop(newToken).catch(
|
|
2807
3072
|
(err) => console.error("[avbridge] decoder pump failed (post-seek):", err)
|
|
2808
3073
|
);
|
|
@@ -3662,5 +3927,5 @@ exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
|
|
|
3662
3927
|
exports.UnifiedPlayer = UnifiedPlayer;
|
|
3663
3928
|
exports.classifyContext = classifyContext;
|
|
3664
3929
|
exports.createPlayer = createPlayer;
|
|
3665
|
-
//# sourceMappingURL=chunk-
|
|
3666
|
-
//# sourceMappingURL=chunk-
|
|
3930
|
+
//# sourceMappingURL=chunk-OFJYEITB.cjs.map
|
|
3931
|
+
//# sourceMappingURL=chunk-OFJYEITB.cjs.map
|