UNPKG

@ktt45678/vidstack

Version:

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

1,499 lines (1,469 loc) 61.5 kB
import { createContext, useContext, Component, computed, provideContext, signal, isBoolean, prop, effect, setAttribute, uppercaseFirstChar, isNil, noop, isString, isFunction, unwrap, isArray, camelToKebabCase, tick, peek, isKeyboardClick, toggleClass, Host, listenEvent } from './vidstack-41uXLVgN.js'; import { useMediaContext, watchColorScheme, useMediaState, getDownloadFile, watchActiveTextTrack, useResizeObserver, useActive, useTransitionActive, isHTMLElement, createSlot } from './vidstack-DPeH8lGJ.js'; import { $signal, LayoutIconsLoader, Icon, SlotManager } from './vidstack-Bw60Npp4.js'; import { LitElement } from './vidstack-D2YigfqZ.js'; import { html } from 'lit-html'; import { ref } from 'lit-html/directives/ref.js'; import { keyed } from 'lit-html/directives/keyed.js'; import { $ariaBool, sortVideoQualities } from './vidstack-BOTZD4tC.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; const defaultLayoutContext = createContext(); function useDefaultLayoutContext() { return useContext(defaultLayoutContext); } const defaultLayoutProps = { colorScheme: "system", download: null, customIcons: false, disableTimeSlider: false, menuContainer: null, menuGroup: "bottom", noAudioGain: false, noGestures: false, noKeyboardAnimations: false, noModal: false, noScrubGesture: false, playbackRates: { min: 0, max: 2, step: 0.25 }, audioGains: { min: 0, max: 300, step: 25 }, seekStep: 10, sliderChaptersMinWidth: 325, hideQualityBitrate: false, smallWhen: false, thumbnails: null, translations: null, when: false }; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __decorateClass = (decorators, target, key, kind) => { var result = __getOwnPropDesc(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(target, key, result); return result; }; class DefaultLayout extends Component { constructor() { super(...arguments); this._when = computed(() => { const when = this.$props.when(); return this._matches(when); }); this._smallWhen = computed(() => { const when = this.$props.smallWhen(); return this._matches(when); }); } static { this.props = defaultLayoutProps; } get isMatch() { return this._when(); } get isSmallLayout() { return this._smallWhen(); } onSetup() { this._media = useMediaContext(); this.setAttributes({ "data-match": this._when, "data-sm": () => this._smallWhen() ? "" : null, "data-lg": () => !this._smallWhen() ? "" : null, "data-size": () => this._smallWhen() ? "sm" : "lg", "data-no-scrub-gesture": this.$props.noScrubGesture }); provideContext(defaultLayoutContext, { ...this.$props, when: this._when, smallWhen: this._smallWhen, userPrefersAnnouncements: signal(true), userPrefersKeyboardAnimations: signal(true), menuPortal: signal(null) }); } onAttach(el) { watchColorScheme(el, this.$props.colorScheme); } _matches(query) { return query !== "never" && (isBoolean(query) ? query : computed(() => query(this._media.player.state))()); } } __decorateClass([ prop ], DefaultLayout.prototype, "isMatch"); __decorateClass([ prop ], DefaultLayout.prototype, "isSmallLayout"); let DefaultAudioLayout$1 = class DefaultAudioLayout extends DefaultLayout { static { this.props = { ...super.props, when: ({ viewType }) => viewType === "audio", smallWhen: ({ width }) => width < 576 }; } }; function setLayoutName(name, isMatch) { effect(() => { const { player } = useMediaContext(), el = player.el; el && setAttribute(el, "data-layout", isMatch() && name); return () => el?.removeAttribute("data-layout"); }); } function i18n(translations, word) { return translations()?.[word] ?? word; } function DefaultAnnouncer() { return $signal(() => { const { translations, userPrefersAnnouncements } = useDefaultLayoutContext(); if (!userPrefersAnnouncements()) return null; return html`<media-announcer .translations=${$signal(translations)}></media-announcer>`; }); } function IconSlot(name, classes = "") { return html`<slot name=${`${name}-icon`} data-class=${`vds-icon vds-${name}-icon${classes ? ` ${classes}` : ""}`} ></slot>`; } function IconSlots(names) { return names.map((name) => IconSlot(name)); } function $i18n(translations, word) { return $signal(() => i18n(translations, word)); } function DefaultAirPlayButton({ tooltip }) { const { translations } = useDefaultLayoutContext(), { remotePlaybackState } = useMediaState(), $label = $signal(() => { const airPlayText = i18n(translations, "AirPlay"), stateText = uppercaseFirstChar(remotePlaybackState()); return `${airPlayText} ${stateText}`; }), $airPlayText = $i18n(translations, "AirPlay"); return html` <media-tooltip class="vds-airplay-tooltip vds-tooltip"> <media-tooltip-trigger> <media-airplay-button class="vds-airplay-button vds-button" aria-label=${$label}> ${IconSlot("airplay")} </media-airplay-button> </media-tooltip-trigger> <media-tooltip-content class="vds-tooltip-content" placement=${tooltip}> <span class="vds-airplay-tooltip-text">${$airPlayText}</span> </media-tooltip-content> </media-tooltip> `; } function DefaultGoogleCastButton({ tooltip }) { const { translations } = useDefaultLayoutContext(), { remotePlaybackState } = useMediaState(), $label = $signal(() => { const googleCastText = i18n(translations, "Google Cast"), stateText = uppercaseFirstChar(remotePlaybackState()); return `${googleCastText} ${stateText}`; }), $googleCastText = $i18n(translations, "Google Cast"); return html` <media-tooltip class="vds-google-cast-tooltip vds-tooltip"> <media-tooltip-trigger> <media-google-cast-button class="vds-google-cast-button vds-button" aria-label=${$label}> ${IconSlot("google-cast")} </media-google-cast-button> </media-tooltip-trigger> <media-tooltip-content class="vds-tooltip-content" placement=${tooltip}> <span class="vds-google-cast-tooltip-text">${$googleCastText}</span> </media-tooltip-content> </media-tooltip> `; } function DefaultPlayButton({ tooltip }) { const { translations } = useDefaultLayoutContext(), $playText = $i18n(translations, "Play"), $pauseText = $i18n(translations, "Pause"); return html` <media-tooltip class="vds-play-tooltip vds-tooltip"> <media-tooltip-trigger> <media-play-button class="vds-play-button vds-button" aria-label=${$i18n(translations, "Play")} > ${IconSlots(["play", "pause", "replay"])} </media-play-button> </media-tooltip-trigger> <media-tooltip-content class="vds-tooltip-content" placement=${tooltip}> <span class="vds-play-tooltip-text">${$playText}</span> <span class="vds-pause-tooltip-text">${$pauseText}</span> </media-tooltip-content> </media-tooltip> `; } function DefaultMuteButton({ tooltip, ref: ref$1 = noop }) { const { translations } = useDefaultLayoutContext(), $muteText = $i18n(translations, "Mute"), $unmuteText = $i18n(translations, "Unmute"); return html` <media-tooltip class="vds-mute-tooltip vds-tooltip"> <media-tooltip-trigger> <media-mute-button class="vds-mute-button vds-button" aria-label=${$i18n(translations, "Mute")} ${ref(ref$1)} > ${IconSlots(["mute", "volume-low", "volume-high"])} </media-mute-button> </media-tooltip-trigger> <media-tooltip-content class="vds-tooltip-content" placement=${tooltip}> <span class="vds-mute-tooltip-text">${$unmuteText}</span> <span class="vds-unmute-tooltip-text">${$muteText}</span> </media-tooltip-content> </media-tooltip> `; } function DefaultCaptionButton({ tooltip }) { const { translations } = useDefaultLayoutContext(), $ccOnText = $i18n(translations, "Closed-Captions On"), $ccOffText = $i18n(translations, "Closed-Captions Off"); return html` <media-tooltip class="vds-caption-tooltip vds-tooltip"> <media-tooltip-trigger> <media-caption-button class="vds-caption-button vds-button" aria-label=${$i18n(translations, "Captions")} > ${IconSlots(["cc-on", "cc-off"])} </media-caption-button> </media-tooltip-trigger> <media-tooltip-content class="vds-tooltip-content" placement=${tooltip}> <span class="vds-cc-on-tooltip-text">${$ccOffText}</span> <span class="vds-cc-off-tooltip-text">${$ccOnText}</span> </media-tooltip-content> </media-tooltip> `; } function DefaultPIPButton() { const { translations } = useDefaultLayoutContext(), $enterText = $i18n(translations, "Enter PiP"), $exitText = $i18n(translations, "Exit PiP"); return html` <media-tooltip class="vds-pip-tooltip vds-tooltip"> <media-tooltip-trigger> <media-pip-button class="vds-pip-button vds-button" aria-label=${$i18n(translations, "PiP")} > ${IconSlots(["pip-enter", "pip-exit"])} </media-pip-button> </media-tooltip-trigger> <media-tooltip-content class="vds-tooltip-content"> <span class="vds-pip-enter-tooltip-text">${$enterText}</span> <span class="vds-pip-exit-tooltip-text">${$exitText}</span> </media-tooltip-content> </media-tooltip> `; } function DefaultFullscreenButton({ tooltip }) { const { translations } = useDefaultLayoutContext(), $enterText = $i18n(translations, "Enter Fullscreen"), $exitText = $i18n(translations, "Exit Fullscreen"); return html` <media-tooltip class="vds-fullscreen-tooltip vds-tooltip"> <media-tooltip-trigger> <media-fullscreen-button class="vds-fullscreen-button vds-button" aria-label=${$i18n(translations, "Fullscreen")} > ${IconSlots(["fs-enter", "fs-exit"])} </media-fullscreen-button> </media-tooltip-trigger> <media-tooltip-content class="vds-tooltip-content" placement=${tooltip}> <span class="vds-fs-enter-tooltip-text">${$enterText}</span> <span class="vds-fs-exit-tooltip-text">${$exitText}</span> </media-tooltip-content> </media-tooltip> `; } function DefaultSeekButton({ backward, tooltip }) { const { translations, seekStep } = useDefaultLayoutContext(), seekText = !backward ? "Seek Forward" : "Seek Backward", $label = $i18n(translations, seekText), $seconds = () => (backward ? -1 : 1) * seekStep(); return html` <media-tooltip class="vds-seek-tooltip vds-tooltip"> <media-tooltip-trigger> <media-seek-button class="vds-seek-button vds-button" seconds=${$signal($seconds)} aria-label=${$label} > ${!backward ? IconSlot("seek-forward") : IconSlot("seek-backward")} </media-seek-button> </media-tooltip-trigger> <media-tooltip-content class="vds-tooltip-content" placement=${tooltip}> ${$i18n(translations, seekText)} </media-tooltip-content> </media-tooltip> `; } function DefaultLiveButton() { const { translations } = useDefaultLayoutContext(), { live } = useMediaState(), $label = $i18n(translations, "Skip To Live"), $liveText = $i18n(translations, "LIVE"); return live() ? html` <media-live-button class="vds-live-button" aria-label=${$label}> <span class="vds-live-button-text">${$liveText}</span> </media-live-button> ` : null; } function DefaultDownloadButton() { return $signal(() => { const { download, translations } = useDefaultLayoutContext(), $download = download(); if (isNil($download)) return null; const { source, title } = useMediaState(), $src = source(), file = getDownloadFile({ title: title(), src: $src, download: $download }); return file ? html` <media-tooltip class="vds-download-tooltip vds-tooltip"> <media-tooltip-trigger> <a role="button" class="vds-download-button vds-button" aria-label=${$i18n(translations, "Download")} href=${file.url + `?download=${file.name}`} download=${file.name} target="_blank" > <slot name="download-icon" data-class="vds-icon" /> </a> </media-tooltip-trigger> <media-tooltip-content class="vds-tooltip-content" placement="top"> ${$i18n(translations, "Download")} </media-tooltip-content> </media-tooltip> ` : null; }); } function DefaultCaptions() { const { translations } = useDefaultLayoutContext(); return html` <media-captions class="vds-captions" .exampleText=${$i18n(translations, "Captions look like this")} ></media-captions> `; } function DefaultControlsSpacer() { return html`<div class="vds-controls-spacer"></div>`; } function MenuPortal(container, template) { return html` <media-menu-portal .container=${$signal(container)} disabled="fullscreen"> ${template} </media-menu-portal> `; } function createMenuContainer(layoutEl, rootSelector, className, isSmallLayout) { let root = isString(rootSelector) ? document.querySelector(rootSelector) : rootSelector; if (!root) root = layoutEl?.closest("dialog"); if (!root) root = document.body; const container = document.createElement("div"); container.style.display = "contents"; container.classList.add(className); root.append(container); effect(() => { if (!container) return; const { viewType } = useMediaState(), isSmall = isSmallLayout(); setAttribute(container, "data-view-type", viewType()); setAttribute(container, "data-sm", isSmall); setAttribute(container, "data-lg", !isSmall); setAttribute(container, "data-size", isSmall ? "sm" : "lg"); }); const { colorScheme } = useDefaultLayoutContext(); watchColorScheme(container, colorScheme); return container; } function DefaultChaptersMenu({ placement, tooltip, portal }) { const { textTracks } = useMediaContext(), { viewType, clipStartTime, clipEndTime } = useMediaState(), { translations, thumbnails, menuPortal, noModal, menuGroup, smallWhen: smWhen } = useDefaultLayoutContext(), $disabled = computed(() => { const $startTime = clipStartTime(), $endTime = clipEndTime() || Infinity, $track = signal(null); watchActiveTextTrack(textTracks, "chapters", $track.set); const cues = $track()?.cues.filter( (cue) => cue.startTime <= $endTime && cue.endTime >= $startTime ); return !cues?.length; }); if ($disabled()) return null; const $placement = computed( () => noModal() ? unwrap(placement) : !smWhen() ? unwrap(placement) : null ), $offset = computed( () => !smWhen() && menuGroup() === "bottom" && viewType() === "video" ? 26 : 0 ), $isOpen = signal(false); function onOpen() { $isOpen.set(true); } function onClose() { $isOpen.set(false); } const items = html` <media-menu-items class="vds-chapters-menu-items vds-menu-items" placement=${$signal($placement)} offset=${$signal($offset)} > ${$signal(() => { if (!$isOpen()) return null; return html` <media-chapters-radio-group class="vds-chapters-radio-group vds-radio-group" .thumbnails=${$signal(thumbnails)} > <template> <media-radio class="vds-chapter-radio vds-radio"> <media-thumbnail class="vds-thumbnail"></media-thumbnail> <div class="vds-chapter-radio-content"> <span class="vds-chapter-radio-label" data-part="label"></span> <span class="vds-chapter-radio-start-time" data-part="start-time"></span> <span class="vds-chapter-radio-duration" data-part="duration"></span> </div> </media-radio> </template> </media-chapters-radio-group> `; })} </media-menu-items> `; return html` <media-menu class="vds-chapters-menu vds-menu" @open=${onOpen} @close=${onClose}> <media-tooltip class="vds-tooltip"> <media-tooltip-trigger> <media-menu-button class="vds-menu-button vds-button" aria-label=${$i18n(translations, "Chapters")} > ${IconSlot("menu-chapters")} </media-menu-button> </media-tooltip-trigger> <media-tooltip-content class="vds-tooltip-content" placement=${isFunction(tooltip) ? $signal(tooltip) : tooltip} > ${$i18n(translations, "Chapters")} </media-tooltip-content> </media-tooltip> ${portal ? MenuPortal(menuPortal, items) : items} </media-menu> `; } const FONT_COLOR_OPTION = { type: "color" }; const FONT_FAMILY_OPTION = { type: "radio", values: { "Monospaced Serif": "mono-serif", "Proportional Serif": "pro-serif", "Monospaced Sans-Serif": "mono-sans", "Proportional Sans-Serif": "pro-sans", Casual: "casual", Cursive: "cursive", "Small Capitals": "capitals" } }; const FONT_SIZE_OPTION = { type: "slider", min: 0, max: 400, step: 25, upIcon: null, downIcon: null }; const FONT_OPACITY_OPTION = { type: "slider", min: 0, max: 100, step: 5, upIcon: null, downIcon: null }; const FONT_TEXT_SHADOW_OPTION = { type: "radio", values: ["None", "Drop Shadow", "Raised", "Depressed", "Outline"] }; const FONT_DEFAULTS = { fontFamily: "pro-sans", fontSize: "100%", textColor: "#ffffff", textOpacity: "100%", textShadow: "none", textBg: "#000000", textBgOpacity: "100%", displayBg: "#000000", displayBgOpacity: "0%" }; const FONT_SIGNALS = Object.keys(FONT_DEFAULTS).reduce( (prev, type) => ({ ...prev, [type]: signal(FONT_DEFAULTS[type]) }), {} ); function onFontReset() { for (const type of Object.keys(FONT_SIGNALS)) { const defaultValue = FONT_DEFAULTS[type]; FONT_SIGNALS[type].set(defaultValue); } } let sectionId = 0; function DefaultMenuSection({ label = "", value = "", children }) { if (!label) { return html` <div class="vds-menu-section"> <div class="vds-menu-section-body">${children}</div> </div> `; } const id = `vds-menu-section-${++sectionId}`; return html` <section class="vds-menu-section" role="group" aria-labelledby=${id}> <div class="vds-menu-section-title"> <header id=${id}>${label}</header> ${value ? html`<div class="vds-menu-section-value">${value}</div>` : null} </div> <div class="vds-menu-section-body">${children}</div> </section> `; } function DefaultMenuItem({ label, children }) { return html` <div class="vds-menu-item"> <div class="vds-menu-item-label">${label}</div> ${children} </div> `; } function DefaultMenuButton({ label, icon, hint }) { return html` <media-menu-button class="vds-menu-item"> ${IconSlot("menu-arrow-left", "vds-menu-close-icon")} ${icon ? IconSlot(icon, "vds-menu-item-icon") : null} <span class="vds-menu-item-label">${$signal(label)}</span> <span class="vds-menu-item-hint" data-part="hint">${hint ? $signal(hint) : null} </span> ${IconSlot("menu-arrow-right", "vds-menu-open-icon")} </media-menu-button> `; } function DefaultRadioGroup({ value = null, options, hideLabel = false, children = null, onChange = null }) { function renderRadio(option) { const { value: value2, label: content } = option; return html` <media-radio class="vds-radio" value=${value2}> ${IconSlot("menu-radio-check")} ${!hideLabel ? html` <span class="vds-radio-label" data-part="label"> ${isString(content) ? content : $signal(content)} </span> ` : null} ${isFunction(children) ? children(option) : children} </media-radio> `; } return html` <media-radio-group class="vds-radio-group" value=${isString(value) ? value : value ? $signal(value) : ""} @change=${onChange} > ${isArray(options) ? options.map(renderRadio) : $signal(() => options().map(renderRadio))} </media-radio-group> `; } function createRadioOptions(entries) { return isArray(entries) ? entries.map((entry) => ({ label: entry, value: entry.toLowerCase() })) : Object.keys(entries).map((label) => ({ label, value: entries[label] })); } function DefaultSliderParts() { return html` <div class="vds-slider-track"></div> <div class="vds-slider-track-fill vds-slider-track"></div> <div class="vds-slider-thumb"></div> `; } function DefaultSliderSteps() { return html` <media-slider-steps class="vds-slider-steps"> <template> <div class="vds-slider-step"></div> </template> </media-slider-steps> `; } function DefaultMenuSliderItem({ label = null, value = null, upIcon = "", downIcon = "", children, isMin, isMax }) { const hasTitle = label || value, content = [ downIcon ? IconSlot(downIcon, "down") : null, children, upIcon ? IconSlot(upIcon, "up") : null ]; return html` <div class=${`vds-menu-item vds-menu-slider-item${hasTitle ? " group" : ""}`} data-min=${$signal(() => isMin() ? "" : null)} data-max=${$signal(() => isMax() ? "" : null)} > ${hasTitle ? html` <div class="vds-menu-slider-title"> ${[ label ? html`<div>${label}</div>` : null, value ? html`<div>${value}</div>` : null ]} </div> <div class="vds-menu-slider-body">${content}</div> ` : content} </div> `; } const FONT_SIZE_OPTION_WITH_ICONS = { ...FONT_SIZE_OPTION, upIcon: "menu-opacity-up", downIcon: "menu-opacity-down" }; const FONT_OPACITY_OPTION_WITH_ICONS = { ...FONT_OPACITY_OPTION, upIcon: "menu-opacity-up", downIcon: "menu-opacity-down" }; function DefaultFontMenu() { return $signal(() => { const { hasCaptions } = useMediaState(), { translations } = useDefaultLayoutContext(); if (!hasCaptions()) return null; return html` <media-menu class="vds-font-menu vds-menu"> ${DefaultMenuButton({ label: () => i18n(translations, "Caption Styles") })} <media-menu-items class="vds-menu-items"> ${[ DefaultMenuSection({ label: $i18n(translations, "Font"), children: [DefaultFontFamilyMenu(), DefaultFontSizeSlider()] }), DefaultMenuSection({ label: $i18n(translations, "Text"), children: [ DefaultTextColorInput(), DefaultTextShadowMenu(), DefaultTextOpacitySlider() ] }), DefaultMenuSection({ label: $i18n(translations, "Text Background"), children: [DefaultTextBgInput(), DefaultTextBgOpacitySlider()] }), DefaultMenuSection({ label: $i18n(translations, "Display Background"), children: [DefaultDisplayBgInput(), DefaultDisplayOpacitySlider()] }), DefaultMenuSection({ children: [DefaultResetMenuItem()] }) ]} </media-menu-items> </media-menu> `; }); } function DefaultFontFamilyMenu() { return DefaultFontSetting({ label: "Family", option: FONT_FAMILY_OPTION, type: "fontFamily" }); } function DefaultFontSizeSlider() { return DefaultFontSetting({ label: "Size", option: FONT_SIZE_OPTION_WITH_ICONS, type: "fontSize" }); } function DefaultTextColorInput() { return DefaultFontSetting({ label: "Color", option: FONT_COLOR_OPTION, type: "textColor" }); } function DefaultTextOpacitySlider() { return DefaultFontSetting({ label: "Opacity", option: FONT_OPACITY_OPTION_WITH_ICONS, type: "textOpacity" }); } function DefaultTextShadowMenu() { return DefaultFontSetting({ label: "Shadow", option: FONT_TEXT_SHADOW_OPTION, type: "textShadow" }); } function DefaultTextBgInput() { return DefaultFontSetting({ label: "Color", option: FONT_COLOR_OPTION, type: "textBg" }); } function DefaultTextBgOpacitySlider() { return DefaultFontSetting({ label: "Opacity", option: FONT_OPACITY_OPTION_WITH_ICONS, type: "textBgOpacity" }); } function DefaultDisplayBgInput() { return DefaultFontSetting({ label: "Color", option: FONT_COLOR_OPTION, type: "displayBg" }); } function DefaultDisplayOpacitySlider() { return DefaultFontSetting({ label: "Opacity", option: FONT_OPACITY_OPTION_WITH_ICONS, type: "displayBgOpacity" }); } function DefaultResetMenuItem() { const { translations } = useDefaultLayoutContext(), $label = () => i18n(translations, "Reset"); return html` <button class="vds-menu-item" role="menuitem" @click=${onFontReset}> <span class="vds-menu-item-label">${$signal($label)}</span> </button> `; } function DefaultFontSetting({ label, option, type }) { const { player } = useMediaContext(), { translations } = useDefaultLayoutContext(), $currentValue = FONT_SIGNALS[type], $label = () => i18n(translations, label); function notify() { tick(); player.dispatchEvent(new Event("vds-font-change")); } if (option.type === "color") { let onColorChange2 = function(event) { $currentValue.set(event.target.value); notify(); }; return DefaultMenuItem({ label: $signal($label), children: html` <input class="vds-color-picker" type="color" .value=${$signal($currentValue)} @input=${onColorChange2} /> ` }); } if (option.type === "slider") { let onSliderValueChange2 = function(event) { $currentValue.set(event.detail + "%"); notify(); }; const { min, max, step, upIcon, downIcon } = option; return DefaultMenuSliderItem({ label: $signal($label), value: $signal($currentValue), upIcon, downIcon, isMin: () => $currentValue() === min + "%", isMax: () => $currentValue() === max + "%", children: html` <media-slider class="vds-slider" min=${min} max=${max} step=${step} key-step=${step} .value=${$signal(() => parseInt($currentValue()))} aria-label=${$signal($label)} @value-change=${onSliderValueChange2} @drag-value-change=${onSliderValueChange2} > ${DefaultSliderParts()}${DefaultSliderSteps()} </media-slider> ` }); } const radioOptions = createRadioOptions(option.values), $hint = () => { const value = $currentValue(), label2 = radioOptions.find((radio) => radio.value === value)?.label || ""; return i18n(translations, isString(label2) ? label2 : label2()); }; return html` <media-menu class=${`vds-${camelToKebabCase(type)}-menu vds-menu`}> ${DefaultMenuButton({ label: $label, hint: $hint })} <media-menu-items class="vds-menu-items"> ${DefaultRadioGroup({ value: $currentValue, options: radioOptions, onChange({ detail: value }) { $currentValue.set(value); notify(); } })} </media-menu-items> </media-menu> `; } function DefaultMenuCheckbox({ label, checked, defaultChecked = false, storageKey, onChange }) { const { translations } = useDefaultLayoutContext(), savedValue = storageKey ? localStorage.getItem(storageKey) : null, $checked = signal(!!(savedValue ?? defaultChecked)), $active = signal(false), $ariaChecked = $signal($ariaBool($checked)), $label = $i18n(translations, label); if (storageKey) onChange(peek($checked)); if (checked) { effect(() => void $checked.set(checked())); } function onPress(event) { if (event?.button === 1) return; $checked.set((checked2) => !checked2); if (storageKey) localStorage.setItem(storageKey, $checked() ? "1" : ""); onChange($checked(), event); $active.set(false); } function onKeyDown(event) { if (isKeyboardClick(event)) onPress(); } function onActive(event) { if (event.button !== 0) return; $active.set(true); } return html` <div class="vds-menu-checkbox" role="menuitemcheckbox" tabindex="0" aria-label=${$label} aria-checked=${$ariaChecked} data-active=${$signal(() => $active() ? "" : null)} @pointerup=${onPress} @pointerdown=${onActive} @keydown=${onKeyDown} ></div> `; } function DefaultAccessibilityMenu() { return $signal(() => { const { translations } = useDefaultLayoutContext(); return html` <media-menu class="vds-accessibility-menu vds-menu"> ${DefaultMenuButton({ label: () => i18n(translations, "Accessibility"), icon: "menu-accessibility" })} <media-menu-items class="vds-menu-items"> ${[ DefaultMenuSection({ children: [ DefaultAnnouncementsMenuCheckbox(), DefaultKeyboardAnimationsMenuCheckbox() ] }), DefaultMenuSection({ children: [DefaultFontMenu()] }) ]} </media-menu-items> </media-menu> `; }); } function DefaultAnnouncementsMenuCheckbox() { const { userPrefersAnnouncements, translations } = useDefaultLayoutContext(), label = "Announcements"; return DefaultMenuItem({ label: $i18n(translations, label), children: DefaultMenuCheckbox({ label, storageKey: "vds-player::announcements", onChange(checked) { userPrefersAnnouncements.set(checked); } }) }); } function DefaultKeyboardAnimationsMenuCheckbox() { return $signal(() => { const { translations, userPrefersKeyboardAnimations, noKeyboardAnimations } = useDefaultLayoutContext(), { viewType } = useMediaState(), $disabled = computed(() => viewType() !== "video" || noKeyboardAnimations()); if ($disabled()) return null; const label = "Keyboard Animations"; return DefaultMenuItem({ label: $i18n(translations, label), children: DefaultMenuCheckbox({ label, defaultChecked: true, storageKey: "vds-player::keyboard-animations", onChange(checked) { userPrefersKeyboardAnimations.set(checked); } }) }); }); } function DefaultAudioMenu() { return $signal(() => { const { noAudioGain, translations } = useDefaultLayoutContext(), { audioTracks, canSetAudioGain } = useMediaState(), $disabled = computed(() => { const hasGainSlider = canSetAudioGain() && !noAudioGain(); return !hasGainSlider && audioTracks().length <= 1; }); if ($disabled()) return null; return html` <media-menu class="vds-audio-menu vds-menu"> ${DefaultMenuButton({ label: () => i18n(translations, "Audio"), icon: "menu-audio" })} <media-menu-items class="vds-menu-items"> ${[DefaultAudioTracksMenu(), DefaultAudioBoostSection()]} </media-menu-items> </media-menu> `; }); } function DefaultAudioTracksMenu() { return $signal(() => { const { translations } = useDefaultLayoutContext(), { audioTracks } = useMediaState(), $defaultText = $i18n(translations, "Default"), $disabled = computed(() => audioTracks().length <= 1); if ($disabled()) return null; return DefaultMenuSection({ children: html` <media-menu class="vds-audio-tracks-menu vds-menu"> ${DefaultMenuButton({ label: () => i18n(translations, "Track") })} <media-menu-items class="vds-menu-items"> <media-audio-radio-group class="vds-audio-track-radio-group vds-radio-group" empty-label=${$defaultText} > <template> <media-radio class="vds-audio-track-radio vds-radio"> <slot name="menu-radio-check-icon" data-class="vds-icon"></slot> <span class="vds-radio-label" data-part="label"></span> </media-radio> </template> </media-audio-radio-group> </media-menu-items> </media-menu> ` }); }); } function DefaultAudioBoostSection() { return $signal(() => { const { noAudioGain, translations } = useDefaultLayoutContext(), { canSetAudioGain } = useMediaState(), $disabled = computed(() => !canSetAudioGain() || noAudioGain()); if ($disabled()) return null; const { audioGain } = useMediaState(); return DefaultMenuSection({ label: $i18n(translations, "Boost"), value: $signal(() => Math.round(((audioGain() ?? 1) - 1) * 100) + "%"), children: [ DefaultMenuSliderItem({ upIcon: "menu-audio-boost-up", downIcon: "menu-audio-boost-down", children: DefaultAudioGainSlider(), isMin: () => ((audioGain() ?? 1) - 1) * 100 <= getGainMin(), isMax: () => ((audioGain() ?? 1) - 1) * 100 === getGainMax() }) ] }); }); } function DefaultAudioGainSlider() { const { translations } = useDefaultLayoutContext(), $label = $i18n(translations, "Boost"), $min = getGainMin, $max = getGainMax, $step = getGainStep; return html` <media-audio-gain-slider class="vds-audio-gain-slider vds-slider" aria-label=${$label} min=${$signal($min)} max=${$signal($max)} step=${$signal($step)} key-step=${$signal($step)} > ${DefaultSliderParts()}${DefaultSliderSteps()} </media-audio-gain-slider> `; } function getGainMin() { const { audioGains } = useDefaultLayoutContext(), gains = audioGains(); return isArray(gains) ? gains[0] ?? 0 : gains.min; } function getGainMax() { const { audioGains } = useDefaultLayoutContext(), gains = audioGains(); return isArray(gains) ? gains[gains.length - 1] ?? 300 : gains.max; } function getGainStep() { const { audioGains } = useDefaultLayoutContext(), gains = audioGains(); return isArray(gains) ? gains[1] - gains[0] || 25 : gains.step; } function DefaultCaptionsMenu() { return $signal(() => { const { translations } = useDefaultLayoutContext(), { hasCaptions } = useMediaState(), $offText = $i18n(translations, "Off"); if (!hasCaptions()) return null; return html` <media-menu class="vds-captions-menu vds-menu"> ${DefaultMenuButton({ label: () => i18n(translations, "Captions"), icon: "menu-captions" })} <media-menu-items class="vds-menu-items"> <media-captions-radio-group class="vds-captions-radio-group vds-radio-group" off-label=${$offText} > <template> <media-radio class="vds-caption-radio vds-radio"> <slot name="menu-radio-check-icon" data-class="vds-icon"></slot> <span class="vds-radio-label" data-part="label"></span> </media-radio> </template> </media-captions-radio-group> </media-menu-items> </media-menu> `; }); } function DefaultPlaybackMenu() { return $signal(() => { const { translations } = useDefaultLayoutContext(); return html` <media-menu class="vds-playback-menu vds-menu"> ${DefaultMenuButton({ label: () => i18n(translations, "Playback"), icon: "menu-playback" })} <media-menu-items class="vds-menu-items"> ${[ DefaultMenuSection({ children: DefaultLoopCheckbox() }), DefaultSpeedMenuSection(), DefaultQualityMenuSection() ]} </media-menu-items> </media-menu> `; }); } function DefaultLoopCheckbox() { const { remote } = useMediaContext(), { translations } = useDefaultLayoutContext(), label = "Loop"; return DefaultMenuItem({ label: $i18n(translations, label), children: DefaultMenuCheckbox({ label, storageKey: "vds-player::user-loop", onChange(checked, trigger) { remote.userPrefersLoopChange(checked, trigger); } }) }); } function DefaultSpeedMenuSection() { return $signal(() => { const { translations } = useDefaultLayoutContext(), { canSetPlaybackRate, playbackRate } = useMediaState(); if (!canSetPlaybackRate()) return null; return DefaultMenuSection({ label: $i18n(translations, "Speed"), value: $signal( () => playbackRate() === 1 ? i18n(translations, "Normal") : playbackRate() + "x" ), children: [ DefaultMenuSliderItem({ upIcon: "menu-speed-up", downIcon: "menu-speed-down", children: DefaultSpeedSlider(), isMin: () => playbackRate() === getSpeedMin(), isMax: () => playbackRate() === getSpeedMax() }) ] }); }); } function getSpeedMin() { const { playbackRates } = useDefaultLayoutContext(), rates = playbackRates(); return isArray(rates) ? rates[0] ?? 0 : rates.min; } function getSpeedMax() { const { playbackRates } = useDefaultLayoutContext(), rates = playbackRates(); return isArray(rates) ? rates[rates.length - 1] ?? 2 : rates.max; } function getSpeedStep() { const { playbackRates } = useDefaultLayoutContext(), rates = playbackRates(); return isArray(rates) ? rates[1] - rates[0] || 0.25 : rates.step; } function DefaultSpeedSlider() { const { translations } = useDefaultLayoutContext(), $label = $i18n(translations, "Speed"), $min = getSpeedMin, $max = getSpeedMax, $step = getSpeedStep; return html` <media-speed-slider class="vds-speed-slider vds-slider" aria-label=${$label} min=${$signal($min)} max=${$signal($max)} step=${$signal($step)} key-step=${$signal($step)} > ${DefaultSliderParts()}${DefaultSliderSteps()} </media-speed-slider> `; } function DefaultAutoQualityCheckbox() { const { remote, qualities } = useMediaContext(), { autoQuality, canSetQuality, qualities: $qualities } = useMediaState(), { translations } = useDefaultLayoutContext(), label = "Auto", $disabled = computed(() => !canSetQuality() || $qualities().length <= 1); if ($disabled()) return null; return DefaultMenuItem({ label: $i18n(translations, label), children: DefaultMenuCheckbox({ label, checked: autoQuality, onChange(checked, trigger) { if (checked) { remote.requestAutoQuality(trigger); } else { remote.changeQuality(qualities.selectedIndex, trigger); } } }) }); } function DefaultQualityMenuSection() { return $signal(() => { const { hideQualityBitrate, translations } = useDefaultLayoutContext(), { canSetQuality, qualities, quality } = useMediaState(), $disabled = computed(() => !canSetQuality() || qualities().length <= 1), $sortedQualities = computed(() => sortVideoQualities(qualities())); if ($disabled()) return null; return DefaultMenuSection({ label: $i18n(translations, "Quality"), value: $signal(() => { const height = quality()?.height, bitrate = !hideQualityBitrate() ? quality()?.bitrate : null, bitrateText = bitrate && bitrate > 0 ? `${(bitrate / 1e6).toFixed(2)} Mbps` : null, autoText = i18n(translations, "Auto"); return height ? `${height}p${bitrateText ? ` (${bitrateText})` : ""}` : autoText; }), children: [ DefaultMenuSliderItem({ upIcon: "menu-quality-up", downIcon: "menu-quality-down", children: DefaultQualitySlider(), isMin: () => $sortedQualities()[0] === quality(), isMax: () => $sortedQualities().at(-1) === quality() }), DefaultAutoQualityCheckbox() ] }); }); } function DefaultQualitySlider() { const { translations } = useDefaultLayoutContext(), $label = $i18n(translations, "Quality"); return html` <media-quality-slider class="vds-quality-slider vds-slider" aria-label=${$label}> ${DefaultSliderParts()}${DefaultSliderSteps()} </media-quality-slider> `; } function DefaultSettingsMenu({ placement, portal, tooltip }) { return $signal(() => { const { viewType } = useMediaState(), { translations, menuPortal, noModal, menuGroup, smallWhen: smWhen } = useDefaultLayoutContext(), $placement = computed( () => noModal() ? unwrap(placement) : !smWhen() ? unwrap(placement) : null ), $offset = computed( () => !smWhen() && menuGroup() === "bottom" && viewType() === "video" ? 26 : 0 ), $isOpen = signal(false); function onOpen() { $isOpen.set(true); } function onClose() { $isOpen.set(false); } const items = html` <media-menu-items class="vds-settings-menu-items vds-menu-items" placement=${$signal($placement)} offset=${$signal($offset)} > ${$signal(() => { if (!$isOpen()) { return null; } return [ DefaultPlaybackMenu(), DefaultAccessibilityMenu(), DefaultAudioMenu(), DefaultCaptionsMenu() ]; })} </media-menu-items> `; return html` <media-menu class="vds-settings-menu vds-menu" @open=${onOpen} @close=${onClose}> <media-tooltip class="vds-tooltip"> <media-tooltip-trigger> <media-menu-button class="vds-menu-button vds-button" aria-label=${$i18n(translations, "Settings")} > ${IconSlot("menu-settings", "vds-rotate-icon")} </media-menu-button> </media-tooltip-trigger> <media-tooltip-content class="vds-tooltip-content" placement=${isFunction(tooltip) ? $signal(tooltip) : tooltip} > ${$i18n(translations, "Settings")} </media-tooltip-content> </media-tooltip> ${portal ? MenuPortal(menuPortal, items) : items} </media-menu> `; }); } function DefaultVolumePopup({ orientation, tooltip }) { return $signal(() => { const { pointer, muted, canSetVolume } = useMediaState(); if (pointer() === "coarse" && !muted()) return null; if (!canSetVolume()) { return DefaultMuteButton({ tooltip }); } const $rootRef = signal(void 0), $isRootActive = useActive($rootRef); return html` <div class="vds-volume" ?data-active=${$signal($isRootActive)} ${ref($rootRef.set)}> ${DefaultMuteButton({ tooltip })} <div class="vds-volume-popup">${DefaultVolumeSlider({ orientation })}</div> </div> `; }); } function DefaultVolumeSlider({ orientation } = {}) { const { translations } = useDefaultLayoutContext(), $label = $i18n(translations, "Volume"); return html` <media-volume-slider class="vds-volume-slider vds-slider" aria-label=${$label} orientation=${ifDefined(orientation)} > <div class="vds-slider-track"></div> <div class="vds-slider-track-fill vds-slider-track"></div> <media-slider-preview class="vds-slider-preview" no-clamp> <media-slider-value class="vds-slider-value"></media-slider-value> </media-slider-preview> <div class="vds-slider-thumb"></div> </media-volume-slider> `; } function DefaultTimeSlider() { const $ref = signal(void 0), $width = signal(0), { thumbnails, translations, sliderChaptersMinWidth, disableTimeSlider, seekStep, noScrubGesture } = useDefaultLayoutContext(), $label = $i18n(translations, "Seek"), $isDisabled = $signal(disableTimeSlider), $isChaptersDisabled = $signal(() => $width() < sliderChaptersMinWidth()), $thumbnails = $signal(thumbnails); useResizeObserver($ref, () => { const el = $ref(); el && $width.set(el.clientWidth); }); return html` <media-time-slider class="vds-time-slider vds-slider" aria-label=${$label} key-step=${$signal(seekStep)} ?disabled=${$isDisabled} ?no-swipe-gesture=${$signal(noScrubGesture)} ${ref($ref.set)} > <media-slider-chapters class="vds-slider-chapters" ?disabled=${$isChaptersDisabled}> <template> <div class="vds-slider-chapter"> <div class="vds-slider-track"></div> <div class="vds-slider-track-fill vds-slider-track"></div> <div class="vds-slider-progress vds-slider-track"></div> </div> </template> </media-slider-chapters> <div class="vds-slider-thumb"></div> <media-slider-preview class="vds-slider-preview"> <media-slider-thumbnail class="vds-slider-thumbnail vds-thumbnail" .src=${$thumbnails} ></media-slider-thumbnail> <div class="vds-slider-chapter-title" data-part="chapter-title"></div> <media-slider-value class="vds-slider-value"></media-slider-value> </media-slider-preview> </media-time-slider> `; } function DefaultTimeGroup() { return html` <div class="vds-time-group"> ${$signal(() => { const { duration } = useMediaState(); if (!duration()) return null; return [ html`<media-time class="vds-time" type="current"></media-time>`, html`<div class="vds-time-divider">/</div>`, html`<media-time class="vds-time" type="duration"></media-time>` ]; })} </div> `; } function DefaultTimeInvert() { return $signal(() => { const { live, duration } = useMediaState(); return live() ? DefaultLiveButton() : duration() ? html`<media-time class="vds-time" type="current" toggle remainder></media-time>` : null; }); } function DefaultTimeInfo() { return $signal(() => { const { live } = useMediaState(); return live() ? DefaultLiveButton() : DefaultTimeGroup(); }); } function DefaultTitle() { return $signal(() => { const { textTracks } = useMediaContext(), { title, started } = useMediaState(), $hasChapters = signal(null); watchActiveTextTrack(textTracks, "chapters", $hasChapters.set); return $hasChapters() && (started() || !title()) ? DefaultChapterTitle() : html`<media-title class="vds-chapter-title"></media-title>`; }); } function DefaultChapterTitle() { return html`<media-chapter-title class="vds-chapter-title"></media-chapter-title>`; } function DefaultAudioLayout() { return [ DefaultAnnouncer(), DefaultCaptions(), html` <media-controls class="vds-controls"> <media-controls-group class="vds-controls-group"> ${[ DefaultSeekButton({ backward: true, tooltip: "top start" }), DefaultPlayButton({ tooltip: "top" }), DefaultSeekButton({ tooltip: "top" }), DefaultAudioTitle(), DefaultTimeSlider(), DefaultTimeInvert(), DefaultVolumePopup({ orientation: "vertical", tooltip: "top" }), DefaultCaptionButton({ tooltip: "top" }), DefaultDownloadButton(), DefaultAirPlayButton({ tooltip: "top" }), DefaultAudioMenus() ]} </media-controls-group> </media-controls> ` ]; } function DefaultAudioTitle() { return $signal(() => { let $ref = signal(void 0), $isTextOverflowing = signal(false), media = useMediaContext(), { title, started, currentTime, ended } = useMediaState(), { translations } = useDefaultLayoutContext(), $isTransitionActive = useTransitionActive($ref), $isContinued = () => started() || currentTime() > 0; const $title = () => { const word = ended() ? "Replay" : $isContinued() ? "Continue" : "Play"; return `${i18n(translations, word)}: ${title()}`; }; effect(() => { if ($isTransitionActive() && document.activeElement === document.body) { media.player.el?.focus({ preventScroll: true }); } }); function onResize() { const el = $ref(), isOverflowing = !!el && !$isTransitionActive() && el.clientWidth < el.children[0].clientWidth; el && toggleClass(el, "vds-marquee", isOverflowing); $isTextOverflowing.set(isOverflowing); } function Title() { return html` <span class="vds-title-text"> ${$signal($title)}${$signal(() => $isContinued() ? DefaultChapterTitle() : null)} </span> `; } useResizeObserver($ref, onResize); return title() ? html` <span class="vds-title" title=${$signal($title)} ${ref($ref.set)}> ${[ Title(), $signal(() => $isTextOverflowing() && !$isTransitionActive() ? Title() : null) ]} </span> ` : DefaultControlsSpacer(); }); } function DefaultAudioMenus() { const placement = "top end"; return [ DefaultChaptersMenu({ tooltip: "top", placement, portal: true }), DefaultSettingsMenu({ tooltip: "top end", placement, portal: true }) ]; } class DefaultLayoutIconsLoader extends LayoutIconsLoader { async _load() { const paths = (await import('./vidstack-VJKvpvVu.js')).icons, icons = {}; for (const iconName of Object.keys(paths)) { icons[iconName] = Icon({ name: iconName, paths: paths[iconName] }); } return icons; } } class MediaAudioLayoutElement extends Host(LitElement, DefaultAudioLayout$1) { constructor() { super(...arguments); this._scrubbing = signal(false); } static { this.tagName = "media-audio-layout"; } static { this.attrs = { smallWhen: { converter(value) { return value !== "never" && !!value; } } }; } onSetup() { this.forwardKeepAlive = false; this._media = useMediaContext(); this.classList.add("vds-audio-layout"); this._setupWatchScrubbing(); } onConnect() { setLayoutName("audio", () => this.isMatch); this._setupMenuContainer(); } render() { return $signal(this._render.bind(this)); } _render() { return this.isMatch ? DefaultAudioLayout()