@ktt45678/vidstack
Version:
UI component library for building high-quality, accessible video and audio experiences on the web.
383 lines (378 loc) • 11.1 kB
JavaScript
import { loadScript, preconnect } from '../chunks/vidstack-CVbXna2m.js';
import { IS_CHROME, isHLSSupported } from '../chunks/vidstack-CTW_LGt6.js';
import { VideoProvider } from './vidstack-video.js';
import { peek, listenEvent, effect, DOMEvent, isString, camelToKebabCase, isUndefined, isFunction } from '../chunks/vidstack-C6myozhB.js';
import { QualitySymbol } from '../chunks/vidstack-DH8xaM_3.js';
import { TextTrack, TextTrackSymbol } from '../chunks/vidstack-CFEqcMSQ.js';
import { ListSymbol } from '../chunks/vidstack-BoSiLpaP.js';
import { RAFLoop } from '../chunks/vidstack-C-clE4br.js';
import { coerceToError } from '../chunks/vidstack-C9vIqaYT.js';
import './vidstack-html.js';
import '../chunks/vidstack-Dihypf8P.js';
import '../chunks/vidstack-B7-_7of1.js';
import 'media-captions';
import '../chunks/vidstack-D2w309v1.js';
const toDOMEventType = (type) => camelToKebabCase(type);
class HLSController {
constructor(_video, _ctx) {
this.m = _video;
this.b = _ctx;
this.d = null;
this.qb = null;
this.rb = {};
this.sb = /* @__PURE__ */ new Set();
}
get instance() {
return this.d;
}
setup(ctor) {
const { streamType } = this.b.$state;
const isLive = peek(streamType).includes("live"), isLiveLowLatency = peek(streamType).includes("ll-");
this.d = new ctor({
lowLatencyMode: isLiveLowLatency,
backBufferLength: isLiveLowLatency ? 4 : isLive ? 8 : void 0,
renderTextTracksNatively: false,
...this.rb
});
const dispatcher = this.Oi.bind(this);
for (const event of Object.values(ctor.Events)) this.d.on(event, dispatcher);
this.d.on(ctor.Events.ERROR, this.Q.bind(this));
for (const callback of this.sb) callback(this.d);
this.b.player.dispatch("hls-instance", {
detail: this.d
});
this.d.attachMedia(this.m);
this.d.on(ctor.Events.AUDIO_TRACK_SWITCHED, this.Qi.bind(this));
this.d.on(ctor.Events.LEVEL_SWITCHED, this.Ri.bind(this));
this.d.on(ctor.Events.LEVEL_LOADED, this.Si.bind(this));
this.d.on(ctor.Events.NON_NATIVE_TEXT_TRACKS_FOUND, this.Ti.bind(this));
this.d.on(ctor.Events.CUES_PARSED, this.Ui.bind(this));
this.b.qualities[QualitySymbol.Ia] = this.je.bind(this);
listenEvent(this.b.qualities, "change", this.ke.bind(this));
listenEvent(this.b.audioTracks, "change", this.le.bind(this));
this.qb = effect(this.me.bind(this));
}
aa(type, data) {
return new DOMEvent(toDOMEventType(type), { detail: data });
}
me() {
if (!this.b.$state.live()) return;
const raf = new RAFLoop(this.ne.bind(this));
raf.Xa();
return raf.$.bind(raf);
}
ne() {
this.b.$state.liveSyncPosition.set(this.d?.liveSyncPosition ?? Infinity);
}
Oi(type, data) {
this.b.player?.dispatch(this.aa(type, data));
}
Ti(eventType, data) {
const event = this.aa(eventType, data);
let currentTrack = -1;
for (let i = 0; i < data.tracks.length; i++) {
const nonNativeTrack = data.tracks[i], init = nonNativeTrack.subtitleTrack ?? nonNativeTrack.closedCaptions, track = new TextTrack({
id: `hls-${nonNativeTrack.kind}-${i}`,
src: init?.url,
label: nonNativeTrack.label,
language: init?.lang,
kind: nonNativeTrack.kind,
default: nonNativeTrack.default
});
track[TextTrackSymbol.ma] = 2;
track[TextTrackSymbol.hb] = () => {
if (track.mode === "showing") {
this.d.subtitleTrack = i;
currentTrack = i;
} else if (currentTrack === i) {
this.d.subtitleTrack = -1;
currentTrack = -1;
}
};
this.b.textTracks.add(track, event);
}
}
Ui(eventType, data) {
const index = this.d?.subtitleTrack, track = this.b.textTracks.getById(`hls-${data.type}-${index}`);
if (!track) return;
const event = this.aa(eventType, data);
for (const cue of data.cues) {
cue.positionAlign = "auto";
track.addCue(cue, event);
}
}
Qi(eventType, data) {
const track = this.b.audioTracks[data.id];
if (track) {
const trigger = this.aa(eventType, data);
this.b.audioTracks[ListSymbol.ea](track, true, trigger);
}
}
Ri(eventType, data) {
const quality = this.b.qualities[data.level];
if (quality) {
const trigger = this.aa(eventType, data);
this.b.qualities[ListSymbol.ea](quality, true, trigger);
}
}
Si(eventType, data) {
if (this.b.$state.canPlay()) return;
const { type, live, totalduration: duration, targetduration } = data.details, trigger = this.aa(eventType, data);
this.b.delegate.c(
"stream-type-change",
live ? type === "EVENT" && Number.isFinite(duration) && targetduration >= 10 ? "live:dvr" : "live" : "on-demand",
trigger
);
this.b.delegate.c("duration-change", duration, trigger);
const media = this.d.media;
if (this.d.currentLevel === -1) {
this.b.qualities[QualitySymbol.Wa](true, trigger);
}
for (const remoteTrack of this.d.audioTracks) {
const localTrack = {
id: remoteTrack.id.toString(),
label: remoteTrack.name,
language: remoteTrack.lang || "",
kind: "main"
};
this.b.audioTracks[ListSymbol.da](localTrack, trigger);
}
for (const level of this.d.levels) {
const videoQuality = {
id: level.id?.toString() ?? level.height + "p",
width: level.width,
height: level.height,
codec: level.codecSet,
bitrate: level.bitrate
};
this.b.qualities[ListSymbol.da](videoQuality, trigger);
}
media.dispatchEvent(new DOMEvent("canplay", { trigger }));
}
Q(eventType, data) {
if (data.fatal) {
switch (data.type) {
case "mediaError":
this.d?.recoverMediaError();
break;
default:
this.qc(data.error);
break;
}
}
}
qc(error) {
this.b.delegate.c("error", {
message: error.message,
code: 1,
error
});
}
je() {
if (this.d) this.d.currentLevel = -1;
}
ke() {
const { qualities } = this.b;
if (!this.d || qualities.auto) return;
this.d[qualities.switch + "Level"] = qualities.selectedIndex;
if (IS_CHROME) {
this.m.currentTime = this.m.currentTime;
}
}
le() {
const { audioTracks } = this.b;
if (this.d && this.d.audioTrack !== audioTracks.selectedIndex) {
this.d.audioTrack = audioTracks.selectedIndex;
}
}
Vi(src) {
if (!isString(src.src)) return;
this.d?.loadSource(src.src);
}
Wi() {
this.d?.destroy();
this.d = null;
this.qb?.();
this.qb = null;
}
}
class HLSLibLoader {
constructor(_lib, _ctx, _callback) {
this.L = _lib;
this.b = _ctx;
this.La = _callback;
this.qe();
}
async qe() {
const callbacks = {
onLoadStart: this.Ma.bind(this),
onLoaded: this.tb.bind(this),
onLoadError: this.re.bind(this)
};
let ctor = await loadHLSScript(this.L, callbacks);
if (isUndefined(ctor) && !isString(this.L)) ctor = await importHLS(this.L, callbacks);
if (!ctor) return null;
if (!ctor.isSupported()) {
const message = "[vidstack] `hls.js` is not supported in this environment";
this.b.player.dispatch(new DOMEvent("hls-unsupported"));
this.b.delegate.c("error", { message, code: 4 });
return null;
}
return ctor;
}
Ma() {
this.b.player.dispatch(new DOMEvent("hls-lib-load-start"));
}
tb(ctor) {
this.b.player.dispatch(
new DOMEvent("hls-lib-loaded", {
detail: ctor
})
);
this.La(ctor);
}
re(e) {
const error = coerceToError(e);
this.b.player.dispatch(
new DOMEvent("hls-lib-load-error", {
detail: error
})
);
this.b.delegate.c("error", {
message: error.message,
code: 4,
error
});
}
}
async function importHLS(loader, callbacks = {}) {
if (isUndefined(loader)) return void 0;
callbacks.onLoadStart?.();
if (loader.prototype && loader.prototype !== Function) {
callbacks.onLoaded?.(loader);
return loader;
}
try {
const ctor = (await loader())?.default;
if (ctor && !!ctor.isSupported) {
callbacks.onLoaded?.(ctor);
} else {
throw Error(
false ? "[vidstack] failed importing `hls.js`. Dynamic import returned invalid constructor." : ""
);
}
return ctor;
} catch (err) {
callbacks.onLoadError?.(err);
}
return void 0;
}
async function loadHLSScript(src, callbacks = {}) {
if (!isString(src)) return void 0;
callbacks.onLoadStart?.();
try {
await loadScript(src);
if (!isFunction(window.Hls)) {
throw Error(
false ? "[vidstack] failed loading `hls.js`. Could not find a valid `Hls` constructor on window" : ""
);
}
const ctor = window.Hls;
callbacks.onLoaded?.(ctor);
return ctor;
} catch (err) {
callbacks.onLoadError?.(err);
}
return void 0;
}
const JS_DELIVR_CDN = "https://cdn.jsdelivr.net";
class HLSProvider extends VideoProvider {
constructor() {
super(...arguments);
this.$$PROVIDER_TYPE = "HLS";
this.rc = null;
this.e = new HLSController(this.video, this.b);
this.oa = `${JS_DELIVR_CDN}/npm/hls.js@^1.5.0/dist/hls${".min.js"}`;
}
/**
* The `hls.js` constructor.
*/
get ctor() {
return this.rc;
}
/**
* The current `hls.js` instance.
*/
get instance() {
return this.e.instance;
}
static {
this.supported = isHLSSupported();
}
get type() {
return "hls";
}
get canLiveSync() {
return true;
}
/**
* The `hls.js` configuration object.
*
* @see {@link https://github.com/video-dev/hls.js/blob/master/docs/API.md#fine-tuning}
*/
get config() {
return this.e.rb;
}
set config(config) {
this.e.rb = config;
}
/**
* The `hls.js` constructor (supports dynamic imports) or a URL of where it can be found.
*
* @defaultValue `https://cdn.jsdelivr.net/npm/hls.js@^1.0.0/dist/hls.min.js`
*/
get library() {
return this.oa;
}
set library(library) {
this.oa = library;
}
preconnect() {
if (!isString(this.oa)) return;
preconnect(this.oa);
}
setup() {
super.setup();
new HLSLibLoader(this.oa, this.b, (ctor) => {
this.rc = ctor;
this.e.setup(ctor);
this.b.delegate.c("provider-setup", this);
const src = peek(this.b.$state.source);
if (src) this.loadSource(src);
});
}
async loadSource(src, preload) {
if (!isString(src.src)) {
this.oc();
return;
}
this.a.preload = preload || "";
this.ge(src, "application/x-mpegurl");
this.e.Vi(src);
this.K = src;
}
/**
* The given callback is invoked when a new `hls.js` instance is created and right before it's
* attached to media.
*/
onInstance(callback) {
const instance = this.e.instance;
if (instance) callback(instance);
this.e.sb.add(callback);
return () => this.e.sb.delete(callback);
}
destroy() {
this.e.Wi();
}
}
export { HLSProvider };