UNPKG

@ktt45678/vidstack

Version:

UI component library for building high-quality, accessible video and audio experiences on the web.

1,711 lines (1,685 loc) 86.3 kB
import { ViewController, onDispose, isArray, isString, isNull, effect, peek, listenEvent, ariaBool, isWriteSignal, Component, State, createContext, functionThrottle, hasProvidedContext, useContext, isNumber, signal, animationFrameThrottle, provideContext, isObject, useState, computed, method, setAttribute, r, wasEnterKeyPressed, isKeyboardEvent, tick, prop, setStyle } from './vidstack-C6myozhB.js'; import { useMediaContext } from './vidstack-Cq-GdDcp.js'; import { $ariaBool, sortVideoQualities } from './vidstack-BOTZD4tC.js'; import { hasAnimation, setAttributeIfEmpty, onPress, setARIALabel, isTouchPinchEvent, observeVisibility, isHTMLElement, isElementParent, isEventInside, isElementVisible, requestScopedAnimationFrame, autoPlacement } from './vidstack-BeyDmEgV.js'; import { isTrackCaptionKind } from './vidstack-CFEqcMSQ.js'; import { FocusVisibleController } from './vidstack-D6_zYTXL.js'; import { assert } from './vidstack-C9vIqaYT.js'; import { getRequestCredentials } from './vidstack-CVbXna2m.js'; import { clampNumber, round, getNumberOfDecimalPlaces } from './vidstack-Dihypf8P.js'; import { watchActiveTextTrack } from './vidstack-D2w309v1.js'; class ARIAKeyShortcuts extends ViewController { constructor(_shortcut) { super(); this.$d = _shortcut; } onAttach(el) { const { $props, ariaKeys } = useMediaContext(), keys = el.getAttribute("aria-keyshortcuts"); if (keys) { ariaKeys[this.$d] = keys; { onDispose(() => { delete ariaKeys[this.$d]; }); } return; } const shortcuts = $props.keyShortcuts()[this.$d]; if (shortcuts) { const keys2 = isArray(shortcuts) ? shortcuts.join(" ") : isString(shortcuts) ? shortcuts : shortcuts?.keys; el.setAttribute("aria-keyshortcuts", isArray(keys2) ? keys2.join(" ") : keys2); } } } function padNumberWithZeroes(num, expectedLength) { const str = String(num); const actualLength = str.length; const shouldPad = actualLength < expectedLength; if (shouldPad) { const padLength = expectedLength - actualLength; const padding = `0`.repeat(padLength); return `${padding}${num}`; } return str; } function parseTime(duration) { const hours = Math.trunc(duration / 3600); const minutes = Math.trunc(duration % 3600 / 60); const seconds = Math.trunc(duration % 60); const fraction = Number((duration - Math.trunc(duration)).toPrecision(3)); return { hours, minutes, seconds, fraction }; } function formatTime(duration, { padHrs = null, padMins = null, showHrs = false, showMs = false } = {}) { const { hours, minutes, seconds, fraction } = parseTime(duration), paddedHours = padHrs ? padNumberWithZeroes(hours, 2) : hours, paddedMinutes = padMins || isNull(padMins) && duration >= 3600 ? padNumberWithZeroes(minutes, 2) : minutes, paddedSeconds = padNumberWithZeroes(seconds, 2), paddedMs = showMs && fraction > 0 ? `.${String(fraction).replace(/^0?\./, "")}` : "", time = `${paddedMinutes}:${paddedSeconds}${paddedMs}`; return hours > 0 || showHrs ? `${paddedHours}:${time}` : time; } function formatSpokenTime(duration) { const spokenParts = []; const { hours, minutes, seconds } = parseTime(duration); if (hours > 0) { spokenParts.push(`${hours} hour`); } if (minutes > 0) { spokenParts.push(`${minutes} min`); } if (seconds > 0 || spokenParts.length === 0) { spokenParts.push(`${seconds} sec`); } return spokenParts.join(" "); } class Popper extends ViewController { constructor(_delegate) { super(); this.j = _delegate; this.zd = -1; this.Ad = -1; this.wb = null; effect(this.Ok.bind(this)); } onDestroy() { this.wb?.(); this.wb = null; } Ok() { const trigger = this.j.M(); if (!trigger) { this.hide(); return; } const show = this.show.bind(this), hide = this.hide.bind(this); this.j.yd(trigger, show, hide); } show(trigger) { this.Ye(); window.cancelAnimationFrame(this.Ad); this.Ad = -1; this.wb?.(); this.wb = null; this.zd = window.setTimeout(() => { this.zd = -1; const content = this.j.q(); if (content) content.style.removeProperty("display"); peek(() => this.j.E(true, trigger)); }, this.j.ih?.() ?? 0); } hide(trigger) { this.Ye(); peek(() => this.j.E(false, trigger)); this.Ad = requestAnimationFrame(() => { this.Ye(); this.Ad = -1; const content = this.j.q(); if (content) { const onHide = () => { content.style.display = "none"; this.wb = null; }; const isAnimated = hasAnimation(content); if (isAnimated) { this.wb?.(); const stop = listenEvent(content, "animationend", onHide, { once: true }); this.wb = stop; } else { onHide(); } } }); } Ye() { window.clearTimeout(this.zd); this.zd = -1; } } class ToggleButtonController extends ViewController { constructor(_delegate) { super(); this.j = _delegate; new FocusVisibleController(); if (_delegate.Sb) { new ARIAKeyShortcuts(_delegate.Sb); } } static { this.props = { disabled: false }; } onSetup() { const { disabled } = this.$props; this.setAttributes({ "data-pressed": this.j.o, "aria-pressed": this.Rk.bind(this), "aria-disabled": () => disabled() ? "true" : null }); } onAttach(el) { setAttributeIfEmpty(el, "tabindex", "0"); setAttributeIfEmpty(el, "role", "button"); setAttributeIfEmpty(el, "type", "button"); } onConnect(el) { onPress(el, this.Sk.bind(this)); for (const type of ["click", "touchstart"]) { this.listen(type, this.Tk.bind(this), { passive: true }); } } Rk() { return ariaBool(this.j.o()); } Uk(event) { if (isWriteSignal(this.j.o)) { this.j.o.set((p) => !p); } } Sk(event) { const disabled = this.$props.disabled() || this.el.hasAttribute("data-disabled"); if (disabled) { event.preventDefault(); event.stopImmediatePropagation(); return; } event.preventDefault(); (this.j.r ?? this.Uk).call(this, event); } Tk(event) { if (this.$props.disabled()) { event.preventDefault(); event.stopImmediatePropagation(); } } } class AirPlayButton extends Component { static { this.props = ToggleButtonController.props; } constructor() { super(); new ToggleButtonController({ o: this.o.bind(this), r: this.r.bind(this) }); } onSetup() { this.a = useMediaContext(); const { canAirPlay, isAirPlayConnected } = this.a.$state; this.setAttributes({ "data-active": isAirPlayConnected, "data-supported": canAirPlay, "data-state": this.Ic.bind(this), "aria-hidden": $ariaBool(() => !canAirPlay()) }); } onAttach(el) { el.setAttribute("data-media-tooltip", "airplay"); setARIALabel(el, this.Jc.bind(this)); } r(event) { const remote = this.a.remote; remote.requestAirPlay(event); } o() { const { remotePlaybackType, remotePlaybackState } = this.a.$state; return remotePlaybackType() === "airplay" && remotePlaybackState() !== "disconnected"; } Ic() { const { remotePlaybackType, remotePlaybackState } = this.a.$state; return remotePlaybackType() === "airplay" && remotePlaybackState(); } Jc() { const { remotePlaybackState } = this.a.$state; return `AirPlay ${remotePlaybackState()}`; } } class PlayButton extends Component { static { this.props = ToggleButtonController.props; } constructor() { super(); new ToggleButtonController({ o: this.o.bind(this), Sb: "togglePaused", r: this.r.bind(this) }); } onSetup() { this.a = useMediaContext(); const { paused, ended } = this.a.$state; this.setAttributes({ "data-paused": paused, "data-ended": ended }); } onAttach(el) { el.setAttribute("data-media-tooltip", "play"); setARIALabel(el, "Play"); } r(event) { const remote = this.a.remote; this.o() ? remote.pause(event) : remote.play(event); } o() { const { paused } = this.a.$state; return !paused(); } } class CaptionButton extends Component { static { this.props = ToggleButtonController.props; } constructor() { super(); new ToggleButtonController({ o: this.o.bind(this), Sb: "toggleCaptions", r: this.r.bind(this) }); } onSetup() { this.a = useMediaContext(); this.setAttributes({ "data-active": this.o.bind(this), "data-supported": () => !this.Tb(), "aria-hidden": $ariaBool(this.Tb.bind(this)) }); } onAttach(el) { el.setAttribute("data-media-tooltip", "caption"); setARIALabel(el, "Captions"); } r(event) { this.a.remote.toggleCaptions(event); } o() { const { textTrack } = this.a.$state, track = textTrack(); return !!track && isTrackCaptionKind(track); } Tb() { const { hasCaptions } = this.a.$state; return !hasCaptions(); } } class FullscreenButton extends Component { static { this.props = { ...ToggleButtonController.props, target: "prefer-media" }; } constructor() { super(); new ToggleButtonController({ o: this.o.bind(this), Sb: "toggleFullscreen", r: this.r.bind(this) }); } onSetup() { this.a = useMediaContext(); const { fullscreen } = this.a.$state, isSupported = this.Kc.bind(this); this.setAttributes({ "data-active": fullscreen, "data-supported": isSupported, "aria-hidden": $ariaBool(() => !isSupported()) }); } onAttach(el) { el.setAttribute("data-media-tooltip", "fullscreen"); setARIALabel(el, "Fullscreen"); } r(event) { const remote = this.a.remote, target = this.$props.target(); this.o() ? remote.exitFullscreen(target, event) : remote.enterFullscreen(target, event); } o() { const { fullscreen } = this.a.$state; return fullscreen(); } Kc() { const { canFullscreen } = this.a.$state; return canFullscreen(); } } class MuteButton extends Component { static { this.props = ToggleButtonController.props; } constructor() { super(); new ToggleButtonController({ o: this.o.bind(this), Sb: "toggleMuted", r: this.r.bind(this) }); } onSetup() { this.a = useMediaContext(); this.setAttributes({ "data-muted": this.o.bind(this), "data-state": this.Ic.bind(this) }); } onAttach(el) { el.setAttribute("data-media-mute-button", ""); el.setAttribute("data-media-tooltip", "mute"); setARIALabel(el, "Mute"); } r(event) { const remote = this.a.remote; this.o() ? remote.unmute(event) : remote.mute(event); } o() { const { muted, volume } = this.a.$state; return muted() || volume() === 0; } Ic() { const { muted, volume } = this.a.$state, $volume = volume(); if (muted() || $volume === 0) return "muted"; else if ($volume >= 0.5) return "high"; else if ($volume < 0.5) return "low"; } } class PIPButton extends Component { static { this.props = ToggleButtonController.props; } constructor() { super(); new ToggleButtonController({ o: this.o.bind(this), Sb: "togglePictureInPicture", r: this.r.bind(this) }); } onSetup() { this.a = useMediaContext(); const { pictureInPicture } = this.a.$state, isSupported = this.Kc.bind(this); this.setAttributes({ "data-active": pictureInPicture, "data-supported": isSupported, "aria-hidden": $ariaBool(() => !isSupported()) }); } onAttach(el) { el.setAttribute("data-media-tooltip", "pip"); setARIALabel(el, "PiP"); } r(event) { const remote = this.a.remote; this.o() ? remote.exitPictureInPicture(event) : remote.enterPictureInPicture(event); } o() { const { pictureInPicture } = this.a.$state; return pictureInPicture(); } Kc() { const { canPictureInPicture } = this.a.$state; return canPictureInPicture(); } } class SeekButton extends Component { static { this.props = { disabled: false, seconds: 30 }; } constructor() { super(); new FocusVisibleController(); } onSetup() { this.a = useMediaContext(); const { seeking } = this.a.$state, { seconds } = this.$props, isSupported = this.Kc.bind(this); this.setAttributes({ seconds, "data-seeking": seeking, "data-supported": isSupported, "aria-hidden": $ariaBool(() => !isSupported()) }); } onAttach(el) { setAttributeIfEmpty(el, "tabindex", "0"); setAttributeIfEmpty(el, "role", "button"); setAttributeIfEmpty(el, "type", "button"); el.setAttribute("data-media-tooltip", "seek"); setARIALabel(el, this.Jc.bind(this)); } onConnect(el) { onPress(el, this.r.bind(this)); } Kc() { const { canSeek } = this.a.$state; return canSeek(); } Jc() { const { seconds } = this.$props; return `Seek ${seconds() > 0 ? "forward" : "backward"} ${seconds()} seconds`; } r(event) { const { seconds, disabled } = this.$props; if (disabled()) return; const { currentTime } = this.a.$state, seekTo = currentTime() + seconds(); this.a.remote.seek(seekTo, event); } } class LiveButton extends Component { static { this.props = { disabled: false }; } constructor() { super(); new FocusVisibleController(); } onSetup() { this.a = useMediaContext(); const { disabled } = this.$props, { live, liveEdge } = this.a.$state, isHidden = () => !live(); this.setAttributes({ "data-edge": liveEdge, "data-hidden": isHidden, "aria-disabled": $ariaBool(() => disabled() || liveEdge()), "aria-hidden": $ariaBool(isHidden) }); } onAttach(el) { setAttributeIfEmpty(el, "tabindex", "0"); setAttributeIfEmpty(el, "role", "button"); setAttributeIfEmpty(el, "type", "button"); el.setAttribute("data-media-tooltip", "live"); } onConnect(el) { onPress(el, this.r.bind(this)); } r(event) { const { disabled } = this.$props, { liveEdge } = this.a.$state; if (disabled() || liveEdge()) return; this.a.remote.seekToLiveEdge(event); } } const sliderState = new State({ min: 0, max: 100, value: 0, step: 1, pointerValue: 0, focused: false, dragging: false, pointing: false, hidden: false, get active() { return this.dragging || this.focused || this.pointing; }, get fillRate() { return calcRate(this.min, this.max, this.value); }, get fillPercent() { return this.fillRate * 100; }, get pointerRate() { return calcRate(this.min, this.max, this.pointerValue); }, get pointerPercent() { return this.pointerRate * 100; } }); function calcRate(min, max, value) { const range = max - min, offset = value - min; return range > 0 ? offset / range : 0; } class IntersectionObserverController extends ViewController { constructor(_init) { super(); this.Hb = _init; } onConnect(el) { this.Ra = new IntersectionObserver((entries) => { this.Hb.callback?.(entries, this.Ra); }, this.Hb); this.Ra.observe(el); onDispose(this.Fa.bind(this)); } /** * Disconnect any active intersection observers. */ Fa() { this.Ra?.disconnect(); this.Ra = void 0; } } const sliderContext = createContext(); const sliderObserverContext = createContext(); function getClampedValue(min, max, value, step) { return clampNumber(min, round(value, getNumberOfDecimalPlaces(step)), max); } function getValueFromRate(min, max, rate, step) { const boundRate = clampNumber(0, rate, 1), range = max - min, fill = range * boundRate, stepRatio = fill / step, steps = step * Math.round(stepRatio); return min + steps; } const SliderKeyDirection = { Left: -1, ArrowLeft: -1, Up: 1, ArrowUp: 1, Right: 1, ArrowRight: 1, Down: -1, ArrowDown: -1 }; class SliderEventsController extends ViewController { constructor(_delegate, _media) { super(); this.j = _delegate; this.a = _media; this.p = null; this.cb = null; this.Ub = null; this.Bn = false; this.cl = functionThrottle( (event) => { this.db(this.Cd(event), event); }, 20, { leading: true } ); } onSetup() { if (hasProvidedContext(sliderObserverContext)) { this.Ra = useContext(sliderObserverContext); } } onConnect() { effect(this.Wk.bind(this)); effect(this.Xk.bind(this)); if (this.j.kh) effect(this.Yk.bind(this)); } Yk() { const { pointer } = this.a.$state; if (pointer() !== "coarse" || !this.j.kh()) { this.p = null; return; } this.p = this.a.player.el?.querySelector( "media-provider,[data-media-provider]" ); if (!this.p) return; listenEvent(this.p, "touchstart", this.Zk.bind(this), { passive: true }); listenEvent(this.p, "touchmove", this._k.bind(this), { passive: false }); } Zk(event) { this.cb = event.touches[0]; } _k(event) { if (isNull(this.cb) || isTouchPinchEvent(event)) return; const touch = event.touches[0], xDiff = touch.clientX - this.cb.clientX, yDiff = touch.clientY - this.cb.clientY, isDragging = this.$state.dragging(); if (!isDragging && Math.abs(yDiff) > 5) { return; } if (isDragging) return; event.preventDefault(); if (Math.abs(xDiff) > 20) { this.cb = touch; this.Ub = this.$state.value(); this.cf(this.Ub, event); } } Wk() { const { hidden } = this.$props; this.listen("focus", this.Ec.bind(this)); this.listen("keydown", this.ic.bind(this)); this.listen("keyup", this.hc.bind(this)); if (hidden() || this.j.v()) return; this.listen("pointerenter", this.Oe.bind(this)); this.listen("pointermove", this.$k.bind(this)); this.listen("pointerleave", this.Pe.bind(this)); this.listen("pointerdown", this.al.bind(this)); } Xk() { if (this.j.v() || !this.$state.dragging()) return; listenEvent(document, "pointerup", this.bl.bind(this), { capture: true }); listenEvent(document, "pointermove", this.cl.bind(this)); listenEvent(document, "touchmove", this.dl.bind(this), { passive: false }); } Ec() { this.db(this.$state.value()); } df(newValue, trigger) { const { value, min, max, dragging } = this.$state; const clampedValue = Math.max(min(), Math.min(newValue, max())); value.set(clampedValue); const event = this.createEvent("value-change", { detail: clampedValue, trigger }); this.dispatch(event); this.j.l?.(event); if (dragging()) { const event2 = this.createEvent("drag-value-change", { detail: clampedValue, trigger }); this.dispatch(event2); this.j.S?.(event2); } } db(value, trigger) { const { pointerValue, dragging } = this.$state; pointerValue.set(value); this.dispatch("pointer-value-change", { detail: value, trigger }); if (dragging()) { this.df(value, trigger); } } Cd(event) { let thumbPositionRate, rect = this.el.getBoundingClientRect(), { min, max } = this.$state; if (this.$props.orientation() === "vertical") { const { bottom: trackBottom, height: trackHeight } = rect; thumbPositionRate = (trackBottom - event.clientY) / trackHeight; } else { if (this.cb && isNumber(this.Ub)) { const { width } = this.p.getBoundingClientRect(), rate = (event.clientX - this.cb.clientX) / width, range = max() - min(), diff = range * Math.abs(rate); thumbPositionRate = (rate < 0 ? this.Ub - diff : this.Ub + diff) / range; } else { const { left: trackLeft, width: trackWidth } = rect; thumbPositionRate = (event.clientX - trackLeft) / trackWidth; } } return Math.max( min(), Math.min( max(), this.j.Da( getValueFromRate(min(), max(), thumbPositionRate, this.j.qa()) ) ) ); } Oe(event) { this.$state.pointing.set(true); } $k(event) { const { dragging } = this.$state; if (dragging()) return; this.db(this.Cd(event), event); } Pe(event) { this.$state.pointing.set(false); } al(event) { if (event.button !== 0) return; const value = this.Cd(event); this.cf(value, event); this.db(value, event); } cf(value, trigger) { const { dragging } = this.$state; if (dragging()) return; dragging.set(true); this.a.remote.pauseControls(trigger); const event = this.createEvent("drag-start", { detail: value, trigger }); this.dispatch(event); this.j.ef?.(event); this.Ra?.onDragStart?.(); } lh(value, trigger) { const { dragging } = this.$state; if (!dragging()) return; dragging.set(false); this.a.remote.resumeControls(trigger); const event = this.createEvent("drag-end", { detail: value, trigger }); this.dispatch(event); this.j.Dd?.(event); this.cb = null; this.Ub = null; this.Ra?.onDragEnd?.(); } ic(event) { const isValidKey = Object.keys(SliderKeyDirection).includes(event.key); if (!isValidKey) return; const { key } = event, jumpValue = this.Cn(event); if (!isNull(jumpValue)) { this.db(jumpValue, event); this.df(jumpValue, event); return; } const newValue = this.Dn(event); if (!this.Bn) { this.Bn = key === this.ff; if (!this.$state.dragging() && this.Bn) { this.cf(newValue, event); } } this.db(newValue, event); this.ff = key; } hc(event) { const isValidKey = Object.keys(SliderKeyDirection).includes(event.key); if (!isValidKey || !isNull(this.Cn(event))) return; const newValue = this.Bn ? this.$state.pointerValue() : this.Dn(event); this.df(newValue, event); this.lh(newValue, event); this.ff = ""; this.Bn = false; } Cn(event) { let key = event.key, { min, max } = this.$state; if (key === "Home" || key === "PageUp") { return min(); } else if (key === "End" || key === "PageDown") { return max(); } else if (!event.metaKey && /^[0-9]$/.test(key)) { return (max() - min()) / 10 * Number(key); } return null; } Dn(event) { const { key, shiftKey } = event; event.preventDefault(); event.stopPropagation(); const { shiftKeyMultiplier } = this.$props; const { min, max, value, pointerValue } = this.$state, step = this.j.qa(), keyStep = this.j.eb(); const modifiedStep = !shiftKey ? keyStep : keyStep * shiftKeyMultiplier(), direction = Number(SliderKeyDirection[key]), diff = modifiedStep * direction, currentValue = this.Bn ? pointerValue() : this.j.Y?.() ?? value(), steps = (currentValue + diff) / step; return Math.max(min(), Math.min(max(), Number((step * steps).toFixed(3)))); } // ------------------------------------------------------------------------------------------- // Document (Pointer Events) // ------------------------------------------------------------------------------------------- bl(event) { if (event.button !== 0) return; event.preventDefault(); event.stopImmediatePropagation(); const value = this.Cd(event); this.db(value, event); this.lh(value, event); } dl(event) { event.preventDefault(); } } const sliderValueFormatContext = createContext(() => ({})); class SliderController extends ViewController { constructor(_delegate) { super(); this.j = _delegate; this.Lc = signal(true); this.Mc = signal(true); this.jl = animationFrameThrottle( (fillPercent, pointerPercent) => { this.el?.style.setProperty("--slider-fill", fillPercent + "%"); this.el?.style.setProperty("--slider-pointer", pointerPercent + "%"); } ); } static { this.props = { hidden: false, disabled: false, step: 1, keyStep: 1, orientation: "horizontal", shiftKeyMultiplier: 5 }; } onSetup() { this.a = useMediaContext(); const focus = new FocusVisibleController(); focus.attach(this); this.$state.focused = focus.focused.bind(focus); if (!hasProvidedContext(sliderValueFormatContext)) { provideContext(sliderValueFormatContext, { default: "value" }); } provideContext(sliderContext, { bb: this.$props.orientation, Ed: this.j.v, nh: signal(null) }); effect(this.N.bind(this)); effect(this.fl.bind(this)); effect(this.Nc.bind(this)); this.gl(); new SliderEventsController(this.j, this.a).attach(this); new IntersectionObserverController({ callback: this.gf.bind(this) }).attach(this); } onAttach(el) { setAttributeIfEmpty(el, "role", "slider"); setAttributeIfEmpty(el, "tabindex", "0"); setAttributeIfEmpty(el, "autocomplete", "off"); effect(this.oh.bind(this)); } onConnect(el) { onDispose(observeVisibility(el, this.Lc.set)); effect(this.Ea.bind(this)); } gf(entries) { this.Mc.set(entries[0].isIntersecting); } // ------------------------------------------------------------------------------------------- // Watch // ------------------------------------------------------------------------------------------- Ea() { const { hidden } = this.$props; this.$state.hidden.set(hidden() || !this.Lc() || !this.Mc.bind(this)); } N() { const { dragging, value, min, max } = this.$state; if (peek(dragging)) return; value.set(getClampedValue(min(), max(), value(), this.j.qa())); } fl() { this.$state.step.set(this.j.qa()); } Nc() { if (!this.j.v()) return; const { dragging, pointing } = this.$state; dragging.set(false); pointing.set(false); } // ------------------------------------------------------------------------------------------- // ARIA // ------------------------------------------------------------------------------------------- il() { return ariaBool(this.j.v()); } // ------------------------------------------------------------------------------------------- // Attributes // ------------------------------------------------------------------------------------------- gl() { const { orientation } = this.$props, { dragging, active, pointing } = this.$state; this.setAttributes({ "data-dragging": dragging, "data-pointing": pointing, "data-active": active, "aria-disabled": this.il.bind(this), "aria-valuemin": this.j.Tm ?? this.$state.min, "aria-valuemax": this.j.hf ?? this.$state.max, "aria-valuenow": this.j.O, "aria-valuetext": this.j.P, "aria-orientation": orientation }); } oh() { const { fillPercent, pointerPercent } = this.$state; this.jl(round(fillPercent(), 3), round(pointerPercent(), 3)); } } class Slider extends Component { static { this.props = { ...SliderController.props, min: 0, max: 100, value: 0 }; } static { this.state = sliderState; } constructor() { super(); new SliderController({ qa: this.$props.step, eb: this.$props.keyStep, Da: Math.round, v: this.$props.disabled, O: this.O.bind(this), P: this.P.bind(this) }); } onSetup() { effect(this.N.bind(this)); effect(this.Oc.bind(this)); } // ------------------------------------------------------------------------------------------- // Props // ------------------------------------------------------------------------------------------- O() { const { value } = this.$state; return Math.round(value()); } P() { const { value, max } = this.$state; return round(value() / max() * 100, 2) + "%"; } // ------------------------------------------------------------------------------------------- // Watch // ------------------------------------------------------------------------------------------- N() { const { value } = this.$props; this.$state.value.set(value()); } Oc() { const { min, max } = this.$props; this.$state.min.set(min()); this.$state.max.set(max()); } } const cache = /* @__PURE__ */ new Map(), pending = /* @__PURE__ */ new Map(); class ThumbnailsLoader { constructor($src, $crossOrigin, _media) { this.$src = $src; this.$crossOrigin = $crossOrigin; this.a = _media; this.$images = signal([]); this.$preloadedThumbs = signal([]); effect(this.kl.bind(this)); } static create($src, $crossOrigin) { const media = useMediaContext(); return new ThumbnailsLoader($src, $crossOrigin, media); } kl() { const { canLoad } = this.a.$state; if (!canLoad()) return; const src = this.$src(); if (!src) return; if (isString(src) && cache.has(src)) { const cues = cache.get(src); cache.delete(src); cache.set(src, cues); if (cache.size > 99) { const firstKey = cache.keys().next().value; cache.delete(firstKey); } this.$images.set(cache.get(src)); } else if (isString(src)) { const crossOrigin = this.$crossOrigin(), currentKey = src + "::" + crossOrigin; if (!pending.has(currentKey)) { const promise = new Promise(async (resolve, reject) => { try { const response = await fetch(src, { credentials: getRequestCredentials(crossOrigin) }), isJSON = response.headers.get("content-type") === "application/json"; if (isJSON) { const json = await response.json(); if (isArray(json)) { if (json[0] && "text" in json[0]) { resolve(this.ph(json)); } else { for (let i = 0; i < json.length; i++) { const image = json[i]; assert(isObject(image), false); assert( "url" in image && isString(image.url), false ); assert( "startTime" in image && isNumber(image.startTime), false ); } resolve(json); } } else { resolve(this.qh(json)); } return; } import('media-captions').then(async ({ parseResponse }) => { try { const { cues } = await parseResponse(response); resolve(this.ph(cues)); } catch (e) { reject(e); } }); } catch (e) { reject(e); } }).then((images) => { cache.set(currentKey, images); return images; }).catch((error) => { this.Q(src, error); }).finally(() => { if (isString(currentKey)) pending.delete(currentKey); }); pending.set(currentKey, promise); } pending.get(currentKey)?.then((images) => { this.$images.set(images || []); }); } else if (isArray(src)) { try { this.$images.set(this.ll(src)); } catch (error) { this.Q(src, error); } } else if ("baseURL" in src) { this.$images.set(this.Xn(src)); } else { try { this.$images.set(this.qh(src)); } catch (error) { this.Q(src, error); } } return () => { this.$images.set([]); }; } ll(images) { const baseURL = this.rh(); return images.map((img, i) => { assert( img.url && isString(img.url)); assert( "startTime" in img && isNumber(img.startTime)); return { ...img, url: isString(img.url) ? this.sh(img.url, baseURL) : img.url }; }); } qh(board) { assert(isString(board.url)); assert(isArray(board.tiles) && board.tiles?.length); const url = new URL(board.url), images = []; const tileWidth = "tile_width" in board ? board.tile_width : board.tileWidth, tileHeight = "tile_height" in board ? board.tile_height : board.tileHeight; for (const tile of board.tiles) { images.push({ url, startTime: "start" in tile ? tile.start : tile.startTime, width: tileWidth, height: tileHeight, coords: { x: tile.x, y: tile.y } }); } return images; } Xn(src) { const baseURL = src.baseURL; this.$preloadedThumbs.set([]); const images = src.thumbs.map((img) => { return { ...img, url: isString(img.url) ? this.sh(img.url, baseURL) : img.url }; }); return images; } ph(cues) { for (let i = 0; i < cues.length; i++) { const cue = cues[i]; assert( "startTime" in cue && isNumber(cue.startTime)); assert( "text" in cue && isString(cue.text)); } const images = [], baseURL = this.rh(); for (const cue of cues) { const [url, hash] = cue.text.split("#"), data = this.ml(hash); images.push({ url: this.sh(url, baseURL), startTime: cue.startTime, endTime: cue.endTime, width: data?.w, height: data?.h, coords: data && isNumber(data.x) && isNumber(data.y) ? { x: data.x, y: data.y } : void 0 }); } return images; } rh() { let baseURL = peek(this.$src); if (!isString(baseURL) || !/^https?:/.test(baseURL)) { return location.href; } return baseURL; } sh(src, baseURL) { let splitBaseURL = ""; if (!src.startsWith("/") && !src.startsWith("http://") && !src.startsWith("https://")) splitBaseURL = baseURL.substring(0, baseURL.lastIndexOf("/") + 1); const srcURL = splitBaseURL + src; return new URL(srcURL); } ml(hash) { if (!hash) return {}; const [hashProps, values] = hash.split("="), hashValues = values?.split(","), data = {}; if (!hashProps || !hashValues) { return null; } for (let i = 0; i < hashProps.length; i++) { const value = +hashValues[i]; if (!isNaN(value)) data[hashProps[i]] = value; } return data; } Q(src, error) { return; } } class Thumbnail extends Component { constructor() { super(...arguments); this.jf = []; } static { this.props = { src: null, time: 0, crossOrigin: null, cdnURL: null, cdnPreload: false }; } static { this.state = new State({ src: "", img: null, thumbnails: [], activeThumbnail: null, crossOrigin: null, loading: false, error: null, hidden: false }); } onSetup() { this.a = useMediaContext(); this.X = ThumbnailsLoader.create(this.$props.src, this.$state.crossOrigin); this.Ca(); this.setAttributes({ "data-loading": this.Pc.bind(this), "data-error": this.fb.bind(this), "data-hidden": this.$state.hidden, "aria-hidden": $ariaBool(this.$state.hidden) }); } onConnect(el) { effect(this.kf.bind(this)); effect(this.Ea.bind(this)); effect(this.Ca.bind(this)); effect(this.Ma.bind(this)); effect(this.nl.bind(this)); effect(this.th.bind(this)); } kf() { const img = this.$state.img(); if (!img) return; listenEvent(img, "load", this.tb.bind(this)); listenEvent(img, "error", this.Q.bind(this)); } Ca() { const { crossOrigin: crossOriginProp } = this.$props, { crossOrigin: crossOriginState } = this.$state, { crossOrigin: mediaCrossOrigin } = this.a.$state, crossOrigin = crossOriginProp() !== null ? crossOriginProp() : mediaCrossOrigin(); crossOriginState.set(crossOrigin === true ? "anonymous" : crossOrigin); } Ma() { const { src, loading, error } = this.$state; if (src()) { loading.set(true); error.set(null); } return () => { this.ol(); loading.set(false); error.set(null); }; } tb() { const { loading, error } = this.$state; this.th(); loading.set(false); error.set(null); } Q(event) { const { loading, error } = this.$state; loading.set(false); error.set(event); } Pc() { const { loading, hidden } = this.$state; return !hidden() && loading(); } fb() { const { error } = this.$state; return !isNull(error()); } Ea() { const { hidden } = this.$state, { duration } = this.a.$state, images = this.X.$images(); hidden.set(this.fb() || !Number.isFinite(duration()) || images.length === 0); } uh() { return this.$props.time(); } nl() { let images = this.X.$images(); if (!images.length) return; let time = this.uh(), { src, activeThumbnail } = this.$state, activeIndex = -1, activeImage = null; for (let i = images.length - 1; i >= 0; i--) { const image = images[i]; if (time >= image.startTime && (!image.endTime || time < image.endTime)) { activeIndex = i; break; } } if (images[activeIndex]) { activeImage = images[activeIndex]; } activeThumbnail.set(activeImage); let srcURL = activeImage?.url.href || ""; const cdnURL = this.$props.cdnURL(); if (cdnURL) { if (this.$props.cdnPreload()) { const preloadedThumbs = this.X.$preloadedThumbs(); if (!preloadedThumbs.includes(srcURL)) { const preloadSrcURL = cdnURL.replace("{url}", encodeURIComponent(srcURL)); fetch(preloadSrcURL, { method: "HEAD" }).then(() => { if (!this.X.$preloadedThumbs().includes(srcURL)) this.X.$preloadedThumbs.set([...preloadedThumbs, srcURL]); }).catch((error) => this.Q(error)); } else { srcURL = cdnURL.replace("{url}", encodeURIComponent(srcURL)); } } } src.set(srcURL); } th() { if (!this.scope || this.$state.hidden()) return; const rootEl = this.el, imgEl = this.$state.img(), thumbnail = this.$state.activeThumbnail(); if (!imgEl || !thumbnail || !rootEl) return; let width = thumbnail.width ?? imgEl.naturalWidth, height = thumbnail?.height ?? imgEl.naturalHeight, { maxWidth, maxHeight, minWidth, minHeight, width: elWidth, height: elHeight } = getComputedStyle(this.el); if (minWidth === "100%") minWidth = parseFloat(elWidth) + ""; if (minHeight === "100%") minHeight = parseFloat(elHeight) + ""; let minRatio = Math.max(parseInt(minWidth) / width, parseInt(minHeight) / height), maxRatio = Math.min( Math.max(parseInt(minWidth), parseInt(maxWidth)) / width, Math.max(parseInt(minHeight), parseInt(maxHeight)) / height ), scale = !isNaN(maxRatio) && maxRatio < 1 ? maxRatio : minRatio > 1 ? minRatio : 1; this.Vb(rootEl, "--thumbnail-width", `${width * scale}px`, false); this.Vb(rootEl, "--thumbnail-height", `${height * scale}px`, false); this.Vb(imgEl, "width", `${imgEl.naturalWidth * scale}px`); this.Vb(imgEl, "height", `${imgEl.naturalHeight * scale}px`); this.Vb( imgEl, "transform", thumbnail.coords ? `translate(-${thumbnail.coords.x * scale}px, -${thumbnail.coords.y * scale}px)` : "" ); this.Vb(imgEl, "max-width", "none"); } Vb(el, name, value, reset = true) { el.style.setProperty(name, value); if (reset) this.jf.push(() => el.style.removeProperty(name)); } ol() { for (const reset of this.jf) reset(); this.jf = []; } } var __defProp$6 = Object.defineProperty; var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor; var __decorateClass$6 = (decorators, target, key, kind) => { var result = __getOwnPropDesc$6(target, key) ; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (decorator(target, key, result) ) || result; if (result) __defProp$6(target, key, result); return result; }; class SliderValue extends Component { static { this.props = { type: "pointer", format: null, showHours: false, showMs: false, padHours: null, padMinutes: null, decimalPlaces: 2 }; } onSetup() { this.ia = useState(Slider.state); this.Qc = useContext(sliderValueFormatContext); this.sl = computed(this.getValueText.bind(this)); } getValueText() { const { type, format, decimalPlaces, padHours, padMinutes, showHours, showMs } = this.$props, { value: sliderValue, pointerValue, min, max } = this.ia, _format = format?.() ?? this.Qc.default; const value = type() === "current" ? sliderValue() : pointerValue(); if (_format === "percent") { const range = max() - min(); const percent = value / range * 100; return (this.Qc.percent ?? round)(percent, decimalPlaces()) + "%"; } else if (_format === "time") { return (this.Qc.time ?? formatTime)(value, { padHrs: padHours(), padMins: padMinutes(), showHrs: showHours(), showMs: showMs() }); } else { return (this.Qc.value?.(value) ?? value.toFixed(2)) + ""; } } } __decorateClass$6([ method ], SliderValue.prototype, "getValueText"); class SliderPreview extends Component { constructor() { super(...arguments); this.vh = animationFrameThrottle(() => { const { Ed: _disabled, bb: _orientation } = this.ia; if (_disabled()) return; const el = this.el, { offset, noClamp } = this.$props; if (!el) return; updateSliderPreviewPlacement(el, { clamp: !noClamp(), offset: offset(), orientation: _orientation() }); }); } static { this.props = { offset: 0, noClamp: false }; } onSetup() { this.ia = useContext(sliderContext); const { active } = useState(Slider.state); this.setAttributes({ "data-visible": active }); } onAttach(el) { Object.assign(el.style, { position: "absolute", top: 0, left: 0, width: "max-content" }); } onConnect(el) { const { nh: _preview } = this.ia; _preview.set(el); onDispose(() => _preview.set(null)); effect(this.vh.bind(this)); const resize = new ResizeObserver(this.vh.bind(this)); resize.observe(el); onDispose(() => resize.disconnect()); } } function updateSliderPreviewPlacement(el, { clamp, offset, orientation }) { const computedStyle = getComputedStyle(el), width = parseFloat(computedStyle.width), height = parseFloat(computedStyle.height), styles = { top: null, right: null, bottom: null, left: null }; styles[orientation === "horizontal" ? "bottom" : "left"] = `calc(100% + var(--media-slider-preview-offset, ${offset}px))`; if (orientation === "horizontal") { const widthHalf = width / 2; if (!clamp) { styles.left = `calc(var(--slider-pointer) - ${widthHalf}px)`; } else { const leftClamp = `max(0px, calc(var(--slider-pointer) - ${widthHalf}px))`, rightClamp = `calc(100% - ${width}px)`; styles.left = `min(${leftClamp}, ${rightClamp})`; } } else { const heightHalf = height / 2; if (!clamp) { styles.bottom = `calc(var(--slider-pointer) - ${heightHalf}px)`; } else { const topClamp = `max(${heightHalf}px, calc(var(--slider-pointer) - ${heightHalf}px))`, bottomClamp = `calc(100% - ${height}px)`; styles.bottom = `min(${topClamp}, ${bottomClamp})`; } } Object.assign(el.style, styles); } class VolumeSlider extends Component { constructor() { super(...arguments); this.wh = functionThrottle(this.Na.bind(this), 25); } static { this.props = { ...SliderController.props, keyStep: 5, shiftKeyMultiplier: 2 }; } static { this.state = sliderState; } onSetup() { this.a = useMediaContext(); const { audioGain } = this.a.$state; provideContext(sliderValueFormatContext, { default: "percent", value(value) { return (value * (audioGain() ?? 1)).toFixed(2); }, percent(value) { return Math.round(value * (audioGain() ?? 1)); } }); new SliderController({ qa: this.$props.step, eb: this.$props.keyStep, Da: Math.round, v: this.v.bind(this), hf: this.hf.bind(this), O: this.O.bind(this), P: this.P.bind(this), S: this.S.bind(this), l: this.l.bind(this) }).attach(this); effect(this.Fc.bind(this)); } onAttach(el) { el.setAttribute("data-media-volume-slider", ""); setAttributeIfEmpty(el, "aria-label", "Volume"); const { canSetVolume } = this.a.$state; this.setAttributes({ "data-supported": canSetVolume, "aria-hidden": $ariaBool(() => !canSetVolume()) }); } O() { const { value } = this.$state, { audioGain } = this.a.$state; return Math.round(value() * (audioGain() ?? 1)); } P() { const { value, max } = this.$state, { audioGain } = this.a.$state; return round(value() / max() * (audioGain() ?? 1) * 100, 2) + "%"; } hf() { const { audioGain } = this.a.$state; return this.$state.max() * (audioGain() ?? 1); } v() { const { disabled } = this.$props, { canSetVolume } = this.a.$state; return disabled() || !canSetVolume(); } Fc() { const { muted, volume } = this.a.$state; const newValue = muted() ? 0 : volume() * 100; this.$state.value.set(newValue); this.dispatch("value-change", { detail: newValue }); } Na(event) { if (!event.trigger) return; const mediaVolume = round(event.detail / 100, 3); this.a.remote.changeVolume(mediaVolume, event); } l(event) { this.wh(event); } S(event) { this.wh(event); } } class TimeSlider extends Component { constructor() { super(); this.Ah = signal(null); this.mf = false; const { noSwipeGesture } = this.$props; new SliderController({ kh: () => !noSwipeGesture(), Y: this.Y.bind(this), qa: this.qa.bind(this), eb: this.eb.bind(this), Da: this.Da, v: this.v.bind(this), O: this.O.bind(this), P: this.P.bind(this), ef: this.ef.bind(this), S: this.S.bind(this), Dd: this.Dd.bind(this), l: this.l.bind(this) }); } static { this.props = { ...SliderController.props, step: 0.1, keyStep: 5, shiftKeyMultiplier: 2, pauseWhileDragging: false, noSwipeGesture: false, seekingRequestThrottle: 100 }; } static { this.state = sliderState; } onSetup() { this.a = useMediaContext(); provideContext(sliderValueFormatContext, { default: "time", value: this.xl.bind(this), time: this.yl.bind(this) }); this.setAttributes({ "data-chapters": this.zl.bind(this) }); this.setStyles({ "--slider-progress": this.Al.bind(this) }); effect(this.Qb.bind(this)); effect(this.Bl.bind(this)); } onAttach(el) { el.setAttribute("data-media-time-slider", ""); setAttributeIfEmpty(el, "aria-label", "Seek"); } onConnect(el) { effect(this.Cl.bind(this)); watchActiveTextTrack(this.a.textTracks, "chapters", this.Ah.set); } Al() { const { bufferedEnd, duration } = this.a.$state; return round(Math.min(bufferedEnd() / Math.max(duration(), 1), 1) * 100, 3) + "%"; } zl() { const { duration } = this.a.$state; return this.Ah()?.cues.length && Number.isFinite(duration()) && duration() > 0; } Bl() { this.lf = functionThrottle( this.Ja.bind(this), this.$props.seekingRequestThrottle() ); } Qb() { if (this.$state.hidden()) return; const { value, dragging } = this.$state, newValue = this.Y(); if (!peek(dragging)) { value.set(newValue); this.dispatch("value-change", { detail: newValue }); } } Cl() { const player = this.a.player.el, { nh: _preview } = useContext(sliderContext); player && _preview() && setAttribute(player, "data-preview", this.$state.active()); } Ja(time, event) { this.a.remote.seeking(time, event); } Dl(time, percent, event) { this.lf.cancel(); const { live } = this.a.$state; if (live() && percent >= 99) { this.a.remote.seekToLiveEdge(event); return; } this.a.remote.seek(time, event); } ef(event) { const { pauseWhileDragging } = this.$props; if (pauseWhileDragging()) { const { paused } = this.a.$state; this.mf = !paused(); this.a.remote.pause(event); } } S(event) { this.lf(this.Wb(event.detail), event); } Dd(event) { const { seeking } = this.a.$state; if (!peek(seeking)) this.Ja(this.Wb(event.detail), event); const percent = event.detail; this.Dl(this.Wb(percent), percent, event); const { pauseWhileDragging } = this.$props; if (pauseWhileDragging() && this.mf) { this.a.remote.play(event); this.mf = false; } } l(event) { const { dragging } = this.$state; if (dragging() || !event.trigger) return; this.Dd(event); } // ------------------------------------------------------------------------------------------- // Props // ------------------------------------------------------------------------------------------- Y() { const { currentTime } = this.a.$state; return this.El(currentTime()); } qa() { const value = this.$props.step() / this.a.$state.duration() * 100; return Number.isFinite(value) ? value : 1; } eb() { const value = this.$props.keyStep() / this.a.$state.duration() * 100; return Number.isFinite(value) ? value : 1; } Da(value) { return round(value, 3); } v() { const { disabled } = this.$props, { canSeek } = this.a.$state; return disabled() || !canSeek(); } // ------------------------------------------------------------------------------------------- // ARIA // ------------------------------------------------------------------------------------