mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
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:
@ -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();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user