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/element-browser.js
CHANGED
|
@@ -31689,6 +31689,22 @@ function classifyContext(ctx) {
|
|
|
31689
31689
|
};
|
|
31690
31690
|
}
|
|
31691
31691
|
if (REMUXABLE_CONTAINERS.has(ctx.container)) {
|
|
31692
|
+
const mime = mp4MimeFor(video, audio);
|
|
31693
|
+
if (mime && typeof MediaSource !== "undefined" && !mseSupports(mime)) {
|
|
31694
|
+
if (webCodecsAvailable()) {
|
|
31695
|
+
return {
|
|
31696
|
+
class: "HYBRID_CANDIDATE",
|
|
31697
|
+
strategy: "hybrid",
|
|
31698
|
+
reason: `${ctx.container} container with ${video.codec}${audio ? "/" + audio.codec : ""}; MSE rejects the remux target mime \u2014 routing to WebCodecs hardware decode`,
|
|
31699
|
+
fallbackChain: ["fallback"]
|
|
31700
|
+
};
|
|
31701
|
+
}
|
|
31702
|
+
return {
|
|
31703
|
+
class: "FALLBACK_REQUIRED",
|
|
31704
|
+
strategy: "fallback",
|
|
31705
|
+
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`
|
|
31706
|
+
};
|
|
31707
|
+
}
|
|
31692
31708
|
return {
|
|
31693
31709
|
class: "REMUX_CANDIDATE",
|
|
31694
31710
|
strategy: "remux",
|
|
@@ -32434,7 +32450,7 @@ var VideoRenderer = class {
|
|
|
32434
32450
|
this.resolveFirstFrame = resolve;
|
|
32435
32451
|
});
|
|
32436
32452
|
this.canvas = document.createElement("canvas");
|
|
32437
|
-
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
|
|
32453
|
+
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:var(--avbridge-fit, contain);";
|
|
32438
32454
|
const parent = target.parentElement ?? target.parentNode;
|
|
32439
32455
|
if (parent && parent instanceof HTMLElement) {
|
|
32440
32456
|
if (getComputedStyle(parent).position === "static") {
|
|
@@ -33783,7 +33799,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
33783
33799
|
init_libav_loader();
|
|
33784
33800
|
init_debug();
|
|
33785
33801
|
async function startDecoder(opts) {
|
|
33786
|
-
const variant =
|
|
33802
|
+
const variant = "avbridge";
|
|
33787
33803
|
const libav = await loadLibav(variant);
|
|
33788
33804
|
const bridge = await loadBridge2();
|
|
33789
33805
|
const { prepareLibavInput: prepareLibavInput2 } = await Promise.resolve().then(() => (init_libav_http_reader(), libav_http_reader_exports));
|
|
@@ -33839,9 +33855,8 @@ async function startDecoder(opts) {
|
|
|
33839
33855
|
videoStream ? `video: ${opts.context.videoTracks[0]?.codec ?? "unknown"}` : null,
|
|
33840
33856
|
audioStream ? `audio: ${opts.context.audioTracks[0]?.codec ?? "unknown"}` : null
|
|
33841
33857
|
].filter(Boolean).join(", ");
|
|
33842
|
-
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.` : "";
|
|
33843
33858
|
throw new Error(
|
|
33844
|
-
`fallback decoder: could not initialize any libav decoders (${codecs})
|
|
33859
|
+
`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.`
|
|
33845
33860
|
);
|
|
33846
33861
|
}
|
|
33847
33862
|
let bsfCtx = null;
|
|
@@ -34972,6 +34987,8 @@ var PREFERRED_STRATEGY_VALUES = /* @__PURE__ */ new Set([
|
|
|
34972
34987
|
"hybrid",
|
|
34973
34988
|
"fallback"
|
|
34974
34989
|
]);
|
|
34990
|
+
var FIT_VALUES = /* @__PURE__ */ new Set(["contain", "cover", "fill"]);
|
|
34991
|
+
var DEFAULT_FIT = "contain";
|
|
34975
34992
|
var FORWARDED_VIDEO_EVENTS = [
|
|
34976
34993
|
"loadstart",
|
|
34977
34994
|
"loadedmetadata",
|
|
@@ -35006,7 +35023,9 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
35006
35023
|
"crossorigin",
|
|
35007
35024
|
"disableremoteplayback",
|
|
35008
35025
|
"diagnostics",
|
|
35009
|
-
"preferstrategy"
|
|
35026
|
+
"preferstrategy",
|
|
35027
|
+
"fit",
|
|
35028
|
+
"no-orientation-lock"
|
|
35010
35029
|
];
|
|
35011
35030
|
// ── Internal state ─────────────────────────────────────────────────────
|
|
35012
35031
|
/** The shadow DOM `<video>` element that strategies render into. */
|
|
@@ -35058,23 +35077,38 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
35058
35077
|
* native fails.
|
|
35059
35078
|
*/
|
|
35060
35079
|
_preferredStrategy = "auto";
|
|
35080
|
+
/** Current fit mode. Applied to the inner `<video>` via object-fit, and
|
|
35081
|
+
* to the fallback canvas via the `--avbridge-fit` CSS custom property on
|
|
35082
|
+
* the stage wrapper (see `src/strategies/fallback/video-renderer.ts`). */
|
|
35083
|
+
_fit = DEFAULT_FIT;
|
|
35084
|
+
/** The stage wrapper — the element the canvas attaches into, and where
|
|
35085
|
+
* the `--avbridge-fit` CSS custom property lives. */
|
|
35086
|
+
_stageEl;
|
|
35061
35087
|
/** Set if currentTime was assigned before the player was ready. */
|
|
35062
35088
|
_pendingSeek = null;
|
|
35063
35089
|
/** Set if play() was called before the player was ready. */
|
|
35064
35090
|
_pendingPlay = false;
|
|
35065
35091
|
/** MutationObserver tracking light-DOM `<track>` children. */
|
|
35066
35092
|
_trackObserver = null;
|
|
35093
|
+
/** Document-level fullscreenchange handler — installed while connected so
|
|
35094
|
+
* the element can lock/unlock screen orientation to match the video's
|
|
35095
|
+
* intrinsic aspect. */
|
|
35096
|
+
_fullscreenChangeHandler = null;
|
|
35097
|
+
/** True if we successfully called screen.orientation.lock() on the last
|
|
35098
|
+
* fullscreen entry. Used to know whether to unlock on exit. */
|
|
35099
|
+
_orientationLocked = false;
|
|
35067
35100
|
// ── Construction & lifecycle ───────────────────────────────────────────
|
|
35068
35101
|
constructor() {
|
|
35069
35102
|
super();
|
|
35070
35103
|
const root = this.attachShadow({ mode: "open" });
|
|
35071
35104
|
const stage = document.createElement("div");
|
|
35072
35105
|
stage.setAttribute("part", "stage");
|
|
35073
|
-
stage.style.cssText =
|
|
35106
|
+
stage.style.cssText = `position:relative;width:100%;height:100%;display:block;--avbridge-fit:${DEFAULT_FIT};`;
|
|
35074
35107
|
root.appendChild(stage);
|
|
35108
|
+
this._stageEl = stage;
|
|
35075
35109
|
this._videoEl = document.createElement("video");
|
|
35076
35110
|
this._videoEl.setAttribute("part", "video");
|
|
35077
|
-
this._videoEl.style.cssText =
|
|
35111
|
+
this._videoEl.style.cssText = `width:100%;height:100%;display:block;background:#000;object-fit:var(--avbridge-fit, ${DEFAULT_FIT});`;
|
|
35078
35112
|
this._videoEl.playsInline = true;
|
|
35079
35113
|
stage.appendChild(this._videoEl);
|
|
35080
35114
|
this._videoEl.addEventListener("progress", () => {
|
|
@@ -35095,6 +35129,10 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
35095
35129
|
this._trackObserver = new MutationObserver(() => this._syncTextTracks());
|
|
35096
35130
|
this._trackObserver.observe(this, { childList: true, subtree: false });
|
|
35097
35131
|
}
|
|
35132
|
+
if (!this._fullscreenChangeHandler) {
|
|
35133
|
+
this._fullscreenChangeHandler = () => this._onFullscreenChange();
|
|
35134
|
+
document.addEventListener("fullscreenchange", this._fullscreenChangeHandler);
|
|
35135
|
+
}
|
|
35098
35136
|
const source = this._activeSource();
|
|
35099
35137
|
if (source != null) {
|
|
35100
35138
|
void this._bootstrap(source);
|
|
@@ -35106,6 +35144,11 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
35106
35144
|
this._trackObserver.disconnect();
|
|
35107
35145
|
this._trackObserver = null;
|
|
35108
35146
|
}
|
|
35147
|
+
if (this._fullscreenChangeHandler) {
|
|
35148
|
+
document.removeEventListener("fullscreenchange", this._fullscreenChangeHandler);
|
|
35149
|
+
this._fullscreenChangeHandler = null;
|
|
35150
|
+
}
|
|
35151
|
+
this._releaseOrientationLock();
|
|
35109
35152
|
this._bootstrapId++;
|
|
35110
35153
|
void this._teardown();
|
|
35111
35154
|
}
|
|
@@ -35139,6 +35182,14 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
35139
35182
|
this._preferredStrategy = "auto";
|
|
35140
35183
|
}
|
|
35141
35184
|
break;
|
|
35185
|
+
case "fit": {
|
|
35186
|
+
const next = newValue && FIT_VALUES.has(newValue) ? newValue : DEFAULT_FIT;
|
|
35187
|
+
if (next === this._fit) break;
|
|
35188
|
+
this._fit = next;
|
|
35189
|
+
this._stageEl.style.setProperty("--avbridge-fit", next);
|
|
35190
|
+
this._dispatch("fitchange", { fit: next });
|
|
35191
|
+
break;
|
|
35192
|
+
}
|
|
35142
35193
|
}
|
|
35143
35194
|
}
|
|
35144
35195
|
// ── Source handling ────────────────────────────────────────────────────
|
|
@@ -35380,6 +35431,13 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
35380
35431
|
if (value) this.setAttribute("diagnostics", "");
|
|
35381
35432
|
else this.removeAttribute("diagnostics");
|
|
35382
35433
|
}
|
|
35434
|
+
get fit() {
|
|
35435
|
+
return this._fit;
|
|
35436
|
+
}
|
|
35437
|
+
set fit(value) {
|
|
35438
|
+
if (!FIT_VALUES.has(value)) return;
|
|
35439
|
+
this.setAttribute("fit", value);
|
|
35440
|
+
}
|
|
35383
35441
|
get preferredStrategy() {
|
|
35384
35442
|
return this._preferredStrategy;
|
|
35385
35443
|
}
|
|
@@ -35553,6 +35611,87 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
35553
35611
|
}
|
|
35554
35612
|
);
|
|
35555
35613
|
}
|
|
35614
|
+
/**
|
|
35615
|
+
* Disable the automatic `screen.orientation.lock()` that runs on
|
|
35616
|
+
* fullscreen entry. Set when you want to honor the device's native
|
|
35617
|
+
* auto-rotate instead of matching the video's intrinsic orientation.
|
|
35618
|
+
*/
|
|
35619
|
+
get noOrientationLock() {
|
|
35620
|
+
return this.hasAttribute("no-orientation-lock");
|
|
35621
|
+
}
|
|
35622
|
+
set noOrientationLock(value) {
|
|
35623
|
+
if (value) this.setAttribute("no-orientation-lock", "");
|
|
35624
|
+
else this.removeAttribute("no-orientation-lock");
|
|
35625
|
+
}
|
|
35626
|
+
// ── Fullscreen orientation lock ────────────────────────────────────────
|
|
35627
|
+
/** Called whenever `document.fullscreenchange` fires. If this element (or
|
|
35628
|
+
* any of its ancestors) is now fullscreen, derive the target orientation
|
|
35629
|
+
* from the video's intrinsic size and call `screen.orientation.lock()`.
|
|
35630
|
+
* On exit, release the lock we took. iOS Safari rejects `lock()` — we
|
|
35631
|
+
* swallow the rejection so nothing breaks on that path. */
|
|
35632
|
+
_onFullscreenChange() {
|
|
35633
|
+
if (this._destroyed) return;
|
|
35634
|
+
const fsEl = document.fullscreenElement;
|
|
35635
|
+
const nowFullscreen = fsEl != null && this._isInsideOrEquals(fsEl);
|
|
35636
|
+
if (nowFullscreen && !this._orientationLocked) {
|
|
35637
|
+
if (this.noOrientationLock) return;
|
|
35638
|
+
const target = this._desiredOrientation();
|
|
35639
|
+
if (!target) return;
|
|
35640
|
+
void this._lockOrientation(target);
|
|
35641
|
+
} else if (!nowFullscreen && this._orientationLocked) {
|
|
35642
|
+
this._releaseOrientationLock();
|
|
35643
|
+
}
|
|
35644
|
+
}
|
|
35645
|
+
/** Walk composed-tree ancestors to see if `target` is this element or
|
|
35646
|
+
* any ancestor across shadow boundaries. `Node.contains()` can't cross
|
|
35647
|
+
* shadow roots, so when `<avbridge-player>` (the fullscreen element)
|
|
35648
|
+
* hosts this `<avbridge-video>` inside its shadow DOM, `contains()`
|
|
35649
|
+
* returns false. */
|
|
35650
|
+
_isInsideOrEquals(target) {
|
|
35651
|
+
let node3 = this;
|
|
35652
|
+
while (node3) {
|
|
35653
|
+
if (node3 === target) return true;
|
|
35654
|
+
const parent = node3.parentNode;
|
|
35655
|
+
if (parent instanceof ShadowRoot) node3 = parent.host;
|
|
35656
|
+
else node3 = parent;
|
|
35657
|
+
}
|
|
35658
|
+
return false;
|
|
35659
|
+
}
|
|
35660
|
+
/** Derive "landscape" / "portrait" from the intrinsic video dimensions.
|
|
35661
|
+
* Returns null when dimensions aren't known yet or the video is square.
|
|
35662
|
+
* Uses `videoWidth` / `videoHeight` from the inner `<video>`, which the
|
|
35663
|
+
* browser sets to the display-aspect-corrected size (so anamorphic
|
|
35664
|
+
* content is judged by its display aspect, not pixel aspect). */
|
|
35665
|
+
_desiredOrientation() {
|
|
35666
|
+
const w = this._videoEl.videoWidth;
|
|
35667
|
+
const h = this._videoEl.videoHeight;
|
|
35668
|
+
if (!w || !h) return null;
|
|
35669
|
+
if (w === h) return null;
|
|
35670
|
+
return w > h ? "landscape" : "portrait";
|
|
35671
|
+
}
|
|
35672
|
+
/** Attempt to lock screen orientation. Swallows rejections — iOS Safari
|
|
35673
|
+
* doesn't implement `lock()`, and desktop / non-fullscreen contexts will
|
|
35674
|
+
* reject too. Records success so we know whether to unlock on exit. */
|
|
35675
|
+
async _lockOrientation(target) {
|
|
35676
|
+
const so = screen.orientation;
|
|
35677
|
+
if (!so || typeof so.lock !== "function") return;
|
|
35678
|
+
try {
|
|
35679
|
+
await so.lock(target);
|
|
35680
|
+
this._orientationLocked = true;
|
|
35681
|
+
} catch {
|
|
35682
|
+
}
|
|
35683
|
+
}
|
|
35684
|
+
_releaseOrientationLock() {
|
|
35685
|
+
if (!this._orientationLocked) return;
|
|
35686
|
+
this._orientationLocked = false;
|
|
35687
|
+
const so = screen.orientation;
|
|
35688
|
+
if (so && typeof so.unlock === "function") {
|
|
35689
|
+
try {
|
|
35690
|
+
so.unlock();
|
|
35691
|
+
} catch {
|
|
35692
|
+
}
|
|
35693
|
+
}
|
|
35694
|
+
}
|
|
35556
35695
|
// ── Public methods ─────────────────────────────────────────────────────
|
|
35557
35696
|
/** Force a (re-)bootstrap if a source is currently set. */
|
|
35558
35697
|
async load() {
|