avbridge 2.2.0 → 2.2.1
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 +45 -0
- package/README.md +98 -71
- package/dist/{chunk-C5VA5U5O.js → chunk-DMWARSEF.js} +76 -21
- package/dist/chunk-DMWARSEF.js.map +1 -0
- package/dist/{chunk-OE66B34H.cjs → chunk-UF2N5L63.cjs} +76 -21
- package/dist/chunk-UF2N5L63.cjs.map +1 -0
- package/dist/element-browser.js +74 -19
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +2 -2
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +1 -1
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{player-DUyvltvy.d.cts → player-U2NPmFvA.d.cts} +1 -0
- package/dist/{player-DUyvltvy.d.ts → player-U2NPmFvA.d.ts} +1 -0
- package/package.json +1 -1
- package/src/player.ts +24 -16
- package/src/strategies/fallback/index.ts +8 -0
- package/src/strategies/fallback/video-renderer.ts +5 -1
- package/src/strategies/hybrid/index.ts +9 -1
- package/src/strategies/remux/index.ts +13 -1
- package/src/strategies/remux/pipeline.ts +6 -0
- package/dist/chunk-C5VA5U5O.js.map +0 -1
- package/dist/chunk-OE66B34H.cjs.map +0 -1
package/dist/element-browser.js
CHANGED
|
@@ -31978,6 +31978,10 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
31978
31978
|
console.error("[avbridge] remux pipeline reseek failed:", err);
|
|
31979
31979
|
});
|
|
31980
31980
|
},
|
|
31981
|
+
setAutoPlay(autoPlay) {
|
|
31982
|
+
pendingAutoPlay = autoPlay;
|
|
31983
|
+
if (sink) sink.setPlayOnSeek(autoPlay);
|
|
31984
|
+
},
|
|
31981
31985
|
async destroy() {
|
|
31982
31986
|
destroyed = true;
|
|
31983
31987
|
pumpToken++;
|
|
@@ -32018,7 +32022,11 @@ async function createRemuxSession(context, video) {
|
|
|
32018
32022
|
await pipeline.start(video.currentTime || 0, true);
|
|
32019
32023
|
return;
|
|
32020
32024
|
}
|
|
32021
|
-
|
|
32025
|
+
pipeline.setAutoPlay(true);
|
|
32026
|
+
try {
|
|
32027
|
+
await video.play();
|
|
32028
|
+
} catch {
|
|
32029
|
+
}
|
|
32022
32030
|
},
|
|
32023
32031
|
pause() {
|
|
32024
32032
|
wantPlay = false;
|
|
@@ -32066,7 +32074,7 @@ var VideoRenderer = class {
|
|
|
32066
32074
|
this.resolveFirstFrame = resolve;
|
|
32067
32075
|
});
|
|
32068
32076
|
this.canvas = document.createElement("canvas");
|
|
32069
|
-
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
|
|
32077
|
+
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
|
|
32070
32078
|
const parent = target.parentElement ?? target.parentNode;
|
|
32071
32079
|
if (parent && parent instanceof HTMLElement) {
|
|
32072
32080
|
if (getComputedStyle(parent).position === "static") {
|
|
@@ -32308,9 +32316,13 @@ var AudioOutput = class {
|
|
|
32308
32316
|
const node3 = this.ctx.createBufferSource();
|
|
32309
32317
|
node3.buffer = buffer;
|
|
32310
32318
|
node3.connect(this.gain);
|
|
32311
|
-
|
|
32312
|
-
|
|
32313
|
-
|
|
32319
|
+
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
|
|
32320
|
+
if (ctxStart < this.ctx.currentTime) {
|
|
32321
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
32322
|
+
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
32323
|
+
ctxStart = this.ctx.currentTime;
|
|
32324
|
+
}
|
|
32325
|
+
node3.start(ctxStart);
|
|
32314
32326
|
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
32315
32327
|
this.framesScheduled++;
|
|
32316
32328
|
}
|
|
@@ -32905,6 +32917,10 @@ async function createHybridSession(ctx, target) {
|
|
|
32905
32917
|
void doSeek(v);
|
|
32906
32918
|
}
|
|
32907
32919
|
});
|
|
32920
|
+
Object.defineProperty(target, "paused", {
|
|
32921
|
+
configurable: true,
|
|
32922
|
+
get: () => !audio.isPlaying()
|
|
32923
|
+
});
|
|
32908
32924
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
32909
32925
|
Object.defineProperty(target, "duration", {
|
|
32910
32926
|
configurable: true,
|
|
@@ -32969,6 +32985,7 @@ async function createHybridSession(ctx, target) {
|
|
|
32969
32985
|
try {
|
|
32970
32986
|
delete target.currentTime;
|
|
32971
32987
|
delete target.duration;
|
|
32988
|
+
delete target.paused;
|
|
32972
32989
|
} catch {
|
|
32973
32990
|
}
|
|
32974
32991
|
},
|
|
@@ -33049,7 +33066,8 @@ async function startDecoder(opts) {
|
|
|
33049
33066
|
let audioFramesDecoded = 0;
|
|
33050
33067
|
let watchdogFirstFrameMs = 0;
|
|
33051
33068
|
let watchdogSlowSinceMs = 0;
|
|
33052
|
-
let
|
|
33069
|
+
let watchdogSlowWarned = false;
|
|
33070
|
+
let watchdogOverflowWarned = false;
|
|
33053
33071
|
let syntheticVideoUs = 0;
|
|
33054
33072
|
let syntheticAudioUs = 0;
|
|
33055
33073
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
@@ -33061,7 +33079,7 @@ async function startDecoder(opts) {
|
|
|
33061
33079
|
let packets;
|
|
33062
33080
|
try {
|
|
33063
33081
|
[readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
|
|
33064
|
-
limit:
|
|
33082
|
+
limit: 16 * 1024
|
|
33065
33083
|
});
|
|
33066
33084
|
} catch (err) {
|
|
33067
33085
|
console.error("[avbridge] ff_read_frame_multi failed:", err);
|
|
@@ -33070,26 +33088,26 @@ async function startDecoder(opts) {
|
|
|
33070
33088
|
if (myToken !== pumpToken || destroyed) return;
|
|
33071
33089
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
33072
33090
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
33073
|
-
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
33074
|
-
await decodeVideoBatch(videoPackets, myToken);
|
|
33075
|
-
}
|
|
33076
|
-
if (myToken !== pumpToken || destroyed) return;
|
|
33077
33091
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
33078
33092
|
await decodeAudioBatch(audioPackets, myToken);
|
|
33079
33093
|
}
|
|
33094
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
33095
|
+
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
33096
|
+
await decodeVideoBatch(videoPackets, myToken);
|
|
33097
|
+
}
|
|
33080
33098
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
33081
33099
|
if (videoFramesDecoded > 0) {
|
|
33082
33100
|
if (watchdogFirstFrameMs === 0) {
|
|
33083
33101
|
watchdogFirstFrameMs = performance.now();
|
|
33084
33102
|
}
|
|
33085
33103
|
const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1e3;
|
|
33086
|
-
if (elapsedSinceFirst > 1 && !
|
|
33104
|
+
if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
|
|
33087
33105
|
const expectedFrames = elapsedSinceFirst * videoFps;
|
|
33088
33106
|
const ratio = videoFramesDecoded / expectedFrames;
|
|
33089
33107
|
if (ratio < 0.6) {
|
|
33090
33108
|
if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
|
|
33091
33109
|
if ((performance.now() - watchdogSlowSinceMs) / 1e3 > 5) {
|
|
33092
|
-
|
|
33110
|
+
watchdogSlowWarned = true;
|
|
33093
33111
|
console.warn(
|
|
33094
33112
|
"[avbridge:decode-rate]",
|
|
33095
33113
|
`decoder is running slower than realtime: ${videoFramesDecoded} frames in ${elapsedSinceFirst.toFixed(1)}s (${(videoFramesDecoded / elapsedSinceFirst).toFixed(1)} fps vs ${videoFps} fps source \u2014 ${(ratio * 100).toFixed(0)}% of realtime). Playback will stutter. Typical causes: software decode of a codec with no WebCodecs support (rv40, mpeg4 @ 720p+, wmv3), or a resolution too large for single-threaded WASM to keep up with.`
|
|
@@ -33099,6 +33117,17 @@ async function startDecoder(opts) {
|
|
|
33099
33117
|
watchdogSlowSinceMs = 0;
|
|
33100
33118
|
}
|
|
33101
33119
|
}
|
|
33120
|
+
if (!watchdogOverflowWarned && videoFramesDecoded > 100) {
|
|
33121
|
+
const rendererStats = opts.renderer.stats();
|
|
33122
|
+
const overflow = rendererStats.framesDroppedOverflow ?? 0;
|
|
33123
|
+
if (overflow / videoFramesDecoded > 0.1) {
|
|
33124
|
+
watchdogOverflowWarned = true;
|
|
33125
|
+
console.warn(
|
|
33126
|
+
"[avbridge:overflow-drop]",
|
|
33127
|
+
`renderer is dropping ${overflow}/${videoFramesDecoded} frames (${(overflow / videoFramesDecoded * 100).toFixed(0)}%) because the decoder is producing bursts faster than the canvas can drain. Symptom: choppy playback despite decoder keeping up on average. Fix would be smaller read batches in the pump loop or a lower queueHighWater cap \u2014 see src/strategies/fallback/decoder.ts.`
|
|
33128
|
+
);
|
|
33129
|
+
}
|
|
33130
|
+
}
|
|
33102
33131
|
}
|
|
33103
33132
|
while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
33104
33133
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -33456,6 +33485,10 @@ async function createFallbackSession(ctx, target) {
|
|
|
33456
33485
|
void doSeek(v);
|
|
33457
33486
|
}
|
|
33458
33487
|
});
|
|
33488
|
+
Object.defineProperty(target, "paused", {
|
|
33489
|
+
configurable: true,
|
|
33490
|
+
get: () => !audio.isPlaying()
|
|
33491
|
+
});
|
|
33459
33492
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
33460
33493
|
Object.defineProperty(target, "duration", {
|
|
33461
33494
|
configurable: true,
|
|
@@ -33464,25 +33497,35 @@ async function createFallbackSession(ctx, target) {
|
|
|
33464
33497
|
}
|
|
33465
33498
|
async function waitForBuffer() {
|
|
33466
33499
|
const start = performance.now();
|
|
33500
|
+
let firstFrameAtMs = 0;
|
|
33467
33501
|
dbg.info(
|
|
33468
33502
|
"cold-start",
|
|
33469
|
-
`gate entry:
|
|
33503
|
+
`gate entry: want audio \u2265 ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
|
|
33470
33504
|
);
|
|
33471
33505
|
while (true) {
|
|
33472
33506
|
const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
|
|
33473
33507
|
const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS2;
|
|
33474
33508
|
const hasFrames = renderer.hasFrames();
|
|
33509
|
+
const nowMs = performance.now();
|
|
33510
|
+
if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
|
|
33475
33511
|
if (audioReady && hasFrames) {
|
|
33476
33512
|
dbg.info(
|
|
33477
33513
|
"cold-start",
|
|
33478
|
-
`gate satisfied in ${(
|
|
33514
|
+
`gate satisfied in ${(nowMs - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
|
|
33479
33515
|
);
|
|
33480
33516
|
return;
|
|
33481
33517
|
}
|
|
33482
|
-
if (
|
|
33518
|
+
if (hasFrames && firstFrameAtMs > 0 && nowMs - firstFrameAtMs >= 500) {
|
|
33519
|
+
dbg.info(
|
|
33520
|
+
"cold-start",
|
|
33521
|
+
`gate released on video-only grace at ${(nowMs - start).toFixed(0)}ms (frames=${renderer.queueDepth()}, audio=${(audioAhead * 1e3).toFixed(0)}ms \u2014 demuxer hasn't delivered audio packets yet, starting anyway and letting the audio scheduler catch up at its media-time anchor)`
|
|
33522
|
+
);
|
|
33523
|
+
return;
|
|
33524
|
+
}
|
|
33525
|
+
if ((nowMs - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
|
|
33483
33526
|
dbg.diag(
|
|
33484
33527
|
"cold-start",
|
|
33485
|
-
`gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651).
|
|
33528
|
+
`gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651). Decoder produced nothing in ${READY_TIMEOUT_SECONDS2}s \u2014 either a corrupt source, a missing codec, or WASM is catastrophically slow on this file. Check getDiagnostics().runtime for decode counters.`
|
|
33486
33529
|
);
|
|
33487
33530
|
return;
|
|
33488
33531
|
}
|
|
@@ -33531,6 +33574,7 @@ async function createFallbackSession(ctx, target) {
|
|
|
33531
33574
|
try {
|
|
33532
33575
|
delete target.currentTime;
|
|
33533
33576
|
delete target.duration;
|
|
33577
|
+
delete target.paused;
|
|
33534
33578
|
} catch {
|
|
33535
33579
|
}
|
|
33536
33580
|
},
|
|
@@ -33704,6 +33748,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33704
33748
|
lastProgressTime = 0;
|
|
33705
33749
|
lastProgressPosition = -1;
|
|
33706
33750
|
errorListener = null;
|
|
33751
|
+
// Bound so we can removeEventListener in destroy(); without this the
|
|
33752
|
+
// listener outlives the player and accumulates on elements that swap
|
|
33753
|
+
// source (e.g. <avbridge-video>).
|
|
33754
|
+
endedListener = null;
|
|
33707
33755
|
// Serializes escalation / setStrategy calls
|
|
33708
33756
|
switchingPromise = Promise.resolve();
|
|
33709
33757
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
@@ -33789,7 +33837,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33789
33837
|
subtitle: ctx.subtitleTracks
|
|
33790
33838
|
});
|
|
33791
33839
|
this.startTimeupdateLoop();
|
|
33792
|
-
this.
|
|
33840
|
+
this.endedListener = () => this.emitter.emit("ended", void 0);
|
|
33841
|
+
this.options.target.addEventListener("ended", this.endedListener);
|
|
33793
33842
|
this.emitter.emitSticky("ready", void 0);
|
|
33794
33843
|
const bootstrapElapsed = performance.now() - bootstrapStart;
|
|
33795
33844
|
dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
|
|
@@ -34075,6 +34124,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
34075
34124
|
this.timeupdateInterval = null;
|
|
34076
34125
|
}
|
|
34077
34126
|
this.clearSupervisor();
|
|
34127
|
+
if (this.endedListener) {
|
|
34128
|
+
this.options.target.removeEventListener("ended", this.endedListener);
|
|
34129
|
+
this.endedListener = null;
|
|
34130
|
+
}
|
|
34078
34131
|
if (this.session) {
|
|
34079
34132
|
await this.session.destroy();
|
|
34080
34133
|
this.session = null;
|
|
@@ -34089,11 +34142,13 @@ async function createPlayer(options) {
|
|
|
34089
34142
|
function buildInitialDecision(initial, ctx) {
|
|
34090
34143
|
const natural = classifyContext(ctx);
|
|
34091
34144
|
const cls = strategyToClass(initial, natural);
|
|
34145
|
+
const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
|
|
34146
|
+
const fallbackChain = inherited.filter((s) => s !== initial);
|
|
34092
34147
|
return {
|
|
34093
34148
|
class: cls,
|
|
34094
34149
|
strategy: initial,
|
|
34095
34150
|
reason: `initial strategy "${initial}" requested via options.initialStrategy`,
|
|
34096
|
-
fallbackChain
|
|
34151
|
+
fallbackChain
|
|
34097
34152
|
};
|
|
34098
34153
|
}
|
|
34099
34154
|
function strategyToClass(strategy, natural) {
|