(function() { let purposeTransferObject = null; let mprisTransferObject = null; let eventCallback = function(e) { e.stopPropagation(); const args = e.detail; if (args.action == "unload") { // TODO: Undo other operations window.removeEventListener("org.kde.pbi.event", eventCallback, {"capture": true}); } else if (args.action == "mediaSessionsRegister") { const MediaSessionsClassName_constructor = function() { this.callbacks = {}; this.pendingCallbacksUpdate = 0; this.metadata = null; this.playbackState = "none"; this.sendMessage = function(action, payload) { let event = new CustomEvent("org.kde.pbi.mpris.message", { detail: { action: action, payload: payload } }); window.dispatchEvent(event); }; this.executeCallback = function(action) { let details = { action: action // for seekforward, seekbackward, seekto there's additional information one would need to add }; this.callbacks[action](details); }; this.setCallback = function(name, cb) { const oldCallbacks = Object.keys(this.callbacks).sort(); if (cb) { this.callbacks[name] = cb; } else { delete this.callbacks[name]; } const newCallbacks = Object.keys(this.callbacks).sort(); if (oldCallbacks.toString() === newCallbacks.toString()) { return; } if (this.pendingCallbacksUpdate) { return; } this.pendingCallbacksUpdate = setTimeout(() => { this.pendingCallbacksUpdate = 0; // Make sure to send the current callbacks, not "newCallbacks" at the time of starting the timeout const callbacks = Object.keys(this.callbacks); this.sendMessage("callbacks", callbacks); }, 0); }; this.setMetadata = function(metadata) { // MediaMetadata is not a regular Object so we cannot just JSON.stringify it let newMetadata = {}; let dirty = (!metadata != !this.metadata); if (metadata) { const keys = ["title", "artist", "album", "artwork"]; const oldMetadata = this.metadata || {}; keys.forEach((key) => { const value = metadata[key]; if (typeof value?.toString !== "function") { return; // continue } // We only have Strings or the "artwork" Array, so a toString() comparison should suffice... dirty |= (value.toString() !== (oldMetadata[key] || "").toString()); newMetadata[key] = value; }); } this.metadata = metadata; if (dirty) { this.sendMessage("metadata", newMetadata); } }; this.setPlaybackState = function(playbackState) { if (this.playbackState === playbackState) { return; } this.playbackState = playbackState; this.sendMessage("playbackState", playbackState); }; }; mprisTransferObject = new MediaSessionsClassName_constructor(); if (!navigator.mediaSession) { navigator.mediaSession = {}; } var noop = function() {}; var oldSetActionHandler = navigator.mediaSession.setActionHandler || noop; navigator.mediaSession.setActionHandler = function(name, cb) { mprisTransferObject.setCallback(name, cb); // Call the original native implementation // "call()" is needed as the real setActionHandler is a class member // and calling it directly is illegal as it lacks the context // This may throw for unsupported actions but we registered the callback // ourselves before return oldSetActionHandler.call(navigator.mediaSession, name, cb); }; Object.defineProperty(navigator.mediaSession, "metadata", { get: () => mprisTransferObject.metadata, set: (newValue) => { mprisTransferObject.setMetadata(newValue); } }); Object.defineProperty(navigator.mediaSession, "playbackState", { get: () => mprisTransferObject.playbackState, set: (newValue) => { mprisTransferObject.setPlaybackState(newValue); } }); if (!window.MediaMetadata) { window.MediaMetadata = function(data) { Object.assign(this, data); }; window.MediaMetadata.prototype.title = ""; window.MediaMetadata.prototype.artist = ""; window.MediaMetadata.prototype.album = ""; window.MediaMetadata.prototype.artwork = []; } // here we replace the document.createElement function with our own so we can detect // when an