import type { RendererContext } from '@/types/contexts'; import type { SkipSilencesPluginConfig } from './index'; let config: SkipSilencesPluginConfig; let isSilent = false; let hasAudioStarted = false; const smoothing = 0.1; const threshold = -100; // DB (-100 = absolute silence, 0 = loudest) const interval = 2; // Ms const history = 10; const speakingHistory = Array.from({ length: history }).fill(0) as number[]; let playOrSeekHandler: (() => void) | undefined; const getMaxVolume = (analyser: AnalyserNode, fftBins: Float32Array) => { let maxVolume = Number.NEGATIVE_INFINITY; analyser.getFloatFrequencyData(fftBins); for (let i = 4, ii = fftBins.length; i < ii; i++) { if (fftBins[i] > maxVolume && fftBins[i] < 0) { maxVolume = fftBins[i]; } } return maxVolume; }; const audioCanPlayListener = (e: CustomEvent) => { const video = document.querySelector('video'); const { audioContext } = e.detail; const sourceNode = e.detail.audioSource; // Use an audio analyser similar to Hark // https://github.com/otalk/hark/blob/master/hark.bundle.js const analyser = audioContext.createAnalyser(); analyser.fftSize = 512; analyser.smoothingTimeConstant = smoothing; const fftBins = new Float32Array(analyser.frequencyBinCount); sourceNode.connect(analyser); const looper = () => { setTimeout(() => { const currentVolume = getMaxVolume(analyser, fftBins); let history = 0; if (currentVolume > threshold && isSilent) { // Trigger quickly, short history for ( let i = speakingHistory.length - 3; i < speakingHistory.length; i++ ) { history += speakingHistory[i]; } if (history >= 2) { // Not silent isSilent = false; hasAudioStarted = true; } } else if (currentVolume < threshold && !isSilent) { for (const element of speakingHistory) { history += element; } if ( history == 0 && // Silent !( video && (video.paused || video.seeking || video.ended || video.muted || video.volume === 0) ) ) { isSilent = true; skipSilence(); } } speakingHistory.shift(); speakingHistory.push(Number(currentVolume > threshold)); looper(); }, interval); }; looper(); const skipSilence = () => { if (config.onlySkipBeginning && hasAudioStarted) { return; } if (isSilent && video && !video.paused) { video.currentTime += 0.2; // In s } }; playOrSeekHandler = () => { hasAudioStarted = false; skipSilence(); }; video?.addEventListener('play', playOrSeekHandler); video?.addEventListener('seeked', playOrSeekHandler); }; export const onRendererLoad = async ({ getConfig, }: RendererContext) => { config = await getConfig(); document.addEventListener('ytmd:audio-can-play', audioCanPlayListener, { passive: true, }); }; export const onRendererUnload = () => { document.removeEventListener('ytmd:audio-can-play', audioCanPlayListener); if (playOrSeekHandler) { const video = document.querySelector('video'); video?.removeEventListener('play', playOrSeekHandler); video?.removeEventListener('seeked', playOrSeekHandler); } };