@tidal-music/player
Version:
Player logic for TIDAL
531 lines (530 loc) • 15.1 kB
JavaScript
import { l as P, e as o, a as d, o as n, s as r, g as m, n as g, q as h, j as y, p, r as T, t as f, u as v, v as b, x as A, y as c, w as k } from "./index-CmV5XX7g.js";
function w(s, e) {
return new CustomEvent(
"media-product-transition",
{
detail: {
mediaProduct: s,
playbackContext: e
}
}
);
}
const R = ({
assetPosition: s,
duration: e,
playbackInfo: t,
streamInfo: a
}) => ({
actualAssetPresentation: t.assetPresentation,
actualAudioMode: "audioMode" in t ? t.audioMode : null,
actualAudioQuality: "audioQuality" in t ? t.audioQuality : null,
actualDuration: e,
actualProductId: String(
"videoId" in t ? t.videoId : t.trackId
),
actualStreamType: "streamType" in t ? t.streamType : null,
actualVideoQuality: "videoQuality" in t ? t.videoQuality : null,
assetPosition: s,
bandwidth: null,
bitDepth: a.bitDepth ?? null,
codec: a.codec ?? null,
playbackSessionId: a.streamingSessionId,
previewReason: t.previewReason ?? void 0,
sampleRate: a.sampleRate ?? null
});
function I(s, e) {
return new CustomEvent("ended", {
detail: {
mediaProduct: e,
reason: s
}
});
}
function E(s) {
return new CustomEvent("playback-state-change", {
detail: {
state: s
}
});
}
function L() {
return new CustomEvent("preload-request");
}
function S(s) {
return Math.min(Math.pow(10, (4 + s) / 20), 1 / 1);
}
function M(s) {
switch (s) {
case "completed":
return "COMPLETE";
case "error":
return "ERROR";
default:
return "OTHER";
}
}
class x {
#e;
#i;
#t;
#r = void 0;
#s;
#a = "IDLE";
#n;
#o;
name;
constructor() {
P.addEventListener("desiredVolumeLevel", () => {
this.isActivePlayer && this.updateVolumeLevel();
});
}
// Implements
#d() {
this.duration && Math.abs(this.#i - this.duration) <= 30 && // A false check, rather than undefined, ensures a media product transition hs been made.
this.#r === !1 && (this.#r = !0, o.dispatchEvent(L()));
}
/**
* This method should be call whenever a playback ends, for **whatever** reason.
*
* ended, completed, skip, reset etc
*/
#c({
endAssetPosition: e,
endReason: t,
streamingSessionId: a
}) {
this.debugLog("mediaProductEnded"), d.preloadedStreamingSessionId && performance.mark(
"streaming_metrics:playback_statistics:idealStartTimestamp",
{
detail: d.preloadedStreamingSessionId,
startTime: n.now()
}
);
const i = r.getMediaProductTransition(a);
i && o.dispatchEvent(
I(t, i.mediaProduct)
), this.eventTrackingStreamingEnded(a, {
endAssetPosition: e,
endReason: t
}), r.deleteSession(a), this.currentStreamingSessionId === a && (this.currentStreamingSessionId = void 0), this.updateVolumeLevelForNextProduct();
}
adjustedVolume(e) {
const t = m("desiredVolumeLevel"), a = m("loudnessNormalizationMode");
let i = t;
return a === "ALBUM" && e.albumReplayGain && (i *= S(e.albumReplayGain)), a === "TRACK" && e.trackReplayGain && (i *= S(e.trackReplayGain)), this.debugLog(
"adjustedVolume",
`Volume adjusted from ${t} to ${i}`
), parseFloat(i.toFixed(2));
}
// Implements
attachPlaybackEngineEndedHandler() {
this.#t || (this.#t = this.playbackEngineEndedHandler.bind(this), o.addEventListener(
"ended",
this.#t
));
}
/**
* Cleans up stored stream info and media product transitions
* for preloadedStreamingSessionId if it does not match
* currentStreamingSessionId.
*/
cleanUpStoredPreloadInfo() {
this.preloadedStreamingSessionId && this.preloadedStreamingSessionId !== this.currentStreamingSessionId && (r.deleteSession(this.preloadedStreamingSessionId), this.preloadedStreamingSessionId = void 0);
}
get currentMediaProduct() {
return r.getMediaProductTransition(
this.currentStreamingSessionId
)?.mediaProduct ?? null;
}
set currentStreamingSessionId(e) {
this.#e = e;
}
get currentStreamingSessionId() {
return this.#e;
}
set currentTime(e) {
this.#i = e, this.#d();
}
get currentTime() {
return this.#i;
}
// Implements
debugLog(...e) {
document.location.href.includes("localhost") && document.location.hash.includes("debug") && console.debug(
`[%cPlayerSDK${this.name ? `%c${d.activePlayer?.name === this.name ? "⚯" : "⚮"}%c` + this.name : ""}${this.#e ? "%c::%c" + this.#e?.split("-").pop() : ""}%c]`,
"color:#00d6ff",
...this.name ? [
"color:inherit",
"color:#b7fa34"
// green foreground
] : [],
...this.#e ? [
"color:inherit",
"color:#d947ff"
// purple foreground
] : [],
"color:inherit",
...e
);
}
detachPlaybackEngineEndedHandler() {
this.#t && (o.removeEventListener(
"ended",
this.#t
), this.#t = void 0);
}
get duration() {
const e = r.getMediaProductTransition(
this.currentStreamingSessionId
);
return e ? e.playbackContext.actualDuration : null;
}
/**
* Commits play_log playbackSession and streaming_metrics playbackStatistics.
*
* @param streamingSessionId
*/
eventTrackingStreamingEnded(e, {
endAssetPosition: t,
endReason: a
}) {
const i = n.now();
g([
h({
endAssetPosition: t,
endTimestamp: i,
streamingSessionId: e
})
]).catch(console.error), y([
p({
endReason: M(a),
endTimestamp: i,
streamingSessionId: e
}),
T({
streamingSessionId: e,
timestamp: i
})
]).catch(console.error);
}
eventTrackingStreamingStarted(e) {
if (!e)
return;
performance.mark(
"streaming_metrics:playback_statistics:actualStartTimestamp",
{
detail: e,
startTime: n.now()
}
), performance.measure("idealStartTimestamp -> actualStartTimestamp", {
detail: e,
end: "streaming_metrics:playback_statistics:actualStartTimestamp",
start: "streaming_metrics:playback_statistics:idealStartTimestamp"
});
try {
p({
actualStartTimestamp: n.timestamp(
"streaming_metrics:playback_statistics:actualStartTimestamp",
e
),
idealStartTimestamp: n.timestamp(
"streaming_metrics:playback_statistics:idealStartTimestamp",
e
),
outputDevice: this.#s,
streamingSessionId: e
});
} catch (l) {
console.error(
l,
"actualStartTimestamp or idealStartTimestamp is missing for this streaming session"
);
} finally {
performance.clearMarks(
"streaming_metrics:playback_statistics:actualStartTimestamp"
), performance.clearMarks(
"streaming_metrics:playback_statistics:idealStartTimestamp"
);
}
const t = r.getMediaProductTransition(e);
if (!t) {
r.hasStartedStreamInfo(e) ? console.error(
`A media product transition for streaming session #${e} has not been saved and could thus not be found for play log reporting.`
) : (r.deleteStreamInfo(e), console.warn(
`Streaming session #${e} has been discarded due to a new load. This could mean you have a bug in your code where you call load on Player more than once time in a very short time frame.`
));
return;
}
const { mediaProduct: a, playbackContext: i } = t, u = n.now();
h({
actualAssetPresentation: i.actualAssetPresentation,
actualAudioMode: "actualAudioMode" in i ? i.actualAudioMode : null,
actualProductId: i.actualProductId,
actualQuality: i.actualAudioQuality || i.actualVideoQuality,
extras: a.extras,
isPostPaywall: v(
i.actualAssetPresentation,
a
),
playbackSessionId: e,
productType: f(
a.productType
),
requestedProductId: a.productId,
sourceId: a.sourceId,
sourceType: a.sourceType,
startAssetPosition: this.startAssetPosition,
startTimestamp: u,
streamingSessionId: e
}).catch(console.error);
}
get expired() {
const e = r.getStreamInfo(
this.currentStreamingSessionId
);
return e ? e.expires <= Date.now() : !1;
}
finishCurrentMediaProduct(e) {
if (!this.hasStarted())
return;
const t = this.#e, a = t ? r.hasStreamInfo(t) : !1;
this.preloadedStreamingSessionId || (this.playbackState = "IDLE"), t && a && this.#c({
endAssetPosition: this.currentTime,
endReason: e,
streamingSessionId: t
});
}
getPosition() {
return 0;
}
/**
* Refetches playbackinfo.
*/
async hardReload(e, t) {
return this.currentStreamingSessionId && this.finishCurrentMediaProduct("skip"), b(e, t);
}
hasNextItem() {
return this.preloadedStreamingSessionId;
}
hasStarted() {
return this.currentStreamingSessionId && r.hasStartedStreamInfo(this.currentStreamingSessionId);
}
get isActivePlayer() {
return d.activePlayer && this.name === d.activePlayer.name;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
load(e, t) {
return Promise.resolve();
}
/**
* If playback info is prefetched or expired, do a hard reload.
*
* @returns {boolean} True if hard reloaded, else false.
*/
async maybeHardReload() {
const e = this.prefetched || this.expired;
return this.currentMediaProduct && e ? (await this.hardReload(this.currentMediaProduct, this.currentTime), !0) : !1;
}
/**
* This method should be call whenever a playback starts, for **whatever** reason.
*
* skip, load.
*
* @param streamingSessionId
*/
mediaProductStarted(e) {
!e || r.hasStartedStreamInfo(e) || (this.debugLog("mediaProductStarted"), this.eventTrackingStreamingStarted(e), r.setStartedStreamInfo(e), this.updateVolumeLevel(), this.#r = !1, this.preloadedStreamingSessionId = void 0, this.unloadPreloadedMediaProduct().catch(console.error), this.attachPlaybackEngineEndedHandler());
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
next(e) {
return Promise.resolve();
}
get nextItem() {
if (this.preloadedStreamingSessionId)
return r.getMediaProductTransition(
this.preloadedStreamingSessionId
);
}
set outputDeviceType(e) {
this.#s = e ? A(e) : void 0;
}
/**
* When re-using a nexted item for a load, overwrite the nexted MediaProduct with the provided one.
* To ensure sourceId, sourceType and referenceId from the load call is correct for the playback -
* and not a stale incorrect one from the next call.
*
* @param streamingSessionId
* @param partialMediaProduct
*/
overwriteMediaProduct(e, t) {
const a = r.getMediaProductTransition(e);
if (a) {
r.deleteMediaProductTransition(e);
const i = {
mediaProduct: {
...a.mediaProduct,
...t
},
playbackContext: {
...a.playbackContext
}
};
r.saveMediaProductTransition(
e,
i
);
}
}
pause() {
}
play() {
return Promise.resolve();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
playbackEngineEndedHandler(e) {
return Promise.resolve();
}
set playbackState(e) {
const t = this.#a;
if (t === e || !this.currentStreamingSessionId)
return;
const a = (u, l) => t === u && l === e;
switch (!0) {
case a("NOT_PLAYING", "STALLED"):
case a("IDLE", "STALLED"):
return;
case a("PLAYING", "NOT_PLAYING"):
case a("PLAYING", "IDLE"): {
this.duration && this.currentTime < this.duration && c(this.currentStreamingSessionId, {
actionType: "PLAYBACK_STOP",
assetPosition: this.currentTime,
timestamp: n.now()
}).catch(console.error);
break;
}
case a("IDLE", "PLAYING"):
case a("NOT_PLAYING", "PLAYING"): {
this.currentTime !== this.startAssetPosition && c(this.currentStreamingSessionId, {
actionType: "PLAYBACK_START",
assetPosition: this.currentTime,
timestamp: n.now()
}).catch(console.error);
break;
}
}
this.#a = e, this.debugLog(`playbackState: ${e}`);
const i = d.activePlayer === void 0;
(this.isActivePlayer || i) && o.dispatchEvent(E(this.#a));
}
get playbackState() {
return this.#a;
}
get prefetched() {
return r.getStreamInfo(
this.currentStreamingSessionId
)?.prefetched;
}
set preloadedStreamingSessionId(e) {
this.#n = e;
}
get preloadedStreamingSessionId() {
return this.#n;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
reset(e) {
return Promise.resolve();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
seek(e) {
}
/**
* Handle play log reporting for seeking.
* Seek start should log a PLAYBACK_START action if playing post seek.
*/
seekEnd(e) {
const t = this.currentStreamingSessionId;
if (t) {
const a = () => c(t, {
actionType: "PLAYBACK_START",
assetPosition: e,
timestamp: n.now()
});
if (this.playbackState === "PLAYING")
a().catch(console.error);
else {
const i = () => {
this.playbackState === "PLAYING" && (a().catch(console.error), o.removeEventListener(
"playback-state-change",
i
));
};
o.addEventListener(
"playback-state-change",
i
);
}
}
}
/**
* Handle play log reporting for seeking.
* Seek start should log a PLAYBACK_STOP action.
*/
seekStart(e) {
this.currentStreamingSessionId && c(this.currentStreamingSessionId, {
actionType: "PLAYBACK_STOP",
assetPosition: e,
timestamp: n.now()
}).catch(console.error);
}
async setStateToXIfNotYInZMs(e, t, a) {
await k(e), this.playbackState !== t && (this.playbackState = a);
}
skipToPreloadedMediaProduct() {
return Promise.resolve();
}
get startAssetPosition() {
return this.#o;
}
set startAssetPosition(e) {
this.#o = e;
}
unloadPreloadedMediaProduct() {
return Promise.resolve();
}
updateOutputDevice() {
return Promise.resolve();
}
/**
* Hydrates the volume level from config, and adjusts
* it before setting, if loudness normalization is
* enabled.
*/
updateVolumeLevel() {
const e = r.getStreamInfo(
this.currentStreamingSessionId
);
e && (this.volume = this.adjustedVolume(e));
}
/**
* Adjusts the volume for the next track.
* Can be called on product ended to have the level ready.
*/
updateVolumeLevelForNextProduct() {
const e = r.getStreamInfo(
this.preloadedStreamingSessionId
);
e && (this.volume = this.adjustedVolume(e));
}
get volume() {
return 1;
}
set volume(e) {
}
}
export {
x as B,
R as c,
w as m
};
//# sourceMappingURL=basePlayer-C99zz0ed.js.map