avbridge 2.10.0 → 2.11.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 +34 -0
- package/dist/{chunk-NQULEIA3.cjs → chunk-37UOSAVI.cjs} +15 -7
- package/dist/chunk-37UOSAVI.cjs.map +1 -0
- package/dist/{chunk-5KVLE6YI.js → chunk-EDDWAN2L.js} +3 -2
- package/dist/chunk-EDDWAN2L.js.map +1 -0
- package/dist/{chunk-3GKM5DFM.js → chunk-IHNHHEA2.js} +11 -3
- package/dist/chunk-IHNHHEA2.js.map +1 -0
- package/dist/{chunk-S4WAZC2T.cjs → chunk-WRKO6Q42.cjs} +3 -2
- package/dist/chunk-WRKO6Q42.cjs.map +1 -0
- package/dist/element-browser.js +23 -1
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +18 -5
- package/dist/element.cjs.map +1 -1
- package/dist/element.js +17 -4
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +10 -10
- package/dist/index.js +2 -2
- package/dist/player.cjs +106 -18
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +15 -0
- package/dist/player.d.ts +15 -0
- package/dist/player.js +102 -14
- package/dist/player.js.map +1 -1
- package/dist/subtitles-5H24MEBJ.js +4 -0
- package/dist/{subtitles-4T74JRGT.js.map → subtitles-5H24MEBJ.js.map} +1 -1
- package/dist/subtitles-HMVGWTU2.cjs +29 -0
- package/dist/{subtitles-QUH4LPI4.cjs.map → subtitles-HMVGWTU2.cjs.map} +1 -1
- package/package.json +1 -1
- package/src/element/avbridge-player.ts +92 -10
- package/src/element/avbridge-subtitles.ts +273 -0
- package/src/element/avbridge-video.ts +21 -1
- package/src/strategies/fallback/audio-output.ts +10 -0
- package/src/subtitles/index.ts +2 -0
- package/dist/chunk-3GKM5DFM.js.map +0 -1
- package/dist/chunk-5KVLE6YI.js.map +0 -1
- package/dist/chunk-NQULEIA3.cjs.map +0 -1
- package/dist/chunk-S4WAZC2T.cjs.map +0 -1
- package/dist/subtitles-4T74JRGT.js +0 -4
- package/dist/subtitles-QUH4LPI4.cjs +0 -29
package/dist/player.cjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
var chunk2XW2O3YI_cjs = require('./chunk-2XW2O3YI.cjs');
|
|
4
4
|
var chunkNNVOHKXJ_cjs = require('./chunk-NNVOHKXJ.cjs');
|
|
5
5
|
require('./chunk-Z33SBWL5.cjs');
|
|
6
|
-
var
|
|
6
|
+
var chunkWRKO6Q42_cjs = require('./chunk-WRKO6Q42.cjs');
|
|
7
7
|
require('./chunk-QDJLQR53.cjs');
|
|
8
8
|
|
|
9
9
|
// src/events.ts
|
|
@@ -1286,7 +1286,7 @@ var VideoRenderer = class {
|
|
|
1286
1286
|
}
|
|
1287
1287
|
target.style.visibility = "hidden";
|
|
1288
1288
|
const overlayParent = parent instanceof HTMLElement ? parent : document.body;
|
|
1289
|
-
this.subtitleOverlay = new
|
|
1289
|
+
this.subtitleOverlay = new chunkWRKO6Q42_cjs.SubtitleOverlay(overlayParent);
|
|
1290
1290
|
this.watchTextTracks(target);
|
|
1291
1291
|
const ctx = this.canvas.getContext("2d");
|
|
1292
1292
|
if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
|
|
@@ -1751,6 +1751,10 @@ var AudioOutput = class {
|
|
|
1751
1751
|
if (this.ctx.state === "suspended") {
|
|
1752
1752
|
await this.ctx.resume();
|
|
1753
1753
|
}
|
|
1754
|
+
try {
|
|
1755
|
+
this.gain.connect(this.ctx.destination);
|
|
1756
|
+
} catch {
|
|
1757
|
+
}
|
|
1754
1758
|
if (this.state === "paused") {
|
|
1755
1759
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1756
1760
|
this.state = "playing";
|
|
@@ -1777,6 +1781,10 @@ var AudioOutput = class {
|
|
|
1777
1781
|
this.mediaTimeOfAnchor = this.now();
|
|
1778
1782
|
this.state = "paused";
|
|
1779
1783
|
if (this.noAudio) return;
|
|
1784
|
+
try {
|
|
1785
|
+
this.gain.disconnect();
|
|
1786
|
+
} catch {
|
|
1787
|
+
}
|
|
1780
1788
|
if (this.ctx.state === "running") {
|
|
1781
1789
|
await this.ctx.suspend();
|
|
1782
1790
|
}
|
|
@@ -3456,7 +3464,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3456
3464
|
switchingPromise = Promise.resolve();
|
|
3457
3465
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
3458
3466
|
// Revoked at destroy() so repeated source swaps don't leak.
|
|
3459
|
-
subtitleResources = new
|
|
3467
|
+
subtitleResources = new chunkWRKO6Q42_cjs.SubtitleResourceBag();
|
|
3460
3468
|
// Transport config extracted from CreatePlayerOptions. Threaded to probe,
|
|
3461
3469
|
// subtitle fetches, and strategy session creators. Not stored on MediaContext
|
|
3462
3470
|
// because it's runtime config, not media analysis.
|
|
@@ -3502,7 +3510,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3502
3510
|
}
|
|
3503
3511
|
}
|
|
3504
3512
|
if (this.options.directory && this.options.source instanceof File) {
|
|
3505
|
-
const found = await
|
|
3513
|
+
const found = await chunkWRKO6Q42_cjs.discoverSidecars(this.options.source, this.options.directory);
|
|
3506
3514
|
for (const s of found) {
|
|
3507
3515
|
this.subtitleResources.track(s.url);
|
|
3508
3516
|
ctx.subtitleTracks.push({
|
|
@@ -3525,7 +3533,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3525
3533
|
reason: decision.reason
|
|
3526
3534
|
});
|
|
3527
3535
|
await this.startSession(decision.strategy, decision.reason);
|
|
3528
|
-
await
|
|
3536
|
+
await chunkWRKO6Q42_cjs.attachSubtitleTracks(
|
|
3529
3537
|
this.options.target,
|
|
3530
3538
|
ctx.subtitleTracks,
|
|
3531
3539
|
this.subtitleResources,
|
|
@@ -4567,7 +4575,7 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4567
4575
|
* strategies pick up the new track via their textTracks watcher.
|
|
4568
4576
|
*/
|
|
4569
4577
|
async addSubtitle(subtitle) {
|
|
4570
|
-
const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-
|
|
4578
|
+
const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-HMVGWTU2.cjs');
|
|
4571
4579
|
const format = subtitle.format ?? (subtitle.url.endsWith(".srt") ? "srt" : "vtt");
|
|
4572
4580
|
const track = {
|
|
4573
4581
|
id: this._subtitleTracks.length,
|
|
@@ -4576,14 +4584,27 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4576
4584
|
sidecarUrl: subtitle.url
|
|
4577
4585
|
};
|
|
4578
4586
|
this._subtitleTracks.push(track);
|
|
4587
|
+
console.log(`[avbridge:subs] addSubtitle id=${track.id} format=${format} lang=${subtitle.language ?? "?"}`);
|
|
4579
4588
|
await attachSubtitleTracks2(
|
|
4580
4589
|
this._videoEl,
|
|
4581
4590
|
this._subtitleTracks,
|
|
4582
4591
|
void 0,
|
|
4583
4592
|
(err, t) => {
|
|
4584
|
-
console.warn(`[avbridge] subtitle ${t.id} failed: ${err.message}`);
|
|
4593
|
+
console.warn(`[avbridge:subs] subtitle ${t.id} failed: ${err.message}`);
|
|
4585
4594
|
}
|
|
4586
4595
|
);
|
|
4596
|
+
const textTracks = this._videoEl.textTracks;
|
|
4597
|
+
for (let i = 0; i < textTracks.length; i++) {
|
|
4598
|
+
if (textTracks[i].label === (subtitle.language ?? `Subtitle ${track.id}`)) {
|
|
4599
|
+
textTracks[i].mode = "showing";
|
|
4600
|
+
console.log(`[avbridge:subs] enabled textTrack[${i}] mode=showing`);
|
|
4601
|
+
break;
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
this._dispatch("trackschange", {
|
|
4605
|
+
audioTracks: this._audioTracks,
|
|
4606
|
+
subtitleTracks: this.subtitleTracks
|
|
4607
|
+
});
|
|
4587
4608
|
}
|
|
4588
4609
|
/**
|
|
4589
4610
|
* Disable the automatic `screen.orientation.lock()` that runs on
|
|
@@ -5397,7 +5418,7 @@ function formatTime(sec) {
|
|
|
5397
5418
|
return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
|
|
5398
5419
|
}
|
|
5399
5420
|
var PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
|
5400
|
-
var
|
|
5421
|
+
var DEFAULT_CONTROLS_HIDE_MS = 3e3;
|
|
5401
5422
|
var FORWARDED_EVENTS = [
|
|
5402
5423
|
"ready",
|
|
5403
5424
|
"error",
|
|
@@ -5440,7 +5461,7 @@ var PROXY_ATTRIBUTES = [
|
|
|
5440
5461
|
];
|
|
5441
5462
|
var PLAYER_ATTRIBUTES = ["show-fit"];
|
|
5442
5463
|
var FIT_MODES = ["contain", "cover", "fill"];
|
|
5443
|
-
var AvbridgePlayerElement = class extends HTMLElement {
|
|
5464
|
+
var AvbridgePlayerElement = class _AvbridgePlayerElement extends HTMLElement {
|
|
5444
5465
|
static observedAttributes = [...PROXY_ATTRIBUTES, ...PLAYER_ATTRIBUTES];
|
|
5445
5466
|
// ── Internal DOM refs ──────────────────────────────────────────────────
|
|
5446
5467
|
_video;
|
|
@@ -5468,6 +5489,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5468
5489
|
_state = "idle";
|
|
5469
5490
|
_controlsTimer = null;
|
|
5470
5491
|
_settingsOpen = false;
|
|
5492
|
+
_activeAudioTrackId = null;
|
|
5493
|
+
_activeSubtitleTrackId = null;
|
|
5471
5494
|
_userSeeking = false;
|
|
5472
5495
|
_holdTimer = null;
|
|
5473
5496
|
_holdSpeedActive = false;
|
|
@@ -5731,6 +5754,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5731
5754
|
const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
5732
5755
|
return frac * (this._video.duration || 0);
|
|
5733
5756
|
}
|
|
5757
|
+
/** Seekbar width below which drag-to-scrub seeks in real-time (vs
|
|
5758
|
+
* preview-only). On narrow bars precise positioning is hard, so
|
|
5759
|
+
* immediate video feedback is more useful than a time tooltip. */
|
|
5760
|
+
static SCRUB_WIDTH_THRESHOLD = 400;
|
|
5734
5761
|
_onSeekPointerDown(e) {
|
|
5735
5762
|
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
5736
5763
|
e.preventDefault();
|
|
@@ -5738,15 +5765,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5738
5765
|
const seekBar = this.shadowRoot.querySelector(".avp-seek");
|
|
5739
5766
|
seekBar.setPointerCapture(e.pointerId);
|
|
5740
5767
|
seekBar.setAttribute("data-seeking", "");
|
|
5768
|
+
const scrubMode = seekBar.getBoundingClientRect().width < _AvbridgePlayerElement.SCRUB_WIDTH_THRESHOLD;
|
|
5769
|
+
let lastScrubCommit = 0;
|
|
5741
5770
|
const initial = this._timeFromSeekPointer(e.clientX);
|
|
5742
5771
|
this._seekInput.value = String(initial);
|
|
5743
5772
|
this._onSeekInput();
|
|
5744
5773
|
this._updateSeekTooltip(e.clientX);
|
|
5774
|
+
if (scrubMode) this._onSeekCommit();
|
|
5745
5775
|
const onMove = (ev) => {
|
|
5746
5776
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
5747
5777
|
this._seekInput.value = String(t);
|
|
5748
5778
|
this._onSeekInput();
|
|
5749
5779
|
this._updateSeekTooltip(ev.clientX);
|
|
5780
|
+
if (scrubMode) {
|
|
5781
|
+
const now = performance.now();
|
|
5782
|
+
if (now - lastScrubCommit > 250) {
|
|
5783
|
+
lastScrubCommit = now;
|
|
5784
|
+
this._onSeekCommit();
|
|
5785
|
+
this._userSeeking = true;
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5750
5788
|
};
|
|
5751
5789
|
const onUp = (ev) => {
|
|
5752
5790
|
const t = this._timeFromSeekPointer(ev.clientX);
|
|
@@ -5847,19 +5885,27 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5847
5885
|
sections.push(selectRow("Speed", speedValue, speedOpts, `data-action="speed"`));
|
|
5848
5886
|
const audios = this._video.audioTracks ?? [];
|
|
5849
5887
|
if (audios.length > 1) {
|
|
5888
|
+
const activeAudioId = this._activeAudioTrackId ?? audios[0]?.id;
|
|
5889
|
+
const activeAudio = audios.find((t) => t.id === activeAudioId) ?? audios[0];
|
|
5890
|
+
const audioValue = activeAudio?.language ?? `Track ${activeAudio?.id ?? 1}`;
|
|
5850
5891
|
let audioOpts = "";
|
|
5851
5892
|
for (const t of audios) {
|
|
5852
|
-
|
|
5893
|
+
const sel = t.id === activeAudioId ? " selected" : "";
|
|
5894
|
+
audioOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
|
|
5853
5895
|
}
|
|
5854
|
-
sections.push(selectRow("Audio",
|
|
5896
|
+
sections.push(selectRow("Audio", audioValue, audioOpts, `data-action="audio"`));
|
|
5855
5897
|
}
|
|
5856
5898
|
const subs = this._video.subtitleTracks ?? [];
|
|
5857
5899
|
if (subs.length > 0) {
|
|
5858
|
-
|
|
5900
|
+
const activeSubId = this._activeSubtitleTrackId;
|
|
5901
|
+
const activeSub = activeSubId != null ? subs.find((t) => t.id === activeSubId) : null;
|
|
5902
|
+
const subValue = activeSub ? activeSub.language ?? `Track ${activeSub.id}` : "Off";
|
|
5903
|
+
let subOpts = `<option value="-1"${activeSubId == null ? " selected" : ""}>Off</option>`;
|
|
5859
5904
|
for (const t of subs) {
|
|
5860
|
-
|
|
5905
|
+
const sel = t.id === activeSubId ? " selected" : "";
|
|
5906
|
+
subOpts += `<option value="${t.id}"${sel}>${t.language ?? `Track ${t.id}`}</option>`;
|
|
5861
5907
|
}
|
|
5862
|
-
sections.push(selectRow("Subtitles",
|
|
5908
|
+
sections.push(selectRow("Subtitles", subValue, subOpts, `data-action="subtitle"`));
|
|
5863
5909
|
}
|
|
5864
5910
|
if (this.hasAttribute("show-fit")) {
|
|
5865
5911
|
const currentFit = this._video.fit ?? "contain";
|
|
@@ -5899,11 +5945,15 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5899
5945
|
this._video.playbackRate = Number(val);
|
|
5900
5946
|
break;
|
|
5901
5947
|
case "audio":
|
|
5948
|
+
this._activeAudioTrackId = Number(val);
|
|
5902
5949
|
void this._video.setAudioTrack(Number(val));
|
|
5903
5950
|
break;
|
|
5904
|
-
case "subtitle":
|
|
5905
|
-
|
|
5951
|
+
case "subtitle": {
|
|
5952
|
+
const subId = Number(val);
|
|
5953
|
+
this._activeSubtitleTrackId = subId >= 0 ? subId : null;
|
|
5954
|
+
void this._video.setSubtitleTrack(subId >= 0 ? subId : null);
|
|
5906
5955
|
break;
|
|
5956
|
+
}
|
|
5907
5957
|
case "fit":
|
|
5908
5958
|
this.setAttribute("fit", val);
|
|
5909
5959
|
break;
|
|
@@ -5998,16 +6048,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5998
6048
|
_showControls() {
|
|
5999
6049
|
this.showControls();
|
|
6000
6050
|
}
|
|
6001
|
-
_scheduleHide(durationMs
|
|
6051
|
+
_scheduleHide(durationMs) {
|
|
6052
|
+
const ms = durationMs ?? this._getControlsTimeout();
|
|
6002
6053
|
if (this._controlsTimer) clearTimeout(this._controlsTimer);
|
|
6003
6054
|
if (this._state !== "playing" && this._state !== "buffering") return;
|
|
6004
6055
|
if (this._settingsOpen) return;
|
|
6056
|
+
if (ms <= 0) return;
|
|
6005
6057
|
this._controlsTimer = setTimeout(() => {
|
|
6006
6058
|
if (this._state === "playing") {
|
|
6007
6059
|
this.setAttribute("data-controls-hidden", "");
|
|
6008
6060
|
this._toolbarTop.setAttribute("data-visible", "false");
|
|
6009
6061
|
}
|
|
6010
|
-
},
|
|
6062
|
+
}, ms);
|
|
6063
|
+
}
|
|
6064
|
+
/** Read the controls-timeout attribute. 0 or negative = never hide.
|
|
6065
|
+
* Unset = default 3000ms. */
|
|
6066
|
+
_getControlsTimeout() {
|
|
6067
|
+
const attr = this.getAttribute("controls-timeout");
|
|
6068
|
+
if (attr == null) return DEFAULT_CONTROLS_HIDE_MS;
|
|
6069
|
+
const n = Number(attr);
|
|
6070
|
+
return Number.isFinite(n) ? n : DEFAULT_CONTROLS_HIDE_MS;
|
|
6011
6071
|
}
|
|
6012
6072
|
// Strategy is visible in Stats for Nerds, no badge in controls bar.
|
|
6013
6073
|
// ── Click / tap handling (YouTube delayed-tap pattern) ──────────────────
|
|
@@ -6019,6 +6079,9 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6019
6079
|
// it's treated as a double-click and the single-click action is cancelled.
|
|
6020
6080
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
6021
6081
|
_lastPointerTypeWasTouch = false;
|
|
6082
|
+
/** True for ~50ms after a touch double-tap was handled, so the
|
|
6083
|
+
* synthetic dblclick from the browser doesn't also fire fullscreen. */
|
|
6084
|
+
_touchDoubleTapConsumed = false;
|
|
6022
6085
|
/** True if the event's composed path passes through consumer-slotted
|
|
6023
6086
|
* content (toolbar or content-overlay). Slotted content lives in the
|
|
6024
6087
|
* light DOM so `.closest(".avp-toolbar-top")` on the event target won't
|
|
@@ -6052,6 +6115,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6052
6115
|
_onContainerDblClick(e) {
|
|
6053
6116
|
if (e.target.closest?.(".avp-controls, .avp-settings")) return;
|
|
6054
6117
|
if (this._isSlottedContentEvent(e)) return;
|
|
6118
|
+
if (this._touchDoubleTapConsumed) return;
|
|
6055
6119
|
if (this._tapTimer) {
|
|
6056
6120
|
clearTimeout(this._tapTimer);
|
|
6057
6121
|
this._tapTimer = null;
|
|
@@ -6093,6 +6157,10 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6093
6157
|
} else {
|
|
6094
6158
|
this._toggleFullscreen();
|
|
6095
6159
|
}
|
|
6160
|
+
this._touchDoubleTapConsumed = true;
|
|
6161
|
+
setTimeout(() => {
|
|
6162
|
+
this._touchDoubleTapConsumed = false;
|
|
6163
|
+
}, 100);
|
|
6096
6164
|
this._lastTapTime = 0;
|
|
6097
6165
|
return;
|
|
6098
6166
|
}
|
|
@@ -6126,6 +6194,13 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6126
6194
|
this._video.currentTime = Math.max(0, this._video.currentTime + delta);
|
|
6127
6195
|
}
|
|
6128
6196
|
// ── Keyboard shortcuts ─────────────────────────────────────────────────
|
|
6197
|
+
/** Duration of one frame in seconds, derived from diagnostics fps or
|
|
6198
|
+
* a 30fps default. Used for frame-step shortcuts (`,` / `.`). */
|
|
6199
|
+
_frameDuration() {
|
|
6200
|
+
const diag = this._video.getDiagnostics();
|
|
6201
|
+
const fps = diag?.fps && diag.fps > 0 ? diag.fps : 30;
|
|
6202
|
+
return 1 / fps;
|
|
6203
|
+
}
|
|
6129
6204
|
_onKeydown(e) {
|
|
6130
6205
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
|
6131
6206
|
switch (e.key) {
|
|
@@ -6170,6 +6245,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
6170
6245
|
this._video.playbackRate = Math.max(0.25, this._video.playbackRate - 0.25);
|
|
6171
6246
|
this._buildSettingsMenu();
|
|
6172
6247
|
break;
|
|
6248
|
+
case ",":
|
|
6249
|
+
e.preventDefault();
|
|
6250
|
+
if (!this._video.paused) this._video.pause();
|
|
6251
|
+
this._video.currentTime = Math.max(0, this._video.currentTime - this._frameDuration());
|
|
6252
|
+
break;
|
|
6253
|
+
case ".":
|
|
6254
|
+
e.preventDefault();
|
|
6255
|
+
if (!this._video.paused) this._video.pause();
|
|
6256
|
+
this._video.currentTime = Math.min(
|
|
6257
|
+
this._video.duration || 0,
|
|
6258
|
+
this._video.currentTime + this._frameDuration()
|
|
6259
|
+
);
|
|
6260
|
+
break;
|
|
6173
6261
|
case "Escape":
|
|
6174
6262
|
if (this._settingsOpen) {
|
|
6175
6263
|
e.preventDefault();
|