├── .gitattributes
├── .eslintignore
├── icon128.png
├── icon64.png
├── .gitignore
├── lib
├── isFirefoxPromise.js
├── webExtensionCommands.js
├── hotkeyManager.js
├── content-scripts-register-polyfill.js
├── pageWorkerManager.js
├── browser-polyfill.min.js
└── browser-polyfill.min.js.map
├── docs
└── img
│ ├── Extract_Element.png
│ └── Inspect_Element.png
├── .web-extension-id
├── .idea
└── watcherTasks.xml
├── data
├── mixcloud.com-view.js
├── pocketcasts.com-view.js
├── tunein.com-view.js
├── di.fm-view.js
├── npr.org-view.js
├── bandcamp.com-view.js
├── hypem.com-view.js
├── soundcloud.com-view.js
├── overcast.fm-view.js
├── radio.yandex.ru-view.js
├── stitcher.com-view.js
├── hulu.com-view.js
├── jango.com-view.js
├── play.pocketcasts.com-view.js
├── play.google.com-view.js
├── cbcmusic.ca-view.js
├── music.yandex.ru-view.js
├── pluralsight.com-view.js
├── netflix.com-view.js
├── plex.tv-view.js
├── udemy.com-view.js
├── player.spotify.com-view.js
├── tidal.com-view.js
├── vk.com-view.js
├── jamstash-view.js
├── kanopy-view.js
├── play.spotify.com-view.js
├── koel-view.js
├── music.apple.com-view.js
├── deezer.com-view.js
├── madsonic-view.js
├── subsonic-view.js
├── pandora.com-view.js
├── music.amazon-view.js
├── open.spotify.com-view.js
├── finder.js
├── w.soundcloud.com-orchestrator.js
├── youtube.com-orchestrator-pageScript.js
├── youtube.com-orchestrator.js
└── orchestrator.js
├── .vscode
├── settings.json
├── tasks.json
└── launch.json
├── tests
├── play.spotify.com.html
├── player.spotify.com.html
├── madsonic.html
├── youtube.com.html
├── incomplete test-pageworkerManager.js
├── test-main.js
├── pocketcasts.com.html
├── pandora.com.html
├── youtube.com.js
├── main.player.spotify.com.html
├── tidal.com.html
├── main.play.spotify.com.html
├── open.spotify.com.html
├── madsonic-playQueue.html
├── test-youtube-orchestrator.js
└── test-orchestrator.js
├── index.html
├── options
├── UserOptions.js
├── options_ui.js
├── Options.js
├── CommandOptions.js
├── ContentScriptOptions.js
├── options_ui.html
└── extension.css
├── jsconfig.json
├── .azure-pipelines.yml
├── .eslintrc.json
├── index.js
├── BrowserMediaPlayers.pem
├── package.json
├── README.md
├── manifest.json
└── LICENSE
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.png filter=lfs diff=lfs merge=lfs -text
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | tests/**
2 | web-ext-artifacts/**
3 | node_modules/**
4 |
--------------------------------------------------------------------------------
/icon128.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:14238f687673bddb87fa8060ab39104cea860babcc9da3ef9ccf313a88ff8fd6
3 | size 22435
4 |
--------------------------------------------------------------------------------
/icon64.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:d6cc771805978b08ebe898ac77ed70a48d8aad2f469b84187b2a20ab3316e19d
3 | size 5930
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Thumbs.db
2 | *.xpi
3 | icon.png
4 | .idea/
5 | node_modules
6 | *.log
7 | *.zip
8 | web-ext-artifacts/**
9 | icon425.png
10 |
--------------------------------------------------------------------------------
/lib/isFirefoxPromise.js:
--------------------------------------------------------------------------------
1 | export default browser.runtime.getBrowserInfo ? browser.runtime.getBrowserInfo().then(({name}) => name == 'Firefox') : Promise.resolve(false);
--------------------------------------------------------------------------------
/docs/img/Extract_Element.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:a68bae8f48474a38cf77be92f0b23276e2ed187f2a8350f4b4e3e95d7cc14660
3 | size 416288
4 |
--------------------------------------------------------------------------------
/docs/img/Inspect_Element.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:daec59d72d592f84cd0d56d09d315ae176a98cf8a17c16991dd163061da3f4c7
3 | size 624338
4 |
--------------------------------------------------------------------------------
/.web-extension-id:
--------------------------------------------------------------------------------
1 | # This file was created by https://github.com/mozilla/web-ext
2 | # Your auto-generated extension ID for addons.mozilla.org is:
3 | jid1-4GP7z3tkUd3Tzg@jetpack
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/data/mixcloud.com-view.js:
--------------------------------------------------------------------------------
1 | /**
2 | * MediaKeys namespace.
3 | */
4 | if (typeof MediaKeys == 'undefined') var MediaKeys = {};
5 |
6 | MediaKeys.playButton = 'div.player-control';
7 | MediaKeys.pauseButton = 'div.player-control.pause-state';
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "json.schemas": [
3 | {
4 | "fileMatch": [
5 | "/manifest.json"
6 | ],
7 | "url": "http://json.schemastore.org/webextension"
8 | }
9 | ]
10 | }
--------------------------------------------------------------------------------
/data/pocketcasts.com-view.js:
--------------------------------------------------------------------------------
1 | /**
2 | * MediaKeys namespace.
3 | */
4 | if (typeof MediaKeys == 'undefined') var MediaKeys = {};
5 |
6 | MediaKeys.playButton = 'div.play_pause_button.play_button';
7 | MediaKeys.pauseButton = 'div.play_pause_button.pause_button';
8 |
--------------------------------------------------------------------------------
/data/tunein.com-view.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * MediaKeys namespace.
4 | */
5 | if (typeof MediaKeys == 'undefined') var MediaKeys = {};
6 |
7 | MediaKeys.playButton = 'div.playbutton:not([style*="none"])';
8 | MediaKeys.pauseButton = 'div.playbutton:not([style*="none"])';
9 |
--------------------------------------------------------------------------------
/tests/play.spotify.com.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
93 |
94 |
--------------------------------------------------------------------------------
/data/orchestrator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * MediaKeys namespace.
3 | *
4 | * Supports backwards compatibility with older Gecko key values.
5 | * See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
6 | */
7 | if (typeof MediaKeys == 'undefined') var MediaKeys = {};
8 |
9 | MediaKeys.Init = function () {
10 | const mouseClickEvents = ['mousedown', 'click', 'mouseup'];
11 | function simulateMouseClick(element){
12 | mouseClickEvents.forEach(mouseEventType =>
13 | element.dispatchEvent(
14 | new MouseEvent(mouseEventType, {
15 | view: window,
16 | bubbles: true,
17 | cancelable: true,
18 | buttons: 1
19 | })
20 | )
21 | );
22 | }
23 |
24 | function MediaPlay() {
25 | var playButton = MediaKeys.find(MediaKeys.playButton, MediaKeys.basePlayer);
26 | if (playButton == null) return;
27 | simulateMouseClick(playButton);
28 | }
29 |
30 | function MediaPause() {
31 | var pauseButton = MediaKeys.find(MediaKeys.pauseButton, MediaKeys.basePlayer)
32 | if (pauseButton != null) {
33 | simulateMouseClick(pauseButton);
34 | }
35 | }
36 |
37 | function MediaPlayPause() {
38 | var playButton = MediaKeys.find(MediaKeys.playButton, MediaKeys.basePlayer);
39 | if (playButton != null) {
40 | simulateMouseClick(playButton);
41 | }
42 | else {
43 | var pauseButton = MediaKeys.find(MediaKeys.pauseButton, MediaKeys.basePlayer);
44 | if (pauseButton == null) return;
45 | simulateMouseClick(pauseButton);
46 | }
47 | }
48 |
49 | function MediaNextTrack() {
50 | var skipButton = MediaKeys.find(MediaKeys.skipButton, MediaKeys.basePlayer);
51 | if (skipButton == null) return;
52 | simulateMouseClick(skipButton);
53 | }
54 |
55 | function MediaPrevTrack() {
56 | var previousButton = MediaKeys.find(MediaKeys.previousButton, MediaKeys.basePlayer);
57 | if (previousButton == null) return;
58 | simulateMouseClick(previousButton);
59 | }
60 |
61 | function MediaStop() {
62 | var pauseButton = MediaKeys.find(MediaKeys.pauseButton, MediaKeys.basePlayer);
63 | if (pauseButton == null) return;
64 | simulateMouseClick(pauseButton);
65 | }
66 |
67 | async function waitUntilExists(func) {
68 | let retriesLeft = 100;
69 | return new Promise((resolve, reject) => {
70 | let interval = setInterval(() => {
71 | let result = func();
72 | if (result) {
73 | clearInterval(interval);
74 | resolve(result);
75 | }
76 | else if (retriesLeft-- === 0)
77 | {
78 | clearInterval(interval);
79 | reject('function failed to return a value within a reasonable amount of time');
80 | }
81 | }, 250);
82 | });
83 | }
84 |
85 | async function setupTrackInfoUpdates() {
86 | let timeout;
87 | function notifyNewTrack() {
88 | // console.log('clearing timeout ' + timeout);
89 | clearTimeout(timeout);
90 | timeout = setTimeout(() => {
91 | new Notification('Now Playing', {
92 | body: MediaKeys.find(MediaKeys.trackInfo, MediaKeys.basePlayer).innerText,
93 | icon: MediaKeys.getTrackImageUrl ? MediaKeys.getTrackImageUrl() : ''
94 | });
95 | }, 3000);
96 | }
97 |
98 | var currentTrackObservable;
99 |
100 | if (MediaKeys.trackInfoContainer)
101 | currentTrackObservable = await waitUntilExists(() => MediaKeys.find(MediaKeys.trackInfoContainer, MediaKeys.basePlayer));
102 | else
103 | currentTrackObservable = await waitUntilExists(() => MediaKeys.find(MediaKeys.trackInfo, MediaKeys.basePlayer));
104 |
105 | if (currentTrackObservable) {
106 | var currentTrackObserver = new MutationObserver(notifyNewTrack);
107 | currentTrackObserver.observe(currentTrackObservable, {
108 | childList: true,
109 | attributes: true,
110 | subtree: true
111 | });
112 | }
113 | }
114 |
115 | if (MediaKeys.trackInfo && Notification.permission != 'denied') {
116 | if (Notification.permission == 'granted') setupTrackInfoUpdates();
117 | else Notification.requestPermission().then(function (result) { if (result == 'granted') setupTrackInfoUpdates(); });
118 | }
119 |
120 | function setupCommunicationChannel(){
121 | let port = browser.runtime.connect(browser.runtime.id, {name: window.location.host});
122 |
123 | port.onMessage.addListener(request => {
124 | //console.log(`page script received ${request}`);
125 | switch (request) {
126 | case 'MediaPlay':
127 | MediaPlay();
128 | break;
129 | case 'MediaPause':
130 | MediaPause();
131 | break;
132 | case 'MediaPlayPause':
133 | MediaPlayPause();
134 | break;
135 |
136 | case 'MediaNextTrack':
137 | MediaNextTrack();
138 | break;
139 |
140 | case 'MediaPrevTrack':
141 | MediaPrevTrack();
142 | break;
143 |
144 | case 'MediaStop':
145 | MediaStop();
146 | break;
147 |
148 | default:
149 | break;
150 | }
151 | });
152 |
153 | port.onDisconnect.addListener(() => setTimeout(setupCommunicationChannel, 1000));
154 | }
155 | setupCommunicationChannel();
156 | };
157 |
158 | MediaKeys.Init();
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://dev.azure.com/BrowserMediaKeys/BrowserMediaPlayers/_build/latest?definitionId=1)
2 | 
3 |
4 |
5 | Description
6 | ==================
7 |
8 | Lets you control various media sites using the media keys on your keyboard.
9 |
10 | Your media keys should work without the browser window active for Chrome and Opera. Firefox only [supports the play/pause key](https://bugzilla.mozilla.org/show_bug.cgi?id=1251795#c13) and only if the [browser window is active](https://bugzilla.mozilla.org/show_bug.cgi?id=1411795). You can however change the key assignments for this extension, to any key combination that you'd like, by using the shortcuts manager for the browser.
11 |
12 | Supported Sites: youtube, pandora, spotify, bandcamp, google play, yandex, soundcloud, tidal, deezer, plex, vk, subsonic, jamstash, jango, overcast.fm, music.amazon.co.uk, music.amazon.com, music.amazon.com.au, di.fm, netflix.com, and tunein.com.
13 |
14 | Please find us on GitHub if you'd like to request features, post issues or contribute to the project.
15 |
16 |
17 | Releases
18 | ========
19 | [Firefox](https://addons.mozilla.org/en-US/firefox/addon/media-keys/)
20 | [Opera/Chrome](https://github.com/carlin-q-scott/browser-media-players/releases/latest) - Still awaiting store approval but you can install using the crx file.
21 |
22 |
23 | Requesting New Sites
24 | ====================
25 | If you'd like support for a multimedia website to be added, please create an issue with the following information:
26 | 1. Link to the website
27 | 2. Name of the website
28 | 3. Player control element html; see below on how to capture that.
29 |
30 |
31 | Capturing player control elements
32 | ---------------------------------
33 | 1. Navigate to the multimedia website you'd like added.
34 | 2. Start playing something on the site
35 | 3. Inspect the pause button
36 | 
37 | 1. Right-Click on the button
38 | 2. Click "Inspect Element"
39 | 4. Copy the outer HTML of the element
40 | 
41 | 1. Right-Click on the highlighted text in the Inspector panel
42 | 2. Click "Copy > Outer HTML"
43 | 5. Paste that HTML into the new issue description
44 | 6. Repeat these steps for the following buttons: Play, Next/Skip, Back/Previous.
45 |
46 | `Note: If you cannot inspect the player controls, then player is a custom page element and you will have to figure out the Application Programming Interface (API) for the element in order to add support for it. The Youtube player is such an element so you can use the code for that as an example of how to do this. Fortunately the youtube player has really good documentation but the player you are attempting to add may not.`
47 |
48 | Adding the site yourself
49 | ------------------------
50 | Most sites have standard html elements for player controls. If you were able to follow the instructions in the previous section then the process for adding support is fairly painless:
51 |
52 | 1. The `data` folder contains -view.js scripts for each website that has standard html player control elements.
53 | 2. Copy one of -view.js scripts for another site you're familiar with, and rename it to match the new site.
54 | 3. Review the html you extracted earlier and look for an html attribute that seems unique to the element and hopefully one that clearly identifies it.
55 | For instance, `