From 2a49a85f3fefabb2a660ef94a048dd680348e246 Mon Sep 17 00:00:00 2001 From: Viktor Dembizki <65288184+some-git-user@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:50:27 -0400 Subject: [PATCH 01/35] add silence skip feature --- .../ft-shaka-video-player.js | 87 ++++- .../watch-video-info/watch-video-info.js | 5 + .../watch-video-info/watch-video-info.vue | 6 + src/renderer/views/Watch/Watch.js | 8 + src/renderer/views/Watch/Watch.vue | 1 + static/locales/af.yaml | 5 + static/locales/ar.yaml | 5 + static/locales/as.yaml | 6 + static/locales/awa.yaml | 5 + static/locales/az.yaml | 5 + static/locales/be.yaml | 5 + static/locales/bg.yaml | 5 + static/locales/bn.yaml | 5 + static/locales/br.yaml | 5 + static/locales/bs.yaml | 5 + static/locales/ca.yaml | 5 + static/locales/ckb.yaml | 5 + static/locales/cs.yaml | 5 + static/locales/cy.yaml | 5 + static/locales/da.yaml | 5 + static/locales/de-DE.yaml | 5 + static/locales/el.yaml | 5 + static/locales/en-GB.yaml | 5 + static/locales/en-US.yaml | 5 + static/locales/eo.yaml | 5 + static/locales/es-AR.yaml | 5 + static/locales/es-MX.yaml | 5 + static/locales/es.yaml | 5 + static/locales/et.yaml | 5 + static/locales/eu.yaml | 5 + static/locales/fa.yaml | 299 +++++++++--------- static/locales/fi.yaml | 5 + static/locales/fil.yaml | 5 + static/locales/fr-FR.yaml | 5 + static/locales/gl.yaml | 5 + static/locales/gsw.yaml | 5 + static/locales/he.yaml | 5 + static/locales/hi.yaml | 5 + static/locales/hr.yaml | 5 + static/locales/hu.yaml | 5 + static/locales/id.yaml | 5 + static/locales/is.yaml | 5 + static/locales/it.yaml | 5 + static/locales/ja.yaml | 5 + static/locales/ka.yaml | 5 + static/locales/km.yaml | 5 + static/locales/ko.yaml | 5 + static/locales/ku.yaml | 5 + static/locales/la.yaml | 5 + static/locales/lt.yaml | 5 + static/locales/lv.yaml | 5 + static/locales/my.yaml | 5 + static/locales/nb-NO.yaml | 5 + static/locales/ne.yaml | 5 + static/locales/nl.yaml | 5 + static/locales/nn.yaml | 5 + static/locales/or.yaml | 5 + static/locales/pl.yaml | 5 + static/locales/pt-BR.yaml | 5 + static/locales/pt-PT.yaml | 5 + static/locales/pt.yaml | 5 + static/locales/ro.yaml | 5 + static/locales/ru.yaml | 5 + static/locales/sat.yaml | 5 + static/locales/si.yaml | 5 + static/locales/sk.yaml | 5 + static/locales/sl.yaml | 5 + static/locales/sm.yaml | 5 + static/locales/sq.yaml | 5 + static/locales/sr.yaml | 5 + static/locales/sv.yaml | 5 + static/locales/ta.yaml | 5 + static/locales/ti.yaml | 5 + static/locales/tig.yaml | 5 + static/locales/tok.yaml | 5 + static/locales/tr.yaml | 5 + static/locales/uk.yaml | 5 + static/locales/ur.yaml | 5 + static/locales/vi.yaml | 5 + static/locales/vls.yaml | 5 + static/locales/zh-CN.yaml | 5 + static/locales/zh-TW.yaml | 5 + 82 files changed, 639 insertions(+), 148 deletions(-) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 14587cfb23ae2..503c5c8296f93 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -191,6 +191,8 @@ export default defineComponent({ let startInFullscreen = props.startInFullscreen let startInPip = props.startInPip + const isSilenceSkipEnabled = ref(false) + /** * @type {{ * url: string, @@ -2998,6 +3000,88 @@ export default defineComponent({ return uiState } + /** + * Toggles and manages the silence skip functionality for the video player. + * + * When silence skip is enabled, the function analyzes the audio stream of the + * video to detect silent segments. If a silent segment is detected, the video + * playback speed is increased to skip over the silence. + * + * The function uses the Web Audio API to create an AudioContext and connect it + * to the video element's audio source. An AnalyserNode is used to obtain the + * time-domain data of the audio signal, which is then processed to calculate + * the maximum and average volume levels. Silence is determined by comparing + * these levels, and if the conditions for silence are met, the player's + * trickPlay method is invoked to increase the playback speed. + * + * If silence skip is disabled, the playback speed is reset to normal. + * + * The function initiates a loop using requestAnimationFrame to continuously + * analyze the audio stream as long as the player exists and silence skip is + * enabled. + */ + function skipSilence() { + if (!isSilenceSkipEnabled.value) { + showValueChange(t('SilenceSkip.Enable Silence Skip')) + isSilenceSkipEnabled.value = true + } else { + showValueChange(t('SilenceSkip.Disable Silence Skip')) + isSilenceSkipEnabled.value = false + } + + const video_ = video.value + + if (video_ && player) { + const audioContext = video_.audioContext ?? new AudioContext() + let source = video_.audioSource + if (!source) { + source = audioContext.createMediaElementSource(video_) + video_.audioSource = source + } + if (!video_.audioContext) { + video_.audioContext = audioContext + } + const analyser = audioContext.createAnalyser() + source.disconnect() + source.connect(analyser) + analyser.disconnect() + analyser.connect(audioContext.destination) + analyser.fftSize = 2048 + const bufferLength = analyser.frequencyBinCount + const amplitudeArray = new Uint8Array(bufferLength) + let loopId = 0 + const loop = () => { + if (!player) { + cancelAnimationFrame(loopId) + return + } + + analyser.getByteTimeDomainData(amplitudeArray) + const volumeValues = Array.from(amplitudeArray) + const filteredVolumes = volumeValues.map(volume => volume - 128).filter(volume => volume !== 0).map(volume => Math.abs(volume)) + const maxVolume = filteredVolumes.length ? Math.max(...filteredVolumes) : 0 + const averageVolume = filteredVolumes.length ? filteredVolumes.reduce((a, b) => a + b, 0) / filteredVolumes.length : 0 + + if (isSilenceSkipEnabled.value) { + const silencePercentage = !isNaN(maxVolume) && !isNaN(averageVolume) ? (averageVolume / maxVolume) * 4 : 0 + + if (maxVolume <= averageVolume || maxVolume <= silencePercentage) { + player.trickPlay(2.5) + } else { + player.cancelTrickPlay() + } + } else { + player.cancelTrickPlay() + return + } + + loopId = requestAnimationFrame(loop) + } + + loop() + } + } + expose({ hasLoaded, @@ -3005,7 +3089,8 @@ export default defineComponent({ pause, getCurrentTime, setCurrentTime, - destroyPlayer + destroyPlayer, + skipSilence }) // #endregion functions used by the watch page diff --git a/src/renderer/components/watch-video-info/watch-video-info.js b/src/renderer/components/watch-video-info/watch-video-info.js index e0a678fefb7b8..3d58cac6b1ac6 100644 --- a/src/renderer/components/watch-video-info/watch-video-info.js +++ b/src/renderer/components/watch-video-info/watch-video-info.js @@ -126,6 +126,7 @@ export default defineComponent({ 'set-info-area-sticky', 'scroll-to-info-area', 'save-watched-progress', + 'skip-silence', ], computed: { hideSharingActions: function() { @@ -372,6 +373,10 @@ export default defineComponent({ } }, + handleSkipSilence: function () { + this.$emit('skip-silence') + }, + grabExtensionFromUrl: function (url) { const regex = /\/(\w*)/i const group = url.match(regex) diff --git a/src/renderer/components/watch-video-info/watch-video-info.vue b/src/renderer/components/watch-video-info/watch-video-info.vue index 8b497c84805ac..3171e1db32a5e 100644 --- a/src/renderer/components/watch-video-info/watch-video-info.vue +++ b/src/renderer/components/watch-video-info/watch-video-info.vue @@ -149,6 +149,12 @@ :get-timestamp="getTimestamp" :playlist-id="playlistId" /> + diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index e6aba8ec56d61..f48deaeda80ad 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -1405,6 +1405,14 @@ export default defineComponent({ this.$refs.watchVideoPlaylist?.playPreviousVideo() }, + handleSkipSilence: function () { + const player = this.$refs.player + if (!player) { + return + } + player?.skipSilence() + }, + abortAutoplayCountdown: function (hideToast = false) { clearTimeout(this.playNextTimeout) clearInterval(this.playNextCountDownIntervalId) diff --git a/src/renderer/views/Watch/Watch.vue b/src/renderer/views/Watch/Watch.vue index bbac535bb25f8..c0f6ed66fc22f 100644 --- a/src/renderer/views/Watch/Watch.vue +++ b/src/renderer/views/Watch/Watch.vue @@ -160,6 +160,7 @@ @set-info-area-sticky="infoAreaSticky = $event" @scroll-to-info-area="$refs.infoArea.scrollIntoView()" @save-watched-progress="handleWatchProgressManualSave" + @skip-silence="handleSkipSilence" />