Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
ish- committed Jan 1, 2022
0 parents commit 098f28b
Show file tree
Hide file tree
Showing 12 changed files with 1,261 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.DS_Store
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions README.md
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.


Binary file added extension/Icon128-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extension/Icon128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions extension/background.html
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>
63 changes: 63 additions & 0 deletions extension/background.js
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') ? '' : 'Ω',
});
}
194 changes: 194 additions & 0 deletions extension/content.js
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);
}
46 changes: 46 additions & 0 deletions extension/manifest.json
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"
]
}
64 changes: 64 additions & 0 deletions extension/popup.html
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>
Loading

0 comments on commit 098f28b

Please sign in to comment.