vidstack
Version:
Build awesome media experiences on the web.
3,513 lines • 107 kB
JavaScript
import { ComponentController, defineProp, Component, defineElement } from 'maverick.js/element';
import { signal, peek, effect, getScope, scoped, createContext, useContext, StoreFactory, tick, onDispose, computed } from 'maverick.js';
import { EventsTarget, DOMEvent, listenEvent, isUndefined, isFunction, waitTimeout, isKeyboardClick, setAttribute, isArray, isNumber, isKeyboardEvent, appendTriggerEvent, isNull, deferredPromise, isString, noop, animationFrameThrottle, setStyle } from 'maverick.js/std';
import { i as isHTMLMediaElement } from './providers/type-check.js';
import { A as AudioProviderLoader } from './providers/audio/loader.js';
import { H as HLSProviderLoader } from './providers/hls/loader.js';
import { V as VideoProviderLoader } from './providers/video/loader.js';
const LIST_ADD = Symbol("LIST_ADD" );
const LIST_REMOVE = Symbol("LIST_REMOVE" );
const LIST_RESET = Symbol("LIST_RESET" );
const LIST_SELECT = Symbol("LIST_SELECT" );
const LIST_READONLY = Symbol("LIST_READONLY" );
const LIST_SET_READONLY = Symbol("LIST_SET_READONLY" );
const LIST_ON_RESET = Symbol("LIST_ON_RESET" );
const LIST_ON_REMOVE = Symbol("LIST_ON_REMOVE" );
const LIST_ON_USER_SELECT = Symbol("LIST_ON_USER_SELECT" );
class List extends EventsTarget {
_items = [];
/* @internal */
[LIST_READONLY] = false;
get length() {
return this._items.length;
}
get readonly() {
return this[LIST_READONLY];
}
/**
* Transform list to an array.
*/
toArray() {
return [...this._items];
}
[Symbol.iterator]() {
return this._items.values();
}
/* @internal */
[LIST_ADD](item, trigger) {
const index = this._items.length;
if (!("" + index in this)) {
Object.defineProperty(this, index, {
get() {
return this._items[index];
}
});
}
if (this._items.includes(item))
return;
this._items.push(item);
this.dispatchEvent(new DOMEvent("add", { detail: item, trigger }));
}
/* @internal */
[LIST_REMOVE](item, trigger) {
const index = this._items.indexOf(item);
if (index >= 0) {
this[LIST_ON_REMOVE]?.(item, trigger);
this._items.splice(index, 1);
this.dispatchEvent(new DOMEvent("remove", { detail: item, trigger }));
}
}
/* @internal */
[LIST_RESET](trigger) {
for (const item of [...this._items])
this[LIST_REMOVE](item, trigger);
this._items = [];
this[LIST_SET_READONLY](false, trigger);
this[LIST_ON_RESET]?.();
}
/* @internal */
[LIST_SET_READONLY](readonly, trigger) {
if (this[LIST_READONLY] === readonly)
return;
this[LIST_READONLY] = readonly;
this.dispatchEvent(new DOMEvent("readonly-change", { detail: readonly, trigger }));
}
}
var key = {
fullscreenEnabled: 0,
fullscreenElement: 1,
requestFullscreen: 2,
exitFullscreen: 3,
fullscreenchange: 4,
fullscreenerror: 5,
fullscreen: 6
};
var webkit = [
'webkitFullscreenEnabled',
'webkitFullscreenElement',
'webkitRequestFullscreen',
'webkitExitFullscreen',
'webkitfullscreenchange',
'webkitfullscreenerror',
'-webkit-full-screen',
];
var moz = [
'mozFullScreenEnabled',
'mozFullScreenElement',
'mozRequestFullScreen',
'mozCancelFullScreen',
'mozfullscreenchange',
'mozfullscreenerror',
'-moz-full-screen',
];
var ms = [
'msFullscreenEnabled',
'msFullscreenElement',
'msRequestFullscreen',
'msExitFullscreen',
'MSFullscreenChange',
'MSFullscreenError',
'-ms-fullscreen',
];
// so it doesn't throw if no window or document
var document$1 = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {};
var vendor = (('fullscreenEnabled' in document$1 && Object.keys(key)) ||
(webkit[0] in document$1 && webkit) ||
(moz[0] in document$1 && moz) ||
(ms[0] in document$1 && ms) ||
[]);
var fscreen = {
requestFullscreen: function (element) { return element[vendor[key.requestFullscreen]](); },
requestFullscreenFunction: function (element) { return element[vendor[key.requestFullscreen]]; },
get exitFullscreen() { return document$1[vendor[key.exitFullscreen]].bind(document$1); },
get fullscreenPseudoClass() { return ":" + vendor[key.fullscreen]; },
addEventListener: function (type, handler, options) { return document$1.addEventListener(vendor[key[type]], handler, options); },
removeEventListener: function (type, handler, options) { return document$1.removeEventListener(vendor[key[type]], handler, options); },
get fullscreenEnabled() { return Boolean(document$1[vendor[key.fullscreenEnabled]]); },
set fullscreenEnabled(val) { },
get fullscreenElement() { return document$1[vendor[key.fullscreenElement]]; },
set fullscreenElement(val) { },
get onfullscreenchange() { return document$1[("on" + vendor[key.fullscreenchange]).toLowerCase()]; },
set onfullscreenchange(handler) { return document$1[("on" + vendor[key.fullscreenchange]).toLowerCase()] = handler; },
get onfullscreenerror() { return document$1[("on" + vendor[key.fullscreenerror]).toLowerCase()]; },
set onfullscreenerror(handler) { return document$1[("on" + vendor[key.fullscreenerror]).toLowerCase()] = handler; },
};
var fscreen$1 = fscreen;
const CAN_FULLSCREEN = fscreen$1.fullscreenEnabled;
class FullscreenController extends ComponentController {
/**
* Tracks whether we're the active fullscreen event listener. Fullscreen events can only be
* listened to globally on the document so we need to know if they relate to the current host
* element or not.
*/
_listening = false;
_active = false;
get active() {
return this._active;
}
get supported() {
return CAN_FULLSCREEN;
}
onConnect() {
listenEvent(fscreen$1, "fullscreenchange", this._onFullscreenChange.bind(this));
listenEvent(fscreen$1, "fullscreenerror", this._onFullscreenError.bind(this));
}
async onDisconnect() {
if (CAN_FULLSCREEN)
await this.exit();
}
_onFullscreenChange(event) {
const active = isFullscreen(this.el);
if (active === this._active)
return;
if (!active)
this._listening = false;
this._active = active;
this.dispatch("fullscreen-change", { detail: active, trigger: event });
}
_onFullscreenError(event) {
if (!this._listening)
return;
this.dispatch("fullscreen-error", { detail: null, trigger: event });
this._listening = false;
}
async enter() {
try {
this._listening = true;
if (!this.el || isFullscreen(this.el))
return;
assertFullscreenAPI();
return fscreen$1.requestFullscreen(this.el);
} catch (error) {
this._listening = false;
throw error;
}
}
async exit() {
if (!this.el || !isFullscreen(this.el))
return;
assertFullscreenAPI();
return fscreen$1.exitFullscreen();
}
}
function canFullscreen() {
return CAN_FULLSCREEN;
}
function isFullscreen(host) {
if (fscreen$1.fullscreenElement === host)
return true;
try {
return host.matches(
// @ts-expect-error - `fullscreenPseudoClass` is missing from `@types/fscreen`.
fscreen$1.fullscreenPseudoClass
);
} catch (error) {
return false;
}
}
function assertFullscreenAPI() {
if (CAN_FULLSCREEN)
return;
throw Error(
"[vidstack] fullscreen API is not enabled or supported in this environment"
);
}
const UA = navigator?.userAgent.toLowerCase();
const IS_IOS = /iphone|ipad|ipod|ios|crios|fxios/i.test(UA);
const IS_IPHONE = /(iphone|ipod)/gi.test(navigator?.platform);
const IS_CHROME = !!window.chrome;
const IS_SAFARI = !!window.safari || IS_IOS;
function canOrientScreen() {
return !isUndefined(screen.orientation) && isFunction(screen.orientation.lock) && isFunction(screen.orientation.unlock);
}
function canPlayHLSNatively(video) {
if (!video)
video = document.createElement("video");
return video.canPlayType("application/vnd.apple.mpegurl").length > 0;
}
function canUsePictureInPicture(video) {
return !!document.pictureInPictureEnabled && !video.disablePictureInPicture;
}
function canUseVideoPresentation(video) {
return isFunction(video.webkitSupportsPresentationMode) && isFunction(video.webkitSetPresentationMode);
}
async function canChangeVolume() {
const video = document.createElement("video");
video.volume = 0.5;
await waitTimeout(0);
return video.volume === 0.5;
}
function getMediaSource() {
return window?.MediaSource ?? window?.WebKitMediaSource;
}
function getSourceBuffer() {
return window?.SourceBuffer ?? window?.WebKitSourceBuffer;
}
function isHLSSupported() {
const MediaSource = getMediaSource();
if (isUndefined(MediaSource))
return false;
const isTypeSupported = MediaSource && isFunction(MediaSource.isTypeSupported) && MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
const SourceBuffer = getSourceBuffer();
const isSourceBufferValid = isUndefined(SourceBuffer) || !isUndefined(SourceBuffer.prototype) && isFunction(SourceBuffer.prototype.appendBuffer) && isFunction(SourceBuffer.prototype.remove);
return !!isTypeSupported && !!isSourceBufferValid;
}
const CAN_USE_SCREEN_ORIENTATION_API = canOrientScreen();
class ScreenOrientationController extends ComponentController {
_type = signal(getScreenOrientation());
_locked = signal(false);
_currentLock;
/**
* The current screen orientation type.
*
* @signal
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation}
* @see https://w3c.github.io/screen-orientation/#screen-orientation-types-and-locks
*/
get type() {
return this._type();
}
/**
* Whether the screen orientation is currently locked.
*
* @signal
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation}
* @see https://w3c.github.io/screen-orientation/#screen-orientation-types-and-locks
*/
get locked() {
return this._locked();
}
/**
* Whether the viewport is in a portrait orientation.
*
* @signal
*/
get portrait() {
return this._type().startsWith("portrait");
}
/**
* Whether the viewport is in a landscape orientation.
*
* @signal
*/
get landscape() {
return this._type().startsWith("landscape");
}
/**
* Whether the native Screen Orientation API is available.
*/
get supported() {
return CAN_USE_SCREEN_ORIENTATION_API;
}
onConnect() {
if (CAN_USE_SCREEN_ORIENTATION_API) {
listenEvent(screen.orientation, "change", this._onOrientationChange.bind(this));
} else {
const query = window.matchMedia("(orientation: landscape)");
query.onchange = this._onOrientationChange.bind(this);
return () => query.onchange = null;
}
}
async onDisconnect() {
if (CAN_USE_SCREEN_ORIENTATION_API && this._locked())
await this.unlock();
}
_onOrientationChange(event) {
this._type.set(getScreenOrientation());
this.dispatch("orientation-change", {
detail: {
orientation: peek(this._type),
lock: this._currentLock
},
trigger: event
});
}
/**
* Locks the orientation of the screen to the desired orientation type using the
* Screen Orientation API.
*
* @param lockType - The screen lock orientation type.
* @throws Error - If screen orientation API is unavailable.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Screen/orientation}
* @see {@link https://w3c.github.io/screen-orientation}
*/
async lock(lockType) {
if (peek(this._locked) || this._currentLock === lockType)
return;
assertScreenOrientationAPI();
await screen.orientation.lock(lockType);
this._locked.set(true);
this._currentLock = lockType;
}
/**
* Unlocks the orientation of the screen to it's default state using the Screen Orientation
* API. This method will throw an error if the API is unavailable.
*
* @throws Error - If screen orientation API is unavailable.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Screen/orientation}
* @see {@link https://w3c.github.io/screen-orientation}
*/
async unlock() {
if (!peek(this._locked))
return;
assertScreenOrientationAPI();
this._currentLock = void 0;
await screen.orientation.unlock();
this._locked.set(false);
}
}
function assertScreenOrientationAPI() {
if (CAN_USE_SCREEN_ORIENTATION_API)
return;
throw Error(
"[vidstack] screen orientation API is not available"
);
}
function getScreenOrientation() {
if (CAN_USE_SCREEN_ORIENTATION_API)
return window.screen.orientation.type;
return window.innerWidth >= window.innerHeight ? "landscape-primary" : "portrait-primary";
}
function setAttributeIfEmpty(target, name, value) {
if (!target.hasAttribute(name))
target.setAttribute(name, value);
}
function setARIALabel(target, label) {
if (target.hasAttribute("aria-label") || target.hasAttribute("aria-describedby"))
return;
function updateAriaDescription() {
setAttribute(target, "aria-label", label());
}
effect(updateAriaDescription);
}
function isElementParent(owner, node, test) {
while (node) {
if (node === owner) {
return true;
} else if (node.localName === owner.localName || test?.(node)) {
break;
} else {
node = node.parentElement;
}
}
return false;
}
function onPress(target, handler) {
listenEvent(target, "pointerup", (event) => {
if (event.button === 0)
handler(event);
});
listenEvent(target, "keydown", (event) => {
if (isKeyboardClick(event))
handler(event);
});
}
function scopedRaf(callback) {
const scope = getScope();
requestAnimationFrame(() => scoped(callback, scope));
}
const mediaContext = createContext();
function useMedia() {
return useContext(mediaContext);
}
const MEDIA_ATTRIBUTES = [
"autoplay",
"autoplayError",
"canFullscreen",
"canPictureInPicture",
"canLoad",
"canPlay",
"canSeek",
"ended",
"error",
"fullscreen",
"loop",
"live",
"liveEdge",
"mediaType",
"muted",
"paused",
"pictureInPicture",
"playing",
"playsinline",
"seeking",
"started",
"streamType",
"userIdle",
"viewType",
"waiting"
];
const MEDIA_KEY_SHORTCUTS = {
togglePaused: "k Space",
toggleMuted: "m",
toggleFullscreen: "f",
togglePictureInPicture: "i",
toggleCaptions: "c",
seekBackward: "ArrowLeft",
seekForward: "ArrowRight",
volumeUp: "ArrowUp",
volumeDown: "ArrowDown"
};
const MODIFIER_KEYS = /* @__PURE__ */ new Set(["Shift", "Alt", "Meta", "Control"]), BUTTON_SELECTORS = 'button, [role="button"]', IGNORE_SELECTORS = 'input, textarea, select, [contenteditable], [role^="menuitem"]';
class MediaKeyboardController extends ComponentController {
constructor(instance, _media) {
super(instance);
this._media = _media;
}
onConnect() {
effect(this._onTargetChange.bind(this));
}
_onTargetChange() {
const { keyDisabled, keyTarget } = this.$props;
if (keyDisabled())
return;
const target = keyTarget() === "player" ? this.el : document, $active = signal(false);
if (target === this.el) {
this.listen("focusin", () => $active.set(true));
this.listen("focusout", (event) => {
if (!this.el.contains(event.target))
$active.set(false);
});
} else {
if (!peek($active))
$active.set(document.querySelector("media-player") === this.el);
listenEvent(document, "focusin", (event) => {
const activePlayer = event.composedPath().find((el) => el instanceof Element && el.localName === "media-player");
if (activePlayer !== void 0)
$active.set(this.el === activePlayer);
});
}
effect(() => {
if (!$active())
return;
listenEvent(target, "keyup", this._onKeyUp.bind(this));
listenEvent(target, "keydown", this._onKeyDown.bind(this));
listenEvent(target, "keydown", this._onPreventVideoKeys.bind(this), { capture: true });
});
}
_onKeyUp(event) {
const focused = document.activeElement, sliderFocused = focused?.hasAttribute("data-media-slider");
if (!event.key || !this.$store.canSeek() || sliderFocused || focused?.matches(IGNORE_SELECTORS)) {
return;
}
const method = this._getMatchingMethod(event);
if (method?.startsWith("seek")) {
event.preventDefault();
event.stopPropagation();
if (this._timeSlider) {
this._forwardTimeKeyboardEvent(event);
this._timeSlider = null;
} else {
this._media.remote.seek(this._seekTotal, event);
this._seekTotal = void 0;
}
}
if (method?.startsWith("volume")) {
const volumeSlider = this.el.querySelector("media-volume-slider");
volumeSlider?.dispatchEvent(new DOMEvent("keyup", { trigger: event }));
}
}
_onKeyDown(event) {
if (!event.key || MODIFIER_KEYS.has(event.key))
return;
const focused = document.activeElement;
if (focused?.matches(IGNORE_SELECTORS) || isKeyboardClick(event) && focused?.matches(BUTTON_SELECTORS)) {
return;
}
const sliderFocused = focused?.hasAttribute("data-media-slider"), method = this._getMatchingMethod(event);
if (!method && !event.metaKey && /[0-9]/.test(event.key) && !sliderFocused) {
event.preventDefault();
event.stopPropagation();
this._media.remote.seek(this.$store.duration() / 10 * Number(event.key), event);
return;
}
if (!method || /volume|seek/.test(method) && sliderFocused)
return;
event.preventDefault();
event.stopPropagation();
switch (method) {
case "seekForward":
case "seekBackward":
this._seeking(event, method);
break;
case "volumeUp":
case "volumeDown":
const volumeSlider = this.el.querySelector("media-volume-slider");
if (volumeSlider) {
volumeSlider.dispatchEvent(new DOMEvent("keydown", { trigger: event }));
} else {
const value = event.shiftKey ? 0.1 : 0.05;
this._media.remote.changeVolume(
this.$store.volume() + (method === "volumeUp" ? +value : -value),
event
);
}
break;
case "toggleFullscreen":
this._media.remote.toggleFullscreen("prefer-media", event);
break;
default:
this._media.remote[method]?.(event);
}
}
_onPreventVideoKeys(event) {
if (isHTMLMediaElement(event.target) && this._getMatchingMethod(event)) {
event.preventDefault();
}
}
_getMatchingMethod(event) {
const keyShortcuts = {
...this.$props.keyShortcuts(),
...this._media.ariaKeys
};
return Object.keys(keyShortcuts).find(
(method) => keyShortcuts[method].split(" ").some(
(keys) => replaceSymbolKeys(keys).replace(/Control/g, "Ctrl").split("+").every(
(key) => MODIFIER_KEYS.has(key) ? event[key.toLowerCase() + "Key"] : event.key === key.replace("Space", " ")
)
)
);
}
_seekTotal;
_calcSeekAmount(event, type) {
const seekBy = event.shiftKey ? 10 : 5;
return this._seekTotal = Math.max(
0,
Math.min(
(this._seekTotal ?? this.$store.currentTime()) + (type === "seekForward" ? +seekBy : -seekBy),
this.$store.duration()
)
);
}
_timeSlider = null;
_forwardTimeKeyboardEvent(event) {
this._timeSlider?.dispatchEvent(new DOMEvent(event.type, { trigger: event }));
}
_seeking(event, type) {
if (!this.$store.canSeek())
return;
if (!this._timeSlider)
this._timeSlider = this.el.querySelector("media-time-slider");
if (this._timeSlider) {
this._forwardTimeKeyboardEvent(event);
} else {
this._media.remote.seeking(this._calcSeekAmount(event, type), event);
}
}
}
const SYMBOL_KEY_MAP = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"];
function replaceSymbolKeys(key) {
return key.replace(/Shift\+(\d)/g, (_, num) => SYMBOL_KEY_MAP[num - 1]);
}
const mediaPlayerProps = {
autoplay: false,
aspectRatio: defineProp({
value: null,
type: {
from(value) {
if (!value)
return null;
if (!value.includes("/"))
return +value;
const [width, height] = value.split("/").map(Number);
return +(width / height).toFixed(4);
}
}
}),
controls: false,
currentTime: 0,
crossorigin: null,
fullscreenOrientation: "landscape",
load: "visible",
logLevel: "silent",
loop: false,
muted: false,
paused: true,
playsinline: false,
playbackRate: 1,
poster: "",
preload: "metadata",
preferNativeHLS: defineProp({
value: false,
attribute: "prefer-native-hls"
}),
src: "",
userIdleDelay: 2e3,
viewType: "unknown",
streamType: "unknown",
volume: 1,
liveEdgeTolerance: 10,
minLiveDVRWindow: 60,
keyDisabled: false,
keyTarget: "player",
keyShortcuts: MEDIA_KEY_SHORTCUTS,
title: "",
thumbnails: null,
textTracks: defineProp({
value: [],
attribute: false
}),
smallBreakpointX: 600,
largeBreakpointX: 980,
smallBreakpointY: 380,
largeBreakpointY: 600
};
class TimeRange {
_ranges;
get length() {
return this._ranges.length;
}
constructor(start, end) {
if (isArray(start)) {
this._ranges = start;
} else if (!isUndefined(start) && !isUndefined(end)) {
this._ranges = [[start, end]];
} else {
this._ranges = [];
}
}
start(index) {
throwIfEmpty(this._ranges.length);
throwIfOutOfRange("start", index, this._ranges.length - 1);
return this._ranges[index][0] ?? Infinity;
}
end(index) {
throwIfEmpty(this._ranges.length);
throwIfOutOfRange("end", index, this._ranges.length - 1);
return this._ranges[index][1] ?? Infinity;
}
}
function getTimeRangesStart(range) {
if (!range.length)
return null;
let min = range.start(0);
for (let i = 1; i < range.length; i++) {
const value = range.start(i);
if (value < min)
min = value;
}
return min;
}
function getTimeRangesEnd(range) {
if (!range.length)
return null;
let max = range.end(0);
for (let i = 1; i < range.length; i++) {
const value = range.end(i);
if (value > max)
max = value;
}
return max;
}
function throwIfEmpty(length) {
if (!length)
throw new Error("`TimeRanges` object is empty." );
}
function throwIfOutOfRange(fnName, index, end) {
if (!isNumber(index) || index < 0 || index > end) {
throw new Error(
`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${end}).`
);
}
}
const MediaStoreFactory = new StoreFactory({
audioTracks: [],
audioTrack: null,
autoplay: false,
autoplayError: void 0,
buffered: new TimeRange(),
duration: 0,
canLoad: false,
canFullscreen: false,
canPictureInPicture: false,
canPlay: false,
controls: false,
crossorigin: null,
poster: "",
currentTime: 0,
ended: false,
error: void 0,
fullscreen: false,
loop: false,
logLevel: "warn" ,
mediaType: "unknown",
muted: false,
paused: true,
played: new TimeRange(),
playing: false,
playsinline: false,
pictureInPicture: false,
preload: "metadata",
playbackRate: 1,
qualities: [],
quality: null,
autoQuality: false,
canSetQuality: true,
seekable: new TimeRange(),
seeking: false,
source: { src: "", type: "" },
sources: [],
started: false,
title: "",
textTracks: [],
textTrack: null,
thumbnails: null,
thumbnailCues: [],
volume: 1,
waiting: false,
get viewType() {
return this.providedViewType !== "unknown" ? this.providedViewType : this.mediaType;
},
get streamType() {
return this.providedStreamType !== "unknown" ? this.providedStreamType : this.inferredStreamType;
},
get currentSrc() {
return this.source;
},
get bufferedStart() {
return getTimeRangesStart(this.buffered) ?? 0;
},
get bufferedEnd() {
return getTimeRangesEnd(this.buffered) ?? 0;
},
get seekableStart() {
return getTimeRangesStart(this.seekable) ?? 0;
},
get seekableEnd() {
return this.canPlay ? getTimeRangesEnd(this.seekable) ?? Infinity : 0;
},
get seekableWindow() {
return Math.max(0, this.seekableEnd - this.seekableStart);
},
// ~~ responsive design ~~
touchPointer: false,
orientation: "landscape",
mediaWidth: 0,
mediaHeight: 0,
breakpointX: "sm",
breakpointY: "sm",
// ~~ user props ~~
userIdle: false,
userBehindLiveEdge: false,
// ~~ live props ~~
liveEdgeTolerance: 10,
minLiveDVRWindow: 60,
get canSeek() {
return /unknown|on-demand|:dvr/.test(this.streamType) && Number.isFinite(this.seekableWindow) && (!this.live || /:dvr/.test(this.streamType) && this.seekableWindow >= this.minLiveDVRWindow);
},
get live() {
return this.streamType.includes("live") || !Number.isFinite(this.duration);
},
get liveEdgeStart() {
return this.live && Number.isFinite(this.seekableEnd) ? Math.max(0, (this.liveSyncPosition ?? this.seekableEnd) - this.liveEdgeTolerance) : 0;
},
get liveEdge() {
return this.live && (!this.canSeek || !this.userBehindLiveEdge && this.currentTime >= this.liveEdgeStart);
},
get liveEdgeWindow() {
return this.live && Number.isFinite(this.seekableEnd) ? this.seekableEnd - this.liveEdgeStart : 0;
},
// ~~ internal props ~~
autoplaying: false,
providedViewType: "unknown",
providedStreamType: "unknown",
inferredStreamType: "unknown",
liveSyncPosition: null
});
const DO_NOT_RESET_ON_SRC_CHANGE = /* @__PURE__ */ new Set([
"autoplay",
"breakpointX",
"breakpointY",
"canFullscreen",
"canLoad",
"canPictureInPicture",
"controls",
"fullscreen",
"logLevel",
"loop",
"mediaHeight",
"mediaWidth",
"muted",
"orientation",
"pictureInPicture",
"playsinline",
"poster",
"preload",
"providedStreamType",
"providedViewType",
"source",
"sources",
"textTrack",
"textTracks",
"thumbnailCues",
"thumbnails",
"title",
"touchPointer",
"volume"
]);
function softResetMediaStore($media) {
MediaStoreFactory.reset($media, (prop) => !DO_NOT_RESET_ON_SRC_CHANGE.has(prop));
tick();
}
const SELECTED = Symbol("SELECTED" );
class SelectList extends List {
get selected() {
return this._items.find((item) => item.selected) ?? null;
}
get selectedIndex() {
return this._items.findIndex((item) => item.selected);
}
/* @internal */
[LIST_ON_REMOVE](item, trigger) {
this[LIST_SELECT](item, false, trigger);
}
/* @internal */
[LIST_ADD](item, trigger) {
item[SELECTED] = false;
Object.defineProperty(item, "selected", {
get() {
return this[SELECTED];
},
set: (selected) => {
if (this.readonly)
return;
this[LIST_ON_USER_SELECT]?.();
this[LIST_SELECT](item, selected);
}
});
super[LIST_ADD](item, trigger);
}
/* @internal */
[LIST_SELECT](item, selected, trigger) {
if (selected === item[SELECTED])
return;
const prev = this.selected;
item[SELECTED] = selected;
const changed = !selected ? prev === item : prev !== item;
if (changed) {
if (prev)
prev[SELECTED] = false;
this.dispatchEvent(
new DOMEvent("change", {
detail: { prev, current: this.selected },
trigger
})
);
}
}
}
const SET_AUTO_QUALITY = Symbol("SET_AUTO_QUALITY" );
const ENABLE_AUTO_QUALITY = Symbol("ENABLE_AUTO_QUALITY" );
class VideoQualityList extends SelectList {
_auto = false;
/**
* Configures quality switching:
*
* - `current`: Trigger an immediate quality level switch. This will abort the current fragment
* request if any, flush the whole buffer, and fetch fragment matching with current position
* and requested quality level.
*
* - `next`: Trigger a quality level switch for next fragment. This could eventually flush
* already buffered next fragment.
*
* - `load`: Set quality level for next loaded fragment.
*
* @see {@link https://vidstack.io/docs/player/core-concepts/quality#switch}
* @see {@link https://github.com/video-dev/hls.js/blob/master/docs/API.md#quality-switch-control-api}
*/
switch = "current";
/**
* Whether automatic quality selection is enabled.
*/
get auto() {
return this._auto || this.readonly;
}
/* @internal */
[ENABLE_AUTO_QUALITY];
/* @internal */
[LIST_ON_USER_SELECT]() {
this[SET_AUTO_QUALITY](false);
}
/* @internal */
[LIST_ON_RESET](trigger) {
this[SET_AUTO_QUALITY](false, trigger);
}
/**
* Request automatic quality selection (if supported). This will be a no-op if the list is
* `readonly` as that already implies auto-selection.
*/
autoSelect(trigger) {
if (this.readonly || this._auto || !this[ENABLE_AUTO_QUALITY])
return;
this[ENABLE_AUTO_QUALITY]();
this[SET_AUTO_QUALITY](true, trigger);
}
/* @internal */
[SET_AUTO_QUALITY](auto, trigger) {
if (this._auto === auto)
return;
this._auto = auto;
this.dispatchEvent(
new DOMEvent("auto-change", {
detail: auto,
trigger
})
);
}
}
const MEDIA_EVENTS = [
"abort",
"can-play",
"can-play-through",
"duration-change",
"emptied",
"ended",
"error",
"fullscreen-change",
"loaded-data",
"loaded-metadata",
"load-start",
"media-type-change",
"pause",
"play",
"playing",
"progress",
"seeked",
"seeking",
"source-change",
"sources-change",
"stalled",
"started",
"suspend",
"stream-type-change",
"replay",
// 'time-update',
"view-type-change",
"volume-change",
"waiting"
] ;
class MediaEventsLogger extends ComponentController {
constructor(instance, _media) {
super(instance);
this._media = _media;
}
onConnect() {
const handler = this._onMediaEvent.bind(this);
for (const eventType of MEDIA_EVENTS)
this.listen(eventType, handler);
}
_onMediaEvent(event) {
this._media.logger?.infoGroup(`\u{1F4E1} dispatching \`${event.type}\``).labelledLog("Media Store", { ...this.$store }).labelledLog("Event", event).dispatch();
}
}
class MediaLoadController extends ComponentController {
constructor(instance, _callback) {
super(instance);
this._callback = _callback;
}
async onAttach(el) {
const load = this.$props.load();
if (load === "eager") {
requestAnimationFrame(this._callback);
} else if (load === "idle") {
const { waitIdlePeriod } = await import('maverick.js/std');
waitIdlePeriod(this._callback);
} else if (load === "visible") {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
observer.disconnect();
this._callback();
}
});
observer.observe(el);
return observer.disconnect.bind(observer);
}
}
}
class MediaPlayerDelegate {
constructor(_handle, _media) {
this._handle = _handle;
this._media = _media;
}
_dispatch(type, ...init) {
this._handle(new DOMEvent(type, init?.[0]));
}
async _ready(info, trigger) {
const { $store, logger } = this._media;
if (peek($store.canPlay))
return;
this._dispatch("can-play", { detail: info, trigger });
tick();
{
logger?.infoGroup("-~-~-~-~-~-~-~-~- \u2705 MEDIA READY -~-~-~-~-~-~-~-~-").labelledLog("Media Store", { ...$store }).labelledLog("Trigger Event", trigger).dispatch();
}
if ($store.canPlay() && $store.autoplay() && !$store.started()) {
await this._attemptAutoplay();
}
}
async _attemptAutoplay() {
const { player, $store } = this._media;
$store.autoplaying.set(true);
try {
await player.play();
this._dispatch("autoplay", { detail: { muted: $store.muted() } });
} catch (error) {
this._dispatch("autoplay-fail", {
detail: {
muted: $store.muted(),
error
}
});
} finally {
$store.autoplaying.set(false);
}
}
}
class Queue {
_queue = /* @__PURE__ */ new Map();
/**
* Queue the given `item` under the given `key` to be processed at a later time by calling
* `serve(key)`.
*/
_enqueue(key, item) {
if (!this._queue.has(key))
this._queue.set(key, /* @__PURE__ */ new Set());
this._queue.get(key).add(item);
}
/**
* Process all items in queue for the given `key`.
*/
_serve(key, callback) {
const items = this._queue.get(key);
if (items)
for (const item of items)
callback(item);
this._queue.delete(key);
}
/**
* Removes all queued items under the given `key`.
*/
_delete(key) {
this._queue.delete(key);
}
/**
* The number of items currently queued under the given `key`.
*/
_size(key) {
return this._queue.get(key)?.size ?? 0;
}
/**
* Clear all items in the queue.
*/
_reset() {
this._queue.clear();
}
}
function coerceToError(error) {
return error instanceof Error ? error : Error(JSON.stringify(error));
}
class MediaUserController extends ComponentController {
_idleTimer = -2;
_delay = 2e3;
_pausedTracking = false;
_focusedItem = null;
/**
* Whether the media user is currently idle.
*/
get idling() {
return this.$store.userIdle();
}
/**
* The amount of delay in milliseconds while media playback is progressing without user
* activity to indicate an idle state.
*
* @defaultValue 2000
*/
get idleDelay() {
return this._delay;
}
set idleDelay(newDelay) {
this._delay = newDelay;
}
/**
* Change the user idle state.
*/
idle(idle, delay = this._delay, trigger) {
this._clearIdleTimer();
if (!this._pausedTracking)
this._requestIdleChange(idle, delay, trigger);
}
/**
* Whether all idle tracking should be paused until resumed again.
*/
pauseIdleTracking(paused, trigger) {
this._pausedTracking = paused;
if (paused) {
this._clearIdleTimer();
this._requestIdleChange(false, 0, trigger);
}
}
onConnect() {
effect(this._watchPaused.bind(this));
listenEvent(this.el, "play", this._onMediaPlay.bind(this));
listenEvent(this.el, "pause", this._onMediaPause.bind(this));
}
_watchPaused() {
if (this.$store.paused())
return;
const onStopIdle = this._onStopIdle.bind(this);
for (const eventType of ["pointerup", "keydown"]) {
listenEvent(this.el, eventType, onStopIdle);
}
effect(() => {
if (!this.$store.touchPointer())
listenEvent(this.el, "pointermove", onStopIdle);
});
}
_onMediaPlay(event) {
this.idle(true, this._delay, event);
}
_onMediaPause(event) {
this.idle(false, 0, event);
}
_clearIdleTimer() {
window.clearTimeout(this._idleTimer);
this._idleTimer = -1;
}
_onStopIdle(event) {
if (event.MEDIA_GESTURE)
return;
if (isKeyboardEvent(event)) {
if (event.key === "Escape") {
this.el?.focus();
this._focusedItem = null;
} else if (this._focusedItem) {
event.preventDefault();
requestAnimationFrame(() => {
this._focusedItem?.focus();
this._focusedItem = null;
});
}
}
this.idle(false, 0, event);
this.idle(true, this._delay, event);
}
_requestIdleChange(idle, delay, trigger) {
if (delay === 0) {
this._onIdleChange(idle, trigger);
return;
}
this._idleTimer = window.setTimeout(() => {
this._onIdleChange(idle && !this._pausedTracking, trigger);
}, delay);
}
_onIdleChange(idle, trigger) {
if (this.$store.userIdle() === idle)
return;
this.$store.userIdle.set(idle);
if (idle && document.activeElement && this.el?.contains(document.activeElement)) {
this._focusedItem = document.activeElement;
requestAnimationFrame(() => this.el?.focus());
}
this.dispatch("user-idle-change", {
detail: idle,
trigger
});
}
}
class MediaRequestContext {
_seeking = false;
_looping = false;
_replaying = false;
_queue = new Queue();
}
class MediaRequestManager extends ComponentController {
constructor(instance, _stateMgr, _request, _media) {
super(instance);
this._stateMgr = _stateMgr;
this._request = _request;
this._media = _media;
this._store = _media.$store;
this._provider = _media.$provider;
this._user = new MediaUserController(instance);
this._fullscreen = new FullscreenController(instance);
this._orientation = new ScreenOrientationController(instance);
}
_user;
_fullscreen;
_orientation;
_store;
_provider;
onConnect() {
effect(this._onIdleDelayChange.bind(this));
effect(this._onFullscreenSupportChange.bind(this));
effect(this._onPiPSupportChange.bind(this));
const names = Object.getOwnPropertyNames(Object.getPrototypeOf(this)), handle = this._handleRequest.bind(this);
for (const name of names) {
if (name.startsWith("media-")) {
this.listen(name, handle);
}
}
this.listen("fullscreen-change", this._onFullscreenChange.bind(this));
}
_handleRequest(event) {
event.stopPropagation();
{
this._media.logger?.infoGroup(`\u{1F4EC} received \`${event.type}\``).labelledLog("Request", event).dispatch();
}
if (peek(this._provider))
this[event.type]?.(event);
}
async _play() {
const { canPlay, paused, ended, autoplaying, seekableStart } = this._store;
if (!peek(paused))
return;
try {
const provider = peek(this._provider);
throwIfNotReadyForPlayback(provider, peek(canPlay));
if (peek(ended)) {
provider.currentTime = seekableStart() + 0.1;
}
return provider.play();
} catch (error) {
const errorEvent = this.createEvent("play-fail", { detail: coerceToError(error) });
errorEvent.autoplay = autoplaying();
this._stateMgr._handle(errorEvent);
throw error;
}
}
async _pause() {
const { canPlay, paused } = this._store;
if (peek(paused))
return;
const provider = peek(this._provider);
throwIfNotReadyForPlayback(provider, peek(canPlay));
return provider.pause();
}
_seekToLiveEdge() {
const { canPlay, live, liveEdge, canSeek, liveSyncPosition, seekableEnd, userBehindLiveEdge } = this._store;
userBehindLiveEdge.set(false);
if (peek(() => !live() || liveEdge() || !canSeek()))
return;
const provider = peek(this._provider);
throwIfNotReadyForPlayback(provider, peek(canPlay));
provider.currentTime = liveSyncPosition() ?? seekableEnd() - 2;
}
_wasPIPActive = false;
async _enterFullscreen(target = "prefer-media") {
const provider = peek(this._provider);
const adapter = target === "prefer-media" && this._fullscreen.supported || target === "media" ? this._fullscreen : provider?.fullscreen;
throwIfFullscreenNotSupported(target, adapter);
if (adapter.active)
return;
if (peek(this._store.pictureInPicture)) {
this._wasPIPActive = true;
await this._exitPictureInPicture();
}
return adapter.enter();
}
async _exitFullscreen(target = "prefer-media") {
const provider = peek(this._provider);
const adapter = target === "prefer-media" && this._fullscreen.supported || target === "media" ? this._fullscreen : provider?.fullscreen;
throwIfFullscreenNotSupported(target, adapter);
if (!adapter.active)
return;
if (this._orientation.locked)
await this._orientation.unlock();
try {
const result = await adapter.exit();
if (this._wasPIPActive && peek(this._store.canPictureInPicture)) {
await this._enterPictureInPicture();
}
return result;
} finally {
this._wasPIPActive = false;
}
}
async _enterPictureInPicture() {
this._throwIfPIPNotSupported();
if (this._store.pictureInPicture())
return;
return await this._provider().pictureInPicture.enter();
}
async _exitPictureInPicture() {
this._throwIfPIPNotSupported();
if (!this._store.pictureInPicture())
return;
return await this._provider().pictureInPicture.exit();
}
_throwIfPIPNotSupported() {
if (this._store.canPictureInPicture())
return;
throw Error(
`[vidstack] picture-in-picture is not currently available`
);
}
_onIdleDelayChange() {
this._user.idleDelay = this.$props.userIdleDelay();
}
_onFullscreenSupportChange() {
const { canLoad, canFullscreen } = this._store, supported = this._fullscreen.supported || this._provider()?.fullscreen?.supported || false;
if (canLoad() && peek(canFullscreen) === supported)
return;
canFullscreen.set(supported);
}
_onPiPSupportChange() {
const { canLoad, canPictureInPicture } = this._store, supported = this._provider()?.pictureInPicture?.supported || false;
if (canLoad() && peek(canPictureInPicture) === supported)
return;
canPictureInPicture.set(supported);
}
["media-audio-track-change-request"](event) {
if (this._media.audioTracks.readonly) {
{
this._media.logger?.warnGroup(`[vidstack] attempted to change audio track but it is currently read-only`).labelledLog("Event", event).dispatch();
}
return;
}
const index = event.detail, track = this._media.audioTracks[index];
if (track) {
this._request._queue._enqueue("audioTrack", event);
track.selected = true;
} else {
this._media.logger?.warnGroup("[vidstack] failed audio track change request (invalid index)").labelledLog("Audio Tracks", this._media.audioTracks.toArray()).labelledLog("Index", index).labelledLog("Event", event).dispatch();
}
}
async ["media-enter-fullscreen-request"](event) {
try {
this._request._queue._enqueue("fullscreen", event);
await this._enterFullscreen(event.detail);
} catch (error) {
this._onFullscreenError(error);
}
}
async ["media-exit-fullscreen-request"](event) {
try {
this._request._queue._enqueue("fullscreen", event);
await this._exitFullscreen(event.detail);
} catch (error) {
this._onFullscreenError(error);
}
}
async _onFullscreenChange(event) {
if (!event.detail)
return;
try {
const lockType = peek(this.$props.fullscreenOrientation);
if (this._orientation.supported && !isUndefined(lockType)) {
await this._orientation.lock(lockType);
}
} catch (e) {
}
}
_onFullscreenError(error) {
this._stateMgr._handle(
this.createEvent("fullscreen-error", {
detail: coerceToError(error)
})
);
}
async ["media-enter-pip-request"](event) {
try {
this._request._queue._enqueue("pip", event);
await this._enterPictureInPicture();
} catch (error) {
this._onPictureInPictureError(error);
}
}
async ["media-exit-pip-request"](event) {
try {
this._request._queue._enqueue("pip", event);
await this._exitPictureInPicture();
} catch (error) {
this._onPictureInPictureError(error);
}
}
_onPictureInPictureError(error) {
this._stateMgr._handle(
this.createEvent("picture-in-picture-error", {
detail: coerceToError(error)
})
);
}
["media-live-edge-request"](event) {
const { live, liveEdge, canSeek } = this._store;
if (!live() || liveEdge() || !canSeek())
return;
this._request._queue._enqueue("seeked", event);
try {
this._seekToLiveEdge();
} catch (e) {
this._media.logger?.error("seek to live edge fail", e);
}
}
["media-loop-request"]() {
window.requestAnimationFrame(async () => {
try {
this._request._looping = true;
this._request._replaying = true;
await this._play();
} catch (e) {
this._request._looping = false;
this._request._replaying = false;
}
});
}
async ["media-pause-request"](event) {
if (this._store.paused())
return;
try {
this._request._queue._enqueue("pause", event);
await this._provider().pause();
} catch (e) {
this._request._queue._delete("pause");
this._media.logger?.error("pause-fail", e);
}
}
async ["media-play-request"](event) {
if (!this._store.paused())
return;
try {
this._request._queue._enqueue("play", event);
await this._provider().play();
} catch (e) {
const errorEvent = this.createEvent("play-fail", { detail: coerceToError(e) });
this._stateMgr._handle(errorEvent);
}
}
["media-rate-change-request"](event) {
if (this._store.playbackRate() === event.detail)
return;
this._request._queue._enqueue("rate", event);
this._provider().playbackRate = event.detail;
}
["media-quality-change-request"](event) {
if (this._media.qualities.readonly) {
{
this._media.logger?.warnGroup(`[vidstack] attempted to change video quality but it is currently read-only`).labelledLog("Event", event).dispatch();
}
return;
}
this._request._queue._enqueue("quality", event);
const index = event.detail;
if (index < 0) {
this._media.qualities.autoSelect(event);
} else {
const quality = this._media.qualities[index];
if (quality) {
quality.selected = true;
} else {
this._media.logger?.warnGroup("[vidstack] failed quality change request (invalid index)").labelledLog("Qualities", this._media.qualities.toArray()).labelledLog("Index", index).labelledLog("Event", event).dispatch();
}
}
}
["media-resume-user-idle-request"](event) {
this._request._queue._enqueue("userIdle", event);
this._user.pauseIdleTracking(false, event);
}
["media-pause-user-idle-request"](event) {
this._request._queue._enqueue("userIdle", event);
this._user.pauseIdleTracking(true, event);
}
["media-seek-request"](event) {
const { seekableStart, seekableEnd, ended, canSeek, live, userBehindLiveEdge } = this._store;
if (ended())
this._request._replaying = true;
this._request._seeking = false;
this._request._queue._delete("seeking");
const boundTime = Math.min(Math.max(seekableStart() + 0.1, event.detail), seekableEnd() - 0.1);
if (!Number.isFinite(boundTime) || !canSeek())
return;
this._request._queue._enqueue("seeked", event);
this._provider().currentTime = boundTime;
if (live() && event.isOriginTrusted && Math.abs(seekableEnd() - boundTime) >= 2) {
userBehindLiveEdge.set(true);
}
}
["media-seeking-request"](event) {
this._request._queue._enqueue("seeking", event);
this._store.seeking.set(true);
this._request._seeking = true;
}
["media-start-loading"](event) {
if (this._store.canLoad())
return;
this._request._queue._enqueue("load", event);
this._stateMgr._handle(this.createEvent("can-load"));
}
["media-text-track-change-request"](event) {
const { index, mode } = event.detail, track = this._media.textTracks[index];
if (track) {
this._request._queue._enqueue("textTrack", event);
track.setMode(mode, event);
} else {
this._media.logger?.warnGroup("[vidstack] failed text track change request (invalid index)").labelledLog("Text Tracks", this._media.textTracks.toArray()).labelledLog("Index", index).labelledLog("Event", event).dispatch();
}
}
["media-mute-request"](event) {
if (this._store.muted())
return;
this._request._queue._enqueue("volume", event);
this._provider().muted = true;
}
["media-unmute-request"](event) {
const { muted, volume } = this._store;
if (!muted())
return;
this._request._queue._enqueue("volume", event);
this._media.$provider().muted = false;
if (volume() === 0) {
this._request._queue._enqueue("volume", event);
this._provider().volume = 0.25;
}
}
["media-volume-change-request"](event) {
const { muted, volume } = this._store;
const newVolume = event.detail;
if (volume() === newVolume)
return;
this._request._queue._enqueue("volume", event);
this._provider().volume = newVolume;
if (newVolume > 0 && muted()) {
this._request._queue._enqueue("volume", event);
this._provider().muted = false;
}
}
}
function throwIfNotReadyForPlayback(provider, canPlay) {
if (provider && canPlay)
return;
throw Error(
`[vidstack] media is not ready - wait for \`can-play\` event.`
);
}
function throwIfFullscreenNotSupported(target, fullscreen) {
if (fullscreen?.supported)
return;
throw Error(
`[vidstack] fullscreen is not currently available on target \`${target}\``
);
}
var functionDebounce = debounce;
function debounce(fn, wait, callFirst) {
var timeout = null;
var debouncedFn = null;
var clear = function() {
if (timeout) {
clearTimeout(timeout);
debouncedFn = null;
timeout = null;
}
};
var flush = function() {
var call = debouncedFn;
clear();
if (call) {
call();
}
};
var debounceWrapper = function() {
if (!wait) {
return fn.apply(this, arguments);
}
var context = this;
var args = arguments;
var callNow = callFirst && !timeout;
clear();
debouncedFn = function() {
fn.apply(context, args);
};
timeout = setTimeout(function() {
timeout = null;
if (!callNow) {
var call = debouncedFn;
debouncedFn = null;
return call();
}
}, wait);
if (callNow) {
return debouncedFn();
}
};
debounceWrapper.cancel = clear;
debounceWrapper.flush = flush;
return debounceWrapper;
}
var functionThrottle = throttle;
function throttle(fn, interval, options) {
var timeoutId = null;
var throttledFn = null;
var leading = (options && options.leading);
var trailing = (options && options.trailing);
if (leading == null) {
leading = true; // default
}
if (trailing == null) {
trailing = !leading; //default
}
if (leading == true) {
trailing = false; // forced because there should be invocation per call
}
var cancel = function() {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
};
var flush = function() {
var call = throttledFn;
cancel();
if (call) {
call();
}
};
var throttleWrapper = function() {
var callNow = leading && !timeoutId;
var context = this;
var args = arguments;
throttledFn = function() {
return fn.apply(context, args);
};
if (!timeoutId) {
timeoutId = setTimeout(function() {
timeoutId = null;
if (trailing) {
return throttledFn();
}
}, interval);
}
if (callNow) {
callNow = false;
return throttledFn();
}
};
throttleWrapper.cancel = cancel;
throttleWrapper.flush = flush;
return throttleWrapper;
}
const ATTACH_VIDEO = Symbol("ATTACH_VIDEO" );
const TEXT_TRACK_CROSSORIGIN = Symbol("TEXT_TRACK_CROSSORIGIN" );
const TEXT_TRACK_READY_STATE = Symbol("TEXT_TRACK_READY_STATE" );
const TEXT_TRACK_UPDATE_ACTIVE_CUES = Symbol("TEXT_TRACK_UPDATE_ACTIVE_CUES" );
const TEXT_TRACK_CAN_LOAD = Symbol("TEXT_TRACK_CAN_LOAD" );
const TEXT_TRACK_ON_MODE_CHANGE = Symbol("TEXT_TRACK_ON_MODE_CHANGE" );
const TEXT_TRACK_NATIVE = Symbol("TEXT_TRACK_NATIVE" );
const TEXT_TRACK_NATIVE_HLS = Symbol("TEXT_TRACK_NATIVE_HLS" );
const TRACKED_EVENT = /* @__PURE__ */ new Set([
"autoplay",
"autoplay-fail",
"can-load",
"sources-change",
"source-change",
"load-start",
"abort",
"error",
"loaded-metadata",
"loaded-data",
"can-play",
"play",
"play-fail",
"pause",
"playing",
"seeking",
"seeked",
"waiting"
]);
class MediaStateManager extends ComponentController {
constructor(instance, _request, _media) {
super(instance);
this._request = _request;
this._media = _media;
this._store = _media.$store;
}
_store;
_trackedEvents = /* @__PURE__ */ new Map();
_skipInitialSrcChange = true;
_firingWaiting = false;
_waitingTrigger;
onAttach(el) {
el.setAttribute("aria-busy", "true");
}
onConnect(el) {
this._addTextTrackListeners();
this._addQualityListeners();
this._addAudioTrackListeners();
this.listen("fullscreen-change", this["fullscreen-change"].bind(this));
this.listen("fullscreen-error", this["fullscreen-error"].bind(this));
}
_handle(event) {
const type = event.type;
this[event.type]?.(event);
{
if (TRACKED_EVENT.has(type))
this._trackedEvents.set(type, event);
this.el?.dispatchEvent(event);
}
}
_resetTracking() {
this._stopWaiting();
this._request._replaying = false;
this._request._looping = false;
this._firingWaiting = false;
this._waitingTrigger = void 0;
this._trackedEvents.clear();
}
_satisfyRequest(request, event) {
this._request._queue._serve(request, (requestEvent) => {
event.request = requestEvent;
appendTriggerEvent(event, requestEvent);
});
}
_addTextTrackListeners() {
this._onTextTracksChange();
this._onTextTrackModeChange();
const textTracks = this._media.textTracks;
listenEvent(textTracks, "add", this._onTextTracksChange.bind(this));
listenEvent(textTracks, "remove", this._onTextTracksChange.bind(this));
listenEvent(textTracks, "mode-change", this._onTextTrackModeChange.bind(this));
}
_addQualityListeners() {
const qualities = this._media.qualities;
listenEvent(qualities, "add", this._onQualitiesChange.bind(this));
listenEvent(qualities, "remove", this._onQualitiesChange.bind(this));
listenEvent(qualities, "change", this._onQualityChange.bind(this));
listenEvent(qualities, "auto-change", this._onAutoQualityChange.bind(this));
listenEvent(qualities, "readonly-change", this._onCanSetQualityChange.bind(this));
}
_addAudioTrackListeners() {
const audioTracks = this._media.audioTracks;
listenEvent(audioTracks, "add", this._onAudioTracksChange.bind(this));
listenEvent(audioTracks, "remove", this._onAudioTracksChange.bind(this));
listenEvent(audioTracks, "change", this._onAudioTrackChange.bind(this));
}
_onTextTracksChange(event) {
const { textTracks } = this._store;
textTracks.set(this._media.textTracks.toArray());
this.dispatch("text-tracks-change", {
detail: textTracks(),
trigger: event
});
}
_onTextTrackModeChange(event) {
if (event)
this._satisfyRequest("textTrack", event);
const current = this._media.textTracks.selected, { textTrack } = this._store;
if (textTrack() !== current) {
textTrack.set(current);
this.dispatch("text-track-change", {
detail: current,
trigger: event
});
}
}
_onAudioTracksChange(event) {
const { audioTracks } = this._store;
audioTracks.set(this._media.audioTracks.toArray());
this.dispatch("audio-tracks-change", {
detail: audioTracks(),
trigger: event
});
}
_onAudioTrackChange(event) {
const { audioTrack } = this._store;
audioTrack.set(this._media.audioTracks.selected);
this._satisfyRequest("audioTrack", event);
this.dispatch("audio-track-change", {
detail: audioTrack(),
trigger: event
});
}
_onQualitiesChange(event) {
const { qualities } = this._store;
qualities.set(this._media.qualities.toArray());
this.dispatch("qualities-change", {
detail: qualities(),
trigger: event
});
}
_onQualityChange(event) {
const { quality } = this._store;
quality.set(this._media.qualities.selected);
this._satisfyRequest("quality", event);
this.dispatch("quality-change", {
detail: quality(),
trigger: event
});
}
_onAutoQualityChange() {
this._store.autoQuality.set(this._media.qualities.auto);
}
_onCanSetQualityChange() {
this._store.canSetQuality.set(!this._media.qualities.readonly);
}
["provider-change"](event) {
this._media.$provider.set(event.detail);
}
["autoplay"](event) {
appendTriggerEvent(event, this._trackedEvents.get("play"));
appendTriggerEvent(event, this._trackedEvents.get("can-play"));
this._store.autoplayError.set(void 0);
}
["autoplay-fail"](event) {
appendTriggerEvent(event, this._trackedEvents.get("play-fail"));
appendTriggerEvent(event, this._trackedEvents.get("can-play"));
this._store.autoplayError.set(event.detail);
this._resetTracking();
}
["can-load"](event) {
this._store.canLoad.set(true);
this._trackedEvents.set("can-load", event);
this._satisfyRequest("load", event);
this._media.textTracks[TEXT_TRACK_CAN_LOAD]();
}
["media-type-change"](event) {
appendTriggerEvent(event, this._trackedEvents.get("source-change"));
const viewType = this._store.viewType();
this._store.mediaType.set(event.detail);
if (viewType !== this._store.viewType()) {
setTimeout(
() => this.dispatch("view-type-change", {
detail: this._store.viewType(),
trigger: event
}),
0
);
}
}
["stream-type-change"](event) {
const { streamType, inferredStreamType } = this._store;
appendTriggerEvent(event, this._trackedEvents.get("source-change"));
inferredStreamType.set(event.detail);
event.detail = streamType();
}
["rate-change"](event) {
this._store.playbackRate.set(event.detail);
this._satisfyRequest("rate", event);
}
["sources-change"](event) {
this._store.sources.set(event.detail);
}
["source-change"](event) {
appendTriggerEvent(event, this._trackedEvents.get("sources-change"));
this._store.source.set(event.detail);
this.el?.setAttribute("aria-busy", "true");
{
this._media.logger?.infoGroup("\u{1F4FC} Media source change").labelledLog("Source", event.detail).dispatch();
}
if (this._skipInitialSrcChange) {
this._skipInitialSrcChange = false;
return;
}
this._media.audioTracks[LIST_RESET](event);
this._media.qualities[LIST_RESET](event);
this._resetTracking();
softResetMediaStore(this._media.$store);
this._trackedEvents.set(event.type, event);
}
["abort"](event) {
appendTriggerEvent(event, this._trackedEvents.get("source-change"));
appendTriggerEvent(event, this._trackedEvents.get("can-load"));
}
["load-start"](event) {
appendTriggerEvent(event, this._trackedEvents.get("source-change"));
}
["error"](event) {
this._store.error.set(event.detail);
appendTriggerEvent(event, this._trackedEvents.get("abort"));
}
["loaded-metadata"](event) {
appendTriggerEvent(event, this._trackedEvents.get("load-start"));
}
["loaded-data"](event) {
appendTriggerEvent(event, this._trackedEvents.get("load-start"));
}
["can-play"](event) {
if (event.trigger?.type !== "loadedmetadata") {
appendTriggerEvent(event, this._trackedEvents.get("loaded-metadata"));
}
this._onCanPlayDetail(event.detail);
this.el?.setAttribute("aria-busy", "false");
}
["can-play-through"](event) {
this._onCanPlayDetail(event.detail);
appendTriggerEvent(event, this._trackedEvents.get("can-play"));
}
_onCanPlayDetail(detail) {
const { seekable, seekableEnd, buffered, duration, canPlay } = this._store;
seekable.set(detail.seekable);
buffered.set(detail.buffered);
duration.set(seekableEnd);
canPlay.set(true);
}
["duration-change"](event) {
const { live, duration } = this._store, time = event.detail;
if (!live())
duration.set(!Number.isNaN(time) ? time : 0);
}
["progress"](event) {
const { buffered, seekable, live, duration, seekableEnd } = this._store, detail = event.detail;
buffered.set(detail.buffered);
seekable.set(detail.seekable);
if (live()) {
duration.set(seekableEnd);
this.dispatch("duration-change", {
detail: seekableEnd(),
trigger: event
});
}
}
["play"](event) {
const { paused, autoplayError, ended, autoplaying } = this._store;
event.autoplay = autoplaying();
if (this._request._looping || !paused()) {
event.stopImmediatePropagation();
return;
}
appendTriggerEvent(event, this._trackedEvents.get("waiting"));
this._satisfyRequest("play", event);
paused.set(false);
autoplayError.set(void 0);
if (ended() || this._request._replaying) {
this._request._replaying = false;
ended.set(false);
this._handle(this.createEvent("replay", { trigger: event }));
}
}
["play-fail"](event) {
appendTriggerEvent(event, this._trackedEvents.get("play"));
this._satisfyRequest("play", event);
const { paused, playing } = this._store;
paused.set(true);
playing.set(false);
this._resetTracking();
}
["playing"](event) {
const playEvent = this._trackedEvents.get("play");
if (playEvent) {
appendTriggerEvent(event, this._trackedEvents.get("waiting"));
appendTriggerEvent(event, playEvent);
} else {
appendTriggerEvent(event, this._trackedEvents.get("seeked"));
}
setTimeout(() => this._resetTracking(), 0);
const { paused, playing, seeking, ended } = this._store;
paused.set(false);
playing.set(true);
seeking.set(false);
ended.set(false);
if (this._request._looping) {
event.stopImmediatePropagation();
this._request._looping = false;
return;
}
this["started"](event);
}
["started"](event) {
const { started, live, liveSyncPosition, seekableEnd } = this._store;
if (!started()) {
if (live()) {
const end = liveSyncPosition() ?? seekableEnd() - 2;
if (Number.isFinite(end))
this._media.$provider().currentTime = end;
}
started.set(true);
this._handle(this.createEvent("started", { trigger: event }));
}
}
["pause"](event) {
if (this._request._looping) {
event.stopImmediatePropagation();
return;
}
appendTriggerEvent(event, this._trackedEvents.get("seeked"));
this._satisfyRequest("pause", event);
const { paused, playing, seeking } = this._store;
paused.set(true);
playing.set(false);
seeking.set(false);
this._resetTracking();
}
["time-update"](event) {
const { currentTime, played, waiting } = this._store, detail = event.detail;
currentTime.set(detail.currentTime);
played.set(detail.played);
waiting.set(false);
for (const track of this._media.textTracks) {
track[TEXT_TRACK_UPDATE_ACTIVE_CUES](detail.currentTime, event);
}
}
["volume-change"](event) {
const { volume, muted } = this._store, detail = event.detail;
volume.set(detail.volume);
muted.set(detail.muted || detail.volume === 0);
this._satisfyRequest("volume", event);
}
["seeking"] = functionThrottle(
(event) => {
const { seeking, currentTime, paused } = this._store;
seeking.set(true);
currentTime.set(event.detail);
this._satisfyRequest("seeking", event);
if (paused()) {
this._waitingTrigger = event;
this._fireWaiting();
}
},
150,
{ leading: true }
);
["seeked"](event) {
const { seeking, currentTime, paused, duration, ended } = this._store;
if (this._request._seeking) {
seeking.set(true);
event.stopImmediatePropagation();
} else if (seeking()) {
const waitingEvent = this._trackedEvents.get("waiting");
appendTriggerEvent(event, waitingEvent);
if (waitingEvent?.trigger?.type !== "seeking") {
appendTriggerEvent(event, this._trackedEvents.get("seeking"));
}
if (paused())
this._stopWaiting();
seeking.set(false);
if (event.detail !== duration())
ended.set(false);
currentTime.set(event.detail);
this._satisfyRequest("seeked", event);
const origin = event.originEvent;
if (origin && origin.isTrusted && !/seek/.test(origin.type)) {
this["started"](event);
}
}
}
["waiting"](event) {
if (this._firingWaiting || this._request._seeking)
return;
event.stopImmediatePropagation();
this._waitingTrigger = event;
this._fireWaiting();
}
_fireWaiting = functionDebounce(() => {
if (!this._waitingTrigger)
return;
this._firingWaiting = true;
const { waiting, playing } = this._store;
waiting.set(true);
playing.set(false);
const event = this.createEvent("waiting", { trigger: this._waitingTrigger });
this._trackedEvents.set("waiting", event);
this.el.dispatchEvent(event);
this._waitingTrigger = void 0;
this._firingWaiting = false;
}, 300);
["ended"](event) {
if (this._request._looping) {
event.stopImmediatePropagation();
return;
}
const { paused, playing, seeking, ended } = this._store;
paused.set(true);
playing.set(false);
seeking.set(false);
ended.set(true);
this._resetTracking();
}
_stopWaiting() {
this._fireWaiting.cancel();
this._store.waiting.set(false);
}
["fullscreen-change"](event) {
this._store.fullscreen.set(event.detail);
this._satisfyRequest("fullscreen", event);
}
["fullscreen-error"](event) {
this._satisfyRequest("fullscreen", event);
}
["picture-in-picture-change"](event) {
this._store.pictureInPicture.set(event.detail);
this._satisfyRequest("pip", event);
}
["picture-in-picture-error"](event) {
this._satisfyRequest("pip", event);
}
}
class MediaStoreSync extends ComponentController {
onAttach(el) {
effect(this._onLogLevelChange.bind(this));
effect(this._onAutoplayChange.bind(this));
effect(this._onPosterChange.bind(this));
effect(this._onLoopChange.bind(this));
effect(this._onControlsChange.bind(this));
effect(this._onCrossOriginChange.bind(this));
effect(this._onPlaysinlineChange.bind(this));
effect(this._onLiveToleranceChange.bind(this));
effect(this._onLiveChange.bind(this));
effect(this._onLiveEdgeChange.bind(this));
effect(this._onThumbnailsChange.bind(this));
}
_onLogLevelChange() {
this.$store.logLevel.set(this.$props.logLevel());
}
_onAutoplayChange() {
const autoplay = this.$props.autoplay();
this.$store.autoplay.set(autoplay);
this.dispatch("autoplay-change", { detail: autoplay });
}
_onLoopChange() {
const loop = this.$props.loop();
this.$store.loop.set(loop);
this.dispatch("loop-change", { detail: loop });
}
_onControlsChange() {
const controls = this.$props.controls();
this.$store.controls.set(controls);
this.dispatch("controls-change", { detail: controls });
}
_onPosterChange() {
const poster = this.$props.poster();
this.$store.poster.set(poster);
this.dispatch("poster-change", { detail: poster });
}
_onCrossOriginChange() {
this.$store.crossorigin.set(this.$props.crossorigin());
}
_onPlaysinlineChange() {
const playsinline = this.$props.playsinline();
this.$store.playsinline.set(playsinline);
this.dispatch("playsinline-change", { detail: playsinline });
}
_onLiveChange() {
this.dispatch("live-change", { detail: this.$store.live() });
}
_onLiveToleranceChange() {
this.$store.liveEdgeTolerance.set(this.$props.liveEdgeTolerance());
this.$store.minLiveDVRWindow.set(this.$props.minLiveDVRWindow());
}
_onLiveEdgeChange() {
this.dispatch("live-edge-change", { detail: this.$store.liveEdge() });
}
_onThumbnailsChange() {
this.$store.thumbnails.set(this.$props.thumbnails());
}
}
function preconnect(url, rel = "preconnect") {
const exists = document.querySelector(`link[href="${url}"]`);
if (!isNull(exists))
return true;
const link = document.createElement("link");
link.rel = rel;
link.href = url;
link.crossOrigin = "true";
document.head.append(link);
return true;
}
const pendingRequests = {};
function loadScript(src) {
if (pendingRequests[src])
return pendingRequests[src].promise;
const promise = deferredPromise(), exists = document.querySelector(`script[src="${src}"]`);
if (!isNull(exists)) {
promise.resolve();
return promise.promise;
}
const script = document.createElement("script");
script.src = src;
script.onload = () => {
promise.resolve();
delete pendingRequests[src];
};
script.onerror = () => {
promise.reject();
delete pendingRequests[src];
};
setTimeout(() => document.head.append(script), 0);
return promise.promise;
}
function getRequestCredentials(crossorigin) {
return crossorigin === "use-credentials" ? "include" : isString(crossorigin) ? "same-origin" : void 0;
}
function findActiveCue(time, cues) {
for (let i = 0, len = cues.length; i < len; i++) {
if (isCueActive(cues[i], time))
return cues[i];
}
return null;
}
function isCueActive(cue, time) {
return time >= cue.startTime && time < cue.endTime;
}
function onTrackChapterChange(tracks, currentTrack, onChange) {
const track = tracks.toArray().find((track2) => track2.kind === "chapters" && track2.mode === "showing");
if (track === currentTrack)
return;
if (!track) {
onChange(null);
return;
}
if (track.readyState == 2) {
onChange(track);
} else {
onChange(null);
track.addEventListener("load", () => onChange(track), { once: true });
}
}
class TextTrack extends EventsTarget {
static createId(track) {
return `id::${track.type}-${track.kind}-${track.src ?? track.label}`;
}
src;
content;
type;
encoding;
id = "";
label = "";
language = "";
kind;
default = false;
_canLoad = false;
_currentTime = 0;
_mode = "disabled";
_metadata = {};
_regions = [];
_cues = [];
_activeCues = [];
/* @internal */
[TEXT_TRACK_READY_STATE] = 0;
/* @internal */
[TEXT_TRACK_CROSSORIGIN];
/* @internal */
[TEXT_TRACK_ON_MODE_CHANGE] = null;
/* @internal */
[TEXT_TRACK_NATIVE] = null;
get metadata() {
return this._metadata;
}
get regions() {
return this._regions;
}
get cues() {
return this._cues;
}
get activeCues() {
return this._activeCues;
}
/**
* - 0: Not Loading
* - 1: Loading
* - 2: Ready
* - 3: Error
*/
get readyState() {
return this[TEXT_TRACK_READY_STATE];
}
get mode() {
return this._mode;
}
set mode(mode) {
this.setMode(mode);
}
constructor(init) {
super();
for (const prop of Object.keys(init))
this[prop] = init[prop];
if (!this.type)
this.type = "vtt";
if (init.content) {
import('media-captions').then(({ parseText, VTTCue, VTTRegion }) => {
if (init.type === "json") {
this._parseJSON(init.content, VTTCue, VTTRegion);
} else {
parseText(init.content, { type: init.type }).then(({ cues, regions }) => {
this._cues = cues;
this._regions = regions;
this._readyState();
});
}
});
} else if (!init.src)
this[TEXT_TRACK_READY_STATE] = 2;
if (isTrackCaptionKind(this) && !this.label) {
throw Error(`[vidstack]: captions text track created without label: \`${this.src}\``);
}
}
addCue(cue, trigger) {
let i = 0, length = this._cues.length;
for (i = 0; i < length; i++)
if (cue.endTime <= this._cues[i].startTime)
break;
if (i === length)
this._cues.push(cue);
else
this._cues.splice(i, 0, cue);
if (trigger?.type !== "cuechange") {
this[TEXT_TRACK_NATIVE]?.track.addCue(cue);
}
this.dispatchEvent(new DOMEvent("add-cue", { detail: cue, trigger }));
if (isCueActive(cue, this._currentTime)) {
this[TEXT_TRACK_UPDATE_ACTIVE_CUES](this._currentTime, trigger);
}
}
removeCue(cue, trigger) {
const index = this._cues.indexOf(cue);
if (index >= 0) {
const isActive = this._activeCues.includes(cue);
this._cues.splice(index, 1);
this[TEXT_TRACK_NATIVE]?.track.removeCue(cue);
this.dispatchEvent(new DOMEvent("remove-cue", { detail: cue, trigger }));
if (isActive) {
this[TEXT_TRACK_UPDATE_ACTIVE_CUES](this._currentTime, trigger);
}
}
}
setMode(mode, trigger) {
if (this._mode === mode)
return;
this._mode = mode;
if (mode === "disabled") {
this._activeCues = [];
this._activeCuesChanged();
} else if (this.readyState === 2) {
this[TEXT_TRACK_UPDATE_ACTIVE_CUES](this._currentTime, trigger);
} else {
this._load();
}
this.dispatchEvent(new DOMEvent("mode-change", { detail: this, trigger }));
this[TEXT_TRACK_ON_MODE_CHANGE]?.();
}
/* @internal */
[TEXT_TRACK_UPDATE_ACTIVE_CUES](currentTime, trigger) {
this._currentTime = currentTime;
if (this.mode === "disabled" || !this._cues.length)
return;
const activeCues = [];
for (let i = 0, length = this._cues.length; i < length; i++) {
const cue = this._cues[i];
if (isCueActive(cue, currentTime))
activeCues.push(cue);
}
let changed = activeCues.length !== this._activeCues.length;
if (!changed) {
for (let i = 0; i < activeCues.length; i++) {
if (!this._activeCues.includes(activeCues[i])) {
changed = true;
break;
}
}
}
this._activeCues = activeCues;
if (changed)
this._activeCuesChanged(trigger);
}
/* @internal */
[TEXT_TRACK_CAN_LOAD]() {
this._canLoad = true;
if (this._mode !== "disabled")
this._load();
}
async _load() {
if (!this._canLoad || !this.src || this[TEXT_TRACK_READY_STATE] > 0)
return;
this[TEXT_TRACK_READY_STATE] = 1;
this.dispatchEvent(new DOMEvent("load-start"));
try {
const { parseResponse, VTTCue, VTTRegion } = await import('media-captions'), crossorigin = this[TEXT_TRACK_CROSSORIGIN]?.();
const response = fetch(this.src, {
headers: this.type === "json" ? { "Content-Type": "application/json" } : void 0,
credentials: getRequestCredentials(crossorigin)
});
if (this.type === "json") {
this._parseJSON(await (await response).text(), VTTCue, VTTRegion);
} else {
const { errors, metadata, regions, cues } = await parseResponse(response, {
type: this.type,
encoding: this.encoding
});
if (errors[0]?.code === 0) {
throw errors[0];
} else {
this._metadata = metadata;
this._regions = regions;
this._cues = cues;
}
}
this._readyState();
} catch (error) {
this._errorState(error);
}
}
_readyState() {
this[TEXT_TRACK_READY_STATE] = 2;
if (!this.src || this.type !== "vtt") {
const nativeTrack = this[TEXT_TRACK_NATIVE]?.track;
if (nativeTrack)
for (const cue of this._cues)
nativeTrack.addCue(cue);
}
const loadEvent = new DOMEvent("load");
this[TEXT_TRACK_UPDATE_ACTIVE_CUES](this._currentTime, loadEvent);
this.dispatchEvent(loadEvent);
}
_errorState(error) {
this[TEXT_TRACK_READY_STATE] = 3;
this.dispatchEvent(new DOMEvent("error", { detail: error }));
}
_parseJSON(json, VTTCue, VTTRegion) {
try {
json = JSON.parse(json);
if (json.regions) {
this._regions = json.regions.map((json2) => Object.assign(new VTTRegion(), json2));
}
if (json.cues) {
this._cues = json.cues.filter((json2) => isNumber(json2.startTime) && isNumber(json2.endTime)).map((json2) => Object.assign(new VTTCue(0, 0, ""), json2));
}
} catch (error) {
{
console.error(`[vidstack] failed to parse JSON captions at: \`${this.src}\`
`, error);
}
this._errorState(error);
}
}
_activeCuesChanged(trigger) {
this.dispatchEvent(new DOMEvent("cue-change", { trigger }));
}
}
const captionRE = /captions|subtitles/;
function isTrackCaptionKind(track) {
return captionRE.test(track.kind);
}
class MediaRemoteControl {
constructor(_logger) {
this._logger = _logger;
}
_target = null;
_player = null;
_prevTrackIndex = -1;
/**
* Set the target from which to dispatch media requests events from. The events should bubble
* up from this target to the `<media-player>` element.
*
* @example
* ```ts
* const button = document.querySelector('button');
* remote.setTarget(button);
* ```
*/
setTarget(target) {
this._target = target;
this._logger?.setTarget(target);
}
/**
* Returns the current `<media-player>` element. This method will attempt to find the player by
* searching up from either the given `target` or default target set via `remote.setTarget`.
*
* @example
* ```ts
* const player = remote.getPlayer();
* ```
*/
getPlayer(target) {
if (this._player)
return this._player;
(target ?? this._target)?.dispatchEvent(
new DOMEvent("find-media-player", {
detail: (player) => void (this._player = player),
bubbles: true,
composed: true
})
);
return this._player;
}
/**
* Set the current `<media-player>` element so the remote can support toggle methods such as
* `togglePaused` as they rely on the current media state.
*/
setPlayer(player) {
this._player = player;
}
/**
* Dispatch a request to start the media loading process. This will only work if the media
* player has been initialized with a custom loading strategy `<media-player load="custom">`.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/loading#loading-strategies}
*/
startLoading(trigger) {
this._dispatchRequest("media-start-loading", trigger);
}
/**
* Dispatch a request to begin/resume media playback.
*/
play(trigger) {
this._dispatchRequest("media-play-request", trigger);
}
/**
* Dispatch a request to pause media playback.
*/
pause(trigger) {
this._dispatchRequest("media-pause-request", trigger);
}
/**
* Dispatch a request to set the media volume to mute (0).
*/
mute(trigger) {
this._dispatchRequest("media-mute-request", trigger);
}
/**
* Dispatch a request to unmute the media volume and set it back to it's previous state.
*/
unmute(trigger) {
this._dispatchRequest("media-unmute-request", trigger);
}
/**
* Dispatch a request to enter fullscreen.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/fullscreen#media-remote}
*/
enterFullscreen(target, trigger) {
this._dispatchRequest("media-enter-fullscreen-request", trigger, target);
}
/**
* Dispatch a request to exit fullscreen.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/fullscreen#media-remote}
*/
exitFullscreen(target, trigger) {
this._dispatchRequest("media-exit-fullscreen-request", trigger, target);
}
/**
* Dispatch a request to enter picture-in-picture mode.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/picture-in-picture#media-remote}
*/
enterPictureInPicture(trigger) {
this._dispatchRequest("media-enter-pip-request", trigger);
}
/**
* Dispatch a request to exit picture-in-picture mode.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/picture-in-picture#media-remote}
*/
exitPictureInPicture(trigger) {
this._dispatchRequest("media-exit-pip-request", trigger);
}
/**
* Notify the media player that a seeking process is happening and to seek to the given `time`.
*/
seeking(time, trigger) {
this._dispatchRequest("media-seeking-request", trigger, time);
}
/**
* Notify the media player that a seeking operation has completed and to seek to the given `time`.
* This is generally called after a series of `remote.seeking()` calls.
*/
seek(time, trigger) {
this._dispatchRequest("media-seek-request", trigger, time);
}
seekToLiveEdge(trigger) {
this._dispatchRequest("media-live-edge-request", trigger);
}
/**
* Dispatch a request to update the media volume to the given `volume` level which is a value
* between 0 and 1.
*
* @example
* ```ts
* remote.changeVolume(0); // 0%
* remote.changeVolume(0.05); // 5%
* remote.changeVolume(0.5); // 50%
* remote.changeVolume(0.75); // 70%
* remote.changeVolume(1); // 100%
* ```
*/
changeVolume(volume, trigger) {
this._dispatchRequest("media-volume-change-request", trigger, Math.max(0, Math.min(1, volume)));
}
/**
* Dispatch a request to change the current audio track.
*
* @example
* ```ts
* remote.changeAudioTrack(1); // track at index 1
* ```
*/
changeAudioTrack(index, trigger) {
this._dispatchRequest("media-audio-track-change-request", trigger, index);
}
/**
* Dispatch a request to change the video quality. The special value `-1` represents auto quality
* selection.
*
* @example
* ```ts
* remote.changeQuality(-1); // auto
* remote.changeQuality(1); // quality at index 1
* ```
*/
changeQuality(index, trigger) {
this._dispatchRequest("media-quality-change-request", trigger, index);
}
/**
* Dispatch a request to change the mode of the text track at the given index.
*
* @example
* ```ts
* remote.changeTextTrackMode(1, 'showing'); // track at index 1
* ```
*/
changeTextTrackMode(index, mode, trigger) {
this._dispatchRequest("media-text-track-change-request", trigger, {
index,
mode
});
}
/**
* Dispatch a request to change the media playback rate.
*
* @example
* ```ts
* remote.changePlaybackRate(0.5); // Half the normal speed
* remote.changePlaybackRate(1); // Normal speed
* remote.changePlaybackRate(1.5); // 50% faster than normal
* remote.changePlaybackRate(2); // Double the normal speed
* ```
*/
changePlaybackRate(rate, trigger) {
this._dispatchRequest("media-rate-change-request", trigger, rate);
}
/**
* Dispatch a request to resume user idle tracking. Refer to {@link MediaRemoteControl.pauseUserIdle}
* for more information.
*/
resumeUserIdle(trigger) {
this._dispatchRequest("media-resume-user-idle-request", trigger);
}
/**
* Dispatch a request to pause user idle tracking. Pausing tracking will result in the `user-idle`
* attribute and state being `false` until `remote.resumeUserIdle()` is called. This method
* is generally used when building custom controls and you'd like to prevent the UI from
* dissapearing.
*
* @example
* ```ts
* // Prevent user idling while menu is being interacted with.
* function onSettingsOpen() {
* remote.pauseUserIdle();
* }
*
* function onSettingsClose() {
* remote.resumeUserIdle();
* }
* ```
*/
pauseUserIdle(trigger) {
this._dispatchRequest("media-pause-user-idle-request", trigger);
}
/**
* Dispatch a request to toggle the media playback state.
*/
togglePaused(trigger) {
const player = this.getPlayer(trigger?.target);
if (!player) {
this._noPlayerWarning(this.togglePaused.name);
return;
}
if (player.state.paused)
this.play(trigger);
else
this.pause(trigger);
}
/**
* Dispatch a request to toggle the user idle state.
*/
toggleUserIdle(trigger) {
const player = this.getPlayer(trigger?.target);
if (!player) {
this._noPlayerWarning(this.toggleUserIdle.name);
return;
}
player.user.idle(!player.user.idling, 0, trigger);
}
/**
* Dispatch a request to toggle the media muted state.
*/
toggleMuted(trigger) {
const player = this.getPlayer(trigger?.target);
if (!player) {
this._noPlayerWarning(this.toggleMuted.name);
return;
}
if (player.state.muted)
this.unmute(trigger);
else
this.mute(trigger);
}
/**
* Dispatch a request to toggle the media fullscreen state.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/fullscreen#media-remote}
*/
toggleFullscreen(target, trigger) {
const player = this.getPlayer(trigger?.target);
if (!player) {
this._noPlayerWarning(this.toggleFullscreen.name);
return;
}
if (player.state.fullscreen)
this.exitFullscreen(target, trigger);
else
this.enterFullscreen(target, trigger);
}
/**
* Dispatch a request to toggle the media picture-in-picture mode.
*
* @docs {@link https://www.vidstack.io/docs/player/core-concepts/picture-in-picture#media-remote}
*/
togglePictureInPicture(trigger) {
const player = this.getPlayer(trigger?.target);
if (!player) {
this._noPlayerWarning(this.togglePictureInPicture.name);
return;
}
if (player.state.pictureInPicture)
this.exitPictureInPicture(trigger);
else
this.enterPictureInPicture(trigger);
}
/**
* Dispatch a request to toggle the current captions mode.
*/
toggleCaptions(trigger) {
const player = this.getPlayer(trigger?.target);
if (!player) {
this._noPlayerWarning(this.toggleCaptions.name);
return;
}
const tracks = player.state.textTracks, track = player.state.textTrack;
if (track) {
const index = tracks.indexOf(track);
this.changeTextTrackMode(index, "disabled", trigger);
this._prevTrackIndex = index;
} else {
let index = this._prevTrackIndex;
if (!tracks[index] || !isTrackCaptionKind(tracks[index])) {
index = -1;
}
if (index === -1) {
index = tracks.findIndex((track2) => isTrackCaptionKind(track2) && track2.default);
}
if (index === -1) {
index = tracks.findIndex((track2) => isTrackCaptionKind(track2));
}
if (index >= 0)
this.changeTextTrackMode(index, "showing", trigger);
this._prevTrackIndex = -1;
}
}
_dispatchRequest(type, trigger, detail) {
const request = new DOMEvent(type, {
bubbles: true,
composed: true,
detail,
trigger
});
const shouldUsePlayer = trigger?.target && (trigger.target === document || trigger.target === window || trigger.target === document.body || this._player && !this._player.contains(trigger.target));
const target = shouldUsePlayer ? this._target ?? this.getPlayer() : trigger?.target ?? this._target;
{
this._logger?.infoGroup(`\u{1F4E8} dispatching \`${type}\``).labelledLog("Target", target).labelledLog("Player", this._player).labelledLog("Request Event", request).labelledLog("Trigger Event", trigger).dispatch();
}
target?.dispatchEvent(request);
}
_noPlayerWarning(method) {
{
console.warn(
`[vidstack] attempted to call \`MediaRemoteControl.${method}\`() that requires player but failed because remote could not find a parent player element from target`
);
}
}
}
class ThumbnailsLoader extends ComponentController {
_media;
onConnect() {
this._media = useMedia();
effect(this._onLoadCues.bind(this));
}
_onLoadCues() {
const { canLoad, thumbnailCues } = this._media.$store;
if (!canLoad())
return;
const controller = new AbortController(), { crossorigin, thumbnails } = this._media.$store;
const src = thumbnails();
if (!src)
return;
import('media-captions').then(({ parseResponse }) => {
parseResponse(
fetch(src, {
signal: controller.signal,
credentials: getRequestCredentials(crossorigin())
})
).then(({ cues }) => thumbnailCues.set(cues)).catch(noop);
});
return () => {
controller.abort();
thumbnailCues.set([]);
};
}
}
class AudioTrackList extends SelectList {
getById(id) {
if (id === "")
return null;
return this._items.find((track) => track.id === id) ?? null;
}
}
class NativeTextRenderer {
priority = 0;
_display = true;
_video = null;
_track = null;
_tracks = /* @__PURE__ */ new Set();
canRender() {
return true;
}
attach(video) {
this._video = video;
video.textTracks.onchange = this._onChange.bind(this);
}
addTrack(track) {
this._tracks.add(track);
this._attachTrack(track);
}
removeTrack(track) {
track[TEXT_TRACK_NATIVE]?.remove?.();
track[TEXT_TRACK_NATIVE] = null;
this._tracks.delete(track);
}
changeTrack(track) {
const current = track?.[TEXT_TRACK_NATIVE];
if (current && current.track.mode !== "showing") {
current.track.mode = "showing";
}
this._track = track;
}
setDisplay(display) {
this._display = display;
this._onChange();
}
detach() {
if (this._video)
this._video.textTracks.onchange = null;
for (const track of this._tracks)
this.removeTrack(track);
this._tracks.clear();
this._video = null;
this._track = null;
}
_attachTrack(track) {
if (!this._video)
return;
const el = track[TEXT_TRACK_NATIVE] ??= this._createTrackElement(track);
if (el instanceof HTMLElement) {
this._video.append(el);
el.track.mode = el.default ? "showing" : "hidden";
}
}
_createTrackElement(track) {
const el = document.createElement("track"), isDefault = track.default || track.mode === "showing", isSupported = track.src && track.type === "vtt";
el.id = track.id;
el.src = isSupported ? track.src : "https://cdn.jsdelivr.net/npm/vidstack@0.6.12/empty.vtt";
el.label = track.label;
el.kind = track.kind;
el.default = isDefault;
track.language && (el.srclang = track.language);
if (isDefault && !isSupported) {
this._copyCues(track, el.track);
}
return el;
}
_copyCues(track, native) {
if (track.src && track.type === "vtt" || native.cues?.length)
return;
for (const cue of track.cues)
native.addCue(cue);
}
_onChange(event) {
for (const track of this._tracks) {
const nativeTrack = track[TEXT_TRACK_NATIVE]?.track;
if (!nativeTrack)
continue;
if (!this._display) {
nativeTrack.mode = "disabled";
continue;
}
const isShowing = nativeTrack.mode === "showing";
if (isShowing)
this._copyCues(track, nativeTrack);
track.setMode(isShowing ? "showing" : "disabled", event);
}
}
}
class TextRenderers {
constructor(_media) {
this._media = _media;
const textTracks = _media.textTracks;
this._textTracks = textTracks;
effect(this._watchControls.bind(this));
onDispose(this._detach.bind(this));
listenEvent(textTracks, "add", this._onAddTrack.bind(this));
listenEvent(textTracks, "remove", this._onRemoveTrack.bind(this));
listenEvent(textTracks, "mode-change", this._update.bind(this));
}
_video = null;
_textTracks;
_renderers = [];
_nativeDisplay = false;
_nativeRenderer = null;
_customRenderer = null;
_watchControls() {
const { $store, $iosControls } = this._media;
this._nativeDisplay = $store.controls() || $iosControls();
this._update();
}
add(renderer) {
this._renderers.push(renderer);
this._update();
}
remove(renderer) {
renderer.detach();
this._renderers.splice(this._renderers.indexOf(renderer), 1);
this._update();
}
/* @internal */
[ATTACH_VIDEO](video) {
requestAnimationFrame(() => {
this._video = video;
if (video) {
this._nativeRenderer = new NativeTextRenderer();
this._nativeRenderer.attach(video);
for (const track of this._textTracks)
this._addNativeTrack(track);
}
this._update();
});
}
_addNativeTrack(track) {
if (!isTrackCaptionKind(track))
return;
this._nativeRenderer?.addTrack(track);
}
_removeNativeTrack(track) {
if (!isTrackCaptionKind(track))
return;
this._nativeRenderer?.removeTrack(track);
}
_onAddTrack(event) {
this._addNativeTrack(event.detail);
}
_onRemoveTrack(event) {
this._removeNativeTrack(event.detail);
}
_update() {
if (!this._video) {
this._detach();
return;
}
const currentTrack = this._textTracks.selected;
if (this._nativeDisplay || currentTrack?.[TEXT_TRACK_NATIVE_HLS]) {
this._customRenderer?.changeTrack(null);
this._nativeRenderer.setDisplay(true);
this._nativeRenderer.changeTrack(currentTrack);
return;
}
this._nativeRenderer.setDisplay(false);
this._nativeRenderer.changeTrack(null);
if (!currentTrack) {
this._customRenderer?.changeTrack(null);
return;
}
const customRenderer = this._renderers.sort((a, b) => a.priority - b.priority).find((loader) => loader.canRender(currentTrack));
if (this._customRenderer !== customRenderer) {
this._customRenderer?.detach();
customRenderer?.attach(this._video);
this._customRenderer = customRenderer ?? null;
}
customRenderer?.changeTrack(currentTrack);
}
_detach() {
this._nativeRenderer?.detach();
this._nativeRenderer = null;
this._customRenderer?.detach();
this._customRenderer = null;
}
}
class TextTrackList extends List {
_canLoad = false;
_defaults = {};
/** @internal */
[TEXT_TRACK_CROSSORIGIN];
get selected() {
const track = this._items.find((t) => t.mode === "showing" && isTrackCaptionKind(t));
return track ?? null;
}
add(init, trigger) {
const isTrack = init instanceof TextTrack, track = isTrack ? init : new TextTrack(init);
if (this._defaults[init.kind] && init.default)
delete init.default;
track.addEventListener("mode-change", this._onTrackModeChangeBind);
this[LIST_ADD](track, trigger);
track[TEXT_TRACK_CROSSORIGIN] = this[TEXT_TRACK_CROSSORIGIN];
if (this._canLoad)
track[TEXT_TRACK_CAN_LOAD]();
if (init.default) {
this._defaults[init.kind] = track;
track.mode = "showing";
}
return this;
}
remove(track, trigger) {
if (!this._items.includes(track))
return;
if (track === this._defaults[track.kind])
delete this._defaults[track.kind];
track.mode = "disabled";
track[TEXT_TRACK_ON_MODE_CHANGE] = null;
track.removeEventListener("mode-change", this._onTrackModeChangeBind);
this[LIST_REMOVE](track, trigger);
return this;
}
clear(trigger) {
for (const track of this._items)
this.remove(track, trigger);
return this;
}
getById(id) {
return this._items.find((track) => track.id === id) ?? null;
}
getByKind(kind) {
const kinds = Array.isArray(kind) ? kind : [kind];
return this._items.filter((track) => kinds.includes(track.kind));
}
/* @internal */
[TEXT_TRACK_CAN_LOAD]() {
if (this._canLoad)
return;
for (const track of this._items)
track[TEXT_TRACK_CAN_LOAD]();
this._canLoad = true;
}
_onTrackModeChangeBind = this._onTrackModeChange.bind(this);
_onTrackModeChange(event) {
const track = event.detail;
if (track.mode === "showing") {
const kinds = isTrackCaptionKind(track) ? ["captions", "subtitles"] : [track.kind];
for (const t of this._items) {
if (t.mode === "showing" && t != track && kinds.includes(t.kind)) {
t.mode = "disabled";
}
}
}
this.dispatchEvent(
new DOMEvent("mode-change", {
detail: event.detail,
trigger: event
})
);
}
}
let warned = /* @__PURE__ */ new Set() ;
class SourceSelection {
constructor(_domSources, _media, _loader) {
this._domSources = _domSources;
this._media = _media;
this._loader = _loader;
const HLS_LOADER = new HLSProviderLoader(), VIDEO_LOADER = new VideoProviderLoader(), AUDIO_LOADER = new AudioProviderLoader();
this._loaders = computed(() => {
return _media.$props.preferNativeHLS() ? [VIDEO_LOADER, AUDIO_LOADER, HLS_LOADER] : [HLS_LOADER, VIDEO_LOADER, AUDIO_LOADER];
});
effect(this._onSourcesChange.bind(this));
effect(this._onSourceChange.bind(this));
effect(this._onPreconnect.bind(this));
effect(this._onLoadSource.bind(this));
}
_loaders;
_onSourcesChange() {
this._media.delegate._dispatch("sources-change", {
detail: [...normalizeSrc(this._media.$props.src()), ...this._domSources()]
});
}
_onSourceChange() {
const { $store } = this._media;
const sources = $store.sources(), currentSource = peek($store.source), newSource = this._findNewSource(currentSource, sources), noMatch = sources[0]?.src && !newSource.src && !newSource.type;
if (noMatch && !warned.has(newSource.src) && !peek(this._loader)) {
const source = sources[0];
console.warn(
`[vidstack] could not find a loader for any of the given media sources, consider providing \`type\`:
<media-outlet>
<source src="${source.src}" type="video/mp4" />
</media-outlet>"
Falling back to fetching source headers...`
);
warned.add(newSource.src);
}
if (noMatch) {
const { crossorigin } = $store, credentials = getRequestCredentials(crossorigin()), abort = new AbortController();
Promise.all(
sources.map(
(source) => isString(source.src) && source.type === "?" ? fetch(source.src, {
method: "HEAD",
credentials,
signal: abort.signal
}).then((res) => {
source.type = res.headers.get("content-type") || "??";
return source;
}).catch(() => source) : source
)
).then((sources2) => {
if (abort.signal.aborted)
return;
this._findNewSource(peek($store.source), sources2);
tick();
});
return () => abort.abort();
}
tick();
}
_findNewSource(currentSource, sources) {
let newSource = { src: "", type: "" }, newLoader = null;
for (const src of sources) {
const loader = peek(this._loaders).find((loader2) => loader2.canPlay(src));
if (loader) {
newSource = src;
newLoader = loader;
}
}
this._notifySourceChange(currentSource, newSource, newLoader);
this._notifyLoaderChange(peek(this._loader), newLoader);
return newSource;
}
_notifySourceChange(currentSource, newSource, newLoader) {
if (newSource.src === currentSource.src && newSource.type === currentSource.type)
return;
this._media.delegate._dispatch("source-change", { detail: newSource });
this._media.delegate._dispatch("media-type-change", {
detail: newLoader?.mediaType(newSource) || "unknown"
});
}
_notifyLoaderChange(currentLoader, newLoader) {
if (newLoader === currentLoader)
return;
this._media.delegate._dispatch("provider-change", { detail: null });
newLoader && peek(() => newLoader.preconnect?.(this._media));
this._loader.set(newLoader);
this._media.delegate._dispatch("provider-loader-change", { detail: newLoader });
}
_onPreconnect() {
const provider = this._media.$provider();
if (!provider)
return;
if (this._media.$store.canLoad()) {
peek(
() => provider.setup({
...this._media,
player: this._media.player
})
);
return;
}
peek(() => provider.preconnect?.(this._media));
}
_onLoadSource() {
const provider = this._media.$provider(), source = this._media.$store.source();
if (this._media.$store.canLoad()) {
peek(() => provider?.loadSource(source, peek(this._media.$store.preload)));
return;
}
try {
isString(source.src) && preconnect(new URL(source.src).origin, "preconnect");
} catch (e) {
{
this._media.logger?.infoGroup(`Failed to preconnect to source: ${source.src}`).labelledLog("Error", e).dispatch();
}
}
}
}
function normalizeSrc(src) {
return (isArray(src) ? src : [!isString(src) && "src" in src ? src : { src }]).map(
({ src: src2, type }) => ({
src: src2,
type: type ?? (!isString(src2) || src2.startsWith("blob:") ? "video/object" : "?")
})
);
}
class Tracks {
constructor(_domTracks, _media) {
this._domTracks = _domTracks;
this._media = _media;
effect(this._onTracksChange.bind(this));
}
_prevTracks = [];
_onTracksChange() {
const newTracks = [...this._media.$props.textTracks(), ...this._domTracks()];
for (const oldTrack of this._prevTracks) {
if (!newTracks.some((t) => t.id === oldTrack.id)) {
const track = oldTrack.id && this._media.textTracks.getById(oldTrack.id);
if (track)
this._media.textTracks.remove(track);
}
}
for (const newTrack of newTracks) {
const id = newTrack.id || TextTrack.createId(newTrack);
if (!this._media.textTracks.getById(id)) {
newTrack.id = id;
this._media.textTracks.add(newTrack);
}
}
this._prevTracks = newTracks;
}
}
class Outlet extends Component {
static el = defineElement({
tagName: "media-outlet"
});
_media;
_domSources = signal([]);
_domTracks = signal([]);
_loader = signal(null);
constructor(instance) {
super(instance);
this._media = useMedia();
new SourceSelection(this._domSources, this._media, this._loader);
new Tracks(this._domTracks, this._media);
}
onAttach(el) {
el.setAttribute("keep-alive", "");
}
onConnect(el) {
const resize = new ResizeObserver(animationFrameThrottle(this._onResize.bind(this)));
resize.observe(el);
const mutation = new MutationObserver(this._onMutation.bind(this));
mutation.observe(el, { attributes: true, childList: true });
if (IS_SAFARI) {
listenEvent(el, "touchstart", (e) => e.preventDefault(), { passive: false });
}
scopedRaf(() => {
this._onResize();
this._onMutation();
});
return () => {
resize.disconnect();
mutation.disconnect();
};
}
onDestroy() {
this._media.$store.currentTime.set(0);
}
_onResize() {
const player = this._media.player, width = this.el.offsetWidth, height = this.el.offsetHeight;
if (!player)
return;
player.$store.mediaWidth.set(width);
player.$store.mediaHeight.set(height);
setStyle(player, "--media-width", width + "px");
setStyle(player, "--media-height", height + "px");
}
_onMutation() {
const sources = [], tracks = [], children = this.el.children;
for (const el of children) {
if (el instanceof HTMLSourceElement) {
sources.push({
src: el.src,
type: el.type
});
} else if (el instanceof HTMLTrackElement) {
tracks.push({
id: el.id,
src: el.src,
kind: el.track.kind,
language: el.srclang,
label: el.label,
default: el.default,
type: el.getAttribute("data-type")
});
}
}
this._domSources.set(sources);
this._domTracks.set(tracks);
tick();
}
render() {
let currentProvider;
onDispose(() => currentProvider?.destroy?.());
return () => {
currentProvider?.destroy();
const loader = this._loader();
if (!loader)
return null;
const el = loader.render(this._media.$store);
{
peek(() => {
loader.load(this._media).then((provider) => {
if (peek(this._loader) !== loader)
return;
this._media.delegate._dispatch("provider-change", {
detail: provider
});
currentProvider = provider;
});
});
}
return el;
};
}
}
class LibASSTextRenderer {
constructor(loader, config) {
this.loader = loader;
this.config = config;
}
priority = 1;
_instance = null;
_track = null;
_typeRE = /(ssa|ass)$/;
canRender(track) {
return !!track.src && (isString(track.type) && this._typeRE.test(track.type) || this._typeRE.test(track.src));
}
attach(video) {
this.loader().then(async (mod) => {
this._instance = new mod.default({
...this.config,
video,
subUrl: this._track?.src || ""
});
listenEvent(this._instance, "ready", () => {
const canvas = this._instance?._canvas;
if (canvas)
canvas.style.pointerEvents = "none";
});
listenEvent(this._instance, "error", (event) => {
if (this._track) {
this._track[TEXT_TRACK_READY_STATE] = 3;
this._track.dispatchEvent(
new DOMEvent("error", {
trigger: event,
detail: event.error
})
);
}
});
});
}
changeTrack(track) {
if (!track || track.readyState === 3) {
this._freeTrack();
} else if (this._track !== track) {
this._instance?.setTrackByUrl(track.src);
this._track = track;
}
}
detach() {
this._freeTrack();
}
_freeTrack() {
this._instance?.freeTrack();
this._track = null;
}
}
class ARIAKeyShortcuts extends ComponentController {
constructor(instance, _shortcut) {
super(instance);
this._shortcut = _shortcut;
}
onAttach(el) {
const { $props, ariaKeys } = useMedia(), keys = el.getAttribute("aria-keyshortcuts");
if (keys) {
ariaKeys[this._shortcut] = keys;
{
onDispose(() => {
delete ariaKeys[this._shortcut];
});
}
return;
}
const shortcuts = $props.keyShortcuts()[this._shortcut];
if (shortcuts)
el.setAttribute("aria-keyshortcuts", shortcuts);
}
}
export { canUsePictureInPicture as $, ARIAKeyShortcuts as A, IS_IPHONE as B, MEDIA_ATTRIBUTES as C, canFullscreen as D, MediaRemoteControl as E, MediaRequestContext as F, ENABLE_AUTO_QUALITY as G, TextTrack as H, IS_SAFARI as I, TEXT_TRACK_READY_STATE as J, TEXT_TRACK_ON_MODE_CHANGE as K, LIST_SELECT as L, MediaStoreFactory as M, SET_AUTO_QUALITY as N, Outlet as O, LIST_ADD as P, IS_CHROME as Q, coerceToError as R, ScreenOrientationController as S, TextTrackList as T, loadScript as U, VideoQualityList as V, isHLSSupported as W, LIST_REMOVE as X, canPlayHLSNatively as Y, TEXT_TRACK_NATIVE as Z, TEXT_TRACK_NATIVE_HLS as _, setAttributeIfEmpty as a, canUseVideoPresentation as a0, ATTACH_VIDEO as a1, MediaUserController as a2, MEDIA_KEY_SHORTCUTS as a3, List as a4, FullscreenController as a5, softResetMediaStore as a6, TimeRange as a7, getTimeRangesStart as a8, getTimeRangesEnd as a9, LibASSTextRenderer as aa, setARIALabel as b, findActiveCue as c, canChangeVolume as d, onTrackChapterChange as e, functionThrottle as f, isElementParent as g, isCueActive as h, isTrackCaptionKind as i, MediaStoreSync as j, AudioTrackList as k, TEXT_TRACK_CROSSORIGIN as l, mediaPlayerProps as m, TextRenderers as n, onPress as o, preconnect as p, mediaContext as q, MediaKeyboardController as r, scopedRaf as s, ThumbnailsLoader as t, useMedia as u, MediaEventsLogger as v, MediaStateManager as w, MediaRequestManager as x, MediaPlayerDelegate as y, MediaLoadController as z };