UNPKG

@ktt45678/vidstack

Version:

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

550 lines (546 loc) 16.4 kB
import { createScope, signal, effect, peek, isString, deferredPromise, listenEvent, isArray } from '../chunks/vidstack-fG_Sx3Q9.js'; import { QualitySymbol } from '../chunks/vidstack-BYmCj-36.js'; import { TimeRange } from '../chunks/vidstack-CLRUrTzh.js'; import { TextTrack } from '../chunks/vidstack-DSRs3D8P.js'; import { RAFLoop, ListSymbol } from '../chunks/vidstack-BXMqlVv4.js'; import { preconnect } from '../chunks/vidstack-BnCZ4oyK.js'; import { EmbedProvider } from '../chunks/vidstack-DAbDXxJu.js'; import { resolveVimeoVideoId, getVimeoVideoInfo } from '../chunks/vidstack-krOAtKMi.js'; import 'media-captions'; import '../chunks/vidstack-C_9SlM6s.js'; import '../chunks/vidstack-BpOkecTJ.js'; const trackedVimeoEvents = [ "bufferend", "bufferstart", // 'cuechange', "durationchange", "ended", "enterpictureinpicture", "error", "fullscreenchange", "leavepictureinpicture", "loaded", // 'loadeddata', // 'loadedmetadata', // 'loadstart', "playProgress", "loadProgress", "pause", "play", "playbackratechange", // 'progress', "qualitychange", "seeked", "seeking", // 'texttrackchange', "timeupdate", "volumechange", "waiting" // 'adstarted', // 'adcompleted', // 'aderror', // 'adskipped', // 'adallcompleted', // 'adclicked', // 'chapterchange', // 'chromecastconnected', // 'remoteplaybackavailabilitychange', // 'remoteplaybackconnecting', // 'remoteplaybackconnect', // 'remoteplaybackdisconnect', // 'liveeventended', // 'liveeventstarted', // 'livestreamoffline', // 'livestreamonline', ]; class VimeoProvider extends EmbedProvider { constructor(iframe, _ctx) { super(iframe); this._ctx = _ctx; this.$$PROVIDER_TYPE = "VIMEO"; this.scope = createScope(); this._videoId = signal(""); this._pro = signal(false); this._hash = null; this._currentSrc = null; this._fullscreenActive = false; this._seekableRange = new TimeRange(0, 0); this._timeRAF = new RAFLoop(this._onAnimationFrame.bind(this)); this._currentCue = null; this._chaptersTrack = null; this._promises = /* @__PURE__ */ new Map(); this._videoInfoPromise = null; /** * Whether tracking session data should be enabled on the embed, including cookies and analytics. * This is turned off by default to be GDPR-compliant. * * @defaultValue `false` */ this.cookies = false; this.title = true; this.byline = true; this.portrait = true; this.color = "00ADEF"; // Embed will sometimes dispatch 0 at end of playback. this._preventTimeUpdates = false; const self = this; this.fullscreen = { get active() { return self._fullscreenActive; }, supported: true, enter: () => this._remote("requestFullscreen"), exit: () => this._remote("exitFullscreen") }; } get _notify() { return this._ctx.delegate._notify; } get type() { return "vimeo"; } get currentSrc() { return this._currentSrc; } get videoId() { return this._videoId(); } get hash() { return this._hash; } get isPro() { return this._pro(); } preconnect() { preconnect(this._getOrigin()); } setup() { super.setup(); effect(this._watchVideoId.bind(this)); effect(this._watchVideoInfo.bind(this)); effect(this._watchPro.bind(this)); this._notify("provider-setup", this); } destroy() { this._reset(); this.fullscreen = void 0; const message = "provider destroyed"; for (const promises of this._promises.values()) { for (const { reject } of promises) reject(message); } this._promises.clear(); this._remote("destroy"); } async play() { return this._remote("play"); } async pause() { return this._remote("pause"); } setMuted(muted) { this._remote("setMuted", muted); } setCurrentTime(time) { this._remote("seekTo", time); this._notify("seeking", time); } setVolume(volume) { this._remote("setVolume", volume); this._remote("setMuted", peek(this._ctx.$state.muted)); } setPlaybackRate(rate) { this._remote("setPlaybackRate", rate); } async loadSource(src) { if (!isString(src.src)) { this._currentSrc = null; this._hash = null; this._videoId.set(""); return; } const { videoId, hash } = resolveVimeoVideoId(src.src); this._videoId.set(videoId ?? ""); this._hash = hash ?? null; this._currentSrc = src; } _watchVideoId() { this._reset(); const videoId = this._videoId(); if (!videoId) { this._src.set(""); return; } this._src.set(`${this._getOrigin()}/video/${videoId}`); this._notify("load-start"); } _watchVideoInfo() { const videoId = this._videoId(); if (!videoId) return; const promise = deferredPromise(), abort = new AbortController(); this._videoInfoPromise = promise; getVimeoVideoInfo(videoId, abort, this._hash).then((info) => { promise.resolve(info); }).catch((e) => { promise.reject(); { this._ctx.logger?.warnGroup(`Failed to fetch vimeo video info for id \`${videoId}\`.`).labelledLog("Error", e).dispatch(); } }); return () => { promise.reject(); abort.abort(); }; } _watchPro() { const isPro = this._pro(), { $state, qualities } = this._ctx; $state.canSetPlaybackRate.set(isPro); qualities[ListSymbol._setReadonly](!isPro); if (isPro) { return listenEvent(qualities, "change", () => { if (qualities.auto) return; const id = qualities.selected?.id; if (id) this._remote("setQuality", id); }); } } _getOrigin() { return "https://player.vimeo.com"; } _buildParams() { const { keyDisabled } = this._ctx.$props, { playsInline, nativeControls } = this._ctx.$state, showControls = nativeControls(); return { title: this.title, byline: this.byline, color: this.color, portrait: this.portrait, controls: showControls, h: this.hash, keyboard: showControls && !keyDisabled(), transparent: true, playsinline: playsInline(), dnt: !this.cookies }; } _onAnimationFrame() { this._remote("getCurrentTime"); } _onTimeUpdate(time, trigger) { if (this._preventTimeUpdates && time === 0) return; const { realCurrentTime, realDuration, paused, bufferedEnd } = this._ctx.$state; if (realCurrentTime() === time) return; const prevTime = realCurrentTime(); this._notify("time-change", time, trigger); if (Math.abs(prevTime - time) > 1.5) { this._notify("seeking", time, trigger); if (!paused() && bufferedEnd() < time) { this._notify("waiting", void 0, trigger); } } if (realDuration() - time < 0.01) { this._notify("end", void 0, trigger); this._preventTimeUpdates = true; setTimeout(() => { this._preventTimeUpdates = false; }, 500); } } _onSeeked(time, trigger) { this._notify("seeked", time, trigger); } _onLoaded(trigger) { const videoId = this._videoId(); this._videoInfoPromise?.promise.then((info) => { if (!info) return; const { title, poster, duration, pro } = info; this._pro.set(pro); this._notify("title-change", title, trigger); this._notify("poster-change", poster, trigger); this._notify("duration-change", duration, trigger); this._onReady(duration, trigger); }).catch(() => { if (videoId !== this._videoId()) return; this._remote("getVideoTitle"); this._remote("getDuration"); }); } _onReady(duration, trigger) { const { nativeControls } = this._ctx.$state, showEmbedControls = nativeControls(); this._seekableRange = new TimeRange(0, duration); const detail = { buffered: new TimeRange(0, 0), seekable: this._seekableRange, duration }; this._ctx.delegate._ready(detail, trigger); if (!showEmbedControls) { this._remote("_hideOverlay"); } this._remote("getQualities"); this._remote("getChapters"); } _onMethod(method, data, trigger) { switch (method) { case "getVideoTitle": const videoTitle = data; this._notify("title-change", videoTitle, trigger); break; case "getDuration": const duration = data; if (!this._ctx.$state.canPlay()) { this._onReady(duration, trigger); } else { this._notify("duration-change", duration, trigger); } break; case "getCurrentTime": this._onTimeUpdate(data, trigger); break; case "getBuffered": if (isArray(data) && data.length) { this._onLoadProgress(data[data.length - 1][1], trigger); } break; case "setMuted": this._onVolumeChange(peek(this._ctx.$state.volume), data, trigger); break; case "getChapters": this._onChaptersChange(data); break; case "getQualities": this._onQualitiesChange(data, trigger); break; } this._getPromise(method)?.resolve(); } _attachListeners() { for (const type of trackedVimeoEvents) { this._remote("addEventListener", type); } } _onPause(trigger) { this._timeRAF._stop(); this._notify("pause", void 0, trigger); } _onPlay(trigger) { this._timeRAF._start(); this._notify("play", void 0, trigger); } _onPlayProgress(trigger) { const { paused } = this._ctx.$state; if (!paused() && !this._preventTimeUpdates) { this._notify("playing", void 0, trigger); } } _onLoadProgress(buffered, trigger) { const detail = { buffered: new TimeRange(0, buffered), seekable: this._seekableRange }; this._notify("progress", detail, trigger); } _onBufferStart(trigger) { this._notify("waiting", void 0, trigger); } _onBufferEnd(trigger) { const { paused } = this._ctx.$state; if (!paused()) this._notify("playing", void 0, trigger); } _onWaiting(trigger) { const { paused } = this._ctx.$state; if (paused()) { this._notify("play", void 0, trigger); } this._notify("waiting", void 0, trigger); } _onVolumeChange(volume, muted, trigger) { const detail = { volume, muted }; this._notify("volume-change", detail, trigger); } // protected _onTextTrackChange(track: VimeoTextTrack, trigger: Event) { // const textTrack = this._ctx.textTracks.toArray().find((t) => t.language === track.language); // if (textTrack) textTrack.mode = track.mode; // } // protected _onTextTracksChange(tracks: VimeoTextTrack[], trigger: Event) { // for (const init of tracks) { // const textTrack = new TextTrack({ // ...init, // label: init.label.replace('auto-generated', 'auto'), // }); // textTrack[TextTrackSymbol._readyState] = 2; // this._ctx.textTracks.add(textTrack, trigger); // textTrack.setMode(init.mode, trigger); // } // } // protected _onCueChange(cue: VimeoTextCue, trigger: Event) { // const { textTracks, $state } = this._ctx, // { currentTime } = $state, // track = textTracks.selected; // if (this._currentCue) track?.removeCue(this._currentCue, trigger); // this._currentCue = new window.VTTCue(currentTime(), Number.MAX_SAFE_INTEGER, cue.text); // track?.addCue(this._currentCue, trigger); // } _onChaptersChange(chapters) { this._removeChapters(); if (!chapters.length) return; const track = new TextTrack({ kind: "chapters", default: true }), { realDuration } = this._ctx.$state; for (let i = 0; i < chapters.length; i++) { const chapter = chapters[i], nextChapter = chapters[i + 1]; track.addCue( new window.VTTCue( chapter.startTime, nextChapter?.startTime ?? realDuration(), chapter.title ) ); } this._chaptersTrack = track; this._ctx.textTracks.add(track); } _removeChapters() { if (!this._chaptersTrack) return; this._ctx.textTracks.remove(this._chaptersTrack); this._chaptersTrack = null; } _onQualitiesChange(qualities, trigger) { this._ctx.qualities[QualitySymbol._enableAuto] = qualities.some((q) => q.id === "auto") ? () => this._remote("setQuality", "auto") : void 0; for (const quality of qualities) { if (quality.id === "auto") continue; const height = +quality.id.slice(0, -1); if (isNaN(height)) continue; this._ctx.qualities[ListSymbol._add]( { id: quality.id, width: height * (16 / 9), height, codec: "avc1,h.264", bitrate: -1 }, trigger ); } this._onQualityChange( qualities.find((q) => q.active), trigger ); } _onQualityChange({ id } = {}, trigger) { if (!id) return; const isAuto = id === "auto", newQuality = this._ctx.qualities.getById(id); if (isAuto) { this._ctx.qualities[QualitySymbol._setAuto](isAuto, trigger); this._ctx.qualities[ListSymbol._select](void 0, true, trigger); } else { this._ctx.qualities[ListSymbol._select](newQuality ?? void 0, true, trigger); } } _onEvent(event, payload, trigger) { switch (event) { case "ready": this._attachListeners(); break; case "loaded": this._onLoaded(trigger); break; case "play": this._onPlay(trigger); break; case "playProgress": this._onPlayProgress(trigger); break; case "pause": this._onPause(trigger); break; case "loadProgress": this._onLoadProgress(payload.seconds, trigger); break; case "waiting": this._onWaiting(trigger); break; case "bufferstart": this._onBufferStart(trigger); break; case "bufferend": this._onBufferEnd(trigger); break; case "volumechange": this._onVolumeChange(payload.volume, peek(this._ctx.$state.muted), trigger); break; case "durationchange": this._seekableRange = new TimeRange(0, payload.duration); this._notify("duration-change", payload.duration, trigger); break; case "playbackratechange": this._notify("rate-change", payload.playbackRate, trigger); break; case "qualitychange": this._onQualityChange(payload, trigger); break; case "fullscreenchange": this._fullscreenActive = payload.fullscreen; this._notify("fullscreen-change", payload.fullscreen, trigger); break; case "enterpictureinpicture": this._notify("picture-in-picture-change", true, trigger); break; case "leavepictureinpicture": this._notify("picture-in-picture-change", false, trigger); break; case "ended": this._notify("end", void 0, trigger); break; case "error": this._onError(payload, trigger); break; case "seek": case "seeked": this._onSeeked(payload.seconds, trigger); break; } } _onError(error, trigger) { const { message, method } = error; if (method === "setPlaybackRate") { this._pro.set(false); } if (method) { this._getPromise(method)?.reject(message); } { this._ctx.logger?.errorGroup(`[vimeo]: ${message}`).labelledLog("Error", error).labelledLog("Provider", this).labelledLog("Event", trigger).dispatch(); } } _onMessage(message, event) { if (message.event) { this._onEvent(message.event, message.data, event); } else if (message.method) { this._onMethod(message.method, message.value, event); } } _onLoad() { } async _remote(command, arg) { let promise = deferredPromise(), promises = this._promises.get(command); if (!promises) this._promises.set(command, promises = []); promises.push(promise); this._postMessage({ method: command, value: arg }); return promise.promise; } _reset() { this._timeRAF._stop(); this._seekableRange = new TimeRange(0, 0); this._videoInfoPromise = null; this._currentCue = null; this._pro.set(false); this._removeChapters(); } _getPromise(command) { return this._promises.get(command)?.shift(); } } export { VimeoProvider };