@7sage/vidstack
Version:
UI component library for building high-quality, accessible video and audio experiences on the web.
552 lines (548 loc) • 16.5 kB
JavaScript
import { createScope, signal, effect, peek, isString, deferredPromise, listenEvent, isArray } from '../chunks/vidstack-Bu2kfzUd.js';
import { QualitySymbol } from '../chunks/vidstack-Bpr4fI4n.js';
import { TimeRange } from '../chunks/vidstack-BFg1ZqiG.js';
import { TextTrack } from '../chunks/vidstack-BYNmVJLa.js';
import { ListSymbol } from '../chunks/vidstack-Dv_LIPFu.js';
import { RAFLoop } from '../chunks/vidstack-qh1N5_f_.js';
import { preconnect } from '../chunks/vidstack-zG6PIeGg.js';
import { EmbedProvider } from '../chunks/vidstack-BoAGnlRt.js';
import { resolveVimeoVideoId, getVimeoVideoInfo } from '../chunks/vidstack-krOAtKMi.js';
import '../chunks/vidstack-28cU2iGK.js';
import '../chunks/vidstack-CjhKISI0.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 {
$$PROVIDER_TYPE = "VIMEO";
scope = createScope();
fullscreen;
#ctx;
#videoId = signal("");
#pro = signal(false);
#hash = null;
#currentSrc = null;
#fullscreenActive = false;
#seekableRange = new TimeRange(0, 0);
#timeRAF = new RAFLoop(this.#onAnimationFrame.bind(this));
#currentCue = null;
#chaptersTrack = null;
#promises = /* @__PURE__ */ new Map();
#videoInfoPromise = null;
constructor(iframe, ctx) {
super(iframe);
this.#ctx = ctx;
const self = this;
this.fullscreen = {
get active() {
return self.#fullscreenActive;
},
supported: true,
enter: () => this.#remote("requestFullscreen"),
exit: () => this.#remote("exitFullscreen")
};
}
/**
* 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`
*/
cookies = false;
title = true;
byline = true;
portrait = true;
color = "00ADEF";
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.#ctx.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.#ctx.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.#ctx.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");
}
// Embed will sometimes dispatch 0 at end of playback.
#preventTimeUpdates = false;
#onTimeUpdate(time, trigger) {
if (this.#preventTimeUpdates && time === 0) return;
const { realCurrentTime, paused, bufferedEnd, seekableEnd, live } = this.#ctx.$state;
if (realCurrentTime() === time) return;
const prevTime = realCurrentTime();
this.#ctx.notify("time-change", time, trigger);
if (Math.abs(prevTime - time) > 1.5) {
this.#ctx.notify("seeking", time, trigger);
if (!paused() && bufferedEnd() < time) {
this.#ctx.notify("waiting", void 0, trigger);
}
}
if (!live() && seekableEnd() - time < 0.01) {
this.#ctx.notify("end", void 0, trigger);
this.#preventTimeUpdates = true;
setTimeout(() => {
this.#preventTimeUpdates = false;
}, 500);
}
}
#onSeeked(time, trigger) {
this.#ctx.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.#ctx.notify("title-change", title, trigger);
this.#ctx.notify("poster-change", poster, trigger);
this.#ctx.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.#ctx.notify("title-change", videoTitle, trigger);
break;
case "getDuration":
const duration = data;
if (!this.#ctx.$state.canPlay()) {
this.#onReady(duration, trigger);
} else {
this.#ctx.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 'getTextTracks':
// this.#onTextTracksChange(data as VimeoTextTrack[], 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.#ctx.notify("pause", void 0, trigger);
}
#onPlay(trigger) {
this.#timeRAF.start();
this.#ctx.notify("play", void 0, trigger);
}
#onPlayProgress(trigger) {
const { paused } = this.#ctx.$state;
if (!paused() && !this.#preventTimeUpdates) {
this.#ctx.notify("playing", void 0, trigger);
}
}
#onLoadProgress(buffered, trigger) {
const detail = {
buffered: new TimeRange(0, buffered),
seekable: this.#seekableRange
};
this.#ctx.notify("progress", detail, trigger);
}
#onBufferStart(trigger) {
this.#ctx.notify("waiting", void 0, trigger);
}
#onBufferEnd(trigger) {
const { paused } = this.#ctx.$state;
if (!paused()) this.#ctx.notify("playing", void 0, trigger);
}
#onWaiting(trigger) {
const { paused } = this.#ctx.$state;
if (paused()) {
this.#ctx.notify("play", void 0, trigger);
}
this.#ctx.notify("waiting", void 0, trigger);
}
#onVolumeChange(volume, muted, trigger) {
const detail = { volume, muted };
this.#ctx.notify("volume-change", detail, trigger);
}
// #onTextTrackChange(track: VimeoTextTrack, trigger: Event) {
// const textTrack = this.#ctx.textTracks.toArray().find((t) => t.language === track.language);
// if (textTrack) textTrack.mode = track.mode;
// }
// #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);
// }
// }
// #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
}), { seekableEnd } = 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 ?? seekableEnd(),
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.#ctx.notify("duration-change", payload.duration, trigger);
break;
case "playbackratechange":
this.#ctx.notify("rate-change", payload.playbackRate, trigger);
break;
case "qualitychange":
this.#onQualityChange(payload, trigger);
break;
case "fullscreenchange":
this.#fullscreenActive = payload.fullscreen;
this.#ctx.notify("fullscreen-change", payload.fullscreen, trigger);
break;
case "enterpictureinpicture":
this.#ctx.notify("picture-in-picture-change", true, trigger);
break;
case "leavepictureinpicture":
this.#ctx.notify("picture-in-picture-change", false, trigger);
break;
case "ended":
this.#ctx.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 };