avbridge 2.9.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 +65 -0
- package/dist/{chunk-EY6DZEDT.cjs → chunk-37UOSAVI.cjs} +55 -10
- 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-SN4WZE24.js → chunk-IHNHHEA2.js} +51 -6
- 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 +63 -4
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +18 -5
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +17 -4
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +10 -10
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{player-DEcidWk6.d.cts → player-DDdNVFDv.d.cts} +23 -1
- package/dist/{player-DEcidWk6.d.ts → player-DDdNVFDv.d.ts} +23 -1
- package/dist/player.cjs +329 -109
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +42 -0
- package/dist/player.d.ts +42 -0
- package/dist/player.js +325 -105
- 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 +235 -78
- package/src/element/avbridge-subtitles.ts +273 -0
- package/src/element/avbridge-video.ts +21 -1
- package/src/element/player-styles.ts +85 -35
- package/src/index.ts +1 -0
- package/src/strategies/fallback/audio-output.ts +39 -4
- package/src/strategies/fallback/index.ts +12 -0
- package/src/strategies/hybrid/index.ts +9 -0
- package/src/subtitles/index.ts +2 -0
- package/src/types.ts +25 -0
- package/dist/chunk-5KVLE6YI.js.map +0 -1
- package/dist/chunk-EY6DZEDT.cjs.map +0 -1
- package/dist/chunk-S4WAZC2T.cjs.map +0 -1
- package/dist/chunk-SN4WZE24.js.map +0 -1
- package/dist/subtitles-4T74JRGT.js +0 -4
- package/dist/subtitles-QUH4LPI4.cjs +0 -29
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,71 @@ All notable changes to **avbridge.js** are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [2.11.0]
|
|
8
|
+
|
|
9
|
+
Subtitle panel, interaction fixes, and quality-of-life.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`<avbridge-subtitles>` element** — scrollable cue timeline panel.
|
|
14
|
+
Connects to a player via `for` attribute, renders all subtitle cues
|
|
15
|
+
as timestamped rows, highlights the active cue, auto-scrolls to
|
|
16
|
+
follow playback, and click-to-seek. Shadow DOM with dark styles.
|
|
17
|
+
- **Frame-by-frame keyboard shortcuts** (`,` / `.`) — YouTube-style.
|
|
18
|
+
Pauses if playing, then steps back/forward one frame (1/fps).
|
|
19
|
+
- **Real-time scrub seeking on narrow seekbars** — when the seekbar
|
|
20
|
+
is <400px wide, dragging seeks in real-time (throttled to 4 Hz)
|
|
21
|
+
instead of preview-only. Immediate video feedback on small players.
|
|
22
|
+
- **`controls-timeout` attribute** on `<avbridge-player>`. Customize
|
|
23
|
+
the auto-hide duration (default 3000ms). Set to `"0"` to disable
|
|
24
|
+
auto-hide entirely (always-visible controls).
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Subtitles not showing** — `addSubtitle()` didn't dispatch
|
|
29
|
+
`trackschange` (settings sheet never showed the new track) and the
|
|
30
|
+
`<track>` element was created with `mode="disabled"` (never
|
|
31
|
+
rendered on native strategy). Now dispatches + auto-enables.
|
|
32
|
+
- **Audio bleed on pause** for hybrid/fallback — already-scheduled
|
|
33
|
+
AudioBufferSourceNodes kept playing ~200ms after pause. Now
|
|
34
|
+
disconnects the gain node immediately (same pattern as seek/reset).
|
|
35
|
+
- **Double-tap fires both ff/rw AND fullscreen on touch** — browser's
|
|
36
|
+
synthetic `dblclick` after two rapid taps was calling fullscreen
|
|
37
|
+
on top of the touch handler's ff/rw. Blocked via a consumed flag.
|
|
38
|
+
- **Settings sheet didn't show active audio/subtitle selection** —
|
|
39
|
+
always displayed "Track 1" / "Off" regardless of actual state.
|
|
40
|
+
|
|
41
|
+
## [2.10.0]
|
|
42
|
+
|
|
43
|
+
Settings UI overhaul + playback rate on all strategies.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **Bottom-sheet settings panel** replacing the popup menu. Slides up
|
|
48
|
+
from the controls bar with a scrim overlay. Each section uses a
|
|
49
|
+
native `<select>` picker overlaid on a styled row — the OS picker
|
|
50
|
+
renders outside the player bounds (intentional for small players).
|
|
51
|
+
Rows show label left, current value right. Tapping anywhere outside
|
|
52
|
+
the sheet or pressing Escape dismisses it.
|
|
53
|
+
- **Consumer extensibility API**: `player.addSettingsSection({ id,
|
|
54
|
+
label, items, onSelect })` / `player.removeSettingsSection(id)`.
|
|
55
|
+
Custom sections render after built-in ones using the same native
|
|
56
|
+
`<select>` pattern. New `SettingsSectionConfig` type exported.
|
|
57
|
+
- **Playback rate on hybrid + fallback strategies.** `playbackRate`
|
|
58
|
+
was a no-op on canvas strategies because the inner `<video>` has
|
|
59
|
+
no `src`. Now patched via `Object.defineProperty` — drives the
|
|
60
|
+
`AudioOutput` clock speed + `AudioBufferSourceNode.playbackRate`
|
|
61
|
+
for pitch-shifted audio. Video renderer follows automatically
|
|
62
|
+
since it syncs to `audio.now()`. The `ratechange` event fires.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **Settings menu sizing** — JS-measured max-height (70% of player)
|
|
67
|
+
replaces the broken CSS percentage approach.
|
|
68
|
+
- **Blue tap-highlight flash** suppressed on `<avbridge-player>`.
|
|
69
|
+
- **`cursor: pointer` removed** from the player container — the
|
|
70
|
+
video surface isn't a button.
|
|
71
|
+
|
|
7
72
|
## [2.9.0]
|
|
8
73
|
|
|
9
74
|
Player chrome ergonomics — four changes driven by explorer integration
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkWRKO6Q42_cjs = require('./chunk-WRKO6Q42.cjs');
|
|
4
4
|
var chunkBYGZN4Z5_cjs = require('./chunk-BYGZN4Z5.cjs');
|
|
5
5
|
var chunk2IJ66NTD_cjs = require('./chunk-2IJ66NTD.cjs');
|
|
6
6
|
var chunkL7A3ECI2_cjs = require('./chunk-L7A3ECI2.cjs');
|
|
@@ -1055,7 +1055,7 @@ var VideoRenderer = class {
|
|
|
1055
1055
|
}
|
|
1056
1056
|
target.style.visibility = "hidden";
|
|
1057
1057
|
const overlayParent = parent instanceof HTMLElement ? parent : document.body;
|
|
1058
|
-
this.subtitleOverlay = new
|
|
1058
|
+
this.subtitleOverlay = new chunkWRKO6Q42_cjs.SubtitleOverlay(overlayParent);
|
|
1059
1059
|
this.watchTextTracks(target);
|
|
1060
1060
|
const ctx = this.canvas.getContext("2d");
|
|
1061
1061
|
if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
|
|
@@ -1376,6 +1376,10 @@ var AudioOutput = class {
|
|
|
1376
1376
|
_volume = 1;
|
|
1377
1377
|
/** User-set muted flag. When true, gain is forced to 0. */
|
|
1378
1378
|
_muted = false;
|
|
1379
|
+
/** Playback rate. Scales the media clock and each AudioBufferSourceNode's
|
|
1380
|
+
* playbackRate so audio pitches up/down accordingly (same as native
|
|
1381
|
+
* <video>.playbackRate). Default 1. */
|
|
1382
|
+
_rate = 1;
|
|
1379
1383
|
constructor() {
|
|
1380
1384
|
this.ctx = new AudioContext();
|
|
1381
1385
|
this.gain = this.ctx.createGain();
|
|
@@ -1397,6 +1401,20 @@ var AudioOutput = class {
|
|
|
1397
1401
|
getMuted() {
|
|
1398
1402
|
return this._muted;
|
|
1399
1403
|
}
|
|
1404
|
+
/** Set playback rate. Scales the media clock and pitches audio output
|
|
1405
|
+
* (same as native <video>.playbackRate — speed without pitch correction).
|
|
1406
|
+
* Rebases the anchor so the clock transition is seamless. */
|
|
1407
|
+
setPlaybackRate(rate) {
|
|
1408
|
+
if (rate === this._rate) return;
|
|
1409
|
+
const t = this.now();
|
|
1410
|
+
this.mediaTimeOfAnchor = t;
|
|
1411
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1412
|
+
this.wallAnchorMs = performance.now();
|
|
1413
|
+
this._rate = rate;
|
|
1414
|
+
}
|
|
1415
|
+
getPlaybackRate() {
|
|
1416
|
+
return this._rate;
|
|
1417
|
+
}
|
|
1400
1418
|
applyGain() {
|
|
1401
1419
|
const target = this._muted ? 0 : this._volume;
|
|
1402
1420
|
try {
|
|
@@ -1417,12 +1435,12 @@ var AudioOutput = class {
|
|
|
1417
1435
|
now() {
|
|
1418
1436
|
if (this.noAudio) {
|
|
1419
1437
|
if (this.state === "playing") {
|
|
1420
|
-
return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3;
|
|
1438
|
+
return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3 * this._rate;
|
|
1421
1439
|
}
|
|
1422
1440
|
return this.mediaTimeOfAnchor;
|
|
1423
1441
|
}
|
|
1424
1442
|
if (this.state === "playing") {
|
|
1425
|
-
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor);
|
|
1443
|
+
return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
|
|
1426
1444
|
}
|
|
1427
1445
|
return this.mediaTimeOfAnchor;
|
|
1428
1446
|
}
|
|
@@ -1474,7 +1492,8 @@ var AudioOutput = class {
|
|
|
1474
1492
|
const node = this.ctx.createBufferSource();
|
|
1475
1493
|
node.buffer = buffer;
|
|
1476
1494
|
node.connect(this.gain);
|
|
1477
|
-
|
|
1495
|
+
if (this._rate !== 1) node.playbackRate.value = this._rate;
|
|
1496
|
+
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
|
|
1478
1497
|
if (ctxStart < this.ctx.currentTime) {
|
|
1479
1498
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1480
1499
|
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
@@ -1501,6 +1520,10 @@ var AudioOutput = class {
|
|
|
1501
1520
|
if (this.ctx.state === "suspended") {
|
|
1502
1521
|
await this.ctx.resume();
|
|
1503
1522
|
}
|
|
1523
|
+
try {
|
|
1524
|
+
this.gain.connect(this.ctx.destination);
|
|
1525
|
+
} catch {
|
|
1526
|
+
}
|
|
1504
1527
|
if (this.state === "paused") {
|
|
1505
1528
|
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1506
1529
|
this.state = "playing";
|
|
@@ -1527,6 +1550,10 @@ var AudioOutput = class {
|
|
|
1527
1550
|
this.mediaTimeOfAnchor = this.now();
|
|
1528
1551
|
this.state = "paused";
|
|
1529
1552
|
if (this.noAudio) return;
|
|
1553
|
+
try {
|
|
1554
|
+
this.gain.disconnect();
|
|
1555
|
+
} catch {
|
|
1556
|
+
}
|
|
1530
1557
|
if (this.ctx.state === "running") {
|
|
1531
1558
|
await this.ctx.suspend();
|
|
1532
1559
|
}
|
|
@@ -2122,6 +2149,14 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2122
2149
|
get: () => ctx.duration ?? NaN
|
|
2123
2150
|
});
|
|
2124
2151
|
}
|
|
2152
|
+
Object.defineProperty(target, "playbackRate", {
|
|
2153
|
+
configurable: true,
|
|
2154
|
+
get: () => audio.getPlaybackRate(),
|
|
2155
|
+
set: (v) => {
|
|
2156
|
+
audio.setPlaybackRate(v);
|
|
2157
|
+
target.dispatchEvent(new Event("ratechange"));
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2125
2160
|
Object.defineProperty(target, "readyState", {
|
|
2126
2161
|
configurable: true,
|
|
2127
2162
|
get: () => {
|
|
@@ -2232,6 +2267,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2232
2267
|
delete target.muted;
|
|
2233
2268
|
delete target.readyState;
|
|
2234
2269
|
delete target.seekable;
|
|
2270
|
+
delete target.playbackRate;
|
|
2235
2271
|
} catch {
|
|
2236
2272
|
}
|
|
2237
2273
|
},
|
|
@@ -2767,6 +2803,14 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
2767
2803
|
get: () => ctx.duration ?? NaN
|
|
2768
2804
|
});
|
|
2769
2805
|
}
|
|
2806
|
+
Object.defineProperty(target, "playbackRate", {
|
|
2807
|
+
configurable: true,
|
|
2808
|
+
get: () => audio.getPlaybackRate(),
|
|
2809
|
+
set: (v) => {
|
|
2810
|
+
audio.setPlaybackRate(v);
|
|
2811
|
+
target.dispatchEvent(new Event("ratechange"));
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2770
2814
|
Object.defineProperty(target, "readyState", {
|
|
2771
2815
|
configurable: true,
|
|
2772
2816
|
get: () => {
|
|
@@ -2898,6 +2942,7 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
2898
2942
|
delete target.muted;
|
|
2899
2943
|
delete target.readyState;
|
|
2900
2944
|
delete target.seekable;
|
|
2945
|
+
delete target.playbackRate;
|
|
2901
2946
|
} catch {
|
|
2902
2947
|
}
|
|
2903
2948
|
},
|
|
@@ -3008,7 +3053,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3008
3053
|
switchingPromise = Promise.resolve();
|
|
3009
3054
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
3010
3055
|
// Revoked at destroy() so repeated source swaps don't leak.
|
|
3011
|
-
subtitleResources = new
|
|
3056
|
+
subtitleResources = new chunkWRKO6Q42_cjs.SubtitleResourceBag();
|
|
3012
3057
|
// Transport config extracted from CreatePlayerOptions. Threaded to probe,
|
|
3013
3058
|
// subtitle fetches, and strategy session creators. Not stored on MediaContext
|
|
3014
3059
|
// because it's runtime config, not media analysis.
|
|
@@ -3054,7 +3099,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3054
3099
|
}
|
|
3055
3100
|
}
|
|
3056
3101
|
if (this.options.directory && this.options.source instanceof File) {
|
|
3057
|
-
const found = await
|
|
3102
|
+
const found = await chunkWRKO6Q42_cjs.discoverSidecars(this.options.source, this.options.directory);
|
|
3058
3103
|
for (const s of found) {
|
|
3059
3104
|
this.subtitleResources.track(s.url);
|
|
3060
3105
|
ctx.subtitleTracks.push({
|
|
@@ -3077,7 +3122,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3077
3122
|
reason: decision.reason
|
|
3078
3123
|
});
|
|
3079
3124
|
await this.startSession(decision.strategy, decision.reason);
|
|
3080
|
-
await
|
|
3125
|
+
await chunkWRKO6Q42_cjs.attachSubtitleTracks(
|
|
3081
3126
|
this.options.target,
|
|
3082
3127
|
ctx.subtitleTracks,
|
|
3083
3128
|
this.subtitleResources,
|
|
@@ -3506,5 +3551,5 @@ exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
|
|
|
3506
3551
|
exports.UnifiedPlayer = UnifiedPlayer;
|
|
3507
3552
|
exports.classifyContext = classifyContext;
|
|
3508
3553
|
exports.createPlayer = createPlayer;
|
|
3509
|
-
//# sourceMappingURL=chunk-
|
|
3510
|
-
//# sourceMappingURL=chunk-
|
|
3554
|
+
//# sourceMappingURL=chunk-37UOSAVI.cjs.map
|
|
3555
|
+
//# sourceMappingURL=chunk-37UOSAVI.cjs.map
|