avbridge 2.6.0 → 2.8.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 +131 -0
- package/README.md +23 -0
- package/dist/{chunk-6SOFJV44.cjs → chunk-IUSFLVLJ.cjs} +21 -6
- package/dist/chunk-IUSFLVLJ.cjs.map +1 -0
- package/dist/{chunk-OGYHFY6K.js → chunk-JSQOBUQB.js} +21 -6
- package/dist/chunk-JSQOBUQB.js.map +1 -0
- package/dist/element-browser.js +146 -7
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +129 -5
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +52 -3
- package/dist/element.d.ts +52 -3
- package/dist/element.js +128 -4
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +8 -8
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{player-DGXeCNfD.d.cts → player-DXEKOky8.d.cts} +3 -0
- package/dist/{player-DGXeCNfD.d.ts → player-DXEKOky8.d.ts} +3 -0
- package/dist/player.cjs +222 -11
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +61 -3
- package/dist/player.d.ts +61 -3
- package/dist/player.js +222 -11
- package/dist/player.js.map +1 -1
- package/package.json +7 -1
- package/src/classify/rules.ts +23 -0
- package/src/element/avbridge-player.ts +40 -5
- package/src/element/avbridge-video.ts +148 -4
- package/src/element/player-styles.ts +44 -0
- package/src/element.ts +3 -3
- package/src/strategies/fallback/decoder.ts +14 -7
- package/src/strategies/fallback/video-renderer.ts +7 -5
- package/src/types.ts +1 -0
- package/dist/chunk-6SOFJV44.cjs.map +0 -1
- package/dist/chunk-OGYHFY6K.js.map +0 -1
package/dist/player.d.cts
CHANGED
|
@@ -283,6 +283,9 @@ interface AvbridgeVideoElementEventMap {
|
|
|
283
283
|
buffered: TimeRanges;
|
|
284
284
|
}>;
|
|
285
285
|
loadstart: CustomEvent<Record<string, never>>;
|
|
286
|
+
fitchange: CustomEvent<{
|
|
287
|
+
fit: "contain" | "cover" | "fill";
|
|
288
|
+
}>;
|
|
286
289
|
}
|
|
287
290
|
|
|
288
291
|
/**
|
|
@@ -297,7 +300,7 @@ interface AvbridgeVideoElementEventMap {
|
|
|
297
300
|
*/
|
|
298
301
|
|
|
299
302
|
declare class AvbridgePlayerElement extends HTMLElement {
|
|
300
|
-
static readonly observedAttributes: ("src" | "muted" | "autoplay" | "loop" | "preload" | "poster" | "playsinline" | "crossorigin" | "disableremoteplayback" | "preferstrategy")[];
|
|
303
|
+
static readonly observedAttributes: ("src" | "muted" | "autoplay" | "loop" | "preload" | "poster" | "playsinline" | "crossorigin" | "disableremoteplayback" | "preferstrategy" | "fit")[];
|
|
301
304
|
private _video;
|
|
302
305
|
private _playBtn;
|
|
303
306
|
private _overlayBtn;
|
|
@@ -328,6 +331,7 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
328
331
|
private _statsEl;
|
|
329
332
|
private _statsInterval;
|
|
330
333
|
private _eventCleanup;
|
|
334
|
+
private _updateToolbarEmpty;
|
|
331
335
|
constructor();
|
|
332
336
|
private _template;
|
|
333
337
|
private _bindEvents;
|
|
@@ -357,6 +361,11 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
357
361
|
private _scheduleHide;
|
|
358
362
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
359
363
|
private _lastPointerTypeWasTouch;
|
|
364
|
+
/** True if the event's composed path passes through consumer-slotted toolbar
|
|
365
|
+
* content. Slotted content lives in the light DOM so `.closest(".avp-toolbar-top")`
|
|
366
|
+
* on the event target won't find the shadow-DOM wrapper — `composedPath()`
|
|
367
|
+
* does. */
|
|
368
|
+
private _isToolbarEvent;
|
|
360
369
|
private _onContainerClick;
|
|
361
370
|
private _onContainerDblClick;
|
|
362
371
|
private _onPointerDown;
|
|
@@ -506,14 +515,17 @@ declare class UnifiedPlayer {
|
|
|
506
515
|
* 3. Give consumers a `<video>`-compatible primitive they can wrap with
|
|
507
516
|
* their own UI.
|
|
508
517
|
*
|
|
509
|
-
* **It is not a player UI framework.**
|
|
510
|
-
*
|
|
518
|
+
* **It is not a player UI framework.** For YouTube-style chrome (seek
|
|
519
|
+
* bar, play/pause, settings menu, fullscreen, auto-hiding controls) use
|
|
520
|
+
* `<avbridge-player>` — it wraps this element with a full UI. See
|
|
511
521
|
* `docs/dev/WEB_COMPONENT_SPEC.md` for the full spec, lifecycle invariants,
|
|
512
522
|
* and edge case list.
|
|
513
523
|
*/
|
|
514
524
|
|
|
515
525
|
/** Strategy preference passed via the `preferstrategy` attribute. */
|
|
516
526
|
type PreferredStrategy = "auto" | StrategyName;
|
|
527
|
+
/** Fit mode — how the video fills the element's box. Mirrors CSS object-fit. */
|
|
528
|
+
type FitMode = "contain" | "cover" | "fill";
|
|
517
529
|
/**
|
|
518
530
|
* `HTMLElement` is a browser-only global. SSR frameworks (Next.js, Astro,
|
|
519
531
|
* Remix, etc.) commonly import library modules on the server to extract
|
|
@@ -586,12 +598,26 @@ declare class AvbridgeVideoElement extends HTMLElementCtor {
|
|
|
586
598
|
* native fails.
|
|
587
599
|
*/
|
|
588
600
|
private _preferredStrategy;
|
|
601
|
+
/** Current fit mode. Applied to the inner `<video>` via object-fit, and
|
|
602
|
+
* to the fallback canvas via the `--avbridge-fit` CSS custom property on
|
|
603
|
+
* the stage wrapper (see `src/strategies/fallback/video-renderer.ts`). */
|
|
604
|
+
private _fit;
|
|
605
|
+
/** The stage wrapper — the element the canvas attaches into, and where
|
|
606
|
+
* the `--avbridge-fit` CSS custom property lives. */
|
|
607
|
+
private _stageEl;
|
|
589
608
|
/** Set if currentTime was assigned before the player was ready. */
|
|
590
609
|
private _pendingSeek;
|
|
591
610
|
/** Set if play() was called before the player was ready. */
|
|
592
611
|
private _pendingPlay;
|
|
593
612
|
/** MutationObserver tracking light-DOM `<track>` children. */
|
|
594
613
|
private _trackObserver;
|
|
614
|
+
/** Document-level fullscreenchange handler — installed while connected so
|
|
615
|
+
* the element can lock/unlock screen orientation to match the video's
|
|
616
|
+
* intrinsic aspect. */
|
|
617
|
+
private _fullscreenChangeHandler;
|
|
618
|
+
/** True if we successfully called screen.orientation.lock() on the last
|
|
619
|
+
* fullscreen entry. Used to know whether to unlock on exit. */
|
|
620
|
+
private _orientationLocked;
|
|
595
621
|
constructor();
|
|
596
622
|
connectedCallback(): void;
|
|
597
623
|
disconnectedCallback(): void;
|
|
@@ -635,6 +661,8 @@ declare class AvbridgeVideoElement extends HTMLElementCtor {
|
|
|
635
661
|
set preload(value: "none" | "metadata" | "auto");
|
|
636
662
|
get diagnostics(): boolean;
|
|
637
663
|
set diagnostics(value: boolean);
|
|
664
|
+
get fit(): FitMode;
|
|
665
|
+
set fit(value: FitMode);
|
|
638
666
|
get preferredStrategy(): PreferredStrategy;
|
|
639
667
|
set preferredStrategy(value: PreferredStrategy);
|
|
640
668
|
get currentTime(): number;
|
|
@@ -724,6 +752,36 @@ declare class AvbridgeVideoElement extends HTMLElementCtor {
|
|
|
724
752
|
language?: string;
|
|
725
753
|
format?: "vtt" | "srt";
|
|
726
754
|
}): Promise<void>;
|
|
755
|
+
/**
|
|
756
|
+
* Disable the automatic `screen.orientation.lock()` that runs on
|
|
757
|
+
* fullscreen entry. Set when you want to honor the device's native
|
|
758
|
+
* auto-rotate instead of matching the video's intrinsic orientation.
|
|
759
|
+
*/
|
|
760
|
+
get noOrientationLock(): boolean;
|
|
761
|
+
set noOrientationLock(value: boolean);
|
|
762
|
+
/** Called whenever `document.fullscreenchange` fires. If this element (or
|
|
763
|
+
* any of its ancestors) is now fullscreen, derive the target orientation
|
|
764
|
+
* from the video's intrinsic size and call `screen.orientation.lock()`.
|
|
765
|
+
* On exit, release the lock we took. iOS Safari rejects `lock()` — we
|
|
766
|
+
* swallow the rejection so nothing breaks on that path. */
|
|
767
|
+
private _onFullscreenChange;
|
|
768
|
+
/** Walk composed-tree ancestors to see if `target` is this element or
|
|
769
|
+
* any ancestor across shadow boundaries. `Node.contains()` can't cross
|
|
770
|
+
* shadow roots, so when `<avbridge-player>` (the fullscreen element)
|
|
771
|
+
* hosts this `<avbridge-video>` inside its shadow DOM, `contains()`
|
|
772
|
+
* returns false. */
|
|
773
|
+
private _isInsideOrEquals;
|
|
774
|
+
/** Derive "landscape" / "portrait" from the intrinsic video dimensions.
|
|
775
|
+
* Returns null when dimensions aren't known yet or the video is square.
|
|
776
|
+
* Uses `videoWidth` / `videoHeight` from the inner `<video>`, which the
|
|
777
|
+
* browser sets to the display-aspect-corrected size (so anamorphic
|
|
778
|
+
* content is judged by its display aspect, not pixel aspect). */
|
|
779
|
+
private _desiredOrientation;
|
|
780
|
+
/** Attempt to lock screen orientation. Swallows rejections — iOS Safari
|
|
781
|
+
* doesn't implement `lock()`, and desktop / non-fullscreen contexts will
|
|
782
|
+
* reject too. Records success so we know whether to unlock on exit. */
|
|
783
|
+
private _lockOrientation;
|
|
784
|
+
private _releaseOrientationLock;
|
|
727
785
|
/** Force a (re-)bootstrap if a source is currently set. */
|
|
728
786
|
load(): Promise<void>;
|
|
729
787
|
/**
|
package/dist/player.d.ts
CHANGED
|
@@ -283,6 +283,9 @@ interface AvbridgeVideoElementEventMap {
|
|
|
283
283
|
buffered: TimeRanges;
|
|
284
284
|
}>;
|
|
285
285
|
loadstart: CustomEvent<Record<string, never>>;
|
|
286
|
+
fitchange: CustomEvent<{
|
|
287
|
+
fit: "contain" | "cover" | "fill";
|
|
288
|
+
}>;
|
|
286
289
|
}
|
|
287
290
|
|
|
288
291
|
/**
|
|
@@ -297,7 +300,7 @@ interface AvbridgeVideoElementEventMap {
|
|
|
297
300
|
*/
|
|
298
301
|
|
|
299
302
|
declare class AvbridgePlayerElement extends HTMLElement {
|
|
300
|
-
static readonly observedAttributes: ("src" | "muted" | "autoplay" | "loop" | "preload" | "poster" | "playsinline" | "crossorigin" | "disableremoteplayback" | "preferstrategy")[];
|
|
303
|
+
static readonly observedAttributes: ("src" | "muted" | "autoplay" | "loop" | "preload" | "poster" | "playsinline" | "crossorigin" | "disableremoteplayback" | "preferstrategy" | "fit")[];
|
|
301
304
|
private _video;
|
|
302
305
|
private _playBtn;
|
|
303
306
|
private _overlayBtn;
|
|
@@ -328,6 +331,7 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
328
331
|
private _statsEl;
|
|
329
332
|
private _statsInterval;
|
|
330
333
|
private _eventCleanup;
|
|
334
|
+
private _updateToolbarEmpty;
|
|
331
335
|
constructor();
|
|
332
336
|
private _template;
|
|
333
337
|
private _bindEvents;
|
|
@@ -357,6 +361,11 @@ declare class AvbridgePlayerElement extends HTMLElement {
|
|
|
357
361
|
private _scheduleHide;
|
|
358
362
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
359
363
|
private _lastPointerTypeWasTouch;
|
|
364
|
+
/** True if the event's composed path passes through consumer-slotted toolbar
|
|
365
|
+
* content. Slotted content lives in the light DOM so `.closest(".avp-toolbar-top")`
|
|
366
|
+
* on the event target won't find the shadow-DOM wrapper — `composedPath()`
|
|
367
|
+
* does. */
|
|
368
|
+
private _isToolbarEvent;
|
|
360
369
|
private _onContainerClick;
|
|
361
370
|
private _onContainerDblClick;
|
|
362
371
|
private _onPointerDown;
|
|
@@ -506,14 +515,17 @@ declare class UnifiedPlayer {
|
|
|
506
515
|
* 3. Give consumers a `<video>`-compatible primitive they can wrap with
|
|
507
516
|
* their own UI.
|
|
508
517
|
*
|
|
509
|
-
* **It is not a player UI framework.**
|
|
510
|
-
*
|
|
518
|
+
* **It is not a player UI framework.** For YouTube-style chrome (seek
|
|
519
|
+
* bar, play/pause, settings menu, fullscreen, auto-hiding controls) use
|
|
520
|
+
* `<avbridge-player>` — it wraps this element with a full UI. See
|
|
511
521
|
* `docs/dev/WEB_COMPONENT_SPEC.md` for the full spec, lifecycle invariants,
|
|
512
522
|
* and edge case list.
|
|
513
523
|
*/
|
|
514
524
|
|
|
515
525
|
/** Strategy preference passed via the `preferstrategy` attribute. */
|
|
516
526
|
type PreferredStrategy = "auto" | StrategyName;
|
|
527
|
+
/** Fit mode — how the video fills the element's box. Mirrors CSS object-fit. */
|
|
528
|
+
type FitMode = "contain" | "cover" | "fill";
|
|
517
529
|
/**
|
|
518
530
|
* `HTMLElement` is a browser-only global. SSR frameworks (Next.js, Astro,
|
|
519
531
|
* Remix, etc.) commonly import library modules on the server to extract
|
|
@@ -586,12 +598,26 @@ declare class AvbridgeVideoElement extends HTMLElementCtor {
|
|
|
586
598
|
* native fails.
|
|
587
599
|
*/
|
|
588
600
|
private _preferredStrategy;
|
|
601
|
+
/** Current fit mode. Applied to the inner `<video>` via object-fit, and
|
|
602
|
+
* to the fallback canvas via the `--avbridge-fit` CSS custom property on
|
|
603
|
+
* the stage wrapper (see `src/strategies/fallback/video-renderer.ts`). */
|
|
604
|
+
private _fit;
|
|
605
|
+
/** The stage wrapper — the element the canvas attaches into, and where
|
|
606
|
+
* the `--avbridge-fit` CSS custom property lives. */
|
|
607
|
+
private _stageEl;
|
|
589
608
|
/** Set if currentTime was assigned before the player was ready. */
|
|
590
609
|
private _pendingSeek;
|
|
591
610
|
/** Set if play() was called before the player was ready. */
|
|
592
611
|
private _pendingPlay;
|
|
593
612
|
/** MutationObserver tracking light-DOM `<track>` children. */
|
|
594
613
|
private _trackObserver;
|
|
614
|
+
/** Document-level fullscreenchange handler — installed while connected so
|
|
615
|
+
* the element can lock/unlock screen orientation to match the video's
|
|
616
|
+
* intrinsic aspect. */
|
|
617
|
+
private _fullscreenChangeHandler;
|
|
618
|
+
/** True if we successfully called screen.orientation.lock() on the last
|
|
619
|
+
* fullscreen entry. Used to know whether to unlock on exit. */
|
|
620
|
+
private _orientationLocked;
|
|
595
621
|
constructor();
|
|
596
622
|
connectedCallback(): void;
|
|
597
623
|
disconnectedCallback(): void;
|
|
@@ -635,6 +661,8 @@ declare class AvbridgeVideoElement extends HTMLElementCtor {
|
|
|
635
661
|
set preload(value: "none" | "metadata" | "auto");
|
|
636
662
|
get diagnostics(): boolean;
|
|
637
663
|
set diagnostics(value: boolean);
|
|
664
|
+
get fit(): FitMode;
|
|
665
|
+
set fit(value: FitMode);
|
|
638
666
|
get preferredStrategy(): PreferredStrategy;
|
|
639
667
|
set preferredStrategy(value: PreferredStrategy);
|
|
640
668
|
get currentTime(): number;
|
|
@@ -724,6 +752,36 @@ declare class AvbridgeVideoElement extends HTMLElementCtor {
|
|
|
724
752
|
language?: string;
|
|
725
753
|
format?: "vtt" | "srt";
|
|
726
754
|
}): Promise<void>;
|
|
755
|
+
/**
|
|
756
|
+
* Disable the automatic `screen.orientation.lock()` that runs on
|
|
757
|
+
* fullscreen entry. Set when you want to honor the device's native
|
|
758
|
+
* auto-rotate instead of matching the video's intrinsic orientation.
|
|
759
|
+
*/
|
|
760
|
+
get noOrientationLock(): boolean;
|
|
761
|
+
set noOrientationLock(value: boolean);
|
|
762
|
+
/** Called whenever `document.fullscreenchange` fires. If this element (or
|
|
763
|
+
* any of its ancestors) is now fullscreen, derive the target orientation
|
|
764
|
+
* from the video's intrinsic size and call `screen.orientation.lock()`.
|
|
765
|
+
* On exit, release the lock we took. iOS Safari rejects `lock()` — we
|
|
766
|
+
* swallow the rejection so nothing breaks on that path. */
|
|
767
|
+
private _onFullscreenChange;
|
|
768
|
+
/** Walk composed-tree ancestors to see if `target` is this element or
|
|
769
|
+
* any ancestor across shadow boundaries. `Node.contains()` can't cross
|
|
770
|
+
* shadow roots, so when `<avbridge-player>` (the fullscreen element)
|
|
771
|
+
* hosts this `<avbridge-video>` inside its shadow DOM, `contains()`
|
|
772
|
+
* returns false. */
|
|
773
|
+
private _isInsideOrEquals;
|
|
774
|
+
/** Derive "landscape" / "portrait" from the intrinsic video dimensions.
|
|
775
|
+
* Returns null when dimensions aren't known yet or the video is square.
|
|
776
|
+
* Uses `videoWidth` / `videoHeight` from the inner `<video>`, which the
|
|
777
|
+
* browser sets to the display-aspect-corrected size (so anamorphic
|
|
778
|
+
* content is judged by its display aspect, not pixel aspect). */
|
|
779
|
+
private _desiredOrientation;
|
|
780
|
+
/** Attempt to lock screen orientation. Swallows rejections — iOS Safari
|
|
781
|
+
* doesn't implement `lock()`, and desktop / non-fullscreen contexts will
|
|
782
|
+
* reject too. Records success so we know whether to unlock on exit. */
|
|
783
|
+
private _lockOrientation;
|
|
784
|
+
private _releaseOrientationLock;
|
|
727
785
|
/** Force a (re-)bootstrap if a source is currently set. */
|
|
728
786
|
load(): Promise<void>;
|
|
729
787
|
/**
|
package/dist/player.js
CHANGED
|
@@ -494,6 +494,22 @@ function classifyContext(ctx) {
|
|
|
494
494
|
};
|
|
495
495
|
}
|
|
496
496
|
if (REMUXABLE_CONTAINERS.has(ctx.container)) {
|
|
497
|
+
const mime = mp4MimeFor(video, audio);
|
|
498
|
+
if (mime && typeof MediaSource !== "undefined" && !mseSupports(mime)) {
|
|
499
|
+
if (webCodecsAvailable()) {
|
|
500
|
+
return {
|
|
501
|
+
class: "HYBRID_CANDIDATE",
|
|
502
|
+
strategy: "hybrid",
|
|
503
|
+
reason: `${ctx.container} container with ${video.codec}${audio ? "/" + audio.codec : ""}; MSE rejects the remux target mime \u2014 routing to WebCodecs hardware decode`,
|
|
504
|
+
fallbackChain: ["fallback"]
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
class: "FALLBACK_REQUIRED",
|
|
509
|
+
strategy: "fallback",
|
|
510
|
+
reason: `${ctx.container} container with ${video.codec}${audio ? "/" + audio.codec : ""}; MSE rejects the remux target mime and WebCodecs is unavailable \u2014 falling back to WASM decode`
|
|
511
|
+
};
|
|
512
|
+
}
|
|
497
513
|
return {
|
|
498
514
|
class: "REMUX_CANDIDATE",
|
|
499
515
|
strategy: "remux",
|
|
@@ -1237,7 +1253,7 @@ var VideoRenderer = class {
|
|
|
1237
1253
|
this.resolveFirstFrame = resolve;
|
|
1238
1254
|
});
|
|
1239
1255
|
this.canvas = document.createElement("canvas");
|
|
1240
|
-
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
|
|
1256
|
+
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:var(--avbridge-fit, contain);";
|
|
1241
1257
|
const parent = target.parentElement ?? target.parentNode;
|
|
1242
1258
|
if (parent && parent instanceof HTMLElement) {
|
|
1243
1259
|
if (getComputedStyle(parent).position === "static") {
|
|
@@ -2580,7 +2596,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2580
2596
|
|
|
2581
2597
|
// src/strategies/fallback/decoder.ts
|
|
2582
2598
|
async function startDecoder(opts) {
|
|
2583
|
-
const variant =
|
|
2599
|
+
const variant = "avbridge";
|
|
2584
2600
|
const libav = await loadLibav(variant);
|
|
2585
2601
|
const bridge = await loadBridge2();
|
|
2586
2602
|
const { prepareLibavInput } = await import('./libav-http-reader-WXG3Z7AI.js');
|
|
@@ -2636,9 +2652,8 @@ async function startDecoder(opts) {
|
|
|
2636
2652
|
videoStream ? `video: ${opts.context.videoTracks[0]?.codec ?? "unknown"}` : null,
|
|
2637
2653
|
audioStream ? `audio: ${opts.context.audioTracks[0]?.codec ?? "unknown"}` : null
|
|
2638
2654
|
].filter(Boolean).join(", ");
|
|
2639
|
-
const hint = variant === "webcodecs" ? ` The "${variant}" libav variant does not include software decoders for these codecs. Try the custom "avbridge" variant (scripts/build-libav.sh) for broader codec support, or use a lighter strategy (native, remux, hybrid) instead.` : "";
|
|
2640
2655
|
throw new Error(
|
|
2641
|
-
`fallback decoder: could not initialize any libav decoders (${codecs})
|
|
2656
|
+
`fallback decoder: could not initialize any libav decoders (${codecs}). The "${variant}" libav variant lacks software decoders for these codecs \u2014 rebuild with scripts/build-libav.sh including the missing decoder, or use a lighter strategy (native, remux, hybrid) instead.`
|
|
2642
2657
|
);
|
|
2643
2658
|
}
|
|
2644
2659
|
let bsfCtx = null;
|
|
@@ -3765,6 +3780,8 @@ var PREFERRED_STRATEGY_VALUES = /* @__PURE__ */ new Set([
|
|
|
3765
3780
|
"hybrid",
|
|
3766
3781
|
"fallback"
|
|
3767
3782
|
]);
|
|
3783
|
+
var FIT_VALUES = /* @__PURE__ */ new Set(["contain", "cover", "fill"]);
|
|
3784
|
+
var DEFAULT_FIT = "contain";
|
|
3768
3785
|
var FORWARDED_VIDEO_EVENTS = [
|
|
3769
3786
|
"loadstart",
|
|
3770
3787
|
"loadedmetadata",
|
|
@@ -3799,7 +3816,9 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
3799
3816
|
"crossorigin",
|
|
3800
3817
|
"disableremoteplayback",
|
|
3801
3818
|
"diagnostics",
|
|
3802
|
-
"preferstrategy"
|
|
3819
|
+
"preferstrategy",
|
|
3820
|
+
"fit",
|
|
3821
|
+
"no-orientation-lock"
|
|
3803
3822
|
];
|
|
3804
3823
|
// ── Internal state ─────────────────────────────────────────────────────
|
|
3805
3824
|
/** The shadow DOM `<video>` element that strategies render into. */
|
|
@@ -3851,23 +3870,38 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
3851
3870
|
* native fails.
|
|
3852
3871
|
*/
|
|
3853
3872
|
_preferredStrategy = "auto";
|
|
3873
|
+
/** Current fit mode. Applied to the inner `<video>` via object-fit, and
|
|
3874
|
+
* to the fallback canvas via the `--avbridge-fit` CSS custom property on
|
|
3875
|
+
* the stage wrapper (see `src/strategies/fallback/video-renderer.ts`). */
|
|
3876
|
+
_fit = DEFAULT_FIT;
|
|
3877
|
+
/** The stage wrapper — the element the canvas attaches into, and where
|
|
3878
|
+
* the `--avbridge-fit` CSS custom property lives. */
|
|
3879
|
+
_stageEl;
|
|
3854
3880
|
/** Set if currentTime was assigned before the player was ready. */
|
|
3855
3881
|
_pendingSeek = null;
|
|
3856
3882
|
/** Set if play() was called before the player was ready. */
|
|
3857
3883
|
_pendingPlay = false;
|
|
3858
3884
|
/** MutationObserver tracking light-DOM `<track>` children. */
|
|
3859
3885
|
_trackObserver = null;
|
|
3886
|
+
/** Document-level fullscreenchange handler — installed while connected so
|
|
3887
|
+
* the element can lock/unlock screen orientation to match the video's
|
|
3888
|
+
* intrinsic aspect. */
|
|
3889
|
+
_fullscreenChangeHandler = null;
|
|
3890
|
+
/** True if we successfully called screen.orientation.lock() on the last
|
|
3891
|
+
* fullscreen entry. Used to know whether to unlock on exit. */
|
|
3892
|
+
_orientationLocked = false;
|
|
3860
3893
|
// ── Construction & lifecycle ───────────────────────────────────────────
|
|
3861
3894
|
constructor() {
|
|
3862
3895
|
super();
|
|
3863
3896
|
const root = this.attachShadow({ mode: "open" });
|
|
3864
3897
|
const stage = document.createElement("div");
|
|
3865
3898
|
stage.setAttribute("part", "stage");
|
|
3866
|
-
stage.style.cssText =
|
|
3899
|
+
stage.style.cssText = `position:relative;width:100%;height:100%;display:block;--avbridge-fit:${DEFAULT_FIT};`;
|
|
3867
3900
|
root.appendChild(stage);
|
|
3901
|
+
this._stageEl = stage;
|
|
3868
3902
|
this._videoEl = document.createElement("video");
|
|
3869
3903
|
this._videoEl.setAttribute("part", "video");
|
|
3870
|
-
this._videoEl.style.cssText =
|
|
3904
|
+
this._videoEl.style.cssText = `width:100%;height:100%;display:block;background:#000;object-fit:var(--avbridge-fit, ${DEFAULT_FIT});`;
|
|
3871
3905
|
this._videoEl.playsInline = true;
|
|
3872
3906
|
stage.appendChild(this._videoEl);
|
|
3873
3907
|
this._videoEl.addEventListener("progress", () => {
|
|
@@ -3888,6 +3922,10 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
3888
3922
|
this._trackObserver = new MutationObserver(() => this._syncTextTracks());
|
|
3889
3923
|
this._trackObserver.observe(this, { childList: true, subtree: false });
|
|
3890
3924
|
}
|
|
3925
|
+
if (!this._fullscreenChangeHandler) {
|
|
3926
|
+
this._fullscreenChangeHandler = () => this._onFullscreenChange();
|
|
3927
|
+
document.addEventListener("fullscreenchange", this._fullscreenChangeHandler);
|
|
3928
|
+
}
|
|
3891
3929
|
const source = this._activeSource();
|
|
3892
3930
|
if (source != null) {
|
|
3893
3931
|
void this._bootstrap(source);
|
|
@@ -3899,6 +3937,11 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
3899
3937
|
this._trackObserver.disconnect();
|
|
3900
3938
|
this._trackObserver = null;
|
|
3901
3939
|
}
|
|
3940
|
+
if (this._fullscreenChangeHandler) {
|
|
3941
|
+
document.removeEventListener("fullscreenchange", this._fullscreenChangeHandler);
|
|
3942
|
+
this._fullscreenChangeHandler = null;
|
|
3943
|
+
}
|
|
3944
|
+
this._releaseOrientationLock();
|
|
3902
3945
|
this._bootstrapId++;
|
|
3903
3946
|
void this._teardown();
|
|
3904
3947
|
}
|
|
@@ -3932,6 +3975,14 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
3932
3975
|
this._preferredStrategy = "auto";
|
|
3933
3976
|
}
|
|
3934
3977
|
break;
|
|
3978
|
+
case "fit": {
|
|
3979
|
+
const next = newValue && FIT_VALUES.has(newValue) ? newValue : DEFAULT_FIT;
|
|
3980
|
+
if (next === this._fit) break;
|
|
3981
|
+
this._fit = next;
|
|
3982
|
+
this._stageEl.style.setProperty("--avbridge-fit", next);
|
|
3983
|
+
this._dispatch("fitchange", { fit: next });
|
|
3984
|
+
break;
|
|
3985
|
+
}
|
|
3935
3986
|
}
|
|
3936
3987
|
}
|
|
3937
3988
|
// ── Source handling ────────────────────────────────────────────────────
|
|
@@ -4173,6 +4224,13 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4173
4224
|
if (value) this.setAttribute("diagnostics", "");
|
|
4174
4225
|
else this.removeAttribute("diagnostics");
|
|
4175
4226
|
}
|
|
4227
|
+
get fit() {
|
|
4228
|
+
return this._fit;
|
|
4229
|
+
}
|
|
4230
|
+
set fit(value) {
|
|
4231
|
+
if (!FIT_VALUES.has(value)) return;
|
|
4232
|
+
this.setAttribute("fit", value);
|
|
4233
|
+
}
|
|
4176
4234
|
get preferredStrategy() {
|
|
4177
4235
|
return this._preferredStrategy;
|
|
4178
4236
|
}
|
|
@@ -4346,6 +4404,87 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4346
4404
|
}
|
|
4347
4405
|
);
|
|
4348
4406
|
}
|
|
4407
|
+
/**
|
|
4408
|
+
* Disable the automatic `screen.orientation.lock()` that runs on
|
|
4409
|
+
* fullscreen entry. Set when you want to honor the device's native
|
|
4410
|
+
* auto-rotate instead of matching the video's intrinsic orientation.
|
|
4411
|
+
*/
|
|
4412
|
+
get noOrientationLock() {
|
|
4413
|
+
return this.hasAttribute("no-orientation-lock");
|
|
4414
|
+
}
|
|
4415
|
+
set noOrientationLock(value) {
|
|
4416
|
+
if (value) this.setAttribute("no-orientation-lock", "");
|
|
4417
|
+
else this.removeAttribute("no-orientation-lock");
|
|
4418
|
+
}
|
|
4419
|
+
// ── Fullscreen orientation lock ────────────────────────────────────────
|
|
4420
|
+
/** Called whenever `document.fullscreenchange` fires. If this element (or
|
|
4421
|
+
* any of its ancestors) is now fullscreen, derive the target orientation
|
|
4422
|
+
* from the video's intrinsic size and call `screen.orientation.lock()`.
|
|
4423
|
+
* On exit, release the lock we took. iOS Safari rejects `lock()` — we
|
|
4424
|
+
* swallow the rejection so nothing breaks on that path. */
|
|
4425
|
+
_onFullscreenChange() {
|
|
4426
|
+
if (this._destroyed) return;
|
|
4427
|
+
const fsEl = document.fullscreenElement;
|
|
4428
|
+
const nowFullscreen = fsEl != null && this._isInsideOrEquals(fsEl);
|
|
4429
|
+
if (nowFullscreen && !this._orientationLocked) {
|
|
4430
|
+
if (this.noOrientationLock) return;
|
|
4431
|
+
const target = this._desiredOrientation();
|
|
4432
|
+
if (!target) return;
|
|
4433
|
+
void this._lockOrientation(target);
|
|
4434
|
+
} else if (!nowFullscreen && this._orientationLocked) {
|
|
4435
|
+
this._releaseOrientationLock();
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
/** Walk composed-tree ancestors to see if `target` is this element or
|
|
4439
|
+
* any ancestor across shadow boundaries. `Node.contains()` can't cross
|
|
4440
|
+
* shadow roots, so when `<avbridge-player>` (the fullscreen element)
|
|
4441
|
+
* hosts this `<avbridge-video>` inside its shadow DOM, `contains()`
|
|
4442
|
+
* returns false. */
|
|
4443
|
+
_isInsideOrEquals(target) {
|
|
4444
|
+
let node = this;
|
|
4445
|
+
while (node) {
|
|
4446
|
+
if (node === target) return true;
|
|
4447
|
+
const parent = node.parentNode;
|
|
4448
|
+
if (parent instanceof ShadowRoot) node = parent.host;
|
|
4449
|
+
else node = parent;
|
|
4450
|
+
}
|
|
4451
|
+
return false;
|
|
4452
|
+
}
|
|
4453
|
+
/** Derive "landscape" / "portrait" from the intrinsic video dimensions.
|
|
4454
|
+
* Returns null when dimensions aren't known yet or the video is square.
|
|
4455
|
+
* Uses `videoWidth` / `videoHeight` from the inner `<video>`, which the
|
|
4456
|
+
* browser sets to the display-aspect-corrected size (so anamorphic
|
|
4457
|
+
* content is judged by its display aspect, not pixel aspect). */
|
|
4458
|
+
_desiredOrientation() {
|
|
4459
|
+
const w = this._videoEl.videoWidth;
|
|
4460
|
+
const h = this._videoEl.videoHeight;
|
|
4461
|
+
if (!w || !h) return null;
|
|
4462
|
+
if (w === h) return null;
|
|
4463
|
+
return w > h ? "landscape" : "portrait";
|
|
4464
|
+
}
|
|
4465
|
+
/** Attempt to lock screen orientation. Swallows rejections — iOS Safari
|
|
4466
|
+
* doesn't implement `lock()`, and desktop / non-fullscreen contexts will
|
|
4467
|
+
* reject too. Records success so we know whether to unlock on exit. */
|
|
4468
|
+
async _lockOrientation(target) {
|
|
4469
|
+
const so = screen.orientation;
|
|
4470
|
+
if (!so || typeof so.lock !== "function") return;
|
|
4471
|
+
try {
|
|
4472
|
+
await so.lock(target);
|
|
4473
|
+
this._orientationLocked = true;
|
|
4474
|
+
} catch {
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
_releaseOrientationLock() {
|
|
4478
|
+
if (!this._orientationLocked) return;
|
|
4479
|
+
this._orientationLocked = false;
|
|
4480
|
+
const so = screen.orientation;
|
|
4481
|
+
if (so && typeof so.unlock === "function") {
|
|
4482
|
+
try {
|
|
4483
|
+
so.unlock();
|
|
4484
|
+
} catch {
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4349
4488
|
// ── Public methods ─────────────────────────────────────────────────────
|
|
4350
4489
|
/** Force a (re-)bootstrap if a source is currently set. */
|
|
4351
4490
|
async load() {
|
|
@@ -4605,6 +4744,50 @@ var PLAYER_STYLES = (
|
|
|
4605
4744
|
|
|
4606
4745
|
:host([data-controls-hidden]) { cursor: none; }
|
|
4607
4746
|
|
|
4747
|
+
/* \u2500\u2500 Top toolbar (slotted consumer chrome) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4748
|
+
Two named slots (top-left, top-right) let consumers place back / title /
|
|
4749
|
+
translate buttons inside the auto-hide chrome. Wrapper has
|
|
4750
|
+
pointer-events:none so empty slots don't block container clicks; each
|
|
4751
|
+
side re-enables pointer-events so real buttons remain interactive. */
|
|
4752
|
+
|
|
4753
|
+
.avp-toolbar-top {
|
|
4754
|
+
position: absolute;
|
|
4755
|
+
top: 0;
|
|
4756
|
+
left: 0;
|
|
4757
|
+
right: 0;
|
|
4758
|
+
z-index: 5;
|
|
4759
|
+
padding: 8px 12px 24px;
|
|
4760
|
+
background: linear-gradient(rgba(0, 0, 0, 0.6), transparent);
|
|
4761
|
+
display: flex;
|
|
4762
|
+
align-items: flex-start;
|
|
4763
|
+
justify-content: space-between;
|
|
4764
|
+
gap: 8px;
|
|
4765
|
+
opacity: 1;
|
|
4766
|
+
pointer-events: none;
|
|
4767
|
+
transition: opacity 0.25s;
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
.avp-toolbar-top-left,
|
|
4771
|
+
.avp-toolbar-top-right {
|
|
4772
|
+
display: flex;
|
|
4773
|
+
align-items: center;
|
|
4774
|
+
gap: 8px;
|
|
4775
|
+
pointer-events: auto;
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
.avp-toolbar-top-right { margin-left: auto; }
|
|
4779
|
+
|
|
4780
|
+
/* Hide the gradient band when no consumer has slotted anything \u2014 we
|
|
4781
|
+
toggle data-toolbar-empty from JS via slotchange. */
|
|
4782
|
+
:host([data-toolbar-empty]) .avp-toolbar-top {
|
|
4783
|
+
background: none;
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
:host([data-controls-hidden]) .avp-toolbar-top {
|
|
4787
|
+
opacity: 0;
|
|
4788
|
+
pointer-events: none;
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4608
4791
|
/* \u2500\u2500 Seek bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
4609
4792
|
|
|
4610
4793
|
.avp-seek {
|
|
@@ -4956,7 +5139,8 @@ var PROXY_ATTRIBUTES = [
|
|
|
4956
5139
|
"playsinline",
|
|
4957
5140
|
"crossorigin",
|
|
4958
5141
|
"disableremoteplayback",
|
|
4959
|
-
"preferstrategy"
|
|
5142
|
+
"preferstrategy",
|
|
5143
|
+
"fit"
|
|
4960
5144
|
];
|
|
4961
5145
|
var AvbridgePlayerElement = class extends HTMLElement {
|
|
4962
5146
|
static observedAttributes = [...PROXY_ATTRIBUTES];
|
|
@@ -4994,6 +5178,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
4994
5178
|
_statsEl;
|
|
4995
5179
|
_statsInterval = null;
|
|
4996
5180
|
_eventCleanup = [];
|
|
5181
|
+
_updateToolbarEmpty = () => {
|
|
5182
|
+
};
|
|
4997
5183
|
// ── Constructor ────────────────────────────────────────────────────────
|
|
4998
5184
|
constructor() {
|
|
4999
5185
|
super();
|
|
@@ -5017,12 +5203,23 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5017
5203
|
this._statsEl = shadow.querySelector(".avp-stats");
|
|
5018
5204
|
this._rippleLeft = shadow.querySelector(".avp-ripple-left");
|
|
5019
5205
|
this._rippleRight = shadow.querySelector(".avp-ripple-right");
|
|
5206
|
+
const slots = shadow.querySelectorAll('slot[name="top-left"], slot[name="top-right"]');
|
|
5207
|
+
this._updateToolbarEmpty = () => {
|
|
5208
|
+
const hasContent = Array.from(slots).some((s) => s.assignedNodes({ flatten: true }).length > 0);
|
|
5209
|
+
if (hasContent) this.removeAttribute("data-toolbar-empty");
|
|
5210
|
+
else this.setAttribute("data-toolbar-empty", "");
|
|
5211
|
+
};
|
|
5212
|
+
for (const s of slots) s.addEventListener("slotchange", this._updateToolbarEmpty);
|
|
5020
5213
|
this._bindEvents();
|
|
5021
5214
|
}
|
|
5022
5215
|
_template() {
|
|
5023
5216
|
return `
|
|
5024
5217
|
<div part="container" class="avp">
|
|
5025
5218
|
<avbridge-video part="video"></avbridge-video>
|
|
5219
|
+
<div part="toolbar-top" class="avp-toolbar-top">
|
|
5220
|
+
<div part="toolbar-top-left" class="avp-toolbar-top-left"><slot name="top-left"></slot></div>
|
|
5221
|
+
<div part="toolbar-top-right" class="avp-toolbar-top-right"><slot name="top-right"></slot></div>
|
|
5222
|
+
</div>
|
|
5026
5223
|
<div part="overlay" class="avp-overlay">
|
|
5027
5224
|
<button class="avp-overlay-btn" aria-label="Play">${ICON_PLAY}</button>
|
|
5028
5225
|
<div class="avp-spinner"></div>
|
|
@@ -5175,13 +5372,14 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5175
5372
|
});
|
|
5176
5373
|
});
|
|
5177
5374
|
on(this, "keydown", (e) => this._onKeydown(e));
|
|
5178
|
-
if (!this.hasAttribute("tabindex")) {
|
|
5179
|
-
this.setAttribute("tabindex", "0");
|
|
5180
|
-
}
|
|
5181
5375
|
}
|
|
5182
5376
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
5183
5377
|
connectedCallback() {
|
|
5184
5378
|
this._setState("idle");
|
|
5379
|
+
if (!this.hasAttribute("tabindex")) {
|
|
5380
|
+
this.setAttribute("tabindex", "0");
|
|
5381
|
+
}
|
|
5382
|
+
this._updateToolbarEmpty();
|
|
5185
5383
|
}
|
|
5186
5384
|
disconnectedCallback() {
|
|
5187
5385
|
this._clearTimers();
|
|
@@ -5443,8 +5641,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5443
5641
|
// it's treated as a double-click and the single-click action is cancelled.
|
|
5444
5642
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
5445
5643
|
_lastPointerTypeWasTouch = false;
|
|
5644
|
+
/** True if the event's composed path passes through consumer-slotted toolbar
|
|
5645
|
+
* content. Slotted content lives in the light DOM so `.closest(".avp-toolbar-top")`
|
|
5646
|
+
* on the event target won't find the shadow-DOM wrapper — `composedPath()`
|
|
5647
|
+
* does. */
|
|
5648
|
+
_isToolbarEvent(e) {
|
|
5649
|
+
for (const node of e.composedPath()) {
|
|
5650
|
+
if (node instanceof HTMLElement && node.classList.contains("avp-toolbar-top")) return true;
|
|
5651
|
+
}
|
|
5652
|
+
return false;
|
|
5653
|
+
}
|
|
5446
5654
|
_onContainerClick(e) {
|
|
5447
5655
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5656
|
+
if (this._isToolbarEvent(e)) return;
|
|
5448
5657
|
if (this._lastPointerTypeWasTouch) {
|
|
5449
5658
|
this._lastPointerTypeWasTouch = false;
|
|
5450
5659
|
return;
|
|
@@ -5460,6 +5669,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5460
5669
|
}
|
|
5461
5670
|
_onContainerDblClick(e) {
|
|
5462
5671
|
if (e.target.closest?.(".avp-controls, .avp-settings")) return;
|
|
5672
|
+
if (this._isToolbarEvent(e)) return;
|
|
5463
5673
|
if (this._tapTimer) {
|
|
5464
5674
|
clearTimeout(this._tapTimer);
|
|
5465
5675
|
this._tapTimer = null;
|
|
@@ -5481,6 +5691,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5481
5691
|
if (e.pointerType !== "touch") return;
|
|
5482
5692
|
this._lastPointerTypeWasTouch = true;
|
|
5483
5693
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5694
|
+
if (this._isToolbarEvent(e)) return;
|
|
5484
5695
|
const now = Date.now();
|
|
5485
5696
|
if (now - this._lastTapTime < 300) {
|
|
5486
5697
|
if (this._tapTimer) {
|