UNPKG

@tidal-music/player

Version:
324 lines (323 loc) 12.2 kB
import { g as v, e as r, P as u, p as E, l as p, s as c, a as g, b as P, c as S } from "./index-C6ZwgBzI.js"; import { B as L, m as f, c as y } from "./basePlayer-DD7cIuDm.js"; const k = "active-device-disconnected"; function I() { return new CustomEvent(k); } const N = { file_checksum_mismatch: "NPO02", no_such_file: "NPO01", unreadable_file: "NPO03" }, w = { devicedisconnected: "NPD01", deviceexclusivemodenotallowed: "NPD02", deviceformatnotsupported: "NPD03", devicelocked: "NPD04", devicenotfound: "NPD05", deviceunknownerror: "NPD00" }; let a; class C extends L { #s = "default"; /** * A Boolean which is true if the media contained in the element has finished playing. * * (Native player sends multiple "complete" events. * This variable should be set to true on the first call * to be able to ignore subsequent onces; until reset * for a new media product.) * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended */ #i; #e; #a; name = "nativePlayer"; playbackEngineHandlerAttached = !1; constructor() { super(), v("outputDevicesEnabled") && (async () => (a = (await import("./output-devices-B2CSTw2l.js")).outputDevices, this.#e.listDevices()))(), this.#e = // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore window.NativePlayerComponent.Player(), this.playbackState = "IDLE", this.registerEventListeners(), this.#e.setVolume(100); } #t(e) { r.dispatchError( new u("EUnexpected", w[e]) ); } #r(e) { this.debugLog("handleMediaError", e.target); const t = e.target, i = N[t.errorCode]; this.currentStreamingSessionId && E({ errorCode: i, errorMessage: JSON.stringify(e.target), streamingSessionId: this.currentStreamingSessionId }), r.dispatchError(new u("EUnexpected", i)); } #n(e) { switch (this.debugLog("handleNativePlayerStateChange", e), e) { case "active": this.playbackState = "PLAYING"; break; case "idle": case "seeking": this.playbackState = "STALLED"; break; case "paused": case "ready": this.playbackState = "NOT_PLAYING"; break; case "stopped": this.playbackState = "NOT_PLAYING"; break; case "uninitialized": this.playbackState = "IDLE"; break; default: this.debugLog("No handling for state", e); break; } } async #d() { this.debugLog("handleNetworkError"); const e = p.timestamp( "streaming_metrics:playback_statistics:actualStartTimestamp" ); if ((e !== void 0 ? Math.abs(p.now() - e) : 0) >= 36e5) { const d = structuredClone(this.currentMediaProduct), s = this.currentTime; this.finishCurrentMediaProduct("error"), d && (await this.hardReload(d, s), await this.play()); return; } await Promise.race([ this.mediaStateChange("idle"), new Promise((d) => { window.addEventListener("online", () => d("online")); }) ]) === "idle" && r.dispatchError(new u("PENetwork", "NPN01")); } /** * Clean up native player before leaving for another player. */ abandon() { a && a.deviceMode === "exclusive" && this.#e.selectSystemDevice(); } getPosition() { return this.currentTime; } /** * We cannot run multiple instances of native player so this function is * for catching duration for a preloaded item in native player. * * I.e. wait for player to load it and emit mediaduration event, then we * can gather the duration data and send a media product transition. */ async handleAutomaticTransitionToPreloadedMediaProduct() { await this.nativeEvent("mediaduration"), this.#a = void 0; const e = c.getMediaProductTransition( this.preloadedStreamingSessionId ); if (!e) { console.warn( "No media product transition saved for next item. Stopping playback." ), this.playbackState = "NOT_PLAYING"; return; } const { mediaProduct: t, playbackContext: i } = e, n = { ...i, actualDuration: this.#i }; this.preloadedStreamingSessionId && c.saveMediaProductTransition( this.preloadedStreamingSessionId, { mediaProduct: t, playbackContext: n } ), await this.mediaStateChange("active"), r.dispatchEvent( f(t, n) ), this.currentStreamingSessionId = this.preloadedStreamingSessionId, this.mediaProductStarted(this.currentStreamingSessionId); } async load(e, t) { this.debugLog("load", e), this.currentTime = e.assetPosition, this.startAssetPosition = e.assetPosition, await this.reset(); const { assetPosition: i, mediaProduct: n, playbackInfo: d, streamInfo: s } = e, { securityToken: h, streamFormat: o, streamUrl: l } = s; this.currentStreamingSessionId = s.streamingSessionId, t === "explicit" && (this.playbackState = "NOT_PLAYING"); const b = this.nativeEvent("mediaduration"); if (o) this.#e.load(l, o, h); else throw new Error("Stream format is undefined."); if (await b, this.currentStreamingSessionId !== s.streamingSessionId) return; this.debugLog("load() duration is", this.#i), i !== 0 && i < this.#i ? (async () => { await this.mediaStateChange("active"), await this.seek(i), this.currentTime = i; })().catch(console.error) : this.currentTime = 0; const m = y({ assetPosition: i, duration: this.#i, playbackInfo: d, streamInfo: s }); c.saveMediaProductTransition( s.streamingSessionId, { mediaProduct: n, playbackContext: m } ), this.debugLog("load() mediaProductTransition"), r.dispatchEvent( f(n, m) ), this.debugLog("load() pb NOT_PLAYING"), this.debugLog("load() done"); } mediaStateChange(e) { return new Promise((t) => { this.#e.addEventListener( "mediastate", (i) => { i.target === e && t(i.target); } ); }); } nativeEvent(e) { return new Promise((t) => { this.#e.addEventListener( e, (i) => t(i) ); }); } async next(e) { this.debugLog("next", e), this.hasNextItem() && await this.unloadPreloadedMediaProduct(); const { mediaProduct: t, playbackInfo: i, streamInfo: n } = e, { securityToken: d, streamFormat: s, streamUrl: h, streamingSessionId: o } = n; this.preloadedStreamingSessionId = o, this.debugLog("preloading", h, "for", o), s ? (this.#e.preload(h, s, d), this.isActivePlayer || this.#e.pause()) : console.error("Stream format undefined for preload."), this.debugLog("preloading done"); const l = y({ assetPosition: 0, duration: 0, // TODO: Cannot get duration here, try to solve in some other way... playbackInfo: i, streamInfo: n }); c.saveMediaProductTransition(o, { mediaProduct: t, playbackContext: l }), this.#a = e; } pause() { this.#e.pause(); } async play() { if (this.debugLog("play"), await this.maybeHardReload(), this.playbackState === "IDLE") { this.debugLog("play()", this.playbackState, "returning early"); return; } this.setStateToXIfNotYInZMs(1e3, "PLAYING", "STALLED"), await this.updateOutputDevice(), this.mediaProductStarted(this.currentStreamingSessionId), this.debugLog("nativePlayer", "play()"), this.#e.play(); } async playbackEngineEndedHandler(e) { if (this.isActivePlayer) { const { reason: t } = e.detail; t === "completed" && (this.hasNextItem() ? await this.handleAutomaticTransitionToPreloadedMediaProduct() : (g.preloadedStreamingSessionId ? this.debugLog( `Switching player from ${this.name} to ${g.preloadPlayer?.name}` ) : this.debugLog("No next item queued."), this.playbackState = "NOT_PLAYING")); } } registerEventListeners() { this.debugLog("registerEventListeners"), this.#e.addEventListener("mediacurrenttime", (e) => { this.currentTime = Number(e.target); }), this.#e.addEventListener( "mediastate", (e) => { e.target === "completed" ? this.finishCurrentMediaProduct("completed") : this.#n(e.target); } ), this.#e.addEventListener( "devices", (e) => { a ? a.addNativeDevices(e.target) : console.error("Output devices not loaded."); } ), this.#e.addEventListener("devicedisconnected", () => { r.dispatchEvent(I()), this.#t("devicedisconnected"); }), this.#e.addEventListener( "deviceexclusivemodenotallowed", () => this.#t("deviceexclusivemodenotallowed") ), this.#e.addEventListener( "deviceformatnotsupported", () => this.#t("deviceformatnotsupported") ), this.#e.addEventListener( "devicelocked", () => this.#t("devicelocked") ), this.#e.addEventListener( "devicenotfound", () => this.#t("devicenotfound") ), this.#e.addEventListener( "deviceunknownerror", () => this.#t("deviceunknownerror") ), this.#e.addEventListener( "mediaduration", (e) => { this.#i = Number(e.target); } ), this.#e.addEventListener( "mediaerror", (e) => this.#r(e) ), this.#e.addEventListener("mediamaxconnectionsreached", () => { this.#d().catch(console.error); }); } async reset({ keepPreload: e } = { keepPreload: !1 }) { this.currentStreamingSessionId !== void 0 && (this.debugLog("reset"), e || await this.unloadPreloadedMediaProduct(), this.#e.stop(), this.playbackState !== "IDLE" && this.finishCurrentMediaProduct("skip"), this.detachPlaybackEngineEndedHandler(), this.currentStreamingSessionId = void 0, e || (this.preloadedStreamingSessionId = void 0), this.playbackState = "IDLE"); } // eslint-disable-next-line @typescript-eslint/no-misused-promises async seek(e) { this.hasStarted() || await this.mediaStateChange("active"), this.seekStart(this.currentTime), this.currentTime = e, this.#e.seek(e), this.seekEnd(this.currentTime); } // Handles track "skip next" and progressions between shaka and native player async skipToPreloadedMediaProduct() { this.debugLog( "skipToPreloadedMediaProduct", this.preloadedStreamingSessionId ); const e = this.currentStreamingSessionId === void 0; if (this.preloadedStreamingSessionId && this.#a) { const t = c.getMediaProductTransition( this.preloadedStreamingSessionId ); t && (this.#a.mediaProduct = t.mediaProduct), await this.load(this.#a, "implicit"), await this.updateOutputDevice(), e && (this.playbackState = "PLAYING"), this.playbackState === "IDLE" && (this.playbackState = "NOT_PLAYING"); return; } console.warn("No preloaded item in native player."); } // eslint-disable-next-line @typescript-eslint/require-await async unloadPreloadedMediaProduct() { this.debugLog( "unloadPreloadedMediaProduct", this.preloadedStreamingSessionId ), this.hasNextItem() && (this.cleanUpStoredPreloadInfo(), "cancelPreload" in this.#e ? this.#e.cancelPreload() : console.warn("cancelPreload not available. Update native player.")); } updateDeviceMode() { this.updateOutputDevice()?.catch(console.error), a && r.dispatchEvent( P(a.deviceMode) ); } updateOutputDevice() { if (!a || (this.debugLog("updateOutputDevice", a.activeDevice), !a.activeDevice)) return Promise.resolve(); const { nativeDeviceId: e } = a.activeDevice; if (this.outputDeviceType = a.activeDevice.type, e === "default") this.#s !== "default" && (this.#e.selectSystemDevice(), r.dispatchEvent(S("default")), this.#s = "default"); else if (e) { const t = a.getNativeDevice(e); t && (this.#e.selectDevice(t, a.deviceMode), r.dispatchEvent( S(a.activeDevice.id) ), r.dispatchEvent( P(a.deviceMode) ), this.#s = e); } else throw new Error(`Device with sinkId ${e} not found.`); return Promise.resolve(); } get ready() { return Promise.resolve(); } get volume() { return v("desiredVolumeLevel"); } set volume(e) { this.debugLog("Setting volume to", e), this.#e.setVolume(e * 100); } } export { C as default };