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 { 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({
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();
},
},
});