fix(audio-compressor): real-time behavior and duplicated audio bug (#3786)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Benjas333
2025-09-05 14:15:47 -06:00
committed by GitHub
parent 87144e03c2
commit 8dbe151ddd

View File

@ -1,26 +1,133 @@
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
import { t } from '@/i18n'; 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<MediaElementAudioSourceNode, DynamicsCompressorNode> =
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<Compressor>) => {
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({ export default createPlugin({
name: () => t('plugins.audio-compressor.name'), name: () => t('plugins.audio-compressor.name'),
description: () => t('plugins.audio-compressor.description'), description: () => t('plugins.audio-compressor.description'),
renderer() { renderer: {
document.addEventListener( onPlayerApiReady(playerApi) {
'ytmd:audio-can-play', ensureAudioContextLoad(playerApi);
({ detail: { audioSource, audioContext } }) => { },
const compressor = audioContext.createDynamicsCompressor();
compressor.threshold.value = -50; start() {
compressor.ratio.value = 12; document.addEventListener('ytmd:audio-can-play', audioCanPlayHandler, {
compressor.knee.value = 40; passive: true,
compressor.attack.value = 0; });
compressor.release.value = 0.25; storage.connectToCompressor(
storage.lastSource,
storage.lastContext,
storage.lastCompressor,
);
},
audioSource.connect(compressor); stop() {
compressor.connect(audioContext.destination); document.removeEventListener('ytmd:audio-can-play', audioCanPlayHandler);
}, storage.disconnectCompressor();
{ once: true, passive: true }, },
);
}, },
}); });