UNPKG

vidstack

Version:

Build awesome media experiences on the web.

458 lines (454 loc) 13 kB
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 };