@aidenlx/vidstack-react
Version:
UI component library for building high-quality, accessible video and audio experiences on the web.
591 lines (572 loc) • 22.8 kB
JavaScript
"use client"
import { TextTrackSymbol, RadioGroupController, useMediaContext, menuContext, MediaPlayerInstance, Primitive, MediaProviderInstance, mediaState, isRemotionProvider, TextTrack, ToggleButtonInstance, PosterInstance, useMediaState, ThumbnailsLoader, updateSliderPreviewPlacement } from './chunks/vidstack-B1ySk2FQ.js';
export { ARIAKeyShortcuts, AUDIO_EXTENSIONS, AUDIO_TYPES, AirPlayButtonInstance, AudioGainSliderInstance, AudioProviderLoader, AudioTrackList, CaptionButtonInstance, CaptionsInstance, ControlsGroupInstance, ControlsInstance, DASHProviderLoader, DASH_VIDEO_EXTENSIONS, DASH_VIDEO_TYPES, FullscreenButtonInstance, FullscreenController, GestureInstance, GoogleCastButtonInstance, HLSProviderLoader, HLS_VIDEO_EXTENSIONS, HLS_VIDEO_TYPES, List, LiveButtonInstance, LocalMediaStorage, Logger, MEDIA_KEY_SHORTCUTS, MediaAnnouncerInstance, MediaControls, MediaRemoteControl, MenuButtonInstance, MenuInstance, MenuItemInstance, MenuItemsInstance, MenuPortalInstance, MuteButtonInstance, PIPButtonInstance, PlayButtonInstance, QualitySliderInstance, RadioGroupInstance, RadioInstance, ScreenOrientationController, SeekButtonInstance, SliderChaptersInstance, SliderInstance, SliderPreviewInstance, SliderThumbnailInstance, SliderValueInstance, SliderVideoInstance, SpeedSliderInstance, TextRenderers, TextTrackList, ThumbnailInstance, TimeInstance, TimeRange, TimeSliderInstance, TooltipContentInstance, TooltipInstance, TooltipTriggerInstance, VIDEO_EXTENSIONS, VIDEO_TYPES, VideoProviderLoader, VideoQualityList, VimeoProviderLoader, VolumeSliderInstance, YouTubeProviderLoader, boundTime, canChangeVolume, canFullscreen, canGoogleCastSrc, canOrientScreen, canPlayHLSNatively, canRotateScreen, canUsePictureInPicture, canUseVideoPresentation, findActiveCue, formatSpokenTime, formatTime, getDownloadFile, getTimeRangesEnd, getTimeRangesStart, isAudioProvider, isAudioSrc, isCueActive, isDASHProvider, isDASHSrc, isGoogleCastProvider, isHLSProvider, isHLSSrc, isHTMLAudioElement, isHTMLIFrameElement, isHTMLMediaElement, isHTMLVideoElement, isMediaStream, isTrackCaptionKind, isVideoProvider, isVideoQualitySrc, isVideoSrc, isVimeoProvider, isYouTubeProvider, mediaContext, normalizeTimeIntervals, parseJSONCaptionsFile, sliderState, softResetMediaState, sortVideoQualities, updateTimeIntervals, useMediaStore, useSliderState, useSliderStore, watchActiveTextTrack, watchCueTextChange } from './chunks/vidstack-B1ySk2FQ.js';
import * as React from 'react';
import { createReactComponent, useStateContext, useSignal, composeRefs, useSignalRecord, useReactScope } from 'maverick.js/react';
import { createSignal, useScoped } from './chunks/vidstack-DRMlDrT_.js';
export { audioGainSlider as AudioGainSlider, Captions, ChapterTitle, controls as Controls, GoogleCastButton, MediaAnnouncer, qualitySlider as QualitySlider, speedSlider as SpeedSlider, spinner as Spinner, Title, tooltip as Tooltip, useActiveTextCues, useActiveTextTrack, useChapterOptions, useChapterTitle, useTextCues } from './chunks/vidstack-DRMlDrT_.js';
import { isString, EventsController, DOMEvent } from 'maverick.js/std';
export { appendTriggerEvent, findTriggerEvent, hasTriggerEvent, isKeyboardClick, isKeyboardEvent, isPointerEvent, walkTriggerEventChain } from 'maverick.js/std';
import { useMediaContext as useMediaContext$1 } from './chunks/vidstack-DZHxgbQz.js';
export { AirPlayButton, CaptionButton, FullscreenButton, Gesture, LiveButton, menu as Menu, MuteButton, PIPButton, PlayButton, radioGroup as RadioGroup, SeekButton, slider as Slider, thumbnail as Thumbnail, Time, timeSlider as TimeSlider, volumeSlider as VolumeSlider, useAudioOptions, useCaptionOptions, useMediaPlayer } from './chunks/vidstack-DZHxgbQz.js';
import { Icon } from './chunks/vidstack-CBF7iUqu.js';
import { prop, method, Component, hasProvidedContext, useContext, effect, signal } from 'maverick.js';
export { DEFAULT_PLAYBACK_RATES, useMediaRemote, usePlaybackRateOptions, useVideoQualityOptions } from './chunks/vidstack-BFI47c9T.js';
import './chunks/vidstack-CPShcCv0.js';
import '@floating-ui/dom';
import 'react-dom';
class LibASSTextRenderer {
constructor(loader, config) {
this.loader = loader;
this.config = config;
}
priority = 1;
#instance = null;
#track = null;
#typeRE = /(ssa|ass)$/;
canRender(track, video) {
return !!video && !!track.src && (isString(track.type) && this.#typeRE.test(track.type) || this.#typeRE.test(track.src));
}
attach(video) {
if (!video) return;
this.loader().then(async (mod) => {
this.#instance = new mod.default({
...this.config,
video,
subUrl: this.#track?.src || ""
});
new EventsController(this.#instance).add("ready", () => {
const canvas = this.#instance?._canvas;
if (canvas) canvas.style.pointerEvents = "none";
}).add("error", (event) => {
if (!this.#track) return;
this.#track[TextTrackSymbol.readyState] = 3;
this.#track.dispatchEvent(
new DOMEvent("error", {
trigger: event,
detail: event.error
})
);
});
});
}
changeTrack(track) {
if (!track || track.readyState === 3) {
this.#freeTrack();
} else if (this.#track !== track) {
this.#instance?.setTrackByUrl(track.src);
this.#track = track;
}
}
detach() {
this.#freeTrack();
}
#freeTrack() {
this.#instance?.freeTrack();
this.#track = null;
}
}
const DEFAULT_AUDIO_GAINS = [1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4];
class AudioGainRadioGroup extends Component {
static props = {
normalLabel: "Disabled",
gains: DEFAULT_AUDIO_GAINS
};
#media;
#menu;
#controller;
get value() {
return this.#controller.value;
}
get disabled() {
const { gains } = this.$props, { canSetAudioGain } = this.#media.$state;
return !canSetAudioGain() || gains().length === 0;
}
constructor() {
super();
this.#controller = new RadioGroupController();
this.#controller.onValueChange = this.#onValueChange.bind(this);
}
onSetup() {
this.#media = useMediaContext();
if (hasProvidedContext(menuContext)) {
this.#menu = useContext(menuContext);
}
}
onConnect(el) {
effect(this.#watchValue.bind(this));
effect(this.#watchHintText.bind(this));
effect(this.#watchControllerDisabled.bind(this));
}
getOptions() {
const { gains, normalLabel } = this.$props;
return gains().map((gain) => ({
label: gain === 1 || gain === null ? normalLabel : String(gain * 100) + "%",
value: gain.toString()
}));
}
#watchValue() {
this.#controller.value = this.#getValue();
}
#watchHintText() {
const { normalLabel } = this.$props, { audioGain } = this.#media.$state, gain = audioGain();
this.#menu?.hint.set(gain === 1 || gain == null ? normalLabel() : String(gain * 100) + "%");
}
#watchControllerDisabled() {
this.#menu?.disable(this.disabled);
}
#getValue() {
const { audioGain } = this.#media.$state;
return audioGain()?.toString() ?? "1";
}
#onValueChange(value, trigger) {
if (this.disabled) return;
const gain = +value;
this.#media.remote.changeAudioGain(gain, trigger);
this.dispatch("change", { detail: gain, trigger });
}
}
const audiogainradiogroup__proto = AudioGainRadioGroup.prototype;
prop(audiogainradiogroup__proto, "value");
prop(audiogainradiogroup__proto, "disabled");
method(audiogainradiogroup__proto, "getOptions");
const playerCallbacks = [
"onAbort",
"onAudioTrackChange",
"onAudioTracksChange",
"onAutoPlay",
"onAutoPlayChange",
"onAutoPlayFail",
"onCanLoad",
"onCanPlay",
"onCanPlayThrough",
"onControlsChange",
"onDestroy",
"onDurationChange",
"onEmptied",
"onEnd",
"onEnded",
"onError",
"onFindMediaPlayer",
"onFullscreenChange",
"onFullscreenError",
"onLiveChange",
"onLiveEdgeChange",
"onLoadedData",
"onLoadedMetadata",
"onLoadStart",
"onLoopChange",
"onOrientationChange",
"onPause",
"onPictureInPictureChange",
"onPictureInPictureError",
"onPlay",
"onPlayFail",
"onPlaying",
"onPlaysInlineChange",
"onPosterChange",
"onProgress",
"onProviderChange",
"onProviderLoaderChange",
"onProviderSetup",
"onQualitiesChange",
"onQualityChange",
"onRateChange",
"onReplay",
"onSeeked",
"onSeeking",
"onSourceChange",
"onSourceChange",
"onStalled",
"onStarted",
"onStreamTypeChange",
"onSuspend",
"onTextTrackChange",
"onTextTracksChange",
"onTimeUpdate",
"onTitleChange",
"onVdsLog",
"onVideoPresentationChange",
"onVolumeChange",
"onWaiting"
];
const MediaPlayerBridge = createReactComponent(MediaPlayerInstance, {
events: playerCallbacks,
eventsRegex: /^onHls/,
domEventsRegex: /^onMedia/
});
const MediaPlayer = React.forwardRef(
({ aspectRatio, children, ...props }, forwardRef) => {
return /* @__PURE__ */ React.createElement(
MediaPlayerBridge,
{
...props,
src: props.src,
ref: forwardRef,
style: {
aspectRatio,
...props.style
}
},
(props2) => /* @__PURE__ */ React.createElement(Primitive.div, { ...props2 }, children)
);
}
);
MediaPlayer.displayName = "MediaPlayer";
const MediaProviderBridge = createReactComponent(MediaProviderInstance);
const MediaProvider = React.forwardRef(
({ loaders = [], children, iframeProps, mediaProps, ...props }, forwardRef) => {
const reactLoaders = React.useMemo(() => loaders.map((Loader) => new Loader()), loaders);
return /* @__PURE__ */ React.createElement(MediaProviderBridge, { ...props, loaders: reactLoaders, ref: forwardRef }, (props2, instance) => /* @__PURE__ */ React.createElement("div", { ...props2 }, /* @__PURE__ */ React.createElement(MediaOutlet, { provider: instance, mediaProps, iframeProps }), children));
}
);
MediaProvider.displayName = "MediaProvider";
function MediaOutlet({ provider, mediaProps, iframeProps }) {
const { sources, crossOrigin, poster, remotePlaybackInfo, nativeControls, viewType } = useStateContext(mediaState), { loader } = provider.$state, { $provider: $$provider, $providerSetup: $$providerSetup } = useMediaContext$1(), $sources = useSignal(sources), $nativeControls = useSignal(nativeControls), $crossOrigin = useSignal(crossOrigin), $poster = useSignal(poster), $loader = useSignal(loader), $provider = useSignal($$provider), $providerSetup = useSignal($$providerSetup), $remoteInfo = useSignal(remotePlaybackInfo), $mediaType = $loader?.mediaType(), $viewType = useSignal(viewType), isAudioView = $viewType === "audio", isYouTubeEmbed = $loader?.name === "youtube", isVimeoEmbed = $loader?.name === "vimeo", isEmbed = isYouTubeEmbed || isVimeoEmbed, isRemotion = $loader?.name === "remotion", isGoogleCast = $loader?.name === "google-cast", [googleCastIconPaths, setGoogleCastIconPaths] = React.useState(""), [hasMounted, setHasMounted] = React.useState(false);
React.useEffect(() => {
if (!isGoogleCast || googleCastIconPaths) return;
import('./chunks/vidstack-CPShcCv0.js').then(function (n) { return n.chromecast; }).then((mod) => {
setGoogleCastIconPaths(mod.default);
});
}, [isGoogleCast]);
React.useEffect(() => {
setHasMounted(true);
}, []);
if (isGoogleCast) {
return /* @__PURE__ */ React.createElement(
"div",
{
className: "vds-google-cast",
ref: (el) => {
provider.load(el);
}
},
/* @__PURE__ */ React.createElement(Icon, { paths: googleCastIconPaths }),
$remoteInfo?.deviceName ? /* @__PURE__ */ React.createElement("span", { className: "vds-google-cast-info" }, "Google Cast on", " ", /* @__PURE__ */ React.createElement("span", { className: "vds-google-cast-device-name" }, $remoteInfo.deviceName)) : null
);
}
if (isRemotion) {
return /* @__PURE__ */ React.createElement("div", { "data-remotion-canvas": true }, /* @__PURE__ */ React.createElement(
"div",
{
"data-remotion-container": true,
ref: (el) => {
provider.load(el);
}
},
isRemotionProvider($provider) && $providerSetup ? React.createElement($provider.render) : null
));
}
return isEmbed ? React.createElement(
React.Fragment,
null,
React.createElement("iframe", {
...iframeProps,
className: (iframeProps?.className ? `${iframeProps.className} ` : "") + isYouTubeEmbed ? "vds-youtube" : "vds-vimeo",
suppressHydrationWarning: true,
tabIndex: !$nativeControls ? -1 : void 0,
"aria-hidden": "true",
"data-no-controls": !$nativeControls ? "" : void 0,
ref(el) {
provider.load(el);
}
}),
!$nativeControls && !isAudioView ? React.createElement("div", { className: "vds-blocker" }) : null
) : $mediaType ? React.createElement($mediaType === "audio" ? "audio" : "video", {
...mediaProps,
controls: $nativeControls ? true : null,
crossOrigin: typeof $crossOrigin === "boolean" ? "" : $crossOrigin,
poster: $mediaType === "video" && $nativeControls && $poster ? $poster : null,
suppressHydrationWarning: true,
children: !hasMounted ? $sources.map(
({ src, type }) => isString(src) ? /* @__PURE__ */ React.createElement("source", { src, type: type !== "?" ? type : void 0, key: src }) : null
) : null,
ref(el) {
provider.load(el);
}
}) : null;
}
MediaOutlet.displayName = "MediaOutlet";
function createTextTrack(init) {
const media = useMediaContext$1(), track = React.useMemo(() => new TextTrack(init), Object.values(init));
React.useEffect(() => {
media.textTracks.add(track);
return () => void media.textTracks.remove(track);
}, [track]);
return track;
}
function Track({ lang, ...props }) {
createTextTrack({ language: lang, ...props });
return null;
}
Track.displayName = "Track";
const ToggleButtonBridge = createReactComponent(ToggleButtonInstance);
const ToggleButton = React.forwardRef(
({ children, ...props }, forwardRef) => {
return /* @__PURE__ */ React.createElement(ToggleButtonBridge, { ...props }, (props2) => /* @__PURE__ */ React.createElement(
Primitive.button,
{
...props2,
ref: composeRefs(props2.ref, forwardRef)
},
children
));
}
);
ToggleButton.displayName = "ToggleButton";
const PosterBridge = createReactComponent(PosterInstance);
const Poster = React.forwardRef(
({ children, ...props }, forwardRef) => {
return /* @__PURE__ */ React.createElement(
PosterBridge,
{
src: props.asChild && React.isValidElement(children) ? children.props.src : void 0,
...props
},
(props2, instance) => /* @__PURE__ */ React.createElement(
PosterImg,
{
...props2,
instance,
ref: composeRefs(props2.ref, forwardRef)
},
children
)
);
}
);
Poster.displayName = "Poster";
const PosterImg = React.forwardRef(
({ instance, children, ...props }, forwardRef) => {
const { src, img, alt, crossOrigin, hidden } = instance.$state, $src = useSignal(src), $alt = useSignal(alt), $crossOrigin = useSignal(crossOrigin), $hidden = useSignal(hidden);
return /* @__PURE__ */ React.createElement(
Primitive.img,
{
...props,
src: $src || void 0,
alt: $alt || void 0,
crossOrigin: $crossOrigin || void 0,
ref: composeRefs(img.set, forwardRef),
style: { display: $hidden ? "none" : void 0 }
},
children
);
}
);
PosterImg.displayName = "PosterImg";
const Root = React.forwardRef(({ children, ...props }, forwardRef) => {
return /* @__PURE__ */ React.createElement(
Primitive.div,
{
translate: "yes",
"aria-live": "off",
"aria-atomic": "true",
...props,
ref: forwardRef
},
children
);
});
Root.displayName = "Caption";
const Text = React.forwardRef((props, forwardRef) => {
const textTrack = useMediaState("textTrack"), [activeCue, setActiveCue] = React.useState();
React.useEffect(() => {
if (!textTrack) return;
function onCueChange() {
setActiveCue(textTrack?.activeCues[0]);
}
textTrack.addEventListener("cue-change", onCueChange);
return () => {
textTrack.removeEventListener("cue-change", onCueChange);
setActiveCue(void 0);
};
}, [textTrack]);
return /* @__PURE__ */ React.createElement(
Primitive.span,
{
...props,
"data-part": "cue",
dangerouslySetInnerHTML: {
__html: activeCue?.text || ""
},
ref: forwardRef
}
);
});
Text.displayName = "CaptionText";
var caption = /*#__PURE__*/Object.freeze({
__proto__: null,
Root: Root,
Text: Text
});
function useState(ctor, prop, ref) {
const initialValue = React.useMemo(() => ctor.state.record[prop], [ctor, prop]);
return useSignal(ref.current ? ref.current.$state[prop] : initialValue);
}
const storesCache = /* @__PURE__ */ new Map();
function useStore(ctor, ref) {
const initialStore = React.useMemo(() => {
let store = storesCache.get(ctor);
if (!store) {
store = new Proxy(ctor.state.record, {
get: (_, prop) => () => ctor.state.record[prop]
});
storesCache.set(ctor, store);
}
return store;
}, [ctor]);
return useSignalRecord(ref.current ? ref.current.$state : initialStore);
}
function useMediaProvider() {
const [provider, setProvider] = React.useState(null), context = useMediaContext$1();
if (!context) {
throw Error(
"[vidstack] no media context was found - was this called outside of `<MediaPlayer>`?"
);
}
React.useEffect(() => {
if (!context) return;
return effect(() => {
setProvider(context.$provider());
});
}, []);
return provider;
}
function useThumbnails(src, crossOrigin = null) {
const scope = useReactScope(), $src = createSignal(src), $crossOrigin = createSignal(crossOrigin), loader = useScoped(() => ThumbnailsLoader.create($src, $crossOrigin));
if (!scope) {
console.warn(
`[vidstack] \`useThumbnails\` must be called inside a child component of \`<MediaPlayer>\``
);
}
React.useEffect(() => {
$src.set(src);
}, [src]);
React.useEffect(() => {
$crossOrigin.set(crossOrigin);
}, [crossOrigin]);
return useSignal(loader.$images);
}
function useActiveThumbnail(thumbnails, time) {
return React.useMemo(() => {
let activeIndex = -1;
for (let i = thumbnails.length - 1; i >= 0; i--) {
const image = thumbnails[i];
if (time >= image.startTime && (!image.endTime || time < image.endTime)) {
activeIndex = i;
break;
}
}
return thumbnails[activeIndex] || null;
}, [thumbnails, time]);
}
function useSliderPreview({
clamp = false,
offset = 0,
orientation = "horizontal"
} = {}) {
const [rootRef, setRootRef] = React.useState(null), [previewRef, setPreviewRef] = React.useState(null), [pointerValue, setPointerValue] = React.useState(0), [isVisible, setIsVisible] = React.useState(false);
React.useEffect(() => {
if (!rootRef) return;
const dragging = signal(false);
function updatePointerValue(event) {
if (!rootRef) return;
setPointerValue(getPointerValue(rootRef, event, orientation));
}
return effect(() => {
if (!dragging()) {
new EventsController(rootRef).add("pointerenter", () => {
setIsVisible(true);
previewRef?.setAttribute("data-visible", "");
}).add("pointerdown", (event) => {
dragging.set(true);
updatePointerValue(event);
}).add("pointerleave", () => {
setIsVisible(false);
previewRef?.removeAttribute("data-visible");
}).add("pointermove", updatePointerValue);
}
previewRef?.setAttribute("data-dragging", "");
new EventsController(document).add("pointerup", (event) => {
dragging.set(false);
previewRef?.removeAttribute("data-dragging");
updatePointerValue(event);
}).add("pointermove", updatePointerValue).add("touchmove", (e) => e.preventDefault(), { passive: false });
});
}, [rootRef]);
React.useEffect(() => {
if (previewRef) {
previewRef.style.setProperty("--slider-pointer", pointerValue + "%");
}
}, [previewRef, pointerValue]);
React.useEffect(() => {
if (!previewRef) return;
const update = () => {
updateSliderPreviewPlacement(previewRef, {
offset,
clamp,
orientation
});
};
update();
const resize = new ResizeObserver(update);
resize.observe(previewRef);
return () => resize.disconnect();
}, [previewRef, clamp, offset, orientation]);
return {
previewRootRef: setRootRef,
previewRef: setPreviewRef,
previewValue: pointerValue,
isPreviewVisible: isVisible
};
}
function getPointerValue(root, event, orientation) {
let thumbPositionRate, rect = root.getBoundingClientRect();
if (orientation === "vertical") {
const { bottom: trackBottom, height: trackHeight } = rect;
thumbPositionRate = (trackBottom - event.clientY) / trackHeight;
} else {
const { left: trackLeft, width: trackWidth } = rect;
thumbPositionRate = (event.clientX - trackLeft) / trackWidth;
}
return round(Math.max(0, Math.min(100, 100 * thumbPositionRate)));
}
function round(num) {
return Number(num.toFixed(3));
}
function useAudioGainOptions({
gains = DEFAULT_AUDIO_GAINS,
disabledLabel = "disabled"
} = {}) {
const media = useMediaContext$1(), { audioGain, canSetAudioGain } = media.$state;
useSignal(audioGain);
useSignal(canSetAudioGain);
return React.useMemo(() => {
const options = gains.map((opt) => {
const label = typeof opt === "number" ? opt === 1 && disabledLabel ? disabledLabel : opt * 100 + "%" : opt.label, gain = typeof opt === "number" ? opt : opt.gain;
return {
label,
value: gain.toString(),
gain,
get selected() {
return audioGain() === gain;
},
select(trigger) {
media.remote.changeAudioGain(gain, trigger);
}
};
});
Object.defineProperty(options, "disabled", {
get() {
return !canSetAudioGain() || !options.length;
}
});
Object.defineProperty(options, "selectedValue", {
get() {
return audioGain()?.toString();
}
});
return options;
}, [gains]);
}
export { caption as Caption, DEFAULT_AUDIO_GAINS, Icon, LibASSTextRenderer, MediaPlayer, MediaPlayerInstance, MediaProvider, MediaProviderInstance, Poster, PosterInstance, TextTrack, ToggleButton, ToggleButtonInstance, Track, createTextTrack, mediaState, useActiveThumbnail, useAudioGainOptions, useMediaContext$1 as useMediaContext, useMediaProvider, useMediaState, useSliderPreview, useState, useStore, useThumbnails };