mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-16 12:42:06 +00:00
feat(plugin): Custom output device plugin (#3789)
Co-authored-by: Angelos Bouklis <me@arjix.dev> Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
@ -421,6 +421,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"custom-output-device": {
|
||||||
|
"description": "Configure a custom output media device for songs",
|
||||||
|
"menu": {
|
||||||
|
"device-selector": "Select Device"
|
||||||
|
},
|
||||||
|
"name": "Custom Output Device",
|
||||||
|
"prompt": {
|
||||||
|
"device-selector": {
|
||||||
|
"label": "Choose the output media device to be used",
|
||||||
|
"title": "Select Output Device"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"disable-autoplay": {
|
"disable-autoplay": {
|
||||||
"description": "Makes song start in \"paused\" mode",
|
"description": "Makes song start in \"paused\" mode",
|
||||||
"menu": {
|
"menu": {
|
||||||
|
|||||||
@ -421,6 +421,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"custom-output-device": {
|
||||||
|
"description": "Configura un dispositivo de salida de audio personalizado para las canciones",
|
||||||
|
"menu": {
|
||||||
|
"device-selector": "Seleccionar un dispositivo"
|
||||||
|
},
|
||||||
|
"name": "Dispositivo de audio personalizado",
|
||||||
|
"prompt": {
|
||||||
|
"device-selector": {
|
||||||
|
"label": "Escoge el dispositivo de salida de audio que se va a usar",
|
||||||
|
"title": "Seleccionar un dispositivo de audio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"disable-autoplay": {
|
"disable-autoplay": {
|
||||||
"description": "Hace que la canción comience en modo \"pausado\"",
|
"description": "Hace que la canción comience en modo \"pausado\"",
|
||||||
"menu": {
|
"menu": {
|
||||||
|
|||||||
54
src/plugins/custom-output-device/index.ts
Normal file
54
src/plugins/custom-output-device/index.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
import { createPlugin } from '@/utils';
|
||||||
|
import { renderer } from './renderer';
|
||||||
|
|
||||||
|
export interface CustomOutputPluginConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
output: string;
|
||||||
|
devices: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createPlugin({
|
||||||
|
name: () => t('plugins.custom-output-device.name'),
|
||||||
|
description: () => t('plugins.custom-output-device.description'),
|
||||||
|
restartNeeded: true,
|
||||||
|
config: {
|
||||||
|
enabled: false,
|
||||||
|
output: 'default',
|
||||||
|
devices: {},
|
||||||
|
} as CustomOutputPluginConfig,
|
||||||
|
menu: ({ setConfig, getConfig, window }) => {
|
||||||
|
const promptDeviceSelector = async () => {
|
||||||
|
const options = await getConfig();
|
||||||
|
|
||||||
|
const response = await prompt(
|
||||||
|
{
|
||||||
|
title: t('plugins.custom-output-device.prompt.device-selector.title'),
|
||||||
|
label: t('plugins.custom-output-device.prompt.device-selector.label'),
|
||||||
|
value: options.output || 'default',
|
||||||
|
type: 'select',
|
||||||
|
selectOptions: options.devices,
|
||||||
|
width: 500,
|
||||||
|
...promptOptions(),
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
).catch(console.error);
|
||||||
|
|
||||||
|
if (!response) return;
|
||||||
|
options.output = response;
|
||||||
|
setConfig(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('plugins.custom-output-device.menu.device-selector'),
|
||||||
|
click: promptDeviceSelector,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderer,
|
||||||
|
});
|
||||||
70
src/plugins/custom-output-device/renderer.ts
Normal file
70
src/plugins/custom-output-device/renderer.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { createRenderer } from '@/utils';
|
||||||
|
|
||||||
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
import type { RendererContext } from '@/types/contexts';
|
||||||
|
import type { CustomOutputPluginConfig } from './index';
|
||||||
|
|
||||||
|
const updateDeviceList = async (
|
||||||
|
context: RendererContext<CustomOutputPluginConfig>,
|
||||||
|
) => {
|
||||||
|
const newDevices: Record<string, string> = {};
|
||||||
|
const devices = await navigator.mediaDevices
|
||||||
|
.enumerateDevices()
|
||||||
|
.then((devices) =>
|
||||||
|
devices.filter((device) => device.kind === 'audiooutput'),
|
||||||
|
);
|
||||||
|
for (const device of devices) {
|
||||||
|
newDevices[device.deviceId] = device.label;
|
||||||
|
}
|
||||||
|
const options = await context.getConfig();
|
||||||
|
options.devices = newDevices;
|
||||||
|
context.setConfig(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSinkId = async (audioContext?: AudioContext, sinkId?: string) => {
|
||||||
|
if (!audioContext || !sinkId) return;
|
||||||
|
if (!('setSinkId' in audioContext)) return;
|
||||||
|
if (typeof audioContext.setSinkId !== 'function') return;
|
||||||
|
|
||||||
|
await audioContext.setSinkId(sinkId);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderer = createRenderer<
|
||||||
|
{
|
||||||
|
options?: CustomOutputPluginConfig;
|
||||||
|
audioContext?: AudioContext;
|
||||||
|
audioCanPlayHandler: (event: CustomEvent<Compressor>) => Promise<void>;
|
||||||
|
},
|
||||||
|
CustomOutputPluginConfig
|
||||||
|
>({
|
||||||
|
async audioCanPlayHandler({ detail: { audioContext } }) {
|
||||||
|
this.audioContext = audioContext;
|
||||||
|
await updateSinkId(audioContext, this.options!.output);
|
||||||
|
},
|
||||||
|
|
||||||
|
async onPlayerApiReady(_: YoutubePlayer, context) {
|
||||||
|
this.options = await context.getConfig();
|
||||||
|
await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
||||||
|
navigator.mediaDevices.ondevicechange = async () =>
|
||||||
|
await updateDeviceList(context);
|
||||||
|
|
||||||
|
document.addEventListener('ytmd:audio-can-play', this.audioCanPlayHandler, {
|
||||||
|
once: true,
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
await updateDeviceList(context);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
document.removeEventListener(
|
||||||
|
'ytmd:audio-can-play',
|
||||||
|
this.audioCanPlayHandler,
|
||||||
|
);
|
||||||
|
navigator.mediaDevices.ondevicechange = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async onConfigChange(config) {
|
||||||
|
this.options = config;
|
||||||
|
await updateSinkId(this.audioContext, config.output);
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user