From 8dbe151ddd930ce8f5dd0dd764b9a2413e80d9d6 Mon Sep 17 00:00:00 2001 From: Benjas333 <67520048+Benjas333@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:15:47 -0600 Subject: [PATCH] fix(audio-compressor): real-time behavior and duplicated audio bug (#3786) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/plugins/audio-compressor.ts | 137 ++++++++++++++++++++++++++++---- 1 file changed, 122 insertions(+), 15 deletions(-) diff --git a/src/plugins/audio-compressor.ts b/src/plugins/audio-compressor.ts index 1be10e89..ee2f4b46 100644 --- a/src/plugins/audio-compressor.ts +++ b/src/plugins/audio-compressor.ts @@ -1,26 +1,133 @@ import { createPlugin } from '@/utils'; import { t } from '@/i18n'; +import { type YoutubePlayer } from '@/types/youtube-player'; + +const lazySafeTry = (...fns: (() => void)[]) => { + for (const fn of fns) { + try { + fn(); + } catch {} + } +}; + +const createCompressorNode = ( + audioContext: AudioContext, +): DynamicsCompressorNode => { + const compressor = audioContext.createDynamicsCompressor(); + + compressor.threshold.value = -50; + compressor.ratio.value = 12; + compressor.knee.value = 40; + compressor.attack.value = 0; + compressor.release.value = 0.25; + + return compressor; +}; + +class Storage { + lastSource: MediaElementAudioSourceNode | null = null; + lastContext: AudioContext | null = null; + lastCompressor: DynamicsCompressorNode | null = null; + + connected: WeakMap = + new WeakMap(); + + connectToCompressor = ( + source: MediaElementAudioSourceNode | null = null, + audioContext: AudioContext | null = null, + compressor: DynamicsCompressorNode | null = null, + ): boolean => { + if (!(source && audioContext && compressor)) return false; + + const current = this.connected.get(source); + if (current === compressor) return false; + + this.lastSource = source; + this.lastContext = audioContext; + this.lastCompressor = compressor; + + if (current) { + lazySafeTry( + () => source.disconnect(current), + () => current.disconnect(audioContext.destination), + ); + } else { + lazySafeTry(() => source.disconnect(audioContext.destination)); + } + + try { + source.connect(compressor); + compressor.connect(audioContext.destination); + this.connected.set(source, compressor); + return true; + } catch (error) { + console.error('connectToCompressor failed', error); + return false; + } + }; + + disconnectCompressor = (): boolean => { + const source = this.lastSource; + const audioContext = this.lastContext; + if (!(source && audioContext)) return false; + const current = this.connected.get(source); + if (!current) return false; + + lazySafeTry( + () => source.connect(audioContext.destination), + () => source.disconnect(current), + () => current.disconnect(audioContext.destination), + ); + this.connected.delete(source); + return true; + }; +} + +const storage = new Storage(); + +const audioCanPlayHandler = ({ + detail: { audioSource, audioContext }, +}: CustomEvent) => { + storage.connectToCompressor( + audioSource, + audioContext, + createCompressorNode(audioContext), + ); +}; + +const ensureAudioContextLoad = (playerApi: YoutubePlayer) => { + if (playerApi.getPlayerState() !== 1 || storage.lastContext) return; + + playerApi.loadVideoById( + playerApi.getPlayerResponse().videoDetails.videoId, + playerApi.getCurrentTime(), + playerApi.getUserPlaybackQualityPreference(), + ); +}; export default createPlugin({ name: () => t('plugins.audio-compressor.name'), description: () => t('plugins.audio-compressor.description'), - renderer() { - document.addEventListener( - 'ytmd:audio-can-play', - ({ detail: { audioSource, audioContext } }) => { - const compressor = audioContext.createDynamicsCompressor(); + renderer: { + onPlayerApiReady(playerApi) { + ensureAudioContextLoad(playerApi); + }, - compressor.threshold.value = -50; - compressor.ratio.value = 12; - compressor.knee.value = 40; - compressor.attack.value = 0; - compressor.release.value = 0.25; + start() { + document.addEventListener('ytmd:audio-can-play', audioCanPlayHandler, { + passive: true, + }); + storage.connectToCompressor( + storage.lastSource, + storage.lastContext, + storage.lastCompressor, + ); + }, - audioSource.connect(compressor); - compressor.connect(audioContext.destination); - }, - { once: true, passive: true }, - ); + stop() { + document.removeEventListener('ytmd:audio-can-play', audioCanPlayHandler); + storage.disconnectCompressor(); + }, }, });