@vime/core
Version:
Customizable, extensible, accessible and framework agnostic media player.
1,467 lines (1,429 loc) • 319 kB
JavaScript
import { getElement, getRenderingRef, writeTask, h, attachShadow, Host, createEvent, Fragment, proxyCustomElement } from '@stencil/core/internal/client';
export { setAssetPath, setPlatformOptions } from '@stencil/core/internal/client';
/**
* Listen to an event on the given DOM node. Returns a callback to remove the event listener.
*/
function listen(node, event, handler, options) {
node.addEventListener(event, handler, options);
return () => node.removeEventListener(event, handler, options);
}
function fireEventAndRetry(el, event, onFail, interval = 300, maxRetries = 10) {
let timeout;
let attempt = 0;
let found = false;
function retry() {
if (found)
return;
timeout = setTimeout(() => {
if (attempt === maxRetries) {
onFail === null || onFail === void 0 ? void 0 : onFail();
return;
}
el.dispatchEvent(event);
attempt += 1;
retry();
}, interval);
}
retry();
return () => {
window.clearTimeout(timeout);
found = true;
};
}
const isColliding = (a, b, translateAx = 0, translateAy = 0, translateBx = 0, translateBy = 0) => {
const aRect = a.getBoundingClientRect();
const bRect = b.getBoundingClientRect();
return (aRect.left + translateAx < bRect.right + translateBx &&
aRect.right + translateAx > bRect.left + translateBx &&
aRect.top + translateAy < bRect.bottom + translateBy &&
aRect.bottom + translateAy > bRect.top + translateBy);
};
/**
* No-operation (noop).
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const noop = (..._) => {
// ...
};
/**
* Checks if `value` is `null`.
*
* @param value - The value to check.
*/
const isNull = (value) => value === null;
/**
* Checks if `value` is `undefined`.
*
* @param value - The value to check.
*/
const isUndefined = (value) => typeof value === 'undefined';
/**
* Checks if `value` is `null` or `undefined`.
*
* @param value - The value to check.
*/
const isNil = (value) => isNull(value) || isUndefined(value);
/**
* Returns the constructor of the given `value`.
*
* @param value - The value to return the constructor of.
*/
const getConstructor = (value) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
!isNil(value) ? value.constructor : undefined;
/**
* Checks if `value` is classified as a `Object` object.
*
* @param value - The value to check.
*/
const isObject = (value) => getConstructor(value) === Object;
/**
* Checks if `value` is classified as a `Number` object.
*
* @param value - The value to check.
*/
const isNumber = (value) => getConstructor(value) === Number && !Number.isNaN(value);
/**
* Checks if `value` is classified as a `String` object.
*
* @param value - The value to check.
*/
const isString = (value) => getConstructor(value) === String;
/**
* Checks if `value` is classified as a `Boolean` object.
*
* @param value - The value to check.
*/
const isBoolean = (value) => getConstructor(value) === Boolean;
/**
* Checks if `value` is classified as a `Function` object.
*
* @param value - The value to check.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
const isFunction = (value) => getConstructor(value) === Function;
/**
* Checks if `value` is classified as an `Array` object.
*
* @param value - The value to check.
*/
const isArray = (value) => Array.isArray(value);
/**
* Checks if `value` is an instanceof the given `constructor`.
*
* @param value - The value to check.
* @param constructor - The constructor to check against.
*/
const isInstanceOf = (value, constructor) => Boolean(value && constructor && value instanceof constructor);
/**
* Creates an empty Promise and defers resolving/rejecting it.
*/
const deferredPromise = () => {
let resolve = noop;
let reject = noop;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
function wrapStencilHook(component, lifecycle, hook) {
const prevHook = component[lifecycle];
component[lifecycle] = function () {
hook();
return prevHook ? prevHook.call(component) : undefined;
};
}
function createStencilHook(component, onConnect, onDisconnect) {
let hasLoaded = false;
if (!isUndefined(onConnect)) {
wrapStencilHook(component, 'componentWillLoad', () => {
onConnect();
hasLoaded = true;
});
wrapStencilHook(component, 'connectedCallback', () => {
if (hasLoaded)
onConnect();
});
}
if (!isUndefined(onDisconnect)) {
wrapStencilHook(component, 'disconnectedCallback', () => {
onDisconnect();
});
}
}
const FIND_PLAYER_EVENT = 'vmFindPlayer';
function withFindPlayer(player) {
const el = getElement(player);
let off;
createStencilHook(player, () => {
off = listen(el, FIND_PLAYER_EVENT, (event) => {
event.stopPropagation();
event.detail(el);
});
}, () => {
off === null || off === void 0 ? void 0 : off();
});
}
/**
* Finds the closest ancestor player element by firing the `vmFindPlayer` event, and waiting
* for the player to catch it. This function retries finding the player (`maxRetries`) until it
* gives up and fails.
*
* @param ref - A HTMLElement that is within the player's subtree.
* @param interval - The length of the timeout before trying again in milliseconds.
* @param maxRetries - The number of times to retry firing the event.
*/
const findPlayer = (ref, interval = 300, maxRetries = 10) => {
const el = (isInstanceOf(ref, HTMLElement)
? ref
: getElement(ref));
const search = deferredPromise();
// eslint-disable-next-line prefer-const
let stopFiring;
const event = new CustomEvent(FIND_PLAYER_EVENT, {
bubbles: true,
composed: true,
detail: player => {
search.resolve(player);
stopFiring();
},
});
stopFiring = fireEventAndRetry(el, event, () => {
search.reject(`Could not find player for ${el.nodeName}`);
}, interval, maxRetries);
return search.promise;
};
var MediaType;
(function (MediaType) {
MediaType["Audio"] = "audio";
MediaType["Video"] = "video";
})(MediaType || (MediaType = {}));
const STATE_CHANGE_EVENT = 'vmStateChange';
/**
* Creates a dispatcher on the given `ref`, to send updates to the closest ancestor player via
* the custom `vmStateChange` event.
*
* @param ref An element to dispatch the state change events from.
*/
const createDispatcher = (ref) => (prop, value) => {
const el = isInstanceOf(ref, HTMLElement) ? ref : getElement(ref);
const event = new CustomEvent(STATE_CHANGE_EVENT, {
bubbles: true,
composed: true,
detail: { by: el, prop, value },
});
el.dispatchEvent(event);
};
const en = {
play: 'Play',
pause: 'Pause',
playback: 'Playback',
scrubber: 'Scrubber',
scrubberLabel: '{currentTime} of {duration}',
played: 'Played',
duration: 'Duration',
buffered: 'Buffered',
close: 'Close',
currentTime: 'Current time',
live: 'LIVE',
volume: 'Volume',
mute: 'Mute',
unmute: 'Unmute',
audio: 'Audio',
default: 'Default',
captions: 'Captions',
subtitlesOrCc: 'Subtitles/CC',
enableCaptions: 'Enable subtitles/captions',
disableCaptions: 'Disable subtitles/captions',
auto: 'Auto',
fullscreen: 'Fullscreen',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
settings: 'Settings',
seekForward: 'Seek forward',
seekBackward: 'Seek backward',
seekTotal: 'Seek total',
normal: 'Normal',
none: 'None',
playbackRate: 'Playback Rate',
playbackQuality: 'Playback Quality',
loop: 'Loop',
disabled: 'Disabled',
off: 'Off',
enabled: 'Enabled',
pip: 'Picture-in-Picture',
enterPiP: 'Miniplayer',
exitPiP: 'Expand',
};
const initialState = {
theme: undefined,
paused: true,
playing: false,
duration: -1,
currentProvider: undefined,
mediaTitle: undefined,
currentSrc: undefined,
currentPoster: undefined,
textTracks: [],
currentTextTrack: -1,
audioTracks: [],
currentAudioTrack: -1,
isTextTrackVisible: true,
shouldRenderNativeTextTracks: true,
icons: 'vime',
currentTime: 0,
autoplay: false,
ready: false,
playbackReady: false,
loop: false,
muted: false,
buffered: 0,
playbackRate: 1,
playbackRates: [1],
playbackQuality: undefined,
playbackQualities: [],
seeking: false,
debug: false,
playbackStarted: false,
playbackEnded: false,
buffering: false,
controls: false,
isControlsActive: false,
volume: 50,
isFullscreenActive: false,
aspectRatio: '16:9',
viewType: undefined,
isAudioView: false,
isVideoView: false,
mediaType: undefined,
isAudio: false,
isVideo: false,
isMobile: false,
isTouch: false,
isSettingsActive: false,
isLive: false,
isPiPActive: false,
autopause: true,
playsinline: false,
language: 'en',
languages: ['en'],
translations: { en },
i18n: en,
};
const writableProps = new Set([
'autoplay',
'autopause',
'aspectRatio',
'controls',
'theme',
'debug',
'paused',
'currentTime',
'language',
'loop',
'translations',
'playbackQuality',
'muted',
'playbackRate',
'playsinline',
'volume',
'isSettingsActive',
'isControlsActive',
'shouldRenderNativeTextTracks',
]);
const isWritableProp = (prop) => writableProps.has(prop);
/**
* Player properties that should be reset when the media is changed.
*/
const resetableProps = new Set([
'paused',
'currentTime',
'duration',
'buffered',
'seeking',
'playing',
'buffering',
'playbackReady',
'textTracks',
'currentTextTrack',
'audioTracks',
'currentAudioTrack',
'mediaTitle',
'currentSrc',
'currentPoster',
'playbackRate',
'playbackRates',
'playbackStarted',
'playbackEnded',
'playbackQuality',
'playbackQualities',
'mediaType',
]);
const shouldPropResetOnMediaChange = (prop) => resetableProps.has(prop);
var ViewType;
(function (ViewType) {
ViewType["Audio"] = "audio";
ViewType["Video"] = "video";
})(ViewType || (ViewType = {}));
class Disposal {
constructor(dispose = []) {
this.dispose = dispose;
}
add(callback) {
this.dispose.push(callback);
}
empty() {
this.dispose.forEach(fn => fn());
this.dispose = [];
}
}
var __awaiter$y = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const PLAYER_KEY = Symbol('vmPlayerKey');
const COMPONENT_NAME_KEY = Symbol('vmNameKey');
const REGISTRY_KEY = Symbol('vmRegistryKey');
const REGISTRATION_KEY = Symbol('vmRegistrationKey');
const REGISTER_COMPONENT_EVENT = 'vmComponentRegister';
const COMPONENT_REGISTERED_EVENT = 'vmComponentRegistered';
const COMPONENT_DEREGISTERED_EVENT = 'vmComponentDeregistered';
const getRegistrant = (ref) => isInstanceOf(ref, HTMLElement)
? ref
: getElement(ref);
/**
* Handles registering/deregistering the given `component` in the player registry. All registries
* are bound per player subtree.
*
* @param ref - A Stencil component instance or HTMLElement.
*/
function withComponentRegistry(ref, name) {
const registryId = Symbol('vmRegistryId');
const registrant = getRegistrant(ref);
registrant[COMPONENT_NAME_KEY] = name !== null && name !== void 0 ? name : registrant.nodeName.toLowerCase();
registrant[REGISTRATION_KEY] = registryId;
const buildEvent = (eventName) => new CustomEvent(eventName, {
bubbles: true,
composed: true,
detail: registrant,
});
const registerEvent = buildEvent(REGISTER_COMPONENT_EVENT);
createStencilHook(ref, () => {
registrant.dispatchEvent(registerEvent);
});
}
function withComponentRegistrar(player) {
const el = getElement(player);
const registry = new Map();
const disposal = new Disposal();
// TODO properly type this later.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
el[REGISTRY_KEY] = registry;
function onDeregister(registrant) {
delete registrant[PLAYER_KEY];
delete registrant[REGISTRY_KEY];
registry.delete(registrant[REGISTRATION_KEY]);
el.dispatchEvent(new CustomEvent(COMPONENT_DEREGISTERED_EVENT, { detail: registrant }));
}
function onRegister(e) {
const ref = e.detail;
const registrant = getRegistrant(ref);
registrant[PLAYER_KEY] = el;
registrant[REGISTRY_KEY] = registry;
registry.set(registrant[REGISTRATION_KEY], registrant);
el.dispatchEvent(new CustomEvent(COMPONENT_REGISTERED_EVENT, { detail: registrant }));
createStencilHook(ref, undefined, () => onDeregister(registrant));
}
createStencilHook(player, () => {
disposal.add(listen(el, REGISTER_COMPONENT_EVENT, onRegister));
}, () => {
registry.clear();
disposal.empty();
delete player[REGISTRY_KEY];
});
}
/**
* Checks whether any component with the given `name` exists in the registry. All registries
* are bound per player subtree.
*
* @param ref - A Stencil component instance or HTMLElement.
* @param name - The name of the component to search for.
*/
function isComponentRegistered(ref, name) {
var _a;
const registrant = getRegistrant(ref);
const registry = registrant[REGISTRY_KEY];
return Array.from((_a = registry === null || registry === void 0 ? void 0 : registry.values()) !== null && _a !== void 0 ? _a : []).some(r => r[COMPONENT_NAME_KEY] === name);
}
/**
* Returns the player for the given `ref`. This will only work after the component has been
* registered, prefer using `findPlayer`.
*
* @param ref - A Stencil component instance or HTMLElement.
*/
function getPlayerFromRegistry(ref) {
const registrant = getRegistrant(ref);
return registrant[PLAYER_KEY];
}
/**
* Returns a collection of components from the registry for the given `ref`. All registries
* are bound per player subtree.
*
* @param ref - A Stencil component instance or HTMLElement.
* @param name - The name of the components to search for in the registry.
*/
function getComponentFromRegistry(ref, name) {
var _a, _b;
const registrant = getRegistrant(ref);
return Array.from((_b = (_a = registrant[REGISTRY_KEY]) === null || _a === void 0 ? void 0 : _a.values()) !== null && _b !== void 0 ? _b : []).filter(r => r[COMPONENT_NAME_KEY] === name);
}
/**
* Watches the current registry on the given `ref` for changes. All registries are bound per
* player subtree.
*
* @param ref - A Stencil component instance or HTMLElement.
* @param name - The name of the component to watch for.
* @param onChange - A callback that is called when a component is registered/deregistered.
*/
function watchComponentRegistry(ref, name, onChange) {
var _a;
return __awaiter$y(this, void 0, void 0, function* () {
const player = yield findPlayer(ref);
const disposal = new Disposal();
const registry = getRegistrant(ref)[REGISTRY_KEY];
function listener(e) {
if (e.detail[COMPONENT_NAME_KEY] === name)
onChange === null || onChange === void 0 ? void 0 : onChange(getComponentFromRegistry(player, name));
}
// Hydrate.
Array.from((_a = registry === null || registry === void 0 ? void 0 : registry.values()) !== null && _a !== void 0 ? _a : []).forEach(reg => listener(new CustomEvent('', { detail: reg })));
if (!isUndefined(player)) {
disposal.add(listen(player, COMPONENT_REGISTERED_EVENT, listener));
disposal.add(listen(player, COMPONENT_DEREGISTERED_EVENT, listener));
}
createStencilHook(ref, () => {
// no-op
}, () => {
disposal.empty();
});
return () => {
disposal.empty();
};
});
}
var createDeferredPromise = function () {
var resolve;
var promise = new Promise(function (res) { resolve = res; });
return { promise: promise, resolve: resolve };
};
var openWormhole = function (Component, props, isBlocking) {
if (isBlocking === void 0) { isBlocking = true; }
var isConstructor = (Component.constructor.name === 'Function');
var Proto = isConstructor ? Component.prototype : Component;
var componentWillLoad = Proto.componentWillLoad;
Proto.componentWillLoad = function () {
var _this = this;
var el = getElement(this);
var onOpen = createDeferredPromise();
var event = new CustomEvent('openWormhole', {
bubbles: true,
composed: true,
detail: {
consumer: this,
fields: props,
updater: function (prop, value) {
var target = (prop in el) ? el : _this;
target[prop] = value;
},
onOpen: onOpen,
},
});
el.dispatchEvent(event);
var willLoad = function () {
if (componentWillLoad) {
return componentWillLoad.call(_this);
}
};
return isBlocking ? onOpen.promise.then(function () { return willLoad(); }) : (willLoad());
};
};
var multiverse = new Map();
var updateConsumer = function (_a, state) {
var fields = _a.fields, updater = _a.updater;
fields.forEach(function (field) { updater(field, state[field]); });
};
var Universe = {
create: function (creator, initialState) {
var el = getElement(creator);
var wormholes = new Map();
var universe = { wormholes: wormholes, state: initialState };
multiverse.set(creator, universe);
var connectedCallback = creator.connectedCallback;
creator.connectedCallback = function () {
multiverse.set(creator, universe);
if (connectedCallback) {
connectedCallback.call(creator);
}
};
var disconnectedCallback = creator.disconnectedCallback;
creator.disconnectedCallback = function () {
multiverse.delete(creator);
if (disconnectedCallback) {
disconnectedCallback.call(creator);
}
};
el.addEventListener('openWormhole', function (event) {
event.stopPropagation();
var _a = event.detail, consumer = _a.consumer, onOpen = _a.onOpen;
if (wormholes.has(consumer))
return;
if (typeof consumer !== 'symbol') {
var connectedCallback_1 = consumer.connectedCallback, disconnectedCallback_1 = consumer.disconnectedCallback;
consumer.connectedCallback = function () {
wormholes.set(consumer, event.detail);
if (connectedCallback_1) {
connectedCallback_1.call(consumer);
}
};
consumer.disconnectedCallback = function () {
wormholes.delete(consumer);
if (disconnectedCallback_1) {
disconnectedCallback_1.call(consumer);
}
};
}
wormholes.set(consumer, event.detail);
updateConsumer(event.detail, universe.state);
onOpen === null || onOpen === void 0 ? void 0 : onOpen.resolve(function () { wormholes.delete(consumer); });
});
el.addEventListener('closeWormhole', function (event) {
var consumer = event.detail;
wormholes.delete(consumer);
});
},
Provider: function (_a, children) {
var state = _a.state;
var creator = getRenderingRef();
if (multiverse.has(creator)) {
var universe = multiverse.get(creator);
universe.state = state;
universe.wormholes.forEach(function (opening) { updateConsumer(opening, state); });
}
return children;
}
};
const LOAD_START_EVENT = 'vmLoadStart';
// Events that toggle state and the prop is named `is{PropName}...`.
const isToggleStateEvent = new Set([
'isFullscreenActive',
'isControlsActive',
'isTextTrackVisible',
'isPiPActive',
'isLive',
'isTouch',
'isAudio',
'isVideo',
'isAudioView',
'isVideoView',
]);
// Events that are emitted without the 'Change' postfix.
const hasShortenedEventName = new Set([
'ready',
'playbackStarted',
'playbackEnded',
'playbackReady',
]);
const getEventName = (prop) => {
// Example: isFullscreenActive -> vmFullscreenChange
if (isToggleStateEvent.has(prop)) {
return `vm${prop.replace('is', '').replace('Active', '')}Change`;
}
// Example: playbackStarted -> vmPlaybackStarted
if (hasShortenedEventName.has(prop)) {
return `vm${prop.charAt(0).toUpperCase()}${prop.slice(1)}`;
}
// Example: currentTime -> vmCurrentTimeChange
return `vm${prop.charAt(0).toUpperCase()}${prop.slice(1)}Change`;
};
function firePlayerEvent(el, prop, newValue, oldValue) {
const events = [];
events.push(new CustomEvent(getEventName(prop), { detail: newValue }));
if (prop === 'paused' && !newValue)
events.push(new CustomEvent('vmPlay'));
if (prop === 'seeking' && oldValue && !newValue)
events.push(new CustomEvent('vmSeeked'));
events.forEach(event => {
el.dispatchEvent(event);
});
}
var __awaiter$x = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/**
* Binds props between an instance of a given component class and it's closest ancestor player.
*
* @param component A Stencil component instance.
* @param props A set of props to watch and update on the given component instance.
*/
const withPlayerContext = (component, props) => openWormhole(component, props);
/**
* Finds the closest ancestor player to the given `ref` and watches the given props for changes. On
* a prop change the given `updater` fn is called.
*
* @param ref A element within any player's subtree.
* @param props A set of props to watch and call the `updater` fn with.
* @param updater This function is called with the prop/value of any watched properties.
*/
const usePlayerContext = (ref, props, updater, playerRef) => __awaiter$x(void 0, void 0, void 0, function* () {
const player = playerRef !== null && playerRef !== void 0 ? playerRef : (yield findPlayer(ref));
const listeners = !isUndefined(player)
? props.map(prop => {
const event = getEventName(prop);
return listen(player, event, () => {
updater(prop, player[prop]);
});
})
: [];
return () => {
listeners.forEach(off => off());
};
});
var Provider;
(function (Provider) {
Provider["Audio"] = "audio";
Provider["Video"] = "video";
Provider["HLS"] = "hls";
Provider["Dash"] = "dash";
Provider["YouTube"] = "youtube";
Provider["Vimeo"] = "vimeo";
Provider["Dailymotion"] = "dailymotion";
})(Provider || (Provider = {}));
const audioRegex = /\.(m4a|mp4a|mpga|mp2|mp2a|mp3|m2a|m3a|wav|weba|aac|oga|spx)($|\?)/i;
const videoRegex = /\.(mp4|og[gv]|webm|mov|m4v)($|\?)/i;
const hlsRegex = /\.(m3u8)($|\?)/i;
const hlsTypeRegex = /^application\/(x-mpegURL|vnd\.apple\.mpegURL)$/i;
const dashRegex = /\.(mpd)($|\?)/i;
const PROVIDER_CHANGE_EVENT = 'vmProviderChange';
/**
* Creates a dispatcher on the given `ref`, to send updates to the closest ancestor player via
* the custom `vmProviderChange` event.
*
* @param ref A component reference to dispatch the state change events from.
*/
const createProviderDispatcher = (ref) => (prop, value) => {
const el = isInstanceOf(ref, HTMLElement) ? ref : getElement(ref);
const event = new CustomEvent(PROVIDER_CHANGE_EVENT, {
bubbles: true,
composed: true,
detail: { by: el, prop, value },
});
el.dispatchEvent(event);
};
const providerWritableProps = new Set([
'ready',
'playing',
'playbackReady',
'playbackStarted',
'playbackEnded',
'seeking',
'buffered',
'buffering',
'duration',
'viewType',
'mediaTitle',
'mediaType',
'currentSrc',
'currentPoster',
'playbackRates',
'playbackQualities',
'textTracks',
'currentTextTrack',
'isTextTrackVisible',
'audioTracks',
'currentAudioTrack',
'isPiPActive',
'isFullscreenActive',
]);
const isProviderWritableProp = (prop) => isWritableProp(prop) || providerWritableProps.has(prop);
var __awaiter$w = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const PROVIDER_CACHE_KEY = Symbol('vmProviderCache');
const PROVIDER_CONNECT_EVENT = 'vmMediaProviderConnect';
const PROVIDER_DISCONNECT_EVENT = 'vmMediaProviderDisconnect';
function buildProviderConnectEvent(name, host) {
return new CustomEvent(name, {
bubbles: true,
composed: true,
detail: host,
});
}
function withProviderHost(connector) {
const el = getElement(connector);
const disposal = new Disposal();
const cache = new Map();
connector[PROVIDER_CACHE_KEY] = cache;
function initCache() {
Object.keys(connector).forEach(prop => {
cache.set(prop, connector[prop]);
});
}
function onDisconnect() {
writeTask(() => __awaiter$w(this, void 0, void 0, function* () {
var _a;
connector.ready = false;
connector.provider = undefined;
cache.clear();
(_a = connector.onProviderDisconnect) === null || _a === void 0 ? void 0 : _a.call(connector);
el.dispatchEvent(buildProviderConnectEvent(PROVIDER_DISCONNECT_EVENT));
}));
}
function onConnect(event) {
event.stopImmediatePropagation();
initCache();
const hostRef = event.detail;
const host = getElement(event.detail);
if (connector.provider === host)
return;
const name = host === null || host === void 0 ? void 0 : host.nodeName.toLowerCase().replace('vm-', '');
writeTask(() => __awaiter$w(this, void 0, void 0, function* () {
connector.provider = host;
connector.currentProvider = Object.values(Provider).find(provider => name === provider);
createStencilHook(hostRef, undefined, () => onDisconnect());
}));
}
function onChange(event) {
var _a;
event.stopImmediatePropagation();
const { by, prop, value } = event.detail;
if (!isProviderWritableProp(prop)) {
(_a = connector.logger) === null || _a === void 0 ? void 0 : _a.warn(`${by.nodeName} tried to change \`${prop}\` but it is readonly.`);
return;
}
writeTask(() => {
cache.set(prop, value);
connector[prop] = value;
});
}
createStencilHook(connector, () => {
disposal.add(listen(el, PROVIDER_CONNECT_EVENT, onConnect));
disposal.add(listen(el, PROVIDER_CHANGE_EVENT, onChange));
}, () => {
disposal.empty();
cache.clear();
});
}
function withProviderConnect(ref) {
const connectEvent = buildProviderConnectEvent(PROVIDER_CONNECT_EVENT, ref);
createStencilHook(ref, () => {
getElement(ref).dispatchEvent(connectEvent);
});
}
var __awaiter$v = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try {
step(generator.next(value));
}
catch (e) {
reject(e);
} }
function rejected(value) { try {
step(generator["throw"](value));
}
catch (e) {
reject(e);
} }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const Audio = class extends HTMLElement {
constructor() {
super();
this.__registerHost();
/**
* @internal Whether an external SDK will attach itself to the media player and control it.
*/
this.willAttach = false;
/** @inheritdoc */
this.preload = 'metadata';
withComponentRegistry(this);
if (!this.willAttach)
withProviderConnect(this);
}
/** @internal */
getAdapter() {
var _a, _b;
return __awaiter$v(this, void 0, void 0, function* () {
const adapter = (_b = (yield ((_a = this.fileProvider) === null || _a === void 0 ? void 0 : _a.getAdapter()))) !== null && _b !== void 0 ? _b : {};
adapter.canPlay = (type) => __awaiter$v(this, void 0, void 0, function* () { return isString(type) && audioRegex.test(type); });
return adapter;
});
}
render() {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
h("vm-file", { noConnect: true, willAttach: this.willAttach, crossOrigin: this.crossOrigin, preload: this.preload, disableRemotePlayback: this.disableRemotePlayback, mediaTitle: this.mediaTitle, viewType: ViewType.Audio, ref: (el) => {
this.fileProvider = el;
} }, h("slot", null)));
}
};
const captionControlCss = ":host([hidden]){display:none}";
var __awaiter$u = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try {
step(generator.next(value));
}
catch (e) {
reject(e);
} }
function rejected(value) { try {
step(generator["throw"](value));
}
catch (e) {
reject(e);
} }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const CaptionControl = class extends HTMLElement {
constructor() {
super();
this.__registerHost();
attachShadow(this);
this.canToggleCaptionVisibility = false;
/**
* The URL to an SVG element or fragment to load.
*/
this.showIcon = 'captions-on';
/**
* The URL to an SVG element or fragment to load.
*/
this.hideIcon = 'captions-off';
/**
* Whether the tooltip is positioned above/below the control.
*/
this.tooltipPosition = 'top';
/**
* Whether the tooltip should not be displayed.
*/
this.hideTooltip = false;
/** @inheritdoc */
this.keys = 'c';
/** @internal */
this.i18n = {};
/** @internal */
this.playbackReady = false;
/** @internal */
this.textTracks = [];
/** @internal */
this.isTextTrackVisible = false;
withComponentRegistry(this);
withPlayerContext(this, [
'i18n',
'textTracks',
'isTextTrackVisible',
'playbackReady',
]);
}
onTextTracksChange() {
var _a;
return __awaiter$u(this, void 0, void 0, function* () {
const player = getPlayerFromRegistry(this);
this.canToggleCaptionVisibility =
this.textTracks.length > 0 &&
((_a = (yield (player === null || player === void 0 ? void 0 : player.canSetTextTrackVisibility()))) !== null && _a !== void 0 ? _a : false);
});
}
componentDidLoad() {
this.onTextTracksChange();
}
onClick() {
var _a;
const player = getPlayerFromRegistry(this);
(_a = player === null || player === void 0 ? void 0 : player.setTextTrackVisibility) === null || _a === void 0 ? void 0 : _a.call(player, !this.isTextTrackVisible);
}
render() {
const tooltip = this.isTextTrackVisible
? this.i18n.disableCaptions
: this.i18n.enableCaptions;
const tooltipWithHint = !isUndefined(this.keys)
? `${tooltip} (${this.keys})`
: tooltip;
return (h(Host, { hidden: !this.canToggleCaptionVisibility }, h("vm-control", { label: this.i18n.captions, keys: this.keys, hidden: !this.canToggleCaptionVisibility, pressed: this.isTextTrackVisible, onClick: this.onClick.bind(this) }, h("vm-icon", { name: this.isTextTrackVisible ? this.showIcon : this.hideIcon, library: this.icons }), h("vm-tooltip", { hidden: this.hideTooltip, position: this.tooltipPosition, direction: this.tooltipDirection }, tooltipWithHint))));
}
static get watchers() { return {
"textTracks": ["onTextTracksChange"],
"playbackReady": ["onTextTracksChange"]
}; }
static get style() { return captionControlCss; }
};
/* eslint-disable func-names */
const watch$1 = new Set();
const controls = new Set();
// watchedEl -> (controlsEl -> controlsHeight) saved on collision. Basically keeps track of
// every collision with all controls for each watched element.
const collisions = new Map();
function update() {
writeTask(() => {
controls.forEach(controlsEl => {
const controlsHeight = parseFloat(window.getComputedStyle(controlsEl).height);
watch$1.forEach(watchedEl => {
const watchedElCollisions = collisions.get(watchedEl);
const hasCollided = isColliding(watchedEl, controlsEl);
const willCollide = isColliding(watchedEl, controlsEl, 0, controlsHeight) ||
isColliding(watchedEl, controlsEl, 0, -controlsHeight);
watchedElCollisions.set(controlsEl, hasCollided || willCollide ? controlsHeight : 0);
});
});
// Update after assessing all collisions so there are no glitchy movements.
watch$1.forEach(watchedEl => {
const watchedElCollisions = collisions.get(watchedEl);
watchedEl.style.setProperty('--vm-controls-height', `${Math.max(0, Math.max(...watchedElCollisions.values()))}px`);
});
});
}
function registerControlsForCollisionDetection(component) {
const el = getElement(component);
function getInnerEl() {
return el.shadowRoot.querySelector('.controls');
}
createStencilHook(component, () => {
const innerEl = getInnerEl();
if (!isNull(innerEl)) {
controls.add(innerEl);
update();
}
}, () => {
controls.delete(getInnerEl());
update();
});
wrapStencilHook(component, 'componentDidLoad', () => {
controls.add(getInnerEl());
update();
});
wrapStencilHook(component, 'componentDidRender', update);
}
function withControlsCollisionDetection(component) {
const el = getElement(component);
createStencilHook(component, () => {
watch$1.add(el);
collisions.set(el, new Map());
update();
}, () => {
watch$1.delete(el);
collisions.delete(el);
});
}
const captionsCss = ":host{position:absolute;left:0;bottom:0;width:100%;pointer-events:none;z-index:var(--vm-captions-z-index)}.captions{width:100%;text-align:center;color:var(--vm-captions-text-color);font-size:var(--vm-captions-font-size);padding:$control-spacing;display:none;pointer-events:none;transition:transform 0.4s ease-in-out, opacity 0.3s ease-in-out}.captions.enabled{display:inline-block}.captions.hidden{display:none !important}.captions.inactive{opacity:0;visibility:hidden}.captions.fontMd{font-size:var(--vm-captions-font-size-medium)}.captions.fontLg{font-size:var(--vm-captions-font-size-large)}.captions.fontXl{font-size:var(--vm-captions-font-size-xlarge)}.cue{display:inline-block;background:var(--vm-captions-cue-bg-color);border-radius:var(--vm-captions-cue-border-radius);box-decoration-break:clone;line-height:185%;padding:var(--vm-captions-cue-padding);white-space:pre-wrap;pointer-events:none}.cue>div{display:inline}.cue:empty{display:none}";
var __awaiter$t = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try {
step(generator.next(value));
}
catch (e) {
reject(e);
} }
function rejected(value) { try {
step(generator["throw"](value));
}
catch (e) {
reject(e);
} }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const Captions = class extends HTMLElement {
constructor() {
super();
this.__registerHost();
attachShadow(this);
this.sizeDisposal = new Disposal();
this.textDisposal = new Disposal();
this.isEnabled = false;
this.fontSize = 'sm';
/**
* Whether the captions should be visible or not.
*/
this.hidden = false;
/** @internal */
this.isControlsActive = false;
/** @internal */
this.isVideoView = false;
/** @internal */
this.playbackStarted = false;
/** @internal */
this.textTracks = [];
/** @internal */
this.currentTextTrack = -1;
/** @internal */
this.isTextTrackVisible = true;
withComponentRegistry(this);
withControlsCollisionDetection(this);
withPlayerContext(this, [
'isVideoView',
'playbackStarted',
'isControlsActive',
'textTracks',
'currentTextTrack',
'isTextTrackVisible',
]);
}
onEnabledChange() {
this.isEnabled = this.playbackStarted && this.isVideoView;
}
onTextTracksChange() {
const textTrack = this.textTracks[this.currentTextTrack];
const renderCues = () => {
var _a;
const activeCues = Array.from((_a = textTrack.activeCues) !== null && _a !== void 0 ? _a : []);
this.renderCurrentCue(activeCues[0]);
};
this.textDisposal.empty();
if (!isNil(textTrack)) {
renderCues();
this.textDisposal.add(listen(textTrack, 'cuechange', renderCues));
}
}
connectedCallback() {
this.dispatch = createDispatcher(this);
this.dispatch('shouldRenderNativeTextTracks', false);
this.onTextTracksChange();
this.onPlayerResize();
}
disconnectedCallback() {
this.textDisposal.empty();
this.sizeDisposal.empty();
this.dispatch('shouldRenderNativeTextTracks', true);
}
onPlayerResize() {
return __awaiter$t(this, void 0, void 0, function* () {
const player = yield findPlayer(this);
if (isUndefined(player))
return;
const container = (yield player.getContainer());
const resizeObs = new ResizeObserver(entries => {
const entry = entries[0];
const { width } = entry.contentRect;
if (width >= 1360) {
this.fontSize = 'xl';
}
else if (width >= 1024) {
this.fontSize = 'lg';
}
else if (width >= 768) {
this.fontSize = 'md';
}
else {
this.fontSize = 'sm';
}
});
resizeObs.observe(container);
});
}
renderCurrentCue(cue) {
if (isNil(cue)) {
this.cue = '';
return;
}
const div = document.createElement('div');
div.append(cue.getCueAsHTML());
this.cue = div.innerHTML.trim();
}
render() {
return (h("div", { style: {
transform: `translateY(calc(${this.isControlsActive ? 'var(--vm-controls-height)' : '24px'} * -1))`,
}, class: {
captions: true,
enabled: this.isEnabled,
hidden: this.hidden,
fontMd: this.fontSize === 'md',
fontLg: this.fontSize === 'lg',
fontXl: this.fontSize === 'xl',
inactive: !this.isTextTrackVisible,
} }, h("span", { class: "cue" }, this.cue)));
}
static get watchers() { return {
"isVideoView": ["onEnabledChange"],
"playbackStarted": ["onEnabledChange"],
"textTracks": ["onTextTracksChange"],
"currentTextTrack": ["onTextTracksChange"]
}; }
static get style() { return captionsCss; }
};
const clickToPlayCss = ":host{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:var(--vm-click-to-play-z-index)}.clickToPlay{display:none;width:100%;height:100%;pointer-events:none}.clickToPlay.enabled{display:inline-block;pointer-events:auto}";
var __awaiter$s = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try {
step(generator.next(value));
}
catch (e) {
reject(e);
} }
function rejected(value) { try {
step(generator["throw"](value));
}
catch (e) {
reject(e);
} }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const ClickToPlay = class extends HTMLElement {
constructor() {
super();
this.__registerHost();
attachShadow(this);
/**
* By default this is disabled on mobile to not interfere with playback, set this to `true` to
* enable it.
*/
this.useOnMobile = false;
/** @internal */
this.paused = true;
/** @internal */
this.isVideoView = false;
/** @internal */
this.isMobile = false;
withComponentRegistry(this);
withPlayerContext(this, ['paused', 'isVideoView', 'isMobile']);
}
connectedCallback() {
this.dispatch = createDispatcher(this);
}
/** @internal */
forceClick() {
return __awaiter$s(this, void 0, void 0, function* () {
this.onClick();
});
}
onClick() {
this.dispatch('paused', !this.paused);
}
render() {
return (h("div", { class: {
clickToPlay: true,
enabled: this.isVideoView && (!this.isMobile || this.useOnMobile),
}, onClick: this.onClick.bind(this) }));
}
static get style() { return clickToPlayCss; }
};
const controlCss = "button{display:flex;align-items:center;flex-direction:row;border:var(--vm-control-border);cursor:pointer;flex-shrink:0;font-size:var(--vm-control-icon-size);color:var(--vm-control-color);background:var(--vm-control-bg, transparent);border-radius:var(--vm-control-border-radius);padding:var(--vm-control-padding);position:relative;pointer-events:auto;transition:all 0.3s ease;transform:scale(var(--vm-control-scale, 1));touch-action:manipulation;box-sizing:border-box}button.hidden{display:none}button:focus{outline:0}button.tapHighlight{background:var(--vm-control-tap-highlight)}button.notTouch:focus,button.notTouch:hover,button.notTouch[aria-expanded='true']{background:var(--vm-control-focus-bg);color:var(--vm-control-focus-color);transform:scale(calc(var(--vm-control-scale, 1) + 0.06))}";
var __awaiter$r = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try {
step(generator.next(value));
}
catch (e) {
reject(e);
} }
function rejected(value) { try {
step(generator["throw"](value));
}
catch (e) {
reject(e);
} }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const Control = class extends HTMLElement {
constructor() {
super();
this.__registerHost();
attachShadow(this);
this.vmInteractionChange = createEvent(this, "vmInteractionChange", 7);
this.vmFocus = createEvent(this, "vmFocus", 7);
this.vmBlur = createEvent(this, "vmBlur", 7);
this.keyboardDisposal = new Disposal();
this.showTapHighlight = false;
/**
* Whether the control should be displayed or not.
*/
this.hidden = false;
/** @internal */
this.isTouch = false;
withComponentRegistry(this);
withPlayerContext(this, ['isTouch']);
}
onKeysChange() {
return __awaiter$r(this, void 0, void 0, function* () {
this.keyboardDisposal.empty();
if (isUndefined(this.keys))
return;
const player = yield findPlayer(this);
const codes = this.keys.split('/');
if (isUndefined(player))
return;
this.keyboardDisposal.add(listen(player, 'keydown', (event) => {
if (codes.includes(event.key)) {
this.button.click();
}
}));
});
}
connectedCallback() {
this.findTooltip();
this.onKeysChange();
}
componentWillLoad() {
this.findTooltip();
}
disconnectedCallback() {
this.keyboardDisposal.empty();
}
/**
* Focuses the control.
*/
focusControl() {
var _a;
return __awaiter$r(this, void 0, void 0, function* () {
(_a = this.button) === null || _a === void 0 ? void 0 : _a.focus();
});
}
/**
* Removes focus from the control.
*/
blurControl() {
var _a;
return __awaiter$r(this, void 0, void 0, function* () {
(_a = this.button) === null || _a === void 0 ? void 0 : _a.blur();
});
}
onTouchStart() {
this.showTapHighlight = true;
}
onTouchEnd() {
setTimeout(() => {
this.showTapHighlight = false;
}, 100);
}
findTooltip() {
const tooltip = this.host.querySelector('vm-tooltip');
if (!isNull(tooltip))
this.describedBy = tooltip.id;
return tooltip;
}
onShowTooltip() {
const tooltip = this.findTooltip();
if (!isNull(tooltip))
tooltip.active = true;
this.vmInteractionChange.emit(true);
}
onHideTooltip() {
const tooltip = this.findTooltip();
if (!isNull(tooltip))
tooltip.active = false;
this.button.blur();
this.vmInteractionChange.emit(false);
}
onFocus() {
this.vmFocus.emit();
this.onShowTooltip();
}
onBlur() {
this.vmBlur.emit();
this.onHideTooltip();
}
onMouseEnter() {
this.onShowTooltip();
}
onMouseLeave() {
this.onHideTooltip();
}
render() {
const isMenuExpanded = this.expanded ? 'true' : 'false';
const isPressed = this.pressed ? 'true' : 'false';
return (h("button", { class: {
hidden: this.hidden,
notTouch: !this.isTouch,
tapHighlight: this.showTapHighlight,
}, id: this.identifier, type: "button", "aria-label": this.label, "aria-haspopup": !isUndefined(this.menu) ? 'true' : undefined, "aria-controls": this.menu, "aria-expanded": !isUndefined(this.menu) ? isMenuExpanded : undefined, "aria-pressed": !isUndefined(this.pressed) ? isPressed : undefined, "aria-hidden": this.hidden ? 'true' : 'false', "aria-describedby": this.describedBy, onTouchStart: this.onTouchStart.bind(this), onTouchEnd: this.onTouchEnd.bind(this), onFocus: this.onFocus.bind(this), onBlur: this.onBlur.bind(this), onMouseEnter: this.onMouseEnter.bind(this), onMouseLeave: this.onMouseLeave.bind(this), ref: (el) => {
this.button = el;
} }, h("slot", null)));
}
get host() { return this; }
static get watchers() { return {
"keys": ["onKeysChange"]
}; }
static get style() { return controlCss; }
};
const controlGroupCss = ":host{width:100%}.controlGroup{position:relative;width:100%;display:flex;flex-wrap:wrap;flex-direction:inherit;align-items:inherit;justify-content:inherit;box-sizing:border-box}.controlGroup.spaceTop{margin-top:var(--vm-control-group-spacing)}.controlGroup.spaceBottom{margin-bottom:var(--vm-control-group-spacing)}::slotted(*){margin-left:var(--vm-controls-spacing)}::slotted(*:first-child){margin-left:0}";
const ControlNewLine = class extends HTMLElement {
constructor() {
super();
this.__registerHost();
attachShadow(this);
/**
* Determines where to add spacing/margin. The amount of spacing is determined by the CSS variable
* `--control-group-spacing`.
*/
this.space = 'none';
withComponentRegistry(this);
}
render() {
return (h("div", { class: {
controlGroup: true,
spaceTop: this.space !== 'none' && this.space !== 'bottom',
spaceBottom: this.space !== 'none' && this.space !== 'top',
} }, h("slot", null)));
}
get host() { return this; }
static get style() { return controlGroupCss; }
};
const controlSpacerCss = ":host{flex:1}";
const ControlSpacer = class extends HTMLElement {
constructor() {
super();
this.__registerHost();
attachShadow(this);
withComponentRegistry(this);
}
static get style() { return controlSpacerCss; }
};
const debounce = (func, wait = 1000, immediate = false) => {
let timeout;
return function executedFunction(...args) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this;
const later = function delayedFunctionCall() {
timeout = undefined;
if (!immediate)
func.apply(context, args);
};
const callNow = immediate && isUndefined(timeout);
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow)
func.apply(context, args);
};
};
const contro