├── .gitignore ├── extension ├── APV3_Icon_2d_2c_16.png ├── APV3_Icon_2d_2c_24.png ├── APV3_Icon_2d_2c_32.png ├── APV3_Icon_2d_2c_48.png ├── APV3_Icon_2d_2c_64.png ├── APV3_Icon_2d_2c_96.png ├── APV3_Icon_2d_2c_128.png ├── manifest.json ├── worker.js ├── popup.html ├── content.js ├── popup.css ├── main.js └── popup.js ├── resources ├── WebRTC │ ├── audio │ │ └── audio.mp3 │ ├── images │ │ └── poster.jpg │ ├── video │ │ ├── chrome.mp4 │ │ ├── chrome.ogv │ │ └── chrome.webm │ ├── _ │ │ ├── images │ │ │ └── webrtc-icon-192x192.png │ │ └── css │ │ │ ├── main.css │ │ │ └── fonts.css │ └── css │ │ └── main.css ├── Icons │ ├── APV3_Icon_2d_2c_128.png │ ├── APV3_Icon_2d_2c_16.png │ ├── APV3_Icon_2d_2c_192.png │ ├── APV3_Icon_2d_2c_24.png │ ├── APV3_Icon_2d_2c_256.png │ ├── APV3_Icon_2d_2c_32.png │ ├── APV3_Icon_2d_2c_48.png │ ├── APV3_Icon_2d_2c_512.png │ ├── APV3_Icon_2d_2c_64.png │ └── APV3_Icon_2d_2c_96.png ├── index.js ├── index.css └── index.html ├── .editorconfig ├── FAQ.md ├── CHANGELOG.md ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | *.pem 3 | 4 | # temporary folders 5 | extension.* 6 | resources.* 7 | -------------------------------------------------------------------------------- /extension/APV3_Icon_2d_2c_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/extension/APV3_Icon_2d_2c_16.png -------------------------------------------------------------------------------- /extension/APV3_Icon_2d_2c_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/extension/APV3_Icon_2d_2c_24.png -------------------------------------------------------------------------------- /extension/APV3_Icon_2d_2c_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/extension/APV3_Icon_2d_2c_32.png -------------------------------------------------------------------------------- /extension/APV3_Icon_2d_2c_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/extension/APV3_Icon_2d_2c_48.png -------------------------------------------------------------------------------- /extension/APV3_Icon_2d_2c_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/extension/APV3_Icon_2d_2c_64.png -------------------------------------------------------------------------------- /extension/APV3_Icon_2d_2c_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/extension/APV3_Icon_2d_2c_96.png -------------------------------------------------------------------------------- /resources/WebRTC/audio/audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/WebRTC/audio/audio.mp3 -------------------------------------------------------------------------------- /extension/APV3_Icon_2d_2c_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/extension/APV3_Icon_2d_2c_128.png -------------------------------------------------------------------------------- /resources/WebRTC/images/poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/WebRTC/images/poster.jpg -------------------------------------------------------------------------------- /resources/WebRTC/video/chrome.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/WebRTC/video/chrome.mp4 -------------------------------------------------------------------------------- /resources/WebRTC/video/chrome.ogv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/WebRTC/video/chrome.ogv -------------------------------------------------------------------------------- /resources/WebRTC/video/chrome.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/WebRTC/video/chrome.webm -------------------------------------------------------------------------------- /resources/Icons/APV3_Icon_2d_2c_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/Icons/APV3_Icon_2d_2c_128.png -------------------------------------------------------------------------------- /resources/Icons/APV3_Icon_2d_2c_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/Icons/APV3_Icon_2d_2c_16.png -------------------------------------------------------------------------------- /resources/Icons/APV3_Icon_2d_2c_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/Icons/APV3_Icon_2d_2c_192.png -------------------------------------------------------------------------------- /resources/Icons/APV3_Icon_2d_2c_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/Icons/APV3_Icon_2d_2c_24.png -------------------------------------------------------------------------------- /resources/Icons/APV3_Icon_2d_2c_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/Icons/APV3_Icon_2d_2c_256.png -------------------------------------------------------------------------------- /resources/Icons/APV3_Icon_2d_2c_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/Icons/APV3_Icon_2d_2c_32.png -------------------------------------------------------------------------------- /resources/Icons/APV3_Icon_2d_2c_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/Icons/APV3_Icon_2d_2c_48.png -------------------------------------------------------------------------------- /resources/Icons/APV3_Icon_2d_2c_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/Icons/APV3_Icon_2d_2c_512.png -------------------------------------------------------------------------------- /resources/Icons/APV3_Icon_2d_2c_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/Icons/APV3_Icon_2d_2c_64.png -------------------------------------------------------------------------------- /resources/Icons/APV3_Icon_2d_2c_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/Icons/APV3_Icon_2d_2c_96.png -------------------------------------------------------------------------------- /resources/WebRTC/_/images/webrtc-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rain-fighters/AudioPick/HEAD/resources/WebRTC/_/images/webrtc-icon-192x192.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # all files 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = tab 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /resources/WebRTC/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | audio { 10 | margin: 0 0 1.5em 0; 11 | width: 100%; 12 | } 13 | 14 | div#sources > div { 15 | float: left; 16 | margin: 0 1em 0 0; 17 | width: calc(50% - 0.5em); 18 | } 19 | 20 | div#sources > div:last-of-type { 21 | margin: 0; 22 | } 23 | 24 | select { 25 | margin: 0 0 0.5em 0; 26 | } 27 | 28 | video { 29 | background: black; 30 | height: 234px; 31 | } 32 | -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "AudioPick", 4 | "short_name": "AudioPick", 5 | "description": "Pick a preferred audio output device for HTML5 audio and video elements", 6 | "author": "necropola@rain-fighters.net", 7 | "homepage_url": "https://rain-fighters.github.io/AudioPick/", 8 | "version": "0.3.11", 9 | "version_name": "0.3.11", 10 | "action": { 11 | "default_icon": { 12 | "16": "APV3_Icon_2d_2c_16.png", 13 | "32": "APV3_Icon_2d_2c_32.png", 14 | "48": "APV3_Icon_2d_2c_48.png", 15 | "128": "APV3_Icon_2d_2c_128.png" 16 | }, 17 | "default_title": "AudioPick", 18 | "default_popup": "popup.html" 19 | }, 20 | "icons": { 21 | "16": "APV3_Icon_2d_2c_16.png", 22 | "32": "APV3_Icon_2d_2c_32.png", 23 | "48": "APV3_Icon_2d_2c_48.png", 24 | "128": "APV3_Icon_2d_2c_128.png" 25 | }, 26 | "host_permissions": ["https://*/*"], 27 | "background": { 28 | "service_worker": "worker.js" 29 | }, 30 | "content_scripts": [ 31 | { 32 | "matches": ["https://*/*"], 33 | "js": ["main.js"], 34 | "world": "MAIN", 35 | "all_frames" : false 36 | }, 37 | { 38 | "matches": ["https://*/*"], 39 | "js": ["content.js"], 40 | "world": "ISOLATED", 41 | "all_frames" : false 42 | } 43 | ], 44 | "permissions": [ 45 | "contentSettings", 46 | "storage", 47 | "scripting", 48 | "activeTab", 49 | "favicon" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | Work in progress ... 3 | 4 | - TOC 5 | {:toc} 6 | 7 | ### Why does the extension need **Microphone Access**? 8 | ... 9 | 10 | ### Are there **Conflicts** between the extension and sites which manage audio devices on their own, e. g. **Discord**, **Teams**, **Signal**, **Elements**, ...? 11 | ... 12 | 13 | ### Which **Platforms** are supported by the extension? 14 | ... 15 | 16 | ### Which **Sites** are supported by the extension? 17 | Generally only **HTTPS** sites are supported. This excludes internal chrome browser pages like `chrome://settings` as well as sites not offering **SSL** encryption. The latter is rather rare nowadays and usually means that the site is either misconfigured or should not be trusted anyway. 18 | 19 | Another case are sites which (somehow) disable extensions entirely, e. g. the **Chrome WebStore** (`https://chromewebstore.google.com`), or sites which explicitly deny microphone access by sending a `Feature-Policy` or `Permissions-Policy` **Response Header** stating so, e. g. `https://stackoverflow.com`. 20 | 21 | All of these cases are indicated by an Error Message in the footer of the extension popup. 22 | 23 | Other sites should work as long as they are using **HTML5** elements of type `HTMLMediaElement` or `AudioContext` to play audio and we have been smart enough to find them and inject our `changeSinkId` listener. The following table is the result of testing various popoular sites with **AudioPick-0.3.9** on **Chrome 120 (64-bit) / Windows 10** – current as of **January 2024**. 24 | 25 | | Site | Status | Comment | 26 | |------|--------|---------| 27 | | **YouTube**
`https://www.youtube.com` | Fully Working | No known issues. | 28 | | **Twitch**
`https://www.twitch.tv` | Fully Working | No known issues. | 29 | | **YouTube Music**
`https://music.youtube.com` | Fully Working | No known issues. | 30 | | **Spotify**
`https://open.spotify.com` | Fully Working | No known issues. | 31 | | **SoundCloud**
`https://soundcloud.com`| Mostly Working | Might require a page reload when a new tab is opened through a link/bookmark to a specific track and autoplay starts before **AudioPick** is able to inject its `changeSinkId` listener properly. | 32 | | **Deezer**
`https://www.deezer.com` | Kinda Working | Requires to click `Play->Pause->Play` once after (re-)loading the page in order to help **AudioPick** to inject its `changeSinkId` listener properly. | 33 | | **Amazon Music**
`https://music.amazon.com` | Probably Working | The demo podcasts worked (without needing a subscription). | 34 | | **Netflix**
`https://www.netflix.com` | Fully Working | No known issues. | 35 | | **Disney+**
`https://www.disneyplus.com` | Maybe Working | Don't have a subscription and hence cannot test. Reports from other **AudioPick** users via [GitHub Issue](https://github.com/rain-fighters/AudioPick/issues) welcome. | 36 | 37 | ### Why isn't the extension available for **Other Browsers**, e. g. **Firefox**? 38 | ... 39 | 40 | ### Where do I report **Issues** with the extension? 41 | ... 42 | 43 | ### How did you come up with the **Idea** for the extension? 44 | ... 45 | 46 | ### How can I **Support AudioPick**? 47 | ... 48 | 49 | ### What is or rather who are **Rain-Fighters**? 50 | ... 51 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ### AudioPick-0.3.10 4 | - Improve debug logging, e. g. inject `enableDebug` into content scripts running in world **MAIN**. 5 | - Fully support **Deezer** and **SoundCloud**. 6 | - Add a `smartMicAccess` mode (enabled via checkbox in the Popup) which significantly reduces microphone permissions granted by the extension. 7 | - Add an info message to the popup header. 8 | - Add a **site info heading** with `favicon`, `host/domain` and `micPolicy`. 9 | - No longer offer the `communications` device to be picked. 10 | - Improve `default` device label on Windows (and possibly MacOS). 11 | - Improve demo page (`resources/index.*`), e. g. add an `AudioContext` and a video in `iframe` example. 12 | - Fix/enhance communication with sub-frames, but disable injecting 13 | into all_frames for now, since changing sinkIds in sub-frames only works on same-origin or when the `iframe` specifies `allow="microphone"`. 14 | 15 | ### AudioPick-0.3.9 16 | - Add a Popup / UI option to enable/disable content script debug messages. 17 | - Detect and respect site Permissions-Policy for microphone access, 18 | e. g. on https://stackoverflow.com 19 | - Work around a Chrome on linux bug/issue where the service worker does 20 | not wake up (fast enough) from inactive state on message events. 21 | 22 | ### AudioPick-0.3.8 23 | - Rewrite for Manifest V3 24 | - New enhanced Popup / UI with dark/light mode support 25 | - New icon / logo 26 | - Allow to store (star) a preferred audio device per domain 27 | - Remove option to set a global preferred device for the browser 28 | - Inject a content script into `world:MAIN` in order to find 29 | media elements which havent't been inserted into the DOM tree, 30 | i. e. sites like **Spotify** and **SoundCloud** should now work, too. 31 | - Minimize the number of **microphone permissions** added/managed by the extension 32 | - by resetting the permission back to **ask** when the default device has been 33 | chosen again for a tab/domain 34 | - by not having a global preferred device for the browser anymore 35 | 36 | ### AudioPick-0.2.2 - 2016-05-21 37 | - going stable 38 | - revert `page_action back` to `browser_action` 39 | - code cleanup 40 | 41 | ### AudioPick-0.2.1 - 2016-05-18 42 | - quick fix to prevent a loop caused by the *observer* 43 | 44 | ### AudioPick-0.2.0 - 2016-05-18 45 | - substitute calls to `getUserMedia()` by `get_help_with_GUM`, i. e. write directly to `contentSettings['microphone']` thereby allowing the modification of audio/video on unencrypted pages 46 | - code cleanup + better diagnostic output 47 | - fixes to handling of asynchronous actions, especially promises 48 | - overwrite devices for an entire frame set, i. e. when sub frames ask the background page for the `default_no`, it asks the top frame and passes this information back to the sub frame 49 | - popup only ever asks the top frame for its current `sink_no` 50 | 51 | ### AudioPick-0.1.4 - 2016-05-17 52 | - add `'use strict'` to all scripts 53 | - inject the content script into all frames so that `setSinkId()` now also works for embedded audio/video (over HTTPS) 54 | - only call `getUserMedia()` on a site (once) when a call to `setSinkId` actually fails. This should greatly reduce the number of sites added to the list of microphone exceptions. 55 | - change `browser_action` into `page_action` 56 | - immediately commit changes to the popup device list (commit button removed) 57 | - remember the last temporary `sink_no` choice of a content page when creating the popup device list for it 58 | -------------------------------------------------------------------------------- /resources/index.js: -------------------------------------------------------------------------------- 1 | var visible_shadow_audio; 2 | var hidden_audio = new Audio("./WebRTC/audio/audio.mp3"); 3 | var visible_audioContext_source; 4 | var hidden_audioContext; 5 | 6 | function play_dom_audio() { 7 | var dom_audio = document.getElementById("dom_audio") 8 | if (dom_audio.paused) 9 | { 10 | dom_audio.loop = true; 11 | dom_audio.play(); 12 | } else { 13 | dom_audio.pause(); 14 | } 15 | } 16 | 17 | function play_hidden_audio() { 18 | if (hidden_audio) { 19 | if (hidden_audio.paused) 20 | { 21 | hidden_audio.loop = true; 22 | hidden_audio.play(); 23 | } else { 24 | hidden_audio.pause(); 25 | } 26 | } 27 | } 28 | 29 | function add_visible_audio() { 30 | let audio = document.getElementById("visible_audio"); 31 | if (audio) { 32 | if (audio.paused) 33 | { 34 | audio.loop = true; 35 | audio.play(); 36 | } else { 37 | audio.pause(); 38 | } 39 | } else { 40 | audio = new Audio("./WebRTC/audio/audio.mp3"); 41 | audio.id = "visible_audio"; 42 | audio.controls = true; 43 | document.getElementById("div_visible_audio").appendChild(audio); 44 | document.getElementById("add_visible_audio").innerText = "Play/Pause new Visible"; 45 | } 46 | } 47 | 48 | function add_visible_shadow_audio() { 49 | if (visible_shadow_audio) { 50 | if (visible_shadow_audio.paused) 51 | { 52 | visible_shadow_audio.loop = true; 53 | visible_shadow_audio.play(); 54 | } else { 55 | visible_shadow_audio.pause(); 56 | } 57 | } else { 58 | var s = document.getElementById("div_visible_shadow_audio").attachShadow({ mode: "closed" }); 59 | visible_shadow_audio = new Audio("./WebRTC/audio/audio.mp3"); 60 | visible_shadow_audio.id = "visible_shadow_audio"; 61 | visible_shadow_audio.controls = true; 62 | s.appendChild(visible_shadow_audio); 63 | document.getElementById("add_visible_shadow_audio").innerText = "Play/Pause shadowRoot"; 64 | } 65 | } 66 | 67 | function add_visible_audioContext_source() { 68 | if (visible_audioContext_source) { 69 | if (visible_audioContext_source.paused) 70 | { 71 | visible_audioContext_source.loop = true; 72 | visible_audioContext_source.play(); 73 | } else { 74 | visible_audioContext_source.pause(); 75 | } 76 | } else { 77 | visible_audioContext_source = new Audio("./WebRTC/audio/audio.mp3"); 78 | visible_audioContext_source.id = "visible_audioContext_source"; 79 | visible_audioContext_source.controls = true; 80 | document.getElementById("div_visible_audioContext_source").appendChild(visible_audioContext_source); 81 | document.getElementById("add_visible_audioContext_source").innerText = "Play/Pause visible AudioContext Source"; 82 | document.getElementById("add_hidden_audioContext").disabled = false; 83 | } 84 | } 85 | 86 | function add_hidden_audioContext() { 87 | if (hidden_audioContext) { 88 | if (visible_audioContext_source.paused) 89 | { 90 | visible_audioContext_source.loop = true; 91 | visible_audioContext_source.play(); 92 | } else { 93 | visible_audioContext_source.pause(); 94 | } 95 | } else { 96 | hidden_audioContext = new AudioContext; 97 | const source = hidden_audioContext.createMediaElementSource(visible_audioContext_source); 98 | const gainNode = hidden_audioContext.createGain(); 99 | source.connect(gainNode); 100 | gainNode.connect(hidden_audioContext.destination); 101 | document.getElementById("add_hidden_audioContext").innerText = "Play/Pause hidden AudioContext (Source)"; 102 | document.getElementById("div_hidden_audioContext").innerText = "Sink is now determined by AudioContext!"; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /extension/worker.js: -------------------------------------------------------------------------------- 1 | // Worker script handles: 2 | // 1) Providing microphone access to our content script for basic function. 3 | // 2) Relaying messages from any frames/iframes/children back to the main tab. 4 | // 3) Injecting sinkId changes into WORLD:MAIN. 5 | // Worker runs in BACKGROUND. 6 | // 7 | // Prefix used for our local storage variables. 8 | const storagePrefix = "preferredDevice_"; 9 | 10 | function onMessage(request, sender, sendResponse) { 11 | switch (request.action) { 12 | case "getActiveDeviceBG": 13 | // Create a proxy response function to catch errors that can happen 14 | // when the top content scripts is not (yet) resposnding. 15 | const responder = function (...args) { 16 | if (args.length === 0) { 17 | if (chrome.runtime.lastError) { 18 | // Do nothing. 19 | // We just need to read this lastError value to 20 | // prevent an error being logged to the console. 21 | } 22 | // The top content script is not (yet) responding 23 | sendResponse(null); 24 | } else { 25 | sendResponse.apply(this, args); 26 | } 27 | } 28 | // Relay the message to the top (frameId: 0) content script. 29 | chrome.tabs.sendMessage(sender.tab.id, 30 | {action: "getActiveDevice"}, {frameId: 0}, responder); 31 | // We need to return true from onMessage() to keep the channel open, 32 | // since our sendReponse() is called asynchronously here. 33 | return true; 34 | case "setMicAccess": 35 | // Set the current microphone access permissions for the tab. 36 | // Request format: {value: "allow"} 37 | chrome.contentSettings.microphone.set({ 38 | primaryPattern: sender.tab.url.split("/")[0] + "//" + 39 | sender.tab.url.split("/")[2] + "/*", 40 | scope: ( 41 | sender.tab.incognito 42 | ? "incognito_session_only" 43 | : "regular" 44 | ), 45 | setting: request.value 46 | }).then(function() { 47 | sendResponse(true); 48 | }).catch(function() { 49 | sendResponse(false); 50 | }); 51 | return true; 52 | case "injectSink": 53 | // Inject the passed sinkId (request.value) into MAIN. 54 | // Then dispatch a "changeSinkId" event to update all elements. 55 | // 56 | // NOTE that target: {allFrames: true} does not make sense, simce 57 | // 1. we only want to target frames where we actually injected our 58 | // content scripts and 59 | // 2. (even more importantly) sinkIds are unique per frame, i. e. 60 | // the same device has a different deviceId in top and in sub. 61 | // Basically the content scripts for top and sub-frames need to 62 | // independently calculate and pass the correct sinkId for their 63 | // window/frame and the worker needs to explicitely target the 64 | // sender's frameId(s). 65 | chrome.scripting.executeScript({ 66 | args: [request.value], 67 | func: function (activeSinkId) { 68 | window.APV3_UN1QU3_activeSinkId = activeSinkId; 69 | window.dispatchEvent( 70 | new CustomEvent( 71 | "changeSinkId", 72 | {detail: activeSinkId} 73 | ) 74 | ); 75 | }, 76 | target: {tabId: sender.tab.id, frameIds: [sender.frameId], allFrames : false}, 77 | world: "MAIN" 78 | }); 79 | break; 80 | case "injectDebug": 81 | chrome.scripting.executeScript({ 82 | args: [request.value], 83 | func: function (enableDebug) { 84 | window.APV3_UN1QU3_enableDebug = enableDebug; 85 | }, 86 | target: {tabId: sender.tab.id, frameIds: [sender.frameId], allFrames : false}, 87 | world: "MAIN" 88 | }); 89 | break; 90 | case "wakeup": 91 | // A dumnmy message that we are being sent to wake us up. 92 | // This is a hack/workaround for a chrome bug on linux. 93 | break; 94 | } 95 | } 96 | 97 | // Start listening immediately. 98 | chrome.runtime.onMessage.addListener(onMessage); 99 | -------------------------------------------------------------------------------- /extension/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AudioPick 7 | 8 | 9 | 10 | 11 |
12 | 14 |

AudioPick 15 | 19 | 20 | 21 | 22 | 23 |

24 |
25 |
26 | 30 | 31 | 32 |
33 |
34 | Read more about and possibly contribute
on GitHub or show your appreciation. 35 |
36 |
37 | 38 |   39 |
40 | 41 |
42 |
43 | 60 | 61 |
62 | 67 |
68 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /resources/WebRTC/_/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | .hidden { 9 | display: none; 10 | } 11 | 12 | div#errorMsg p { 13 | color: #F00; 14 | } 15 | 16 | body { 17 | font-family: 'Roboto', sans-serif; 18 | font-weight: 300; 19 | } 20 | 21 | a { 22 | color: #6fa8dc; 23 | font-weight: 300; 24 | text-decoration: none; 25 | } 26 | 27 | a:hover { 28 | color: #3d85c6; 29 | text-decoration: underline; 30 | } 31 | 32 | a#viewSource { 33 | display: block; 34 | margin: 1.3em 0 0 0; 35 | border-top: 1px solid #999; 36 | padding: 1em 0 0 0; 37 | } 38 | 39 | div#links a { 40 | display: block; 41 | line-height: 1.3em; 42 | margin: 0 0 1.5em 0; 43 | } 44 | 45 | div.outputSelector { 46 | margin: -1.3em 0 2em 0; 47 | } 48 | 49 | @media screen and (min-width: 1000px) { 50 | /* hack! to detect non-touch devices */ 51 | div#links a { 52 | line-height: 0.8em; 53 | } 54 | } 55 | 56 | h1 a { 57 | font-weight: 300; 58 | margin: 0 10px 0 0; 59 | white-space: nowrap; 60 | } 61 | 62 | audio { 63 | max-width: 100%; 64 | } 65 | 66 | body { 67 | font-family: 'Roboto', sans-serif; 68 | margin: 0; 69 | padding: 1em; 70 | word-break: break-word; 71 | } 72 | 73 | button { 74 | background-color: #d84a38; 75 | border: none; 76 | border-radius: 2px; 77 | color: white; 78 | font-family: 'Roboto', sans-serif; 79 | font-size: 0.8em; 80 | margin: 0 0 1em 0; 81 | padding: 0.5em 0.7em 0.6em 0.7em; 82 | } 83 | 84 | button:active { 85 | background-color: #cf402f; 86 | } 87 | 88 | button:hover { 89 | background-color: #cf402f; 90 | } 91 | 92 | button[disabled] { 93 | color: #ccc; 94 | } 95 | 96 | button[disabled]:hover { 97 | background-color: #d84a38; 98 | } 99 | 100 | canvas { 101 | background-color: #ccc; 102 | max-width: 100%; 103 | width: 100%; 104 | } 105 | 106 | code { 107 | font-family: 'Roboto', sans-serif; 108 | font-weight: 400; 109 | } 110 | 111 | div#container { 112 | margin: 0 auto 0 auto; 113 | max-width: 40em; 114 | padding: 1em 1.5em 1.3em 1.5em; 115 | } 116 | 117 | div#links { 118 | padding: 0.5em 0 0 0; 119 | } 120 | 121 | h1 { 122 | border-bottom: 1px solid #ccc; 123 | font-family: 'Roboto', sans-serif; 124 | font-weight: 500; 125 | margin: 0 0 0.8em 0; 126 | padding: 0 0 0.2em 0; 127 | } 128 | 129 | h2 { 130 | color: #444; 131 | font-size: 1em; 132 | font-weight: 500; 133 | line-height: 1.2em; 134 | margin: 0 0 0.8em 0; 135 | } 136 | 137 | h3 { 138 | border-top: 1px solid #eee; 139 | color: #666; 140 | font-weight: 500; 141 | margin: 20px 0 10px 0; 142 | padding: 10px 0 0 0; 143 | white-space: nowrap; 144 | } 145 | 146 | html { 147 | /* avoid annoying page width change 148 | when moving from the home page */ 149 | overflow-y: scroll; 150 | } 151 | 152 | img { 153 | border: none; 154 | max-width: 100%; 155 | } 156 | 157 | input[type=radio] { 158 | position: relative; 159 | top: -1px; 160 | } 161 | 162 | p { 163 | color: #444; 164 | font-weight: 300; 165 | line-height: 1.6em; 166 | } 167 | 168 | p#data { 169 | border-top: 1px dotted #666; 170 | font-family: Courier New, monospace; 171 | line-height: 1.3em; 172 | max-height: 1000px; 173 | overflow-y: auto; 174 | padding: 1em 0 0 0; 175 | } 176 | 177 | p.borderBelow { 178 | border-bottom: 1px solid #aaa; 179 | padding: 0 0 20px 0; 180 | } 181 | 182 | section p:last-of-type { 183 | margin: 0; 184 | } 185 | 186 | section { 187 | border-bottom: 1px solid #eee; 188 | margin: 0 0 30px 0; 189 | padding: 0 0 20px 0; 190 | } 191 | 192 | section:last-of-type { 193 | border-bottom: none; 194 | padding: 0 0 1em 0; 195 | } 196 | 197 | select { 198 | margin: 0 1em 1em 0; 199 | position: relative; 200 | top: -1px; 201 | } 202 | 203 | h1 span { 204 | white-space: nowrap; 205 | } 206 | 207 | strong { 208 | font-weight: 500; 209 | } 210 | 211 | textarea { 212 | font-family: 'Roboto', sans-serif; 213 | } 214 | 215 | video { 216 | background: #222; 217 | margin: 0 0 20px 0; 218 | width: 100%; 219 | } 220 | 221 | @media screen and (max-width: 650px) { 222 | h1 { 223 | font-size: 24px; 224 | } 225 | } 226 | 227 | @media screen and (max-width: 550px) { 228 | button:active { 229 | background-color: darkRed; 230 | } 231 | h1 { 232 | font-size: 22px; 233 | } 234 | } 235 | 236 | @media screen and (max-width: 450px) { 237 | h1 { 238 | font-size: 20px; 239 | } 240 | } 241 | 242 | 243 | -------------------------------------------------------------------------------- /resources/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* GitHub websafe fonts */ 3 | --sans-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 4 | --mono-font: ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; 5 | 6 | --base-font-size: 1rem; 7 | --medium-font-size: .9rem; 8 | --mono-font-size: .8125rem; 9 | --small-font-size: .8rem; 10 | --tiny-font-size: .625rem; 11 | --line-height: 1.5; 12 | --content-width: 50rem; 13 | 14 | --logo-red: #D04949; 15 | --logo-green: #00C25E; 16 | --logo-blue: #3D67FF; 17 | --logo-yellow: #F5B62E; 18 | --logo-white: #E0E0E0; 19 | --logo-gray: #727C92; 20 | --logo-black: #121322; 21 | 22 | --bg: #F0F0F0; 23 | --accent-bg: #E8E8E8; 24 | --text: #212121; 25 | --text-light: #585858; 26 | --border: #D8DAE1; 27 | --accent: var(--logo-blue); 28 | --accent-light: var(--logo-green); 29 | --code: var(--text); 30 | --preformatted: var(--text); 31 | --disabled: #808080; 32 | } 33 | 34 | @media screen and (prefers-color-scheme: dark) { 35 | :root { 36 | --bg: #212121; 37 | --accent-bg: #2B2B2B; 38 | --text: #DCDCDC; 39 | --text-light: #ABABAB; 40 | --border: #666; 41 | --accent: var(--logo-blue); 42 | --accent-light: var(--logo-green); 43 | --code: var(--text); 44 | --preformatted: var(--text); 45 | --disabled: #808080; 46 | } 47 | } 48 | 49 | html { 50 | color-scheme: light dark; 51 | } 52 | 53 | body { 54 | color: var(--text); 55 | background: var(--bg); 56 | font-family: var(--sans-font); 57 | font-size: var(--base-font-size); 58 | line-height: var(--line-height); 59 | padding: 0; 60 | display: flex; 61 | min-height: 100vh; 62 | flex-direction: column; 63 | width: var(--content-width); 64 | margin: 0 auto 0 calc((100vw - var(--content-width)) / 2) ; 65 | } 66 | 67 | header { /* a>h1>svg | h2 | | img */ 68 | background: var(--accent-bg); 69 | border-bottom: 1px solid var(--border); 70 | margin: 0; 71 | padding: .5rem; 72 | display: flex; 73 | justify-content: space-between; 74 | flex-direction: row; 75 | align-items: center; 76 | width: var(--comtent-width); 77 | } 78 | header img { width: 2rem; height: 2rem; } 79 | 80 | h1, h2, h3 {font-weight: bold; margin: 0;} 81 | h1 {font-size: calc(var(--base-font-size) * 1.25);} 82 | h2 {font-size: calc(var(--base-font-size) * 1.125);} 83 | h3 {font-size: calc(var(--base-font-size) * 1.0);} 84 | 85 | a, a:visited {color: inherit; text-decoration: none;} 86 | a:focus, input:focus {outline: none;} 87 | a:hover { 88 | color: var(--accent-light); 89 | text-decoration: underline var(--accent) .2rem; 90 | } 91 | 92 | svg.lucide { 93 | stroke: currentColor; 94 | width: 1rem; 95 | height: 1rem; 96 | } 97 | header a h1 svg.lucide { 98 | width: 1.25rem; 99 | height: 1.25rem; 100 | stroke-width: 3; 101 | } 102 | 103 | main { 104 | margin: 0; 105 | padding: .5rem; 106 | width: var(--comtent-width); 107 | } 108 | main h2, main h3 {margin-bottom: .5rem;} 109 | 110 | main section { 111 | display: flex; 112 | justify-content: space-between; 113 | flex-direction: row; 114 | flex-wrap: wrap; 115 | align-items: center; 116 | width: var(--comtent-width); 117 | } 118 | 119 | div.video, div.audio { 120 | display: inline-block; 121 | width: calc(var(--content-width) * .45);; 122 | background-color: var(--accent-bg); 123 | padding: .5rem; 124 | margin-top: .5rem; 125 | margin-bottom: .5rem; 126 | } 127 | 128 | div.audio { 129 | height: 10rem; 130 | } 131 | 132 | div.audio div { 133 | margin-top: .5rem; 134 | border-radius: 5px; 135 | background-color: var(--logo-gray); 136 | height: 4rem; 137 | display: flex; 138 | align-items: center; 139 | justify-content: center; 140 | } 141 | 142 | video, iframe { 143 | border-radius: 5px; 144 | border: 0; 145 | width: 100%; 146 | height: auto; 147 | aspect-ratio: 16 / 9; 148 | margin: auto; 149 | } 150 | 151 | audio { 152 | width: calc(100% - .5rem); 153 | height: 3rem; 154 | } 155 | 156 | button { 157 | vertical-align: middle; 158 | text-align: center; 159 | font-size: var(--medium-font-size); 160 | border: 1px solid var(--border); 161 | outline: none; 162 | background-color: var(--bg); 163 | color: var(--text); 164 | width: 100%; 165 | height: 3rem; 166 | border-radius: 5px; 167 | } 168 | 169 | button:hover { 170 | font-weight: bold; 171 | background-color: var(--logo-blue); 172 | color: var(--logo-white); 173 | } 174 | 175 | button[disabled], button[disabled]:hover { 176 | font-weight: normal; 177 | font-style: italic; 178 | background-color: var(--accent-bg); 179 | color: var(--border); 180 | } 181 | 182 | footer { /* a a */ 183 | background: var(--accent-bg); 184 | border-top: 1px solid var(--border); 185 | margin-top: auto; 186 | padding: .5rem; 187 | display: flex; 188 | justify-content: space-between; 189 | flex-direction: row; 190 | width: var(--comtent-width); 191 | font-weight: bold; 192 | font-size: var(--small-font-size); 193 | color: var(--text-light); 194 | } 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AudioPick 2 | A **Chrome Manifest V3 Extension** to pick a preferred audio output device for **HTML5** `