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 }());