bramley-engine 0.4.0

Playback engine that runs Apple's MusicKit JS in an embedded JavaScript runtime with native media and Widevine DRM host objects.
Documentation
(function () {
  "use strict";
  const C = globalThis.__bramley_player;
  delete globalThis.__bramley_player;
  const S = { phase: "start", errors: [], events: [], steps: [], signInNeeded: null, loadFailed: null, playbackError: null, applied: 0, background: 0 };
  const describe = e => e && (e.name || "") + " " + (e.message || e.description || e.errorCode || String(e));
  const err = (where, e) => {
    S.errors.push(where + ": " + describe(e) + (e && e.stack ? " @ " + e.stack : ""));
    if (S.errors.length > 30) S.errors.shift();
  };
  function seedUserTokenIntoMusicKitStorage(developerToken, userToken) {
    const claims = JSON.parse(atob(developerToken.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
    const key = ("music." + claims.iss).toLowerCase() + ".media-user-token";
    if (userToken) localStorage.setItem(key, userToken);
    else localStorage.removeItem(key);
  }
  seedUserTokenIntoMusicKitStorage(C.developerToken, C.userToken);

  const STOREFRONT_BACKOFF_MS = C.storefrontBackoffMs;
  const httpStatus = e => e && e.data && Number.isInteger(e.data.status) ? e.data.status : null;
  const pause = ms => new Promise(resolve => setTimeout(resolve, ms));

  const storefrontRefused = e => ({ signInNeeded: "engine-auth-" + (httpStatus(e) ?? e.errorCode) });
  const storefrontFailed = why => ({ loadFailed: "the Apple Music storefront check failed (" + why + ")" });

  async function checkStorefront() {
    if (m.isAuthorized !== true) return { signInNeeded: "engine-unauthorized" };
    const until = Date.now() + C.storefrontDeadlineMs;
    for (let attempt = 0; ; attempt++) {
      try {
        const answered = await Promise.race([m.api.music("/v1/me/storefront").then(() => true), pause(until - Date.now()).then(() => false)]);
        return answered ? null : storefrontFailed("timed out");
      } catch (e) {
        err("storefront", e);
        const code = e && e.errorCode;
        if (code === "UNAUTHORIZED_ERROR") return storefrontRefused(e);
        const backoff = STOREFRONT_BACKOFF_MS[attempt];
        if (backoff === undefined || Date.now() + backoff >= until) return code === "ACCESS_DENIED" ? storefrontRefused(e) : storefrontFailed(httpStatus(e) ?? "network error");
        await pause(backoff);
      }
    }
  }

  let m = null;
  let signedIn;
  const ready = new Promise(resolve => { signedIn = resolve; });
  document.addEventListener("musickitloaded", async () => {
    S.phase = "loaded";
    try {
      await MusicKit.configure({ developerToken: C.developerToken, app: { name: "Bramley", build: C.build } });
      S.phase = "configured";
    } catch (e) { err("configure", e); S.phase = "configure-failed"; return; }
    m = MusicKit.getInstance();
    m.bitrate = MusicKit.PlaybackBitrate.HIGH;
    const refused = await checkStorefront();
    if (refused) {
      Object.assign(S, refused);
      S.phase = refused.signInNeeded ? "sign-in-needed" : "storefront-failed";
      return;
    }
    for (const ev of ["playbackStateDidChange", "nowPlayingItemDidChange", "mediaPlaybackError", "authorizationStatusDidChange"]) {
      m.addEventListener(ev, e => { if (S.events.length < 200) S.events.push([Math.round(performance.now()), ev, String(e && (e.state ?? e.name ?? (e.item && e.item.id) ?? ""))]); });
    }
    m.addEventListener("mediaPlaybackError", e => { S.playbackError = describe(e); });
    m.addEventListener("authorizationStatusDidChange", () => signInLost());
    S.phase = "ready";
    signedIn();
  });

  const script = document.createElement("script");
  script.src = C.musickitUrl;
  script.onerror = () => { S.phase = "musickit-load-failed"; };
  S.phase = "loading-musickit";
  document.head.appendChild(script);

  function signInLost() {
    if (S.signInNeeded) return true;
    if (m.isAuthorized === true && m.previewOnly !== true) return false;
    S.signInNeeded = m.previewOnly === true ? "previewOnly is true" : "isAuthorized became false";
    S.phase = "sign-in-needed";
    Promise.resolve().then(() => m.stop()).catch(e => err("stop", e));
    return true;
  }

  let last = Promise.resolve(), seq = 0, started = 0, queueGen = 0, queueFailed = false, adding = Promise.resolve();
  const settled = p => p.then(() => {}, () => {});
  const within = (p, ms) => Promise.race([settled(p), new Promise(r => setTimeout(r, ms))]);
  const turnOf = p => within(p, C.commandWaitMs);
  const add = p => { adding = settled(p); return p; };
  const note = (name, what) => { S.steps.push([Math.round(performance.now()), name, what]); if (S.steps.length > 20) S.steps.shift(); if (globalThis.__traceMark) globalThis.__traceMark("step", name, what); };
  function enqueue(name, f, needsQueue) {
    const n = ++seq, prev = last;
    last = ready.then(() => turnOf(prev)).then(async () => {
      S.applied = Math.max(S.applied, n - 1);
      started = n;
      if (S.signInNeeded ? name !== "pause" : needsQueue && (queueFailed || signInLost())) return note(name, "skip");
      note(name, "start");
      try {
        if ((await f()) === false) return note(name, "superseded");
        if (started === n) { S.playbackError = null; if (name === "setQueue") queueFailed = false; }
        note(name, "done");
      } catch (e) {
        err(name, e);
        if (started === n) { S.playbackError = name + ": " + describe(e); if (name === "setQueue") queueFailed = true; }
        note(name, "fail");
      }
    }).then(() => { S.applied = Math.max(S.applied, n); });
  }
  const g = f => { try { return f(); } catch (e) { return "ERR " + e.message; } };
  const state = () => {
    const r = { phase: S.phase, errors: S.errors.slice(), events: S.events.slice(-40), steps: S.steps.slice(), applied: S.applied, signInNeeded: S.signInNeeded, loadFailed: S.loadFailed, playbackError: S.playbackError, musickitDefined: typeof MusicKit !== "undefined" };
    if (!m) return r;
    r.isAuthorized = g(() => m.isAuthorized);
    r.storefrontId = g(() => m.storefrontId);
    r.previewOnly = g(() => m.previewOnly);
    r.playbackState = g(() => m.playbackState);
    r.shuffleMode = g(() => m.shuffleMode);
    r.repeatMode = g(() => m.repeatMode);
    r.currentPlaybackTime = g(() => m.currentPlaybackTime);
    r.currentPlaybackDuration = g(() => m.currentPlaybackDuration);
    r.queuePosition = g(() => m.queue.position);
    r.queue = g(() => {
      const q = m.queue, at = Math.max(q.position, 0);
      const slot = it => it && typeof it.id === "string" ? { id: it.id, title: it.title, durationInMillis: it.attributes && it.attributes.durationInMillis } : { id: null };
      return { position: q.position, length: q.length, items: q.items.slice(at, at + C.queueShown).map(slot) };
    });
    r.background = S.background;
    r.item = g(() => m.nowPlayingItem && { id: m.nowPlayingItem.id, title: m.nowPlayingItem.title, artistName: m.nowPlayingItem.artistName, durationInMillis: m.nowPlayingItem.attributes && m.nowPlayingItem.attributes.durationInMillis });
    return r;
  };
  Object.defineProperty(globalThis, "__player", {
    value: {
      get applied() { return S.applied; },
      state,
      setQueue: options => {
        const gen = ++queueGen;
        enqueue("setQueue", async () => {
          await within(adding, 4 * C.commandWaitMs);
          return gen === queueGen ? m.setQueue(options) : false;
        }, false);
      },
      play: () => enqueue("play", () => m.play(), true),
      pause: () => enqueue("pause", () => m.pause(), false),
      seek: t => enqueue("seek", () => m.seekToTime(t), true),
      next: () => enqueue("next", () => m.skipToNextItem(), true),
      previous: () => enqueue("previous", () => m.skipToPreviousItem(), true),
      volume: v => enqueue("volume", () => { m.volume = v; }, false),
      playLater: options => enqueue("playLater", () => add(m.playLater(options)), true),
      topUp: options => ready.then(() => S.signInNeeded || queueFailed ? null : add(m.playLater(options)))
        .catch(e => err("topUp", e)).then(() => { S.background++; }),
      shuffle: on => enqueue("shuffle", () => { m.shuffleMode = on ? 1 : 0; }, true),
      repeat: mode => enqueue("repeat", () => { m.repeatMode = MusicKit.PlayerRepeatMode[mode]; }, true),
      playNext: options => enqueue("playNext", () => m.playNext(options), true),
      jumpTo: ([index, id]) => enqueue("jumpTo", () => (m.queue.items[index] || {}).id === id ? m.changeToMediaAtIndex(index) : false, true),
    },
  });
})();