UNPKG

@vime/core

Version:

Customizable, extensible, accessible and framework agnostic media player.

1,467 lines (1,429 loc) 319 kB
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