File indexing completed on 2024-04-28 05:32:50

0001 (function() {
0002     let purposeTransferObject = null;
0003     let mprisTransferObject = null;
0004     let eventCallback = function(e) {
0005         e.stopPropagation();
0006         const args = e.detail;
0007         if (args.action == "unload") {
0008             // TODO: Undo other operations
0009             window.removeEventListener("pbiEvent", eventCallback, {"capture": true});
0010         } else if (args.action == "mediaSessionsRegister") {
0011             const MediaSessionsClassName_constructor = function() {
0012                 this.callbacks = {};
0013                 this.pendingCallbacksUpdate = 0;
0014                 this.metadata = null;
0015                 this.playbackState = "none";
0016 
0017                 this.sendMessage = function(action, payload) {
0018                     let event = new CustomEvent("pbiMprisMessage", {
0019                         detail: {
0020                             action: action,
0021                             payload: payload
0022                         }
0023                     });
0024                     window.dispatchEvent(event);
0025                 };
0026 
0027                 this.executeCallback = function(action) {
0028                     let details = {
0029                         action: action
0030                         // for seekforward, seekbackward, seekto there's additional information one would need to add
0031                     };
0032                     this.callbacks[action](details);
0033                 };
0034 
0035                 this.setCallback = function(name, cb) {
0036                     const oldCallbacks = Object.keys(this.callbacks).sort();
0037 
0038                     if (cb) {
0039                         this.callbacks[name] = cb;
0040                     } else {
0041                         delete this.callbacks[name];
0042                     }
0043 
0044                     const newCallbacks = Object.keys(this.callbacks).sort();
0045 
0046                     if (oldCallbacks.toString() === newCallbacks.toString()) {
0047                         return;
0048                     }
0049 
0050                     if (this.pendingCallbacksUpdate) {
0051                         return;
0052                     }
0053 
0054                     this.pendingCallbacksUpdate = setTimeout(() => {
0055                         this.pendingCallbacksUpdate = 0;
0056 
0057                         // Make sure to send the current callbacks, not "newCallbacks" at the time of starting the timeout
0058                         const callbacks = Object.keys(this.callbacks);
0059                         this.sendMessage("callbacks", callbacks);
0060                     }, 0);
0061                 };
0062 
0063                 this.setMetadata = function(metadata) {
0064                     // MediaMetadata is not a regular Object so we cannot just JSON.stringify it
0065                     let newMetadata = {};
0066 
0067                     let dirty = (!metadata != !this.metadata);
0068                     if (metadata) {
0069                         const keys = Object.getOwnPropertyNames(Object.getPrototypeOf(metadata));
0070 
0071                         const oldMetadata = this.metadata || {};
0072 
0073                         keys.forEach((key) => {
0074                             const value = metadata[key];
0075                             if (!value || typeof value === "function") {
0076                                 return; // continue
0077                             }
0078 
0079                             // We only have Strings or the "artwork" Array, so a toString() comparison should suffice...
0080                             dirty |= (value.toString() !== (oldMetadata[key] || "").toString());
0081 
0082                             newMetadata[key] = value;
0083                         });
0084                     }
0085 
0086                     this.metadata = metadata;
0087 
0088                     if (dirty) {
0089                         this.sendMessage("metadata", newMetadata);
0090                     }
0091                 };
0092 
0093                 this.setPlaybackState = function(playbackState) {
0094                     if (this.playbackState === playbackState) {
0095                         return;
0096                     }
0097 
0098                     this.playbackState = playbackState;
0099                     this.sendMessage("playbackState", playbackState);
0100                 };
0101             };
0102 
0103             mprisTransferObject = new MediaSessionsClassName_constructor();
0104 
0105             if (!navigator.mediaSession) {
0106                 navigator.mediaSession = {};
0107             }
0108 
0109             var noop = function() {};
0110 
0111             var oldSetActionHandler = navigator.mediaSession.setActionHandler || noop;
0112             navigator.mediaSession.setActionHandler = function(name, cb) {
0113                 mprisTransferObject.setCallback(name, cb);
0114 
0115                 // Call the original native implementation
0116                 // "call()" is needed as the real setActionHandler is a class member
0117                 // and calling it directly is illegal as it lacks the context
0118                 // This may throw for unsupported actions but we registered the callback
0119                 // ourselves before
0120                 return oldSetActionHandler.call(navigator.mediaSession, name, cb);
0121             };
0122 
0123             Object.defineProperty(navigator.mediaSession, "metadata", {
0124                 get: () => mprisTransferObject.metadata,
0125                 set: (newValue) => {
0126                     mprisTransferObject.setMetadata(newValue);
0127                 }
0128             });
0129             Object.defineProperty(navigator.mediaSession, "playbackState", {
0130                 get: () => mprisTransferObject.playbackState,
0131                 set: (newValue) => {
0132                     mprisTransferObject.setPlaybackState(newValue);
0133                 }
0134             });
0135 
0136             if (!window.MediaMetadata) {
0137                 window.MediaMetadata = function(data) {
0138                     Object.assign(this, data);
0139                 };
0140                 window.MediaMetadata.prototype.title = "";
0141                 window.MediaMetadata.prototype.artist = "";
0142                 window.MediaMetadata.prototype.album = "";
0143                 window.MediaMetadata.prototype.artwork = [];
0144             }
0145 
0146             // here we replace the document.createElement function with our own so we can detect
0147             // when an <audio> tag is created that is not added to the DOM which most pages do
0148             // while a <video> tag typically ends up being displayed to the user, audio is not.
0149             // HACK We cannot really pass variables from the page's scope to our content-script's scope
0150             // so we just blatantly insert the <audio> tag in the DOM and pick it up through our regular
0151             // mechanism. Let's see how this goes :D
0152 
0153             // HACK When removing a media object from DOM it is paused, so what we do here is once the
0154             // player loaded some data we add it (doesn't work earlier since it cannot pause when
0155             // there's nothing loaded to pause) to the DOM and before we remove it, we note down that
0156             // we will now get a paused event because of that. When we get it, we just play() the player
0157             // so it continues playing :-)
0158             let addPlayerToDomEvadingAutoPlayBlocking = function(player) {
0159                 player.registerInDom = () => {
0160                     // Needs to be dataset so it's accessible from mutation observer on webpage
0161                     player.dataset.pbiPausedForDomRemoval = "true";
0162                     player.removeEventListener("play", player.registerInDom);
0163 
0164                     // If it is already in DOM by the time it starts playing, we don't need to do anything
0165                     // Also, if the page already parented it around, don't mess with it
0166                     if (document.documentElement.contains(player)
0167                         || player.parentNode) {
0168                         delete player.dataset.pbiPausedForDomRemoval;
0169                         player.removeEventListener("pause", player.replayAfterRemoval);
0170                     } else {
0171                         (document.head || document.documentElement).appendChild(player);
0172                         player.parentNode.removeChild(player);
0173                     }
0174                 };
0175 
0176                 player.replayAfterRemoval = () => {
0177                     if (player.dataset.pbiPausedForDomRemoval === "true") {
0178                         delete player.dataset.pbiPausedForDomRemoval;
0179                         player.removeEventListener("pause", player.replyAfterRemoval);
0180 
0181                         player.play();
0182                     }
0183                 };
0184 
0185                 player.addEventListener("play", player.registerInDom);
0186                 player.addEventListener("pause", player.replayAfterRemoval);
0187             }
0188 
0189             const oldCreateElement = Document.prototype.createElement;
0190             Document.prototype.createElement = function() {
0191                 const createdTag = oldCreateElement.apply(this, arguments);
0192                 const tagName = arguments[0];
0193 
0194                 if (typeof tagName === "string") {
0195                     if (tagName.toLowerCase() === "audio" || tagName.toLowerCase() === "video") {
0196                         const player = createdTag;
0197                         addPlayerToDomEvadingAutoPlayBlocking(player);
0198                     }
0199                 }
0200                 return createdTag;
0201             };
0202 
0203             // We also briefly add items created as new Audio() to the DOM so we can control it
0204             // similar to the document.createElement hack above since we cannot share variables
0205             // between the actual website and the background script despite them sharing the same DOM
0206             var oldAudio = window.Audio;
0207             window.Audio = function(...args) {
0208                 const player = new oldAudio(...args);
0209                 addPlayerToDomEvadingAutoPlayBlocking(player);
0210                 return player;
0211             };
0212         } else if (args.action == "mpris") {
0213             try {
0214                 mprisTransferObject.executeCallback(args.mprisCallbackName);
0215             } catch (e) {
0216                 console.warn("Exception executing '" + args.mprisCallbackName + "' media sessions callback", e);
0217             }
0218         } else if (args.action == "purposeRegister") {
0219             purposeTransferObject = function() {};
0220             purposeTransferObject.reset = () => {
0221                 purposeTransferObject.pendingResolve = null;
0222                 purposeTransferObject.pendingReject = null;
0223             };
0224             purposeTransferObject.reset();
0225 
0226             if (!navigator.canShare) {
0227                 navigator.canShare = (data) => {
0228                     if (!data) {
0229                         return false;
0230                     }
0231 
0232                     if (data.title === undefined && data.text === undefined && data.url === undefined) {
0233                         return false;
0234                     }
0235 
0236                     if (data.url) {
0237                         // check if URL is valid
0238                         try {
0239                             new URL(data.url, document.location.href);
0240                         } catch (e) {
0241                             return false;
0242                         }
0243                     }
0244 
0245                     return true;
0246                 }
0247             }
0248 
0249             if (!navigator.share) {
0250                 navigator.share = (data) => {
0251                     return new Promise((resolve, reject) => {
0252                         if (!navigator.canShare(data)) {
0253                             return reject(new TypeError());
0254                         }
0255 
0256                         if (data.url) {
0257                             // validity already checked in canShare, hence no catch
0258                             data.url = new URL(data.url, document.location.href).toString();
0259                         }
0260 
0261                         if (!window.event || !window.event.isTrusted) {
0262                             return reject(new DOMException("navigator.share can only be called in response to user interaction", "NotAllowedError"));
0263                         }
0264 
0265                         if (purposeTransferObject.pendingResolve || purposeTransferObject.pendingReject) {
0266                             return reject(new DOMException("A share is already in progress", "AbortError"));
0267                         }
0268 
0269                         purposeTransferObject.pendingResolve = resolve;
0270                         purposeTransferObject.pendingReject = reject;
0271 
0272                         const event = new CustomEvent("pbiPurposeMessage", {
0273                             detail: {
0274                                 action: "share",
0275                                 payload: data
0276                             }
0277                         });
0278                         window.dispatchEvent(event);
0279                     });
0280                 };
0281             }
0282         } else if (args.action == "purposeShare") {
0283             purposeTransferObject.pendingResolve();
0284         } else if (args.action == "purposeReject") {
0285             purposeTransferObject.pendingReject(new DOMException("Share request aborted", "AbortError"));
0286         } else if (args.action == "purposeReset") {
0287             purposeTransferObject.reset();
0288         } else {
0289             console.warn("Unknown page script action" + args.action, args);
0290         }
0291     };
0292     window.addEventListener("pbiEvent", eventCallback, {"capture": true});
0293     window.dispatchEvent(new CustomEvent("pbiInited"));
0294 }());