vidstack
Version:
Build awesome media experiences on the web.
458 lines (454 loc) • 13 kB
JavaScript
import { deferredPromise, listenEvent, setAttribute, uppercaseFirstChar, camelToKebabCase } from 'maverick.js/std';
import { Component, defineElement, prop, method } from 'maverick.js/element';
import { getScope, signal, computed, provideContext, effect, peek } from 'maverick.js';
import { m as mediaPlayerProps, M as MediaStoreFactory, j as MediaStoreSync, V as VideoQualityList, k as AudioTrackList, T as TextTrackList, l as TEXT_TRACK_CROSSORIGIN, n as TextRenderers, q as mediaContext, S as ScreenOrientationController, r as MediaKeyboardController, t as ThumbnailsLoader, v as MediaStateManager, w as MediaRequestManager, x as MediaPlayerDelegate, y as MediaLoadController, a as setAttributeIfEmpty, z as IS_IPHONE, i as isTrackCaptionKind, B as MEDIA_ATTRIBUTES, C as canFullscreen, D as MediaRemoteControl, E as MediaRequestContext } from './media-core.js';
import { w as FocusVisibleController, x as clampNumber } from './media-ui.js';
class RequestQueue {
_e = false;
$e = deferredPromise();
Ze = /* @__PURE__ */ new Map();
/**
* The number of callbacks that are currently in queue.
*/
get df() {
return this.Ze.size;
}
/**
* Whether items in the queue are being served immediately, otherwise they're queued to
* be processed later.
*/
get ef() {
return this._e;
}
/**
* Waits for the queue to be flushed (ie: start serving).
*/
async ff() {
if (this._e)
return;
await this.$e.promise;
}
/**
* Queue the given `callback` to be invoked at a later time by either calling the `serve()` or
* `start()` methods. If the queue has started serving (i.e., `start()` was already called),
* then the callback will be invoked immediately.
*
* @param key - Uniquely identifies this callback so duplicates are ignored.
* @param callback - The function to call when this item in the queue is being served.
*/
t(key, callback) {
if (this._e) {
callback();
return;
}
this.Ze.delete(key);
this.Ze.set(key, callback);
}
/**
* Invokes the callback with the given `key` in the queue (if it exists).
*/
cf(key) {
this.Ze.get(key)?.();
this.Ze.delete(key);
}
/**
* Flush all queued items and start serving future requests immediately until `stop()` is called.
*/
O() {
this.af();
this._e = true;
if (this.Ze.size > 0)
this.af();
}
/**
* Stop serving requests, they'll be queued until you begin processing again by calling `start()`.
*/
P() {
this._e = false;
}
/**
* Stop serving requests, empty the request queue, and release any promises waiting for the
* queue to flush.
*/
gf() {
this.P();
this.Ze.clear();
this.bf();
}
af() {
for (const key of this.Ze.keys())
this.cf(key);
this.bf();
}
bf() {
this.$e.resolve();
this.$e = deferredPromise();
}
}
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result)
__defProp(target, key, result);
return result;
};
class Player extends Component {
static el = defineElement({
tagName: "media-player",
props: mediaPlayerProps,
store: MediaStoreFactory
});
j;
u;
r;
s = new RequestQueue();
get q() {
return this.j.$provider();
}
constructor(instance) {
super(instance);
this.w();
new MediaStoreSync(instance);
const context = {
player: null,
scope: getScope(),
qualities: new VideoQualityList(),
audioTracks: new AudioTrackList(),
$provider: signal(null),
$props: this.$props,
$store: this.$store
};
context.remote = new MediaRemoteControl(void 0);
context.$iosControls = computed(this.x.bind(this));
context.textTracks = new TextTrackList();
context.textTracks[TEXT_TRACK_CROSSORIGIN] = this.$props.crossorigin;
context.textRenderers = new TextRenderers(context);
context.ariaKeys = {};
this.j = context;
provideContext(mediaContext, context);
this.orientation = new ScreenOrientationController(instance);
new FocusVisibleController(instance);
new MediaKeyboardController(instance, context);
new ThumbnailsLoader(instance);
const request = new MediaRequestContext();
this.u = new MediaStateManager(instance, request, context);
this.r = new MediaRequestManager(instance, this.u, request, context);
context.delegate = new MediaPlayerDelegate(
this.u.N.bind(this.u),
context
);
new MediaLoadController(instance, this.startLoading.bind(this));
}
onAttach(el) {
el.setAttribute("tabindex", "0");
setAttributeIfEmpty(el, "role", "region");
effect(this.y.bind(this));
effect(this.z.bind(this));
effect(this.A.bind(this));
effect(this.B.bind(this));
effect(this.C.bind(this));
effect(this.D.bind(this));
effect(this.E.bind(this));
effect(this.F.bind(this));
effect(this.G.bind(this));
this.H();
this.I();
this.j.player = el;
this.j.remote.setTarget(el);
this.j.remote.setPlayer(el);
listenEvent(el, "find-media-player", this.J.bind(this));
}
onConnect(el) {
if (IS_IPHONE)
setAttribute(el, "data-iphone", "");
const pointerQuery = window.matchMedia("(pointer: coarse)");
this.v(pointerQuery);
pointerQuery.onchange = this.v.bind(this);
const resize = new ResizeObserver(this.n.bind(this));
resize.observe(el);
effect(this.n.bind(this));
this.dispatch("media-player-connect", {
detail: this.el,
bubbles: true,
composed: true
});
return () => {
resize.disconnect();
pointerQuery.onchange = null;
};
}
w() {
const providedProps = {
viewType: "providedViewType",
streamType: "providedStreamType"
};
for (const prop2 of Object.keys(this.$props)) {
this.$store[providedProps[prop2] ?? prop2]?.set(this.$props[prop2]());
}
effect(this.K.bind(this));
this.$store.muted.set(this.$props.muted() || this.$props.volume() === 0);
}
y() {
const { title } = this.$props, { live, viewType } = this.$store, isLive = live(), type = uppercaseFirstChar(viewType()), typeText = type !== "Unknown" ? `${isLive ? "Live " : ""}${type}` : isLive ? "Live" : "Media";
const newTitle = title();
if (newTitle) {
this.el?.setAttribute("data-title", newTitle);
this.el?.removeAttribute("title");
}
const currentTitle = this.el?.getAttribute("data-title") || "";
this.$store.title.set(currentTitle);
setAttribute(
this.el,
"aria-label",
currentTitle ? `${typeText} - ${currentTitle}` : typeText + " Player"
);
}
z() {
const orientation = this.orientation.landscape ? "landscape" : "portrait";
this.$store.orientation.set(orientation);
setAttribute(this.el, "data-orientation", orientation);
this.n();
}
A() {
if (this.$store.canPlay() && this.q)
this.s.O();
else
this.s.P();
}
K() {
this.$store.providedViewType.set(this.$props.viewType());
this.$store.providedStreamType.set(this.$props.streamType());
}
H() {
const $attrs = {
"aspect-ratio": this.$props.aspectRatio,
"data-captions": () => {
const track = this.$store.textTrack();
return !!track && isTrackCaptionKind(track);
},
"data-ios-controls": this.j.$iosControls
};
const mediaAttrName = {
canPictureInPicture: "can-pip",
pictureInPicture: "pip"
};
for (const prop2 of MEDIA_ATTRIBUTES) {
const attrName = "data-" + (mediaAttrName[prop2] ?? camelToKebabCase(prop2));
$attrs[attrName] = this.$store[prop2];
}
delete $attrs.title;
this.setAttributes($attrs);
}
I() {
this.setCSSVars({
"--media-aspect-ratio": () => {
const ratio = this.$props.aspectRatio();
return ratio ? +ratio.toFixed(4) : null;
}
});
}
J(event) {
event.detail(this.el);
}
n() {
if (!this.el)
return;
const width = this.el.clientWidth, height = this.el.clientHeight, { smallBreakpointX, smallBreakpointY, largeBreakpointX, largeBreakpointY } = this.$props, bpx = width < smallBreakpointX() ? "sm" : width < largeBreakpointX() ? "md" : "lg", bpy = height < smallBreakpointY() ? "sm" : height < largeBreakpointY() ? "md" : "lg";
this.$store.breakpointX.set(bpx);
this.$store.breakpointY.set(bpy);
setAttribute(this.el, "data-bp-x", bpx);
setAttribute(this.el, "data-bp-y", bpy);
}
v(queryList) {
const isTouch = queryList.matches;
setAttribute(this.el, "data-touch", isTouch);
this.$store.touchPointer.set(isTouch);
this.n();
}
x() {
return !canFullscreen() && this.$store.mediaType() === "video" && (this.$store.controls() && !this.$props.playsinline() || this.$store.fullscreen());
}
get provider() {
return this.q;
}
get user() {
return this.r.Q;
}
orientation;
get qualities() {
return this.j.qualities;
}
get audioTracks() {
return this.j.audioTracks;
}
get textTracks() {
return this.j.textTracks;
}
get textRenderers() {
return this.j.textRenderers;
}
get paused() {
return this.q?.paused ?? true;
}
set paused(paused) {
if (paused) {
this.s.t("paused", () => this.r.L());
} else
this.s.t("paused", () => this.r.M());
}
C() {
this.paused = this.$props.paused();
}
get muted() {
return this.q?.muted ?? false;
}
set muted(muted) {
this.s.t("muted", () => this.q.muted = muted);
}
B() {
this.muted = this.$props.muted();
}
get currentTime() {
return this.q?.currentTime ?? 0;
}
set currentTime(time) {
this.s.t("currentTime", () => {
const adapter = this.q;
if (time !== adapter.currentTime) {
peek(() => {
const boundTime = Math.min(
Math.max(this.$store.seekableStart() + 0.1, time),
this.$store.seekableEnd() - 0.1
);
if (Number.isFinite(boundTime))
adapter.currentTime = boundTime;
});
}
});
}
E() {
this.currentTime = this.$props.currentTime();
}
get volume() {
return this.q?.volume ?? 1;
}
set volume(volume) {
this.s.t("volume", () => this.q.volume = volume);
}
D() {
this.volume = clampNumber(0, this.$props.volume(), 1);
}
get playsinline() {
return this.q?.playsinline ?? false;
}
set playsinline(inline) {
this.s.t("playsinline", () => this.q.playsinline = inline);
}
F() {
this.playsinline = this.$props.playsinline();
}
get playbackRate() {
return this.q?.playbackRate ?? 1;
}
set playbackRate(rate) {
this.s.t("rate", () => this.q.playbackRate = rate);
}
G() {
this.playbackRate = this.$props.playbackRate();
}
async play() {
return this.r.M();
}
async pause() {
return this.r.L();
}
async enterFullscreen(target) {
return this.r.R(target);
}
async exitFullscreen(target) {
return this.r.S(target);
}
enterPictureInPicture() {
return this.r.T();
}
exitPictureInPicture() {
return this.r.U();
}
seekToLiveEdge() {
this.r.V();
}
startLoading() {
this.j.delegate.p("can-load");
}
destroy() {
this.dispatch("destroy");
}
}
__decorateClass([
prop
], Player.prototype, "provider", 1);
__decorateClass([
prop
], Player.prototype, "user", 1);
__decorateClass([
prop
], Player.prototype, "orientation", 2);
__decorateClass([
prop
], Player.prototype, "qualities", 1);
__decorateClass([
prop
], Player.prototype, "audioTracks", 1);
__decorateClass([
prop
], Player.prototype, "textTracks", 1);
__decorateClass([
prop
], Player.prototype, "textRenderers", 1);
__decorateClass([
prop
], Player.prototype, "paused", 1);
__decorateClass([
prop
], Player.prototype, "muted", 1);
__decorateClass([
prop
], Player.prototype, "currentTime", 1);
__decorateClass([
prop
], Player.prototype, "volume", 1);
__decorateClass([
prop
], Player.prototype, "playsinline", 1);
__decorateClass([
prop
], Player.prototype, "playbackRate", 1);
__decorateClass([
method
], Player.prototype, "play", 1);
__decorateClass([
method
], Player.prototype, "pause", 1);
__decorateClass([
method
], Player.prototype, "enterFullscreen", 1);
__decorateClass([
method
], Player.prototype, "exitFullscreen", 1);
__decorateClass([
method
], Player.prototype, "enterPictureInPicture", 1);
__decorateClass([
method
], Player.prototype, "exitPictureInPicture", 1);
__decorateClass([
method
], Player.prototype, "seekToLiveEdge", 1);
__decorateClass([
method
], Player.prototype, "startLoading", 1);
export { Player as P };