@tidal-music/player
Version:
Player logic for TIDAL
324 lines (323 loc) • 12.2 kB
JavaScript
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
};