vidstack
Version:
Build awesome media experiences on the web.
288 lines (285 loc) • 9.22 kB
JavaScript
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 };