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.
@@ -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 = pickLibavVariant(opts.context);
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}).${hint}`
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 = "position:relative;width:100%;height:100%;display:block;";
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 = "width:100%;height:100%;display:block;background:#000;";
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() {