mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31: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 { 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 },
|
},
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user