Skip to content

Commit 8dbe151

Browse files
Benjas333Copilot
andauthored
fix(audio-compressor): real-time behavior and duplicated audio bug (#3786)
Co-authored-by: Copilot <[email protected]>
1 parent 87144e0 commit 8dbe151

File tree

1 file changed

+124
-17
lines changed

1 file changed

+124
-17
lines changed

src/plugins/audio-compressor.ts

Lines changed: 124 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,133 @@
11
import { createPlugin } from '@/utils';
22
import { t } from '@/i18n';
3+
import { type YoutubePlayer } from '@/types/youtube-player';
4+
5+
const lazySafeTry = (...fns: (() => void)[]) => {
6+
for (const fn of fns) {
7+
try {
8+
fn();
9+
} catch {}
10+
}
11+
};
12+
13+
const createCompressorNode = (
14+
audioContext: AudioContext,
15+
): DynamicsCompressorNode => {
16+
const compressor = audioContext.createDynamicsCompressor();
17+
18+
compressor.threshold.value = -50;
19+
compressor.ratio.value = 12;
20+
compressor.knee.value = 40;
21+
compressor.attack.value = 0;
22+
compressor.release.value = 0.25;
23+
24+
return compressor;
25+
};
26+
27+
class Storage {
28+
lastSource: MediaElementAudioSourceNode | null = null;
29+
lastContext: AudioContext | null = null;
30+
lastCompressor: DynamicsCompressorNode | null = null;
31+
32+
connected: WeakMap<MediaElementAudioSourceNode, DynamicsCompressorNode> =
33+
new WeakMap();
34+
35+
connectToCompressor = (
36+
source: MediaElementAudioSourceNode | null = null,
37+
audioContext: AudioContext | null = null,
38+
compressor: DynamicsCompressorNode | null = null,
39+
): boolean => {
40+
if (!(source && audioContext && compressor)) return false;
41+
42+
const current = this.connected.get(source);
43+
if (current === compressor) return false;
44+
45+
this.lastSource = source;
46+
this.lastContext = audioContext;
47+
this.lastCompressor = compressor;
48+
49+
if (current) {
50+
lazySafeTry(
51+
() => source.disconnect(current),
52+
() => current.disconnect(audioContext.destination),
53+
);
54+
} else {
55+
lazySafeTry(() => source.disconnect(audioContext.destination));
56+
}
57+
58+
try {
59+
source.connect(compressor);
60+
compressor.connect(audioContext.destination);
61+
this.connected.set(source, compressor);
62+
return true;
63+
} catch (error) {
64+
console.error('connectToCompressor failed', error);
65+
return false;
66+
}
67+
};
68+
69+
disconnectCompressor = (): boolean => {
70+
const source = this.lastSource;
71+
const audioContext = this.lastContext;
72+
if (!(source && audioContext)) return false;
73+
const current = this.connected.get(source);
74+
if (!current) return false;
75+
76+
lazySafeTry(
77+
() => source.connect(audioContext.destination),
78+
() => source.disconnect(current),
79+
() => current.disconnect(audioContext.destination),
80+
);
81+
this.connected.delete(source);
82+
return true;
83+
};
84+
}
85+
86+
const storage = new Storage();
87+
88+
const audioCanPlayHandler = ({
89+
detail: { audioSource, audioContext },
90+
}: CustomEvent<Compressor>) => {
91+
storage.connectToCompressor(
92+
audioSource,
93+
audioContext,
94+
createCompressorNode(audioContext),
95+
);
96+
};
97+
98+
const ensureAudioContextLoad = (playerApi: YoutubePlayer) => {
99+
if (playerApi.getPlayerState() !== 1 || storage.lastContext) return;
100+
101+
playerApi.loadVideoById(
102+
playerApi.getPlayerResponse().videoDetails.videoId,
103+
playerApi.getCurrentTime(),
104+
playerApi.getUserPlaybackQualityPreference(),
105+
);
106+
};
3107

4108
export default createPlugin({
5109
name: () => t('plugins.audio-compressor.name'),
6110
description: () => t('plugins.audio-compressor.description'),
7111

8-
renderer() {
9-
document.addEventListener(
10-
'ytmd:audio-can-play',
11-
({ detail: { audioSource, audioContext } }) => {
12-
const compressor = audioContext.createDynamicsCompressor();
13-
14-
compressor.threshold.value = -50;
15-
compressor.ratio.value = 12;
16-
compressor.knee.value = 40;
17-
compressor.attack.value = 0;
18-
compressor.release.value = 0.25;
19-
20-
audioSource.connect(compressor);
21-
compressor.connect(audioContext.destination);
22-
},
23-
{ once: true, passive: true },
24-
);
112+
renderer: {
113+
onPlayerApiReady(playerApi) {
114+
ensureAudioContextLoad(playerApi);
115+
},
116+
117+
start() {
118+
document.addEventListener('ytmd:audio-can-play', audioCanPlayHandler, {
119+
passive: true,
120+
});
121+
storage.connectToCompressor(
122+
storage.lastSource,
123+
storage.lastContext,
124+
storage.lastCompressor,
125+
);
126+
},
127+
128+
stop() {
129+
document.removeEventListener('ytmd:audio-can-play', audioCanPlayHandler);
130+
storage.disconnectCompressor();
131+
},
25132
},
26133
});

0 commit comments

Comments
 (0)