2 Copyright (C) 2017-2019 Kai Uwe Broulik <kde@privat.broulik.de>
4 This program is free software; you can redistribute it and/or
5 modify it under the terms of the GNU General Public License as
6 published by the Free Software Foundation; either version 3 of
7 the License, or (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
20 function currentPlayer() {
21 let playerId = playerIds[playerIds.length - 1];
23 // Returning empty object instead of null so you can call player.id returning undefined instead of throwing
27 let segments = playerId.split("-");
30 tabId: parseInt(segments[0]),
31 frameId: parseInt(segments[1])
35 function playerIdFromSender(sender) {
36 return sender.tab.id + "-" + (sender.frameId || 0);
39 function sendPlayerTabMessage(player, action, payload) {
49 message.payload = payload;
52 chrome.tabs.sendMessage(player.tabId, message, {
53 frameId: player.frameId
55 const error = chrome.runtime.lastError;
56 // When player tab crashed, we get this error message.
57 // There's unfortunately no proper signal for this so we can really only know when we try to send a command
58 if (error && error.message === "Could not establish connection. Receiving end does not exist.") {
59 console.warn("Failed to send player command to tab", player.tabId, ", signalling player gone");
60 playerTabGone(player.tabId);
65 function playerTabGone(tabId) {
66 let players = playerIds;
67 players.forEach((playerId) => {
68 if (playerId.startsWith(tabId + "-")) {
74 function playerGone(playerId) {
75 let oldPlayer = currentPlayer();
77 var removedPlayerIdx = playerIds.indexOf(playerId);
78 if (removedPlayerIdx > -1) {
79 playerIds.splice(removedPlayerIdx, 1); // remove that player from the array
82 let newPlayer = currentPlayer();
84 if (oldPlayer.id === newPlayer.id) {
88 // all players gone :(
90 sendPortMessage("mpris", "gone");
94 // ask the now current player to identify to us
95 // we can't just pretend "playing" as the other player might be paused
96 sendPlayerTabMessage(newPlayer, "identify");
99 // when tab is closed, tell the player is gone
100 // below we also have a "gone" signal listener from the content script
101 // which is invoked in the pagehide handler of the page
102 chrome.tabs.onRemoved.addListener((tabId) => {
103 // Since we only get the tab id, search for all players from this tab and signal a "gone"
104 playerTabGone(tabId);
107 // There's no signal for when a tab process crashes (only in browser dev builds).
108 // We watch for the tab becoming inaudible and check if it's still around.
109 // With this heuristic we can at least mitigate MPRIS remaining stuck in a playing state.
110 chrome.tabs.onUpdated.addListener((tabId, changes) => {
111 if (!changes.hasOwnProperty("audible") || changes.audible === true) {
115 // Now check if the tab is actually gone
116 chrome.tabs.executeScript(tabId, {
119 const error = chrome.runtime.lastError;
120 // Chrome error in script_executor.cc "kRendererDestroyed"
121 if (error && error.message === "The tab was closed.") {
122 console.warn("Player tab", tabId, "became inaudible and was considered crashed, signalling player gone");
123 playerTabGone(tabId);
128 // callbacks from host (Plasma) to our extension
129 addCallback("mpris", "raise", function (message) {
130 let player = currentPlayer();
132 raiseTab(player.tabId);
136 addCallback("mpris", ["play", "pause", "playPause", "stop", "next", "previous"], function (message, action) {
137 sendPlayerTabMessage(currentPlayer(), action);
140 addCallback("mpris", "setFullscreen", (message) => {
141 sendPlayerTabMessage(currentPlayer(), "setFullscreen", {
142 fullscreen: message.fullscreen
146 addCallback("mpris", "setVolume", function (message) {
147 sendPlayerTabMessage(currentPlayer(), "setVolume", {
148 volume: message.volume
152 addCallback("mpris", "setLoop", function (message) {
153 sendPlayerTabMessage(currentPlayer(), "setLoop", {
158 addCallback("mpris", "setPosition", function (message) {
159 sendPlayerTabMessage(currentPlayer(), "setPosition", {
160 position: message.position
164 addCallback("mpris", "setPlaybackRate", function (message) {
165 sendPlayerTabMessage(currentPlayer(), "setPlaybackRate", {
166 playbackRate: message.playbackRate
170 // callbacks from a browser tab to our extension
171 addRuntimeCallback("mpris", "playing", function (message, sender) {
172 // Before Firefox 67 it ran extensions in incognito mode by default.
173 // However, after the update the extension keeps running in incognito mode.
174 // So we keep disabling media controls for them to prevent accidental private
175 // information leak on lock screen or now playing auto status in a messenger
176 if (IS_FIREFOX && sender.tab.incognito) {
180 let playerId = playerIdFromSender(sender);
182 let idx = playerIds.indexOf(playerId);
184 // Move it to the end of the list so it becomes current
185 playerIds.push(playerIds.splice(idx, 1)[0]);
187 playerIds.push(playerId);
190 var payload = message || {};
191 payload.tabTitle = sender.tab.title;
192 payload.url = sender.tab.url;
194 sendPortMessage("mpris", "playing", payload);
197 addRuntimeCallback("mpris", "gone", function (message, sender) {
198 playerGone(playerIdFromSender(sender));
201 addRuntimeCallback("mpris", "stopped", function (message, sender) {
202 // When player stopped, check if there's another one we could control now instead
203 let playerId = playerIdFromSender(sender);
204 if (currentPlayer().id === playerId) {
205 if (playerIds.length > 1) {
206 playerGone(playerId);
211 addRuntimeCallback("mpris", ["paused", "waiting", "canplay"], function (message, sender, action) {
212 if (currentPlayer().id === playerIdFromSender(sender)) {
213 sendPortMessage("mpris", action);
217 addRuntimeCallback("mpris", ["duration", "timeupdate", "seeking", "seeked", "ratechange", "volumechange", "titlechange", "fullscreenchange"], function (message, sender, action) {
218 if (currentPlayer().id === playerIdFromSender(sender)) {
219 sendPortMessage("mpris", action, message);
223 addRuntimeCallback("mpris", ["metadata", "callbacks"], function (message, sender, action) {
224 if (currentPlayer().id === playerIdFromSender(sender)) {
226 payload[action] = message;
228 sendPortMessage("mpris", action, payload);
232 addRuntimeCallback("mpris", "hasTabPlayer", (message) => {
233 const playersOnTab = playerIds.filter((playerId) => {
234 return playerId.startsWith(message.tabId + "-");
237 return Promise.resolve(playersOnTab);