UNPKG

vidstack

Version:

Build awesome media experiences on the web.

288 lines (285 loc) 9.22 kB
import { effect, onDispose } from 'maverick.js'; import { useDisposalBin, listenEvent, DOMEvent, isNil } from 'maverick.js/std'; import { R as RAFLoop } from '../hls/hls.js'; import { i as isHLSSrc } from '../audio/loader.js'; import { z as getNumberOfDecimalPlaces } from '../../media-ui.js'; import { I as IS_SAFARI } from '../../media-core.js'; class HTMLMediaEvents { constructor(_provider, _context) { this._provider = _provider; this._context = _context; this._attachInitialListeners(); effect(this._attachTimeUpdate.bind(this)); onDispose(this._onDispose.bind(this)); } _disposal = useDisposalBin(); _waiting = false; _attachedLoadStart = false; _attachedCanPlay = false; _timeRAF = new RAFLoop(this._onRAF.bind(this)); get _media() { return this._provider.media; } get _delegate() { return this._context.delegate; } _onDispose() { this._timeRAF._stop(); this._disposal.empty(); } /** * The `timeupdate` event fires surprisingly infrequently during playback, meaning your progress * bar (or whatever else is synced to the currentTime) moves in a choppy fashion. This helps * resolve that by retrieving time updates in a request animation frame loop. */ _onRAF() { const newTime = this._provider.currentTime; if (this._context.$store.currentTime() !== newTime) this._updateCurrentTime(newTime); } _attachInitialListeners() { this._attachEventListener("loadstart", this._onLoadStart); this._attachEventListener("abort", this._onAbort); this._attachEventListener("emptied", this._onEmptied); this._attachEventListener("error", this._onError); this._context.logger?.debug("attached initial media event listeners"); } _attachLoadStartListeners() { if (this._attachedLoadStart) return; this._disposal.add( this._attachEventListener("loadeddata", this._onLoadedData), this._attachEventListener("loadedmetadata", this._onLoadedMetadata), this._attachEventListener("canplay", this._onCanPlay), this._attachEventListener("canplaythrough", this._onCanPlayThrough), this._attachEventListener("durationchange", this._onDurationChange), this._attachEventListener("play", this._onPlay), this._attachEventListener("progress", this._onProgress), this._attachEventListener("stalled", this._onStalled), this._attachEventListener("suspend", this._onSuspend) ); this._attachedLoadStart = true; } _attachCanPlayListeners() { if (this._attachedCanPlay) return; this._disposal.add( this._attachEventListener("pause", this._onPause), this._attachEventListener("playing", this._onPlaying), this._attachEventListener("ratechange", this._onRateChange), this._attachEventListener("seeked", this._onSeeked), this._attachEventListener("seeking", this._onSeeking), this._attachEventListener("ended", this._onEnded), this._attachEventListener("volumechange", this._onVolumeChange), this._attachEventListener("waiting", this._onWaiting) ); this._attachedCanPlay = true; } _handlers = /* @__PURE__ */ new Map() ; _handleDevEvent = this._onDevEvent.bind(this) ; _attachEventListener(eventType, handler) { this._handlers.set(eventType, handler); return listenEvent( this._media, eventType, this._handleDevEvent ); } _onDevEvent(event2) { this._context.logger?.debugGroup(`\u{1F4FA} fired \`${event2.type}\``).labelledLog("Event", event2).labelledLog("Media Store", { ...this._context.$store }).dispatch(); this._handlers.get(event2.type)?.call(this, event2); } _updateCurrentTime(time, trigger) { this._delegate._dispatch("time-update", { // Avoid errors where `currentTime` can have higher precision. detail: { currentTime: Math.min(time, this._context.$store.seekableEnd()), played: this._media.played }, trigger }); } _onLoadStart(event2) { if (this._media.networkState === 3) { this._onAbort(event2); return; } this._attachLoadStartListeners(); this._delegate._dispatch("load-start", { trigger: event2 }); } _onAbort(event2) { this._delegate._dispatch("abort", { trigger: event2 }); } _onEmptied() { this._delegate._dispatch("emptied", { trigger: event }); } _onLoadedData(event2) { this._delegate._dispatch("loaded-data", { trigger: event2 }); } _onLoadedMetadata(event2) { this._onStreamTypeChange(); this._attachCanPlayListeners(); this._delegate._dispatch("volume-change", { detail: { volume: this._media.volume, muted: this._media.muted } }); this._delegate._dispatch("loaded-metadata", { trigger: event2 }); if (IS_SAFARI && isHLSSrc(this._context.$store.source())) { this._delegate._ready(this._getCanPlayDetail(), event2); } } _getCanPlayDetail() { return { duration: this._media.duration, buffered: this._media.buffered, seekable: this._media.seekable }; } _onStreamTypeChange() { const isLive = !Number.isFinite(this._media.duration); this._delegate._dispatch("stream-type-change", { detail: isLive ? "live" : "on-demand" }); } _onPlay(event2) { if (!this._context.$store.canPlay) return; this._delegate._dispatch("play", { trigger: event2 }); } _onPause(event2) { if (this._media.readyState === 1 && !this._waiting) return; this._waiting = false; this._timeRAF._stop(); this._delegate._dispatch("pause", { trigger: event2 }); } _onCanPlay(event2) { this._delegate._ready(this._getCanPlayDetail(), event2); } _onCanPlayThrough(event2) { if (this._context.$store.started()) return; this._delegate._dispatch("can-play-through", { trigger: event2, detail: this._getCanPlayDetail() }); } _onPlaying(event2) { this._waiting = false; this._delegate._dispatch("playing", { trigger: event2 }); this._timeRAF._start(); } _onStalled(event2) { this._delegate._dispatch("stalled", { trigger: event2 }); if (this._media.readyState < 3) { this._waiting = true; this._delegate._dispatch("waiting", { trigger: event2 }); } } _onWaiting(event2) { if (this._media.readyState < 3) { this._waiting = true; this._delegate._dispatch("waiting", { trigger: event2 }); } } _onEnded(event2) { this._timeRAF._stop(); this._updateCurrentTime(this._media.duration, event2); this._delegate._dispatch("end", { trigger: event2 }); if (this._context.$store.loop()) { this._onLoop(); } else { this._delegate._dispatch("ended", { trigger: event2 }); } } _attachTimeUpdate() { if (this._context.$store.paused()) { listenEvent(this._media, "timeupdate", this._onTimeUpdate.bind(this)); } } _onTimeUpdate(event2) { this._updateCurrentTime(this._media.currentTime, event2); } _onDurationChange(event2) { this._onStreamTypeChange(); if (this._context.$store.ended()) { this._updateCurrentTime(this._media.duration, event2); } this._delegate._dispatch("duration-change", { detail: this._media.duration, trigger: event2 }); } _onVolumeChange(event2) { this._delegate._dispatch("volume-change", { detail: { volume: this._media.volume, muted: this._media.muted }, trigger: event2 }); } _onSeeked(event2) { this._updateCurrentTime(this._media.currentTime, event2); this._delegate._dispatch("seeked", { detail: this._media.currentTime, trigger: event2 }); if (Math.trunc(this._media.currentTime) === Math.trunc(this._media.duration) && getNumberOfDecimalPlaces(this._media.duration) > getNumberOfDecimalPlaces(this._media.currentTime)) { this._updateCurrentTime(this._media.duration, event2); if (!this._media.ended) { this._context.player.dispatchEvent( new DOMEvent("media-play-request", { trigger: event2 }) ); } } } _onSeeking(event2) { this._delegate._dispatch("seeking", { detail: this._media.currentTime, trigger: event2 }); } _onProgress(event2) { this._delegate._dispatch("progress", { detail: { buffered: this._media.buffered, seekable: this._media.seekable }, trigger: event2 }); } _onLoop() { const hasCustomControls = isNil(this._media.controls); if (hasCustomControls) this._media.controls = false; this._context.player.dispatchEvent(new DOMEvent("media-loop-request")); } _onSuspend(event2) { this._delegate._dispatch("suspend", { trigger: event2 }); } _onRateChange(event2) { this._delegate._dispatch("rate-change", { detail: this._media.playbackRate, trigger: event2 }); } _onError(event2) { const error = this._media.error; if (!error) return; this._delegate._dispatch("error", { detail: { message: error.message, code: error.code, mediaError: error }, trigger: event2 }); } } export { HTMLMediaEvents as H };