UNPKG

@7sage/vidstack

Version:

UI component library for building high-quality, accessible video and audio experiences on the web.

308 lines (305 loc) 8.08 kB
import { isString, setStyle, createDisposalBin, isBoolean, DOMEvent } from '../chunks/vidstack-BUJn1if6.js'; import { mediaState, canPlayVideoType } from '../chunks/vidstack-CshLUi9f.js'; import '@floating-ui/dom'; let activePlyr = null, defaults = mediaState.record, eventMap = { ratechange: "rate-change", ready: "can-play", timeupdate: "time-update", volumechange: "volume-change" }; class Plyr { constructor(target, config = {}) { this.target = target; this.config = config; { throw Error("[plyr] can not create player on server."); } } static setup(targets, config) { if (isString(targets)) { targets = document.querySelectorAll(targets); } return [...targets].map((target) => new Plyr(target, config)); } static supported(type, provider) { return true; } player; provider; layout; fullscreen = new PlyrFullscreenAdapter(this); // These are only included for type defs, props are defined in constructor. playing = defaults.playing; paused = defaults.paused; ended = defaults.ended; currentTime = defaults.currentTime; seeking = defaults.seeking; duration = defaults.duration; volume = defaults.volume; muted = defaults.muted; loop = defaults.loop; poster = defaults.poster; get type() { return this.player.provider?.type ?? ""; } get isHTML5() { return /audio|video|hls/.test(this.type); } get isEmbed() { return /youtube|vimeo/.test(this.type); } get buffered() { const { bufferedEnd, seekableEnd } = this.player.state; return seekableEnd > 0 ? bufferedEnd / seekableEnd : 0; } get stopped() { return this.paused && this.currentTime === 0; } get hasAudio() { if (!this.isHTML5) return true; const media = this.player.provider.media; return Boolean( media.mozHasAudio || media.webkitAudioDecodedByteCount || media.audioTracks?.length || this.player.audioTracks.length ); } get speed() { return this.player.playbackRate; } set speed(speed) { this.player.remoteControl.changePlaybackRate(speed); } get currentTrack() { return this.player.textTracks.selectedIndex; } set currentTrack(index) { this.player.remoteControl.changeTextTrackMode(index, "showing"); } get pip() { return this.player.state.pictureInPicture; } set pip(isActive) { if (isActive) this.player.enterPictureInPicture(); else this.player.exitPictureInPicture(); } get quality() { return this.player.state.quality?.height ?? null; } set quality(value) { let qualities = this.player.qualities, index = -1; if (value !== null) { let minScore = Infinity; for (let i = 0; i < qualities.length; i++) { const score = Math.abs(qualities[i].height - value); if (score < minScore) { index = i; minScore = score; } } } this.player.remoteControl.changeQuality(index); } #source = null; get source() { return this.#source; } set source(source) { const { type: viewType = "video", sources = "", title = "", poster = "", thumbnails = "", tracks = [] } = source ?? {}; this.player.src = sources; this.player.viewType = viewType; this.player.title = title; this.player.poster = poster; this.layout.thumbnails = thumbnails; this.player.textTracks.clear(); for (const track of tracks) this.player.textTracks.add(track); this.#source = source; } #ratio = null; get ratio() { return this.#ratio; } set ratio(ratio) { if (ratio) ratio = ratio.replace(/\s*:\s*/, " / "); setStyle(this.player, "aspect-ratio", ratio ?? "unset"); this.#ratio = ratio; } get download() { return this.layout.download; } set download(download) { this.layout.download = download; } #disposal = createDisposalBin(); #onPlay() { if (activePlyr !== this) activePlyr?.pause(); activePlyr = this; } #onReset() { this.currentTime = 0; this.paused = true; } play() { return this.player.play(); } pause() { return this.player.pause(); } togglePlay(toggle = this.paused) { if (toggle) { return this.player.play(); } else { return this.player.pause(); } } toggleCaptions(toggle = !this.player.textTracks.selected) { const controller = this.player.remoteControl; if (toggle) { controller.showCaptions(); } else { controller.disableCaptions(); } } toggleControls(toggle = !this.player.controls.showing) { const controls = this.player.controls; if (toggle) { controls.show(); } else { controls.hide(); } } restart() { this.currentTime = 0; } stop() { this.pause(); this.player.currentTime = 0; } forward(seekTime = this.config.seekTime ?? 10) { this.currentTime += seekTime; } rewind(seekTime = this.config.seekTime ?? 10) { this.currentTime -= seekTime; } increaseVolume(step = 5) { this.volume += step; } decreaseVolume(step = 5) { this.volume -= step; } airplay() { return this.player.requestAirPlay(); } on(type, callback) { this.#listen(type, callback); } once(type, callback) { this.#listen(type, callback, { once: true }); } off(type, callback) { this.#listen(type, callback, { remove: true }); } #listeners = []; #listen(type, callback, options = {}) { let eventType = type, toggle = null; switch (type) { case "captionsenabled": case "captionsdisabled": eventType = "text-track-change"; toggle = type === "captionsenabled"; break; case "controlsshown": case "controlshidden": eventType = "controls-change"; toggle = type === "controlsshown"; break; case "enterfullscreen": case "exitfullscreen": eventType = "fullscreen-change"; toggle = type === "enterfullscreen"; break; } const mappedEventType = eventMap[eventType] ?? eventType; const listener = (event) => { if (isBoolean(toggle) && !!event.detail !== toggle) return; if (mappedEventType !== type) { callback(new DOMEvent(type, { ...event, trigger: event })); return; } callback(event); }; if (options.remove) { let index = -1; do { index = this.#listeners.findIndex((t) => t.type === type && t.callback === callback); if (index >= 0) { const { listener: listener2 } = this.#listeners[index]; this.player.removeEventListener(mappedEventType, listener2); this.#listeners.splice(index, 1); } } while (index >= 0); } else { this.#listeners.push({ type, callback, listener }); this.player.addEventListener(mappedEventType, listener, { once: options.once }); } } supports(type) { return !!type && canPlayVideoType(); } destroy() { for (const { type, listener } of this.#listeners) { this.player.removeEventListener(eventMap[type] ?? type, listener); } this.#source = null; this.#listeners.length = 0; if (activePlyr === this) activePlyr = null; this.#disposal.empty(); this.player.destroy(); } } class PlyrFullscreenAdapter { #plyr; constructor(plyr) { this.#plyr = plyr; } get #player() { return this.#plyr.player; } /** * Returns a boolean indicating if the current player has fullscreen enabled. */ get enabled() { return this.#player.state.canFullscreen; } /** * Returns a boolean indicating if the current player is in fullscreen mode. */ get active() { return this.#player.state.fullscreen; } /** * Request to enter fullscreen. */ enter() { return this.#player.requestFullscreen(); } /** * Request to exit fullscreen. */ exit() { return this.#player.exitFullscreen(); } /** * Request to toggle fullscreen. */ toggle() { if (this.active) return this.exit(); else return this.enter(); } } export { Plyr, PlyrFullscreenAdapter };