File indexing completed on 2024-04-28 05:32:50
0001 /* 0002 Copyright (C) 2017-2019 Kai Uwe Broulik <kde@privat.broulik.de> 0003 0004 This program is free software; you can redistribute it and/or 0005 modify it under the terms of the GNU General Public License as 0006 published by the Free Software Foundation; either version 3 of 0007 the License, or (at your option) any later version. 0008 0009 This program is distributed in the hope that it will be useful, 0010 but WITHOUT ANY WARRANTY; without even the implied warranty of 0011 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0012 GNU General Public License for more details. 0013 0014 You should have received a copy of the GNU General Public License 0015 along with this program. If not, see <http://www.gnu.org/licenses/>. 0016 */ 0017 0018 let playerIds = []; 0019 0020 function currentPlayer() { 0021 let playerId = playerIds[playerIds.length - 1]; 0022 if (!playerId) { 0023 // Returning empty object instead of null so you can call player.id returning undefined instead of throwing 0024 return {}; 0025 } 0026 0027 let segments = playerId.split("-"); 0028 return { 0029 id: playerId, 0030 tabId: parseInt(segments[0]), 0031 frameId: parseInt(segments[1]) 0032 }; 0033 } 0034 0035 function playerIdFromSender(sender) { 0036 return sender.tab.id + "-" + (sender.frameId || 0); 0037 } 0038 0039 function playersOnTab(tabId) { 0040 return playerIds.filter((playerId) => { 0041 return playerId.startsWith(tabId + "-"); 0042 }); 0043 } 0044 0045 function sendPlayerTabMessage(player, action, payload) { 0046 if (!player) { 0047 return; 0048 } 0049 0050 let message = { 0051 subsystem: "mpris", 0052 action: action 0053 }; 0054 if (payload) { 0055 message.payload = payload; 0056 } 0057 0058 chrome.tabs.sendMessage(player.tabId, message, { 0059 frameId: player.frameId 0060 }, (resp) => { 0061 const error = chrome.runtime.lastError; 0062 // When player tab crashed, we get this error message. 0063 // There's unfortunately no proper signal for this so we can really only know when we try to send a command 0064 if (error && error.message === "Could not establish connection. Receiving end does not exist.") { 0065 console.warn("Failed to send player command to tab", player.tabId, ", signalling player gone"); 0066 playerTabGone(player.tabId); 0067 } 0068 }); 0069 } 0070 0071 function playerTabGone(tabId) { 0072 let players = playerIds; 0073 players.forEach((playerId) => { 0074 if (playerId.startsWith(tabId + "-")) { 0075 playerGone(playerId); 0076 } 0077 }); 0078 } 0079 0080 function playerGone(playerId) { 0081 let oldPlayer = currentPlayer(); 0082 0083 var removedPlayerIdx = playerIds.indexOf(playerId); 0084 if (removedPlayerIdx > -1) { 0085 playerIds.splice(removedPlayerIdx, 1); // remove that player from the array 0086 } 0087 0088 // If there is no more player on this tab, remove badge 0089 const gonePlayerTabId = Number(playerId.split("-")[0]); 0090 if (playersOnTab(gonePlayerTabId).length === 0) { 0091 // Check whether that tab still exists before trying to clear the badge 0092 chrome.tabs.get(gonePlayerTabId, (tab) => { 0093 if (chrome.runtime.lastError /*silence error*/ || !tab) { 0094 return; 0095 } 0096 0097 chrome.browserAction.setBadgeText({ 0098 text: null, // null resets tab-specific badge 0099 tabId: gonePlayerTabId // important to pass it as number! 0100 }); 0101 // Important to clear the color, too, so it reverts back to global badge setting 0102 chrome.browserAction.setBadgeBackgroundColor({ 0103 color: null, 0104 tabId: gonePlayerTabId 0105 }); 0106 }); 0107 } 0108 0109 let newPlayer = currentPlayer(); 0110 0111 if (oldPlayer.id === newPlayer.id) { 0112 return; 0113 } 0114 0115 // all players gone :( 0116 if (!newPlayer.id) { 0117 sendPortMessage("mpris", "gone"); 0118 return; 0119 } 0120 0121 // ask the now current player to identify to us 0122 // we can't just pretend "playing" as the other player might be paused 0123 sendPlayerTabMessage(newPlayer, "identify"); 0124 } 0125 0126 // when tab is closed, tell the player is gone 0127 // below we also have a "gone" signal listener from the content script 0128 // which is invoked in the pagehide handler of the page 0129 chrome.tabs.onRemoved.addListener((tabId) => { 0130 // Since we only get the tab id, search for all players from this tab and signal a "gone" 0131 playerTabGone(tabId); 0132 }); 0133 0134 // There's no signal for when a tab process crashes (only in browser dev builds). 0135 // We watch for the tab becoming inaudible and check if it's still around. 0136 // With this heuristic we can at least mitigate MPRIS remaining stuck in a playing state. 0137 chrome.tabs.onUpdated.addListener((tabId, changes) => { 0138 if (!changes.hasOwnProperty("audible") || changes.audible === true) { 0139 return; 0140 } 0141 0142 // Now check if the tab is actually gone 0143 chrome.tabs.executeScript(tabId, { 0144 code: `true` 0145 }, (response) => { 0146 const error = chrome.runtime.lastError; 0147 // Chrome error in script_executor.cc "kRendererDestroyed" 0148 if (error && error.message === "The tab was closed.") { 0149 console.warn("Player tab", tabId, "became inaudible and was considered crashed, signalling player gone"); 0150 playerTabGone(tabId); 0151 } 0152 }); 0153 }); 0154 0155 // callbacks from host (Plasma) to our extension 0156 addCallback("mpris", "raise", function (message) { 0157 let player = currentPlayer(); 0158 if (player.tabId) { 0159 raiseTab(player.tabId); 0160 } 0161 }); 0162 0163 addCallback("mpris", ["play", "pause", "playPause", "stop", "next", "previous"], function (message, action) { 0164 sendPlayerTabMessage(currentPlayer(), action); 0165 }); 0166 0167 addCallback("mpris", "setFullscreen", (message) => { 0168 sendPlayerTabMessage(currentPlayer(), "setFullscreen", { 0169 fullscreen: message.fullscreen 0170 }); 0171 }); 0172 0173 addCallback("mpris", "setVolume", function (message) { 0174 sendPlayerTabMessage(currentPlayer(), "setVolume", { 0175 volume: message.volume 0176 }); 0177 }); 0178 0179 addCallback("mpris", "setLoop", function (message) { 0180 sendPlayerTabMessage(currentPlayer(), "setLoop", { 0181 loop: message.loop 0182 }); 0183 }); 0184 0185 addCallback("mpris", "setPosition", function (message) { 0186 sendPlayerTabMessage(currentPlayer(), "setPosition", { 0187 position: message.position 0188 }); 0189 }) 0190 0191 addCallback("mpris", "setPlaybackRate", function (message) { 0192 sendPlayerTabMessage(currentPlayer(), "setPlaybackRate", { 0193 playbackRate: message.playbackRate 0194 }); 0195 }); 0196 0197 // callbacks from a browser tab to our extension 0198 addRuntimeCallback("mpris", "playing", function (message, sender) { 0199 // Before Firefox 67 it ran extensions in incognito mode by default. 0200 // However, after the update the extension keeps running in incognito mode. 0201 // So we keep disabling media controls for them to prevent accidental private 0202 // information leak on lock screen or now playing auto status in a messenger 0203 if (IS_FIREFOX && sender.tab.incognito) { 0204 return; 0205 } 0206 0207 let playerId = playerIdFromSender(sender); 0208 0209 let idx = playerIds.indexOf(playerId); 0210 if (idx > -1) { 0211 // Move it to the end of the list so it becomes current 0212 playerIds.push(playerIds.splice(idx, 1)[0]); 0213 } else { 0214 playerIds.push(playerId); 0215 } 0216 0217 var payload = message || {}; 0218 payload.tabTitle = sender.tab.title; 0219 payload.url = sender.tab.url; 0220 0221 sendPortMessage("mpris", "playing", payload); 0222 0223 // Add toolbar icon to make it obvious you now have controls to disable the player 0224 chrome.browserAction.setBadgeText({ 0225 text: "♪", 0226 tabId: sender.tab.id 0227 }); 0228 chrome.browserAction.setBadgeBackgroundColor({ 0229 color: "#1d99f3", // Breeze "highlight" color 0230 tabId: sender.tab.id 0231 }); 0232 }); 0233 0234 addRuntimeCallback("mpris", "gone", function (message, sender) { 0235 playerGone(playerIdFromSender(sender)); 0236 }); 0237 0238 addRuntimeCallback("mpris", "stopped", function (message, sender) { 0239 // When player stopped, check if there's another one we could control now instead 0240 let playerId = playerIdFromSender(sender); 0241 if (currentPlayer().id === playerId) { 0242 if (playerIds.length > 1) { 0243 playerGone(playerId); 0244 } 0245 } 0246 }); 0247 0248 addRuntimeCallback("mpris", ["paused", "waiting", "canplay"], function (message, sender, action) { 0249 if (currentPlayer().id === playerIdFromSender(sender)) { 0250 sendPortMessage("mpris", action); 0251 } 0252 }); 0253 0254 addRuntimeCallback("mpris", ["duration", "timeupdate", "seeking", "seeked", "ratechange", "volumechange", "titlechange", "fullscreenchange"], function (message, sender, action) { 0255 if (currentPlayer().id === playerIdFromSender(sender)) { 0256 sendPortMessage("mpris", action, message); 0257 } 0258 }); 0259 0260 addRuntimeCallback("mpris", ["metadata", "callbacks"], function (message, sender, action) { 0261 if (currentPlayer().id === playerIdFromSender(sender)) { 0262 var payload = {}; 0263 payload[action] = message; 0264 0265 sendPortMessage("mpris", action, payload); 0266 } 0267 }); 0268 0269 addRuntimeCallback("mpris", "hasTabPlayer", (message) => { 0270 return Promise.resolve(playersOnTab(message.tabId)); 0271 });