@7sage/vidstack
Version:
UI component library for building high-quality, accessible video and audio experiences on the web.
718 lines (708 loc) • 21.7 kB
JavaScript
import { waitTimeout, isArray, isUndefined, isString, isBoolean, deferredPromise, isNull, listenEvent, scoped, getScope, EventsTarget, DOMEvent, isNumber, State, tick } from './vidstack-BUJn1if6.js';
const IS_IPHONE = false;
const IS_CHROME = false;
function canOrientScreen() {
return canRotateScreen();
}
function canRotateScreen() {
return false;
}
function canPlayVideoType(video, type) {
return false;
}
function canPlayHLSNatively(video) {
return false;
}
function canUsePictureInPicture(video) {
return false;
}
function canUseVideoPresentation(video) {
return false;
}
async function canChangeVolume() {
const video = document.createElement("video");
video.volume = 0.5;
await waitTimeout(0);
return video.volume === 0.5;
}
function isHLSSupported() {
return false;
}
function isDASHSupported() {
return isHLSSupported();
}
class TimeRange {
#ranges;
get length() {
return this.#ranges.length;
}
constructor(start, end) {
if (isArray(start)) {
this.#ranges = start;
} else if (!isUndefined(start) && !isUndefined(end)) {
this.#ranges = [[start, end]];
} else {
this.#ranges = [];
}
}
start(index) {
return this.#ranges[index][0] ?? Infinity;
}
end(index) {
return this.#ranges[index][1] ?? Infinity;
}
}
function getTimeRangesStart(range) {
if (!range.length) return null;
let min = range.start(0);
for (let i = 1; i < range.length; i++) {
const value = range.start(i);
if (value < min) min = value;
}
return min;
}
function getTimeRangesEnd(range) {
if (!range.length) return null;
let max = range.end(0);
for (let i = 1; i < range.length; i++) {
const value = range.end(i);
if (value > max) max = value;
}
return max;
}
function normalizeTimeIntervals(intervals) {
if (intervals.length <= 1) {
return intervals;
}
intervals.sort((a, b) => a[0] - b[0]);
let normalized = [], current = intervals[0];
for (let i = 1; i < intervals.length; i++) {
const next = intervals[i];
if (current[1] >= next[0] - 1) {
current = [current[0], Math.max(current[1], next[1])];
} else {
normalized.push(current);
current = next;
}
}
normalized.push(current);
return normalized;
}
function updateTimeIntervals(intervals, interval, value) {
let start = interval[0], end = interval[1];
if (value < start) {
return [value, -1];
} else if (value === start) {
return interval;
} else if (start === -1) {
interval[0] = value;
return interval;
} else if (value > start) {
interval[1] = value;
if (end === -1) intervals.push(interval);
}
normalizeTimeIntervals(intervals);
return interval;
}
const AUDIO_EXTENSIONS = /\.(m4a|m4b|mp4a|mpga|mp2|mp2a|mp3|m2a|m3a|wav|weba|aac|oga|spx|flac)($|\?)/i;
const AUDIO_TYPES = /* @__PURE__ */ new Set([
"audio/mpeg",
"audio/ogg",
"audio/3gp",
"audio/mp3",
"audio/webm",
"audio/flac",
"audio/m4a",
"audio/m4b",
"audio/mp4a",
"audio/mp4"
]);
const VIDEO_EXTENSIONS = /\.(mp4|og[gv]|webm|mov|m4v)(#t=[,\d+]+)?($|\?)/i;
const VIDEO_TYPES = /* @__PURE__ */ new Set([
"video/mp4",
"video/webm",
"video/3gp",
"video/ogg",
"video/avi",
"video/mpeg"
]);
const HLS_VIDEO_EXTENSIONS = /\.(m3u8)($|\?)/i;
const DASH_VIDEO_EXTENSIONS = /\.(mpd)($|\?)/i;
const HLS_VIDEO_TYPES = /* @__PURE__ */ new Set([
// Apple sanctioned
"application/vnd.apple.mpegurl",
// Apple sanctioned for backwards compatibility
"audio/mpegurl",
// Very common
"audio/x-mpegurl",
// Very common
"application/x-mpegurl",
// Included for completeness
"video/x-mpegurl",
"video/mpegurl",
"application/mpegurl"
]);
const DASH_VIDEO_TYPES = /* @__PURE__ */ new Set(["application/dash+xml"]);
function isAudioSrc({ src, type }) {
return isString(src) ? AUDIO_EXTENSIONS.test(src) || AUDIO_TYPES.has(type) || src.startsWith("blob:") && type === "audio/object" : type === "audio/object";
}
function isVideoSrc(src) {
return isString(src.src) ? VIDEO_EXTENSIONS.test(src.src) || VIDEO_TYPES.has(src.type) || src.src.startsWith("blob:") && src.type === "video/object" || isHLSSrc(src) && true : src.type === "video/object";
}
function isHLSSrc({ src, type }) {
return isString(src) && HLS_VIDEO_EXTENSIONS.test(src) || HLS_VIDEO_TYPES.has(type);
}
function isDASHSrc({ src, type }) {
return isString(src) && DASH_VIDEO_EXTENSIONS.test(src) || DASH_VIDEO_TYPES.has(type);
}
function canGoogleCastSrc(src) {
return isString(src.src) && (isAudioSrc(src) || isVideoSrc(src) || isHLSSrc(src));
}
function isMediaStream(src) {
return false;
}
function preconnect(url, rel = "preconnect") {
return false;
}
const pendingRequests = {};
function loadScript(src) {
if (pendingRequests[src]) return pendingRequests[src].promise;
const promise = deferredPromise(), exists = document.querySelector(`script[src="${src}"]`);
if (!isNull(exists)) {
promise.resolve();
return promise.promise;
}
pendingRequests[src] = promise;
const script = document.createElement("script");
script.src = src;
script.onload = () => {
promise.resolve();
delete pendingRequests[src];
};
script.onerror = () => {
promise.reject();
delete pendingRequests[src];
};
setTimeout(() => document.head.append(script), 0);
return promise.promise;
}
function getRequestCredentials(crossOrigin) {
return crossOrigin === "use-credentials" ? "include" : isString(crossOrigin) ? "same-origin" : void 0;
}
function getDownloadFile({
title,
src,
download
}) {
const url = isBoolean(download) || download === "" ? src.src : isString(download) ? download : download?.url;
if (!isValidFileDownload({ url, src, download })) return null;
return {
url,
name: !isBoolean(download) && !isString(download) && download?.filename || title.toLowerCase() || "media"
};
}
function isValidFileDownload({
url,
src,
download
}) {
return isString(url) && (download && download !== true || isAudioSrc(src) || isVideoSrc(src));
}
const CROSS_ORIGIN = Symbol(0), READY_STATE = Symbol(0), UPDATE_ACTIVE_CUES = Symbol(0), CAN_LOAD = Symbol(0), ON_MODE_CHANGE = Symbol(0), NATIVE = Symbol(0), NATIVE_HLS = Symbol(0);
const TextTrackSymbol = {
crossOrigin: CROSS_ORIGIN,
readyState: READY_STATE,
updateActiveCues: UPDATE_ACTIVE_CUES,
canLoad: CAN_LOAD,
onModeChange: ON_MODE_CHANGE,
native: NATIVE,
nativeHLS: NATIVE_HLS
};
function findActiveCue(cues, time) {
for (let i = 0, len = cues.length; i < len; i++) {
if (isCueActive(cues[i], time)) return cues[i];
}
return null;
}
function isCueActive(cue, time) {
return time >= cue.startTime && time < cue.endTime;
}
function watchActiveTextTrack(tracks, kind, onChange) {
let currentTrack = null, scope = getScope();
function onModeChange() {
const kinds = isString(kind) ? [kind] : kind, track = tracks.toArray().find((track2) => kinds.includes(track2.kind) && track2.mode === "showing");
if (track === currentTrack) return;
if (!track) {
onChange(null);
currentTrack = null;
return;
}
if (track.readyState == 2) {
onChange(track);
} else {
onChange(null);
scoped(() => {
}, scope);
}
currentTrack = track;
}
onModeChange();
return listenEvent();
}
function watchCueTextChange(tracks, kind, callback) {
watchActiveTextTrack(tracks, kind, (track) => {
if (!track) {
callback("");
return;
}
const onCueChange = () => {
const activeCue = track?.activeCues[0];
callback(activeCue?.text || "");
};
onCueChange();
});
}
class TextTrack extends EventsTarget {
static createId(track) {
return `vds-${track.type}-${track.kind}-${track.src ?? track.label ?? "?"}`;
}
src;
content;
type;
encoding;
id = "";
label = "";
language = "";
kind;
default = false;
#canLoad = false;
#currentTime = 0;
#mode = "disabled";
#metadata = {};
#regions = [];
#cues = [];
#activeCues = [];
/** @internal */
[TextTrackSymbol.readyState] = 0;
/** @internal */
[TextTrackSymbol.crossOrigin];
/** @internal */
[TextTrackSymbol.onModeChange] = null;
/** @internal */
[TextTrackSymbol.native] = null;
get metadata() {
return this.#metadata;
}
get regions() {
return this.#regions;
}
get cues() {
return this.#cues;
}
get activeCues() {
return this.#activeCues;
}
/**
* - 0: Not Loading
* - 1: Loading
* - 2: Ready
* - 3: Error
*/
get readyState() {
return this[TextTrackSymbol.readyState];
}
get mode() {
return this.#mode;
}
set mode(mode) {
this.setMode(mode);
}
constructor(init) {
super();
for (const prop of Object.keys(init)) this[prop] = init[prop];
if (!this.type) this.type = "vtt";
if (!init.src) {
this[TextTrackSymbol.readyState] = 2;
}
}
addCue(cue, trigger) {
let i = 0, length = this.#cues.length;
for (i = 0; i < length; i++) if (cue.endTime <= this.#cues[i].startTime) break;
if (i === length) this.#cues.push(cue);
else this.#cues.splice(i, 0, cue);
if (!(cue instanceof TextTrackCue)) {
this[TextTrackSymbol.native]?.track.addCue(cue);
}
this.dispatchEvent(new DOMEvent("add-cue", { detail: cue, trigger }));
if (isCueActive(cue, this.#currentTime)) {
this[TextTrackSymbol.updateActiveCues](this.#currentTime, trigger);
}
}
removeCue(cue, trigger) {
const index = this.#cues.indexOf(cue);
if (index >= 0) {
const isActive = this.#activeCues.includes(cue);
this.#cues.splice(index, 1);
this[TextTrackSymbol.native]?.track.removeCue(cue);
this.dispatchEvent(new DOMEvent("remove-cue", { detail: cue, trigger }));
if (isActive) {
this[TextTrackSymbol.updateActiveCues](this.#currentTime, trigger);
}
}
}
setMode(mode, trigger) {
if (this.#mode === mode) return;
this.#mode = mode;
if (mode === "disabled") {
this.#activeCues = [];
this.#activeCuesChanged();
} else if (this.readyState === 2) {
this[TextTrackSymbol.updateActiveCues](this.#currentTime, trigger);
} else {
this.#load();
}
this.dispatchEvent(new DOMEvent("mode-change", { detail: this, trigger }));
this[TextTrackSymbol.onModeChange]?.();
}
/** @internal */
[TextTrackSymbol.updateActiveCues](currentTime, trigger) {
this.#currentTime = currentTime;
if (this.mode === "disabled" || !this.#cues.length) return;
const activeCues = [];
for (let i = 0, length = this.#cues.length; i < length; i++) {
const cue = this.#cues[i];
if (isCueActive(cue, currentTime)) activeCues.push(cue);
}
let changed = activeCues.length !== this.#activeCues.length;
if (!changed) {
for (let i = 0; i < activeCues.length; i++) {
if (!this.#activeCues.includes(activeCues[i])) {
changed = true;
break;
}
}
}
this.#activeCues = activeCues;
if (changed) this.#activeCuesChanged(trigger);
}
/** @internal */
[TextTrackSymbol.canLoad]() {
this.#canLoad = true;
if (this.#mode !== "disabled") this.#load();
}
#parseContent(init) {
import('media-captions').then(({ parseText, VTTCue, VTTRegion }) => {
if (!isString(init.content) || init.type === "json") {
this.#parseJSON(init.content, VTTCue, VTTRegion);
if (this.readyState !== 3) this.#ready();
} else {
parseText(init.content, { type: init.type }).then(({ cues, regions }) => {
this.#cues = cues;
this.#regions = regions;
this.#ready();
});
}
});
}
async #load() {
if (!this.#canLoad || this[TextTrackSymbol.readyState] > 0) return;
this[TextTrackSymbol.readyState] = 1;
this.dispatchEvent(new DOMEvent("load-start"));
if (!this.src) {
this.#ready();
return;
}
try {
const { parseResponse, VTTCue, VTTRegion } = await import('media-captions'), crossOrigin = this[TextTrackSymbol.crossOrigin]?.();
const response = fetch(this.src, {
headers: this.type === "json" ? { "Content-Type": "application/json" } : void 0,
credentials: getRequestCredentials(crossOrigin)
});
if (this.type === "json") {
this.#parseJSON(await (await response).text(), VTTCue, VTTRegion);
} else {
const { errors, metadata, regions, cues } = await parseResponse(response, {
type: this.type,
encoding: this.encoding
});
if (errors[0]?.code === 0) {
throw errors[0];
} else {
this.#metadata = metadata;
this.#regions = regions;
this.#cues = cues;
}
}
this.#ready();
} catch (error) {
this.#error(error);
}
}
#ready() {
this[TextTrackSymbol.readyState] = 2;
if (!this.src || this.type !== "vtt") {
const native = this[TextTrackSymbol.native];
if (native && !native.managed) {
for (const cue of this.#cues) native.track.addCue(cue);
}
}
const loadEvent = new DOMEvent("load");
this[TextTrackSymbol.updateActiveCues](this.#currentTime, loadEvent);
this.dispatchEvent(loadEvent);
}
#error(error) {
this[TextTrackSymbol.readyState] = 3;
this.dispatchEvent(new DOMEvent("error", { detail: error }));
}
#parseJSON(json, VTTCue, VTTRegion) {
try {
const { regions, cues } = parseJSONCaptionsFile(json, VTTCue, VTTRegion);
this.#regions = regions;
this.#cues = cues;
} catch (error) {
this.#error(error);
}
}
#activeCuesChanged(trigger) {
this.dispatchEvent(new DOMEvent("cue-change", { trigger }));
}
}
const captionRE = /captions|subtitles/;
function isTrackCaptionKind(track) {
return captionRE.test(track.kind);
}
function parseJSONCaptionsFile(json, Cue, Region) {
const content = isString(json) ? JSON.parse(json) : json;
let regions = [], cues = [];
if (content.regions && Region) {
regions = content.regions.map((region) => Object.assign(new Region(), region));
}
if (content.cues || isArray(content)) {
cues = (isArray(content) ? content : content.cues).filter((content2) => isNumber(content2.startTime) && isNumber(content2.endTime)).map((cue) => Object.assign(new Cue(0, 0, ""), cue));
}
return { regions, cues };
}
const mediaState = new State({
artist: "",
artwork: null,
audioTrack: null,
audioTracks: [],
autoPlay: false,
autoPlayError: null,
audioGain: null,
buffered: new TimeRange(),
canLoad: false,
canLoadPoster: false,
canFullscreen: false,
canOrientScreen: canOrientScreen(),
canPictureInPicture: false,
canPlay: false,
clipStartTime: 0,
clipEndTime: 0,
controls: false,
get iOSControls() {
return IS_IPHONE;
},
get nativeControls() {
return this.controls || this.iOSControls;
},
controlsVisible: false,
get controlsHidden() {
return !this.controlsVisible;
},
crossOrigin: null,
ended: false,
error: null,
fullscreen: false,
get loop() {
return this.providedLoop || this.userPrefersLoop;
},
logLevel: "silent",
mediaType: "unknown",
muted: false,
paused: true,
played: new TimeRange(),
playing: false,
playsInline: false,
pictureInPicture: false,
preload: "metadata",
playbackRate: 1,
qualities: [],
quality: null,
autoQuality: false,
canSetQuality: true,
canSetPlaybackRate: true,
canSetVolume: false,
canSetAudioGain: false,
seekable: new TimeRange(),
seeking: false,
source: { src: "", type: "" },
sources: [],
started: false,
textTracks: [],
textTrack: null,
get hasCaptions() {
return this.textTracks.filter(isTrackCaptionKind).length > 0;
},
volume: 1,
waiting: false,
realCurrentTime: 0,
get currentTime() {
return this.ended ? this.duration : this.clipStartTime > 0 ? Math.max(0, Math.min(this.realCurrentTime - this.clipStartTime, this.duration)) : this.realCurrentTime;
},
providedDuration: -1,
intrinsicDuration: 0,
get duration() {
return this.seekableWindow;
},
get title() {
return this.providedTitle || this.inferredTitle;
},
get poster() {
return this.providedPoster || this.inferredPoster;
},
get viewType() {
return this.providedViewType !== "unknown" ? this.providedViewType : this.inferredViewType;
},
get streamType() {
return this.providedStreamType !== "unknown" ? this.providedStreamType : this.inferredStreamType;
},
get currentSrc() {
return this.source;
},
get bufferedStart() {
const start = getTimeRangesStart(this.buffered) ?? 0;
return Math.max(start, this.clipStartTime);
},
get bufferedEnd() {
const end = getTimeRangesEnd(this.buffered) ?? 0;
return Math.min(this.seekableEnd, Math.max(0, end - this.clipStartTime));
},
get bufferedWindow() {
return Math.max(0, this.bufferedEnd - this.bufferedStart);
},
get seekableStart() {
if (this.isLiveDVR && this.liveDVRWindow > 0) {
return Math.max(0, this.seekableEnd - this.liveDVRWindow);
}
const start = getTimeRangesStart(this.seekable) ?? 0;
return Math.max(start, this.clipStartTime);
},
get seekableEnd() {
if (this.providedDuration > 0) return this.providedDuration;
const end = this.liveSyncPosition > 0 ? this.liveSyncPosition : this.canPlay ? getTimeRangesEnd(this.seekable) ?? Infinity : 0;
return this.clipEndTime > 0 ? Math.min(this.clipEndTime, end) : end;
},
get seekableWindow() {
const window = this.seekableEnd - this.seekableStart;
return !isNaN(window) ? Math.max(0, window) : Infinity;
},
// ~~ remote playback ~~
canAirPlay: false,
canGoogleCast: false,
remotePlaybackState: "disconnected",
remotePlaybackType: "none",
remotePlaybackLoader: null,
remotePlaybackInfo: null,
get isAirPlayConnected() {
return this.remotePlaybackType === "airplay" && this.remotePlaybackState === "connected";
},
get isGoogleCastConnected() {
return this.remotePlaybackType === "google-cast" && this.remotePlaybackState === "connected";
},
// ~~ responsive design ~~
pointer: "fine",
orientation: "landscape",
width: 0,
height: 0,
mediaWidth: 0,
mediaHeight: 0,
lastKeyboardAction: null,
// ~~ user props ~~
userBehindLiveEdge: false,
// ~~ live props ~~
liveEdgeTolerance: 10,
minLiveDVRWindow: 60,
get canSeek() {
return /unknown|on-demand|:dvr/.test(this.streamType) && Number.isFinite(this.duration) && (!this.isLiveDVR || this.duration >= this.liveDVRWindow);
},
get live() {
return this.streamType.includes("live") || !Number.isFinite(this.duration);
},
get liveEdgeStart() {
return this.live && Number.isFinite(this.seekableEnd) ? Math.max(0, this.seekableEnd - this.liveEdgeTolerance) : 0;
},
get liveEdge() {
return this.live && (!this.canSeek || !this.userBehindLiveEdge && this.currentTime >= this.liveEdgeStart);
},
get liveEdgeWindow() {
return this.live && Number.isFinite(this.seekableEnd) ? this.seekableEnd - this.liveEdgeStart : 0;
},
get isLiveDVR() {
return /:dvr/.test(this.streamType);
},
get liveDVRWindow() {
return Math.max(this.inferredLiveDVRWindow, this.minLiveDVRWindow);
},
// ~~ internal props ~~
autoPlaying: false,
providedTitle: "",
inferredTitle: "",
providedLoop: false,
userPrefersLoop: false,
providedPoster: "",
inferredPoster: "",
inferredViewType: "unknown",
providedViewType: "unknown",
providedStreamType: "unknown",
inferredStreamType: "unknown",
liveSyncPosition: null,
inferredLiveDVRWindow: 0,
savedState: null
});
const RESET_ON_SRC_QUALITY_CHANGE = /* @__PURE__ */ new Set([
"autoPlayError",
"autoPlaying",
"buffered",
"canPlay",
"error",
"paused",
"played",
"playing",
"seekable",
"seeking",
"waiting"
]);
const RESET_ON_SRC_CHANGE = /* @__PURE__ */ new Set([
...RESET_ON_SRC_QUALITY_CHANGE,
"ended",
"inferredPoster",
"inferredStreamType",
"inferredTitle",
"intrinsicDuration",
"inferredLiveDVRWindow",
"liveSyncPosition",
"realCurrentTime",
"savedState",
"started",
"userBehindLiveEdge"
]);
function softResetMediaState($media, isSourceQualityChange = false) {
const filter = isSourceQualityChange ? RESET_ON_SRC_QUALITY_CHANGE : RESET_ON_SRC_CHANGE;
mediaState.reset($media, (prop) => filter.has(prop));
tick();
}
function boundTime(time, store) {
const clippedTime = time + store.clipStartTime(), isStart = Math.floor(time) === Math.floor(store.seekableStart()), isEnd = Math.floor(clippedTime) === Math.floor(store.seekableEnd());
if (isStart) {
return store.seekableStart();
}
if (isEnd) {
return store.seekableEnd();
}
if (store.isLiveDVR() && store.liveDVRWindow() > 0 && clippedTime < store.seekableEnd() - store.liveDVRWindow()) {
return store.bufferedStart();
}
return Math.min(Math.max(store.seekableStart() + 0.1, clippedTime), store.seekableEnd() - 0.1);
}
export { AUDIO_EXTENSIONS, AUDIO_TYPES, DASH_VIDEO_EXTENSIONS, DASH_VIDEO_TYPES, HLS_VIDEO_EXTENSIONS, HLS_VIDEO_TYPES, IS_CHROME, TextTrack, TextTrackSymbol, TimeRange, VIDEO_EXTENSIONS, VIDEO_TYPES, boundTime, canChangeVolume, canGoogleCastSrc, canOrientScreen, canPlayHLSNatively, canPlayVideoType, canRotateScreen, canUsePictureInPicture, canUseVideoPresentation, findActiveCue, getDownloadFile, getRequestCredentials, getTimeRangesEnd, getTimeRangesStart, isAudioSrc, isCueActive, isDASHSrc, isDASHSupported, isHLSSrc, isHLSSupported, isMediaStream, isTrackCaptionKind, isVideoSrc, loadScript, mediaState, normalizeTimeIntervals, parseJSONCaptionsFile, preconnect, softResetMediaState, updateTimeIntervals, watchActiveTextTrack, watchCueTextChange };