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