@ktt45678/vidstack
Version:
UI component library for building high-quality, accessible video and audio experiences on the web.
267 lines (262 loc) • 8.92 kB
JavaScript
import { EventsTarget, DOMEvent, isString, isArray, isNumber } from './vidstack-fG_Sx3Q9.js';
import { parseText } from 'media-captions';
import { getRequestCredentials } from './vidstack-BnCZ4oyK.js';
import { isCueActive } from './vidstack-C_9SlM6s.js';
const CROSS_ORIGIN = Symbol("TEXT_TRACK_CROSS_ORIGIN" ), READY_STATE = Symbol("TEXT_TRACK_READY_STATE" ), UPDATE_ACTIVE_CUES = Symbol("TEXT_TRACK_UPDATE_ACTIVE_CUES" ), CAN_LOAD = Symbol("TEXT_TRACK_CAN_LOAD" ), ON_MODE_CHANGE = Symbol("TEXT_TRACK_ON_MODE_CHANGE" ), NATIVE = Symbol("TEXT_TRACK_NATIVE" ), NATIVE_HLS = Symbol("TEXT_TRACK_NATIVE_HLS" );
const TextTrackSymbol = {
_crossOrigin: CROSS_ORIGIN,
_readyState: READY_STATE,
_updateActiveCues: UPDATE_ACTIVE_CUES,
_canLoad: CAN_LOAD,
_onModeChange: ON_MODE_CHANGE,
_native: NATIVE,
_nativeHLS: NATIVE_HLS
};
var _a, _b, _c;
class TextTrack extends EventsTarget {
constructor(init) {
super();
this.id = "";
this.label = "";
this.language = "";
this.default = false;
this.contentLoaded = false;
this._canLoad = false;
this._currentTime = 0;
this._mode = "disabled";
this._metadata = {};
this._regions = [];
this._cues = [];
this._activeCues = [];
/** @internal */
this[_c] = 0;
/** @internal */
this[_b] = null;
/** @internal */
this[_a] = null;
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;
}
if (isTrackCaptionKind(this) && !this.label) {
console.warn(`[vidstack] captions text track created without label: \`${this.src}\``);
}
}
static createId(track) {
return `vds-${track.type}-${track.kind}-${track.src ?? track.label ?? "?"}`;
}
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);
}
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 */
[(_c = TextTrackSymbol._readyState, _b = TextTrackSymbol._onModeChange, _a = TextTrackSymbol._native, 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: parseText2, VTTCue, VTTRegion }) => {
if (!isString(init.content) || init.type === "json") {
this._parseJSON(init.content, VTTCue, VTTRegion);
if (this.readyState !== 3) this._ready();
} else {
parseText2(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]?.();
if (!this.subtitleLoader || typeof this.subtitleLoader !== "function") {
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;
}
}
} else {
const content = await Promise.resolve(this.subtitleLoader(this));
if (!content) {
throw new Error("Subtitle loaded error");
}
this.content = content;
this.contentLoaded = true;
if (this.type === "json") {
this._parseJSON(content, VTTCue, VTTRegion);
} else {
const { errors, metadata, regions, cues } = await parseText(content, {
type: this.type
});
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) {
{
console.error(`[vidstack] failed to parse JSON captions at: \`${this.src}\`
`, 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 };