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,14 +1,18 @@
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
import { t } from '@/i18n'; import { t } from '@/i18n';
import { type YoutubePlayer } from '@/types/youtube-player';
export default createPlugin({ const lazySafeTry = (...fns: (() => void)[]) => {
name: () => t('plugins.audio-compressor.name'), for (const fn of fns) {
description: () => t('plugins.audio-compressor.description'), try {
fn();
} catch {}
}
};
renderer() { const createCompressorNode = (
document.addEventListener( audioContext: AudioContext,
'ytmd:audio-can-play', ): DynamicsCompressorNode => {
({ detail: { audioSource, audioContext } }) => {
const compressor = audioContext.createDynamicsCompressor(); const compressor = audioContext.createDynamicsCompressor();
compressor.threshold.value = -50; compressor.threshold.value = -50;
@ -17,10 +21,113 @@ export default createPlugin({
compressor.attack.value = 0; compressor.attack.value = 0;
compressor.release.value = 0.25; compressor.release.value = 0.25;
audioSource.connect(compressor); 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); 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: {
onPlayerApiReady(playerApi) {
ensureAudioContextLoad(playerApi);
}, },
{ once: true, passive: true },
start() {
document.addEventListener('ytmd:audio-can-play', audioCanPlayHandler, {
passive: true,
});
storage.connectToCompressor(
storage.lastSource,
storage.lastContext,
storage.lastCompressor,
); );
}, },
stop() {
document.removeEventListener('ytmd:audio-can-play', audioCanPlayHandler);
storage.disconnectCompressor();
},
},
}); });