bramley-engine 0.1.2

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 G = globalThis;
  const ids = new WeakMap();
  let next = 0;
  const id = o => {
    if (!o || (typeof o !== "object" && typeof o !== "function")) return String(o);
    let v = ids.get(o);
    if (!v) { v = (o.constructor && o.constructor.name || "Obj") + "#" + ++next; ids.set(o, v); }
    return v;
  };
  const SAFE = new Set(("step GET HEAD POST PUT cenc keyids sinf webm segments sequence temporary network decode com.widevine.alpha start done fail skip superseded " +
    "setQueue play pause seek next previous volume playLater shuffle playNext jumpTo addSourceBuffer removeSourceBuffer endOfStream setLiveSeekableRange clearLiveSeekableRange " +
    "appendBuffer remove abort changeType load setMediaKeys removeAttribute createMediaKeys createSession setServerCertificate generateRequest update close " +
    "sourceopen sourceended sourceclose updatestart updateend error playing waiting seeking seeked ended canplay canplaythrough loadedmetadata loadeddata durationchange emptied " +
    "stalled suspend progress ratechange volumechange encrypted waitingforkey readystatechange loadend loadstart timeout message keystatuseschange").split(" "));
  const tok = s => SAFE.has(s) ? s : "str" + String(s).length;
  const figure = s => /^\d+$/.test(s) ? s : "str" + s.length;
  const errName = n => /^[A-Za-z]+(Error|Exception)$/.test(String(n)) ? String(n) : "err";
  const show = v => {
    if (v === null || v === undefined || typeof v === "number" || typeof v === "boolean") return String(v);
    if (typeof v === "string") return tok(v);
    if (v instanceof ArrayBuffer) return "ab" + v.byteLength;
    if (ArrayBuffer.isView(v)) return "u8:" + v.byteLength;
    if (v instanceof G.TimeRanges) { const r = []; for (let i = 0; i < v.length; i++) r.push(v.start(i).toFixed(3) + "-" + v.end(i).toFixed(3)); return "[" + r.join(",") + "]"; }
    if (typeof v === "function") return "fn";
    if (typeof v.then === "function") return "promise";
    return id(v);
  };
  G.__traceMark = (...parts) => console.log("TRACE " + G.performance.now().toFixed(3) + " mark " + parts.map(tok).join(" "));
  const log = (...parts) => console.log("TRACE " + G.performance.now().toFixed(3) + " " + parts.join(" "));
  const host = u => { const m = /^[a-z]+:\/\/(?:[^/?#@]*@)?([^/?#@]+)/i.exec(String(u)); return (m ? m[1] : "rel") + "~" + hash(String(u)); };
  const hash = t => { let h = 0x811c9dc5; for (let i = 0; i < t.length; i++) h = Math.imul(h ^ t.charCodeAt(i), 0x01000193) >>> 0; return h.toString(16); };
  const CACHE_HEADERS = ["cache-control", "expires", "age", "etag", "last-modified", "content-type", "content-length", "vary", "pragma"];
  const cacheInfo = get => CACHE_HEADERS.map(h => [h, get(h)]).filter(([, v]) => v !== null && v !== undefined).map(([h, v]) => h + "=" + figure(String(v))).join(" ");

  function wrapMethods(C, names) {
    for (const name of names) {
      const f = C.prototype[name];
      if (typeof f !== "function") continue;
      C.prototype[name] = function (...a) {
        log("call", id(this), name, a.map(show).join(","));
        try {
          const r = f.apply(this, a);
          const me = id(this);
          log("ret", me, name, show(r));
          if (r && typeof r.then === "function") r.then(v => log("settled", me, name, "ok", show(v)), e => log("settled", me, name, "rejected", errName(e && e.name), "msg" + String(e && e.message).length));
          return r;
        } catch (e) { log("throw", id(this), name, errName(e && e.name)); throw e; }
      };
    }
  }
  function wrapAccessors(C, names) {
    for (const name of names) {
      let proto = C.prototype, d;
      while (proto && !(d = Object.getOwnPropertyDescriptor(proto, name))) proto = Object.getPrototypeOf(proto);
      if (!d || !(d.get || d.set)) continue;
      Object.defineProperty(C.prototype, name, {
        configurable: true,
        get: d.get && function () { const v = d.get.call(this); log("get", id(this), name, show(v)); return v; },
        set: d.set && function (v) { log("set", id(this), name, show(v)); try { d.set.call(this, v); } catch (e) { log("throw", id(this), name + "=", errName(e && e.name)); throw e; } },
      });
    }
  }
  const handlers = new WeakMap();
  function wrapHandlerProps(C, types) {
    for (const t of types) {
      const key = "on" + t;
      Object.defineProperty(C.prototype, key, {
        configurable: true,
        get() { const m = handlers.get(this); return m ? m.get(key) || null : null; },
        set(fn) {
          let m = handlers.get(this);
          if (!m) { m = new Map(); handlers.set(this, m); }
          if (typeof fn !== "function") { if (fn !== null && fn !== undefined) log("set", id(this), key, show(fn)); m.set(key, null); return; }
          const target = this;
          log("set", id(this), key, "fn");
          m.set(key, function (ev) { log("evt", id(target), t, "via", key); try { return fn.call(this, ev); } finally { log("evt-end", id(target), t); } });
        },
      });
    }
  }
  const wrapped = new WeakMap();
  const traced = [];
  const origAdd = G.EventTarget.prototype.addEventListener, origRemove = G.EventTarget.prototype.removeEventListener;
  G.EventTarget.prototype.addEventListener = function (type, cb, opts) {
    if (!traced.some(C => this instanceof C) || !cb) return origAdd.call(this, type, cb, opts);
    let per = wrapped.get(cb);
    if (!per) { per = new Map(); wrapped.set(cb, per); }
    const k = id(this) + "/" + type;
    let w = per.get(k);
    if (!w) {
      const target = this;
      w = function (ev) { log("evt", id(target), tok(type)); try { return typeof cb === "function" ? cb.call(this, ev) : cb.handleEvent(ev); } finally { log("evt-end", id(target), tok(type)); } };
      per.set(k, w);
    }
    log("listen", id(this), tok(type));
    return origAdd.call(this, type, w, opts);
  };
  G.EventTarget.prototype.removeEventListener = function (type, cb, opts) {
    const per = cb && wrapped.get(cb);
    const w = per && per.get(id(this) + "/" + type);
    return origRemove.call(this, type, w || cb, opts);
  };

  const MS = G.MediaSource, SB = G.SourceBuffer, ME = G.HTMLMediaElement, XHR = G.XMLHttpRequest;
  traced.push(MS, SB, ME, XHR, G.SourceBufferList);
  wrapMethods(MS, ["addSourceBuffer", "removeSourceBuffer", "endOfStream", "setLiveSeekableRange", "clearLiveSeekableRange"]);
  wrapAccessors(MS, ["readyState", "duration", "sourceBuffers", "activeSourceBuffers"]);
  wrapHandlerProps(MS, ["sourceopen", "sourceended", "sourceclose"]);
  wrapMethods(SB, ["appendBuffer", "remove", "abort", "changeType"]);
  wrapAccessors(SB, ["updating", "buffered", "timestampOffset", "appendWindowStart", "appendWindowEnd", "mode"]);
  wrapHandlerProps(SB, ["updatestart", "update", "updateend", "error", "abort"]);
  wrapMethods(ME, ["play", "pause", "load", "setMediaKeys", "removeAttribute"]);
  wrapAccessors(ME, ["src", "currentTime", "readyState", "paused", "seeking", "ended", "buffered", "duration", "error", "networkState"]);
  wrapHandlerProps(ME, ["play", "playing", "pause", "waiting", "seeking", "seeked", "ended", "error", "canplay", "canplaythrough", "loadedmetadata", "loadeddata", "durationchange", "emptied", "stalled", "suspend", "progress", "ratechange", "volumechange", "encrypted", "waitingforkey"]);
  wrapHandlerProps(XHR, ["readystatechange", "load", "loadend", "loadstart", "error", "abort", "timeout", "progress"]);
  const MK = G.MediaKeys, MKS = G.MediaKeySession, MKSA = G.MediaKeySystemAccess;
  traced.push(MKS);
  wrapMethods(MKSA, ["createMediaKeys"]);
  wrapMethods(MK, ["createSession", "setServerCertificate"]);
  wrapMethods(MKS, ["generateRequest", "update", "close", "remove", "load"]);
  wrapHandlerProps(MKS, ["message", "keystatuseschange"]);
  const rmksa = G.navigator.requestMediaKeySystemAccess;
  G.navigator.requestMediaKeySystemAccess = function (...a) {
    log("call", "navigator", "requestMediaKeySystemAccess", show(a[0]));
    const r = rmksa.apply(this, a);
    r.then(v => log("settled", "navigator", "requestMediaKeySystemAccess", "ok", show(v)), e => log("settled", "navigator", "requestMediaKeySystemAccess", "rejected", errName(e && e.name)));
    return r;
  };
  const xopen = XHR.prototype.open, xsend = XHR.prototype.send, xabort = XHR.prototype.abort;
  XHR.prototype.open = function (m, u, ...r) { log("call", id(this), "open", tok(m), host(u)); return xopen.call(this, m, u, ...r); };
  XHR.prototype.send = function (...a) {
    const x = this;
    log("call", id(x), "send");
    origAdd.call(x, "load", () => log("done", id(x), x.status, cacheInfo(h => x.getResponseHeader(h))));
    return xsend.apply(this, a);
  };
  XHR.prototype.abort = function (...a) { log("call", id(this), "abort"); return xabort.apply(this, a); };

  let fetches = 0;
  const realFetch = G.fetch;
  G.fetch = function (input, init) {
    const n = "fetch#" + ++fetches, url = typeof input === "string" ? input : input && input.url;
    log("call", n, "fetch", tok((init && init.method) || "GET"), host(url));
    return realFetch.call(this, input, init).then(r => { log("resolve", n, r.status, id(r), cacheInfo(h => r.headers.get(h))); return r; }, e => { log("reject", n, errName(e && e.name)); throw e; });
  };
  for (const name of ["arrayBuffer", "json", "text"]) {
    const f = G.Response.prototype[name];
    G.Response.prototype[name] = function () {
      const me = id(this);
      log("call", me, name);
      return f.call(this).then(v => { log("resolve", me, name); return v; });
    };
  }
  log("trace installed");
})();