Skip to content

Commit aae5239

Browse files
Benjas333ArjixWasTakenJellyBrick
authored
feat(plugin): Custom output device plugin (#3789)
Co-authored-by: Angelos Bouklis <[email protected]> Co-authored-by: JellyBrick <[email protected]>
1 parent afacec9 commit aae5239

File tree

4 files changed

+150
-0
lines changed

4 files changed

+150
-0
lines changed

src/i18n/resources/en.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,19 @@
421421
}
422422
}
423423
},
424+
"custom-output-device": {
425+
"description": "Configure a custom output media device for songs",
426+
"menu": {
427+
"device-selector": "Select Device"
428+
},
429+
"name": "Custom Output Device",
430+
"prompt": {
431+
"device-selector": {
432+
"label": "Choose the output media device to be used",
433+
"title": "Select Output Device"
434+
}
435+
}
436+
},
424437
"disable-autoplay": {
425438
"description": "Makes song start in \"paused\" mode",
426439
"menu": {

src/i18n/resources/es.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,19 @@
421421
}
422422
}
423423
},
424+
"custom-output-device": {
425+
"description": "Configura un dispositivo de salida de audio personalizado para las canciones",
426+
"menu": {
427+
"device-selector": "Seleccionar un dispositivo"
428+
},
429+
"name": "Dispositivo de audio personalizado",
430+
"prompt": {
431+
"device-selector": {
432+
"label": "Escoge el dispositivo de salida de audio que se va a usar",
433+
"title": "Seleccionar un dispositivo de audio"
434+
}
435+
}
436+
},
424437
"disable-autoplay": {
425438
"description": "Hace que la canción comience en modo \"pausado\"",
426439
"menu": {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import prompt from 'custom-electron-prompt';
2+
3+
import { t } from '@/i18n';
4+
import promptOptions from '@/providers/prompt-options';
5+
import { createPlugin } from '@/utils';
6+
import { renderer } from './renderer';
7+
8+
export interface CustomOutputPluginConfig {
9+
enabled: boolean;
10+
output: string;
11+
devices: Record<string, string>;
12+
}
13+
14+
export default createPlugin({
15+
name: () => t('plugins.custom-output-device.name'),
16+
description: () => t('plugins.custom-output-device.description'),
17+
restartNeeded: true,
18+
config: {
19+
enabled: false,
20+
output: 'default',
21+
devices: {},
22+
} as CustomOutputPluginConfig,
23+
menu: ({ setConfig, getConfig, window }) => {
24+
const promptDeviceSelector = async () => {
25+
const options = await getConfig();
26+
27+
const response = await prompt(
28+
{
29+
title: t('plugins.custom-output-device.prompt.device-selector.title'),
30+
label: t('plugins.custom-output-device.prompt.device-selector.label'),
31+
value: options.output || 'default',
32+
type: 'select',
33+
selectOptions: options.devices,
34+
width: 500,
35+
...promptOptions(),
36+
},
37+
window,
38+
).catch(console.error);
39+
40+
if (!response) return;
41+
options.output = response;
42+
setConfig(options);
43+
};
44+
45+
return [
46+
{
47+
label: t('plugins.custom-output-device.menu.device-selector'),
48+
click: promptDeviceSelector,
49+
},
50+
];
51+
},
52+
53+
renderer,
54+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { createRenderer } from '@/utils';
2+
3+
import type { YoutubePlayer } from '@/types/youtube-player';
4+
import type { RendererContext } from '@/types/contexts';
5+
import type { CustomOutputPluginConfig } from './index';
6+
7+
const updateDeviceList = async (
8+
context: RendererContext<CustomOutputPluginConfig>,
9+
) => {
10+
const newDevices: Record<string, string> = {};
11+
const devices = await navigator.mediaDevices
12+
.enumerateDevices()
13+
.then((devices) =>
14+
devices.filter((device) => device.kind === 'audiooutput'),
15+
);
16+
for (const device of devices) {
17+
newDevices[device.deviceId] = device.label;
18+
}
19+
const options = await context.getConfig();
20+
options.devices = newDevices;
21+
context.setConfig(options);
22+
};
23+
24+
const updateSinkId = async (audioContext?: AudioContext, sinkId?: string) => {
25+
if (!audioContext || !sinkId) return;
26+
if (!('setSinkId' in audioContext)) return;
27+
if (typeof audioContext.setSinkId !== 'function') return;
28+
29+
await audioContext.setSinkId(sinkId);
30+
};
31+
32+
export const renderer = createRenderer<
33+
{
34+
options?: CustomOutputPluginConfig;
35+
audioContext?: AudioContext;
36+
audioCanPlayHandler: (event: CustomEvent<Compressor>) => Promise<void>;
37+
},
38+
CustomOutputPluginConfig
39+
>({
40+
async audioCanPlayHandler({ detail: { audioContext } }) {
41+
this.audioContext = audioContext;
42+
await updateSinkId(audioContext, this.options!.output);
43+
},
44+
45+
async onPlayerApiReady(_: YoutubePlayer, context) {
46+
this.options = await context.getConfig();
47+
await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
48+
navigator.mediaDevices.ondevicechange = async () =>
49+
await updateDeviceList(context);
50+
51+
document.addEventListener('ytmd:audio-can-play', this.audioCanPlayHandler, {
52+
once: true,
53+
passive: true,
54+
});
55+
await updateDeviceList(context);
56+
},
57+
58+
stop() {
59+
document.removeEventListener(
60+
'ytmd:audio-can-play',
61+
this.audioCanPlayHandler,
62+
);
63+
navigator.mediaDevices.ondevicechange = null;
64+
},
65+
66+
async onConfigChange(config) {
67+
this.options = config;
68+
await updateSinkId(this.audioContext, config.output);
69+
},
70+
});

0 commit comments

Comments
 (0)