-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 098f28b
Showing
12 changed files
with
1,261 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.DS_Store |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# AuRo | ||
A *Chrome Extension* to pick an audio output device for HTML5 audio and video elements | ||
|
||
## How it works | ||
The extension patches HTML5 audio and video .play() method and manipulates the `sinkId` in order to switch to the desired audio output device. | ||
It also does not and will never work with AudioContext cause setSinkId() is not implemented for it. | ||
To not overhead every page with script injection it require to pause/play media on initialization. | ||
|
||
**Note** that the API requires a successful call to `getUserMedia()` for every site with audio sinks that | ||
need to be manipulated which - as a result - creates entries under `contentSettings['microphone']`, i. e. | ||
it allows those sites to access your microphone. | ||
|
||
|
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head><title>AuRo - audio output device router</title></head> | ||
<script src="background.js" type="module"></script> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { | ||
Storage, | ||
log, | ||
} from './utils.js'; | ||
|
||
var extensionId = chrome.runtime.id; | ||
|
||
chrome.contentSettings['microphone'].set({'primaryPattern':'*://' + extensionId + '/*','setting':'allow'}); | ||
|
||
chrome.runtime.onMessage.addListener((msg, sender) => { | ||
const fn = msgs[msg.name]; | ||
if (fn) | ||
fn(msg, sender); | ||
}); | ||
|
||
const msgs = { | ||
async ['content:init'] ({ frameId }, sender) { | ||
log('content:init', { frameId }, sender); | ||
const { tab } = sender; | ||
const url = new URL(tab.url); | ||
const allKey = 'all'; | ||
const tabKey = `tab_${ tab.id }`; | ||
const hostKey = `host_${ url.host }`; | ||
// all and host is not implemented yet | ||
const res = await Storage.getSome([ allKey, tabKey, hostKey ]); | ||
const hostDeviceId = res[hostKey]; | ||
const tabDeviceId = res[tabKey]; | ||
const allDeviceId = res[allKey]; | ||
|
||
const deviceId = allDeviceId || hostDeviceId || tabDeviceId || null; | ||
const target = allDeviceId ? 'all' : hostDeviceId ? 'host' : tabDeviceId ? 'tab' : null; | ||
|
||
chrome.tabs.sendMessage(tab.id, { | ||
name: 'content:init:resp', | ||
deviceId, | ||
target, | ||
tabId: tab.id, | ||
frameId, | ||
}); | ||
|
||
updatePopupIconText({ tabId: tab.id, deviceId }); | ||
}, | ||
|
||
['updatePopupIconText'] ({ deviceId }, sender) { | ||
updatePopupIconText({ | ||
deviceId, | ||
tabId: sender.tab.id, | ||
}); | ||
}, | ||
|
||
['updatePopupIconTheme'] ({ isDark }) { | ||
chrome.browserAction.setIcon({ | ||
path: isDark ? 'Icon128-white.png' : 'Icon128.png', | ||
}); | ||
} | ||
}; | ||
|
||
function updatePopupIconText ({ tabId, deviceId }) { | ||
chrome.browserAction.setBadgeText({ | ||
tabId, | ||
text: (!deviceId || deviceId === 'default') ? '' : 'Ω', | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
let inited = false; | ||
let currentDeviceId; | ||
const frameId = Date.now(); | ||
function init (deviceId) { | ||
currentDeviceId = deviceId; | ||
if (!inited) { | ||
inited = true; | ||
inject(AuRoPatchContent); | ||
} | ||
inject(AuRoSetOutputDevice(deviceId)) | ||
chrome.runtime.sendMessage({ | ||
name: 'updatePopupIconText', | ||
deviceId, | ||
}); | ||
} | ||
|
||
chrome.runtime.sendMessage({ | ||
name: 'content:init', | ||
frameId, | ||
}); | ||
|
||
function filterOutputDevices (devices) { | ||
return devices.filter(({ kind }) => kind === 'audiooutput') | ||
} | ||
|
||
function log (desc, ...args) { | ||
if (!args.length && typeof desc === 'string') | ||
return (...args) => log(desc, ...args); | ||
console.log('AuRo :: ', desc, ...args); | ||
return args[0]; | ||
} | ||
|
||
function listenStorage (tabId) { | ||
chrome.storage.onChanged.addListener(function (changes, namespace) { | ||
for (let [key, { oldValue, newValue }] of Object.entries(changes)) { | ||
if (key === `tab_${ tabId }`) { | ||
init(newValue); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
chrome.runtime.onMessage.addListener( | ||
(msg, sender, send) => { | ||
log('onMessage', { msg, sender }); | ||
|
||
switch (msg.name) { | ||
case 'content:init:resp': | ||
if (msg.frameId !== frameId) | ||
return; | ||
if (msg.deviceId && msg.deviceId !== 'default') { | ||
init(msg.deviceId); | ||
} | ||
listenStorage(msg.tabId); | ||
break; | ||
|
||
case 'popup:getState': | ||
navigator.mediaDevices.getUserMedia({ audio: true }).then(() => { | ||
return navigator.mediaDevices.enumerateDevices().then(devices => { | ||
chrome.runtime.sendMessage({ | ||
id: msg.id, | ||
name: 'popup:getState:resp', | ||
devices: filterOutputDevices(devices), | ||
currentDeviceId, | ||
frameId, | ||
}); | ||
}); | ||
}).catch(err => {}); | ||
break; | ||
|
||
default: | ||
console.error('AuRo :: Unknown msg!!!'); | ||
log('Unknown msg!')(msg); | ||
} | ||
} | ||
); | ||
|
||
function AuRoSetOutputDevice (deviceId) { | ||
return `() => { $AuRo.update('${ deviceId }') }`; | ||
} | ||
|
||
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; | ||
|
||
chrome.runtime.sendMessage({ | ||
name: 'updatePopupIconTheme', | ||
isDark, | ||
}); | ||
|
||
function AuRoPatchContent () { | ||
window.$AuRo = { | ||
els: [], | ||
addEl (el) { | ||
let i = this.els.indexOf(el); | ||
if (~i) | ||
this.els.splice(i, 1); | ||
this.els.unshift(el); | ||
}, | ||
|
||
deviceId: null, | ||
update (deviceId) { | ||
$AuRo.deviceId = deviceId; | ||
l('update', { deviceId }); | ||
if (this.els[0]) { | ||
this.els[0].pause(); | ||
this.els[0].setSinkId(deviceId); | ||
setTimeout(() => this.els[0].play(), 3); | ||
} | ||
} | ||
}; | ||
|
||
const getDeviceId = () => navigator.mediaDevices.getUserMedia({ audio: true }).then(() => { | ||
return navigator.mediaDevices.enumerateDevices().then(devices => { | ||
l({devices}) | ||
const selectedDevice = devices.find(({ deviceId }) => deviceId === $AuRo.deviceId); | ||
if (selectedDevice) | ||
return selectedDevice.deviceId; | ||
return Promise.reject('deviceId ' + $AuRo.deviceId + ' not found'); | ||
}); | ||
}); | ||
|
||
var frameDepth = (function getDepth(w) { | ||
return w.parent === w ? 0 : 1 + getDepth(w.parent); | ||
})(window); | ||
|
||
var _audioPlay = HTMLAudioElement.prototype.play; | ||
HTMLAudioElement.prototype.play = function () { | ||
l('Audio.play()', this); | ||
$AuRo.addEl(this); | ||
getDeviceId().then(deviceId => { | ||
this.setSinkId($AuRo.deviceId).then(arg => { | ||
l('Audio.play().getDeviceId().setSinkId()', { deviceId }); | ||
_audioPlay.call(this); | ||
}); | ||
}).catch(err => { | ||
_audioPlay.call(this); | ||
le(err); | ||
}); | ||
}; | ||
|
||
var _videoPlay = HTMLVideoElement.prototype.play; | ||
HTMLVideoElement.prototype.play = function () { | ||
l('Video.play()', this); | ||
$AuRo.addEl(this); | ||
getDeviceId().then(deviceId => { | ||
this.setSinkId($AuRo.deviceId).then(arg => { | ||
l('Video.play().getDeviceId().setSinkId()', { deviceId }); | ||
_videoPlay.call(this); | ||
}); | ||
}).catch(err => { | ||
_videoPlay.call(this); | ||
le(err); | ||
}); | ||
}; | ||
|
||
var _aPlay = Audio.prototype.play; | ||
Audio.prototype.play = function () { | ||
l('Video.play()', this); | ||
$AuRo.addEl(this); | ||
getDeviceId().then(deviceId => { | ||
this.setSinkId($AuRo.deviceId).then(arg => { | ||
l('Video.play().getDeviceId().setSinkId()', { deviceId }); | ||
_aPlay.call(this); | ||
}); | ||
}).catch(err => { | ||
_aPlay.call(this); | ||
le(err); | ||
}); | ||
}; | ||
|
||
function l (...args) { | ||
console.log('$AuRo (' + frameDepth + '): ', ...args); | ||
} | ||
function le (...args) { | ||
console.error('$AuRo error!') | ||
console.log('$AuRo error (' + frameDepth + '): ', ...args); | ||
} | ||
|
||
l('Monkeypatched'); | ||
} | ||
|
||
function inject(fn) { | ||
const script = document.createElement('script') | ||
script.text = `(${ typeof fn === 'function' ? fn.toString() : fn })();` | ||
document.documentElement.appendChild(script); | ||
document.documentElement.removeChild(script); | ||
} | ||
|
||
function l (...args) { | ||
console.log('$AuRo: ', ...args); | ||
} | ||
|
||
function onError (err) { | ||
l('Error: ', err); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
{ | ||
"manifest_version": 2, | ||
"name": "AuRo - audio output device router", | ||
"short_name": "AuRo", | ||
"description": "", | ||
"author": "", | ||
"homepage_url": "https://dearwhoever.space/", | ||
"version": "0.1", | ||
"version_name": "0.1", | ||
"minimum_chrome_version": "50.0", | ||
"browser_action": { | ||
"default_icon": { | ||
"128": "Icon128.png" | ||
}, | ||
"default_title": "AuRo - audio output device router (popup)", | ||
"default_popup": "popup.html" | ||
}, | ||
"icons": { | ||
"128": "Icon128.png" | ||
}, | ||
"background": { | ||
"page": "background.html", | ||
"persistent": true | ||
}, | ||
"content_scripts": [ | ||
{ | ||
"matches": [ | ||
"https://*/*", | ||
"http://*/*" | ||
], | ||
"js": ["content.js"], | ||
"all_frames" : true | ||
} | ||
], | ||
"permissions": [ | ||
"contentSettings", | ||
"activeTab", | ||
"storage", | ||
"tabs" | ||
], | ||
"web_accessible_resources": [ | ||
"utils.js", | ||
"Icon128.png", | ||
"Icon128-white.png" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head><title>AuRo - audio output device router</title></head> | ||
<style> | ||
* { | ||
padding: 0; | ||
margin: 0; | ||
} | ||
body { | ||
padding: 16px; | ||
width: 400px; | ||
background: transparent; | ||
} | ||
.bg { | ||
position: absolute; | ||
pointer-events: none; | ||
top: 0; left: 0; right: 0; bottom: 0; | ||
background-repeat: no-repeat no-repeat; | ||
opacity: .1; | ||
background-image: url('Icon128.png'); | ||
background-size: contain; | ||
background-position: right; | ||
filter: blur(6px); | ||
z-index: -1; | ||
} | ||
h3 { margin-bottom: 8px } | ||
ul { | ||
margin: 0; | ||
} | ||
li { | ||
padding:0.125em; | ||
list-style: none; | ||
display: flex; | ||
justify-content: start; | ||
align-items: center; | ||
white-space: nowrap; | ||
height: 24px; | ||
} | ||
li.--default label { font-weight: bold } | ||
label { | ||
text-overflow: ellipsis; | ||
white-space: nowrap; | ||
overflow: hidden; | ||
} | ||
input[type="radio"] { | ||
margin-right: 16px; | ||
} | ||
.tip { | ||
margin-top: 8px; | ||
padding: 4px 8px; | ||
border-radius: 5px; | ||
background: #fdeab3; | ||
border: 1px solid #f9cccc; | ||
} | ||
</style> | ||
<script src="popup.js" type="module"></script> | ||
<body> | ||
<div class="bg"></div> | ||
<h3>Pick the device for current tab:</h3> | ||
<ul id="devices"></ul> | ||
<p class="tip">May be it would need to pause/play media or reload page.<br> | ||
It will not work if interactive site uses AudioContext.</p> | ||
</body> | ||
</html> |
Oops, something went wrong.