@7sage/vidstack
Version:
UI component library for building high-quality, accessible video and audio experiences on the web.
240 lines (236 loc) • 7.51 kB
JavaScript
import { EventsTarget, DOMEvent, isString, isArray, isNumber } from './vidstack-BGSTndAW.js';
import { getRequestCredentials } from './vidstack-CTojmhKq.js';
import { isCueActive } from './vidstack-DYbwIVLq.js';
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
};
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.content) {
this.#parseContent(init);
} else 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 };
}
export { TextTrack, TextTrackSymbol, isTrackCaptionKind, parseJSONCaptionsFile };