Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
2a49a85
add silence skip feature
some-git-user Aug 8, 2025
52213b8
Merge branch 'development' into feature/skip-silence
Aug 9, 2025
e42485d
Remove all the locale files. Only keep en-us
Aug 9, 2025
00516a9
cleanup
Aug 9, 2025
61ab31f
revert to development
Aug 9, 2025
156ca59
Merge branch 'FreeTubeApp:development' into feature/skip-silence
some-git-user Aug 10, 2025
2571bb0
Merge branch 'development' into feature/skip-silence
Aug 13, 2025
b9639cb
move button inside the player
Aug 13, 2025
de85d69
Merge branch 'FreeTubeApp:development' into feature/skip-silence
some-git-user Aug 18, 2025
1535bb0
move the icon to the left of the autoplay icon
Aug 19, 2025
6e2ce03
Merge branch 'development' into feature/skip-silence
Aug 30, 2025
af1ec54
add "remember its state in the watch session"
Aug 30, 2025
660ccf7
add "a toggle in the Player settings called Enable [name of the featu…
Aug 30, 2025
e7750fa
Merge branch 'FreeTubeApp:development' into feature/skip-silence
some-git-user Sep 3, 2025
33bdf7d
Merge branch 'FreeTubeApp:development' into feature/skip-silence
some-git-user Sep 16, 2025
02fdb49
Merge branch 'development' into feature/skip-silence
some-git-user Sep 19, 2025
2fa16b1
remove duplicate value
Sep 19, 2025
75ed668
use new icon format
Sep 19, 2025
651693a
Merge branch 'development' of github.com:some-git-user/FreeTube into …
Oct 2, 2025
aa1dbdf
remove player icon
Oct 2, 2025
766bb9a
move icon into overflow menu beneath loop
Oct 2, 2025
54cd5ec
remove popup message
Oct 2, 2025
88d7593
remove unused translation
Oct 2, 2025
729e84e
alter button for overflow menu
Oct 2, 2025
efcdfcf
cancelTrickPlay does not stop trickPlay anymore? setting explicit to …
Oct 2, 2025
7c513cf
bugfix, prevent fast forward when player is paused or playback ended
Oct 2, 2025
22cdec1
Move toggle to the left side
Oct 2, 2025
c65c858
bugfix, prevent skipSilence() speed multiplier if arrow keys are used…
Oct 2, 2025
6843abe
Merge branch 'development' of github.com:some-git-user/FreeTube into …
Oct 5, 2025
c19d74c
Merge branch 'development' of github.com:some-git-user/FreeTube into …
Oct 7, 2025
9d63f44
tune silence skip logic
Oct 8, 2025
441fdd3
Merge branch 'development' of github.com:some-git-user/FreeTube into …
Oct 14, 2025
b1ea3c3
Merge branch 'development' of github.com:some-git-user/FreeTube into …
Oct 17, 2025
759a61d
Merge branch 'development' of github.com:some-git-user/FreeTube into …
Oct 30, 2025
ecbc530
fix icon name
Oct 30, 2025
338cd09
movie into player section
Oct 30, 2025
c291226
remove from "The settings below have side effects" section
Oct 30, 2025
c32e4a7
move into video event region
Oct 30, 2025
9bdcaf2
remove duplicate entry
Oct 30, 2025
a9ee002
fix watch session
Oct 30, 2025
6be694e
improve reset logic
Oct 30, 2025
b20cb98
Update src/renderer/components/ft-shaka-video-player/ft-shaka-video-p…
efb4f5ff-1298-471a-8973-3d47447115dc Oct 30, 2025
3fa2a80
Update src/renderer/components/ft-shaka-video-player/ft-shaka-video-p…
efb4f5ff-1298-471a-8973-3d47447115dc Oct 30, 2025
3546fbb
Update src/renderer/components/ft-shaka-video-player/ft-shaka-video-p…
efb4f5ff-1298-471a-8973-3d47447115dc Oct 30, 2025
2d6626f
Update src/renderer/components/ft-shaka-video-player/player-component…
some-git-user Nov 3, 2025
8081994
Update static/locales/en-US.yaml
some-git-user Nov 3, 2025
cd2b1c6
Merge branch 'development' of github.com:some-git-user/FreeTube into …
Nov 3, 2025
4f32185
fix playback rates
Nov 3, 2025
5737f36
fix translation
Nov 3, 2025
160b21d
Merge branch 'development' of github.com:some-git-user/FreeTube into …
Nov 5, 2025
d399a2a
Merge branch 'development' of github.com:some-git-user/FreeTube into …
Nov 23, 2025
1a5a789
Merge branch 'development' of github.com:some-git-user/FreeTube into …
Nov 28, 2025
37b82c4
commit suggestion
Nov 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ const PlayerIcons = {
PLAY_CIRCLE_FILLED: 'm426-330 195-125q14-9 14-25t-14-25L426-630q-15-10-30.5-1.5T380-605v250q0 18 15.5 26.5T426-330Zm54 250q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z',
RECORD_VOICE_OVER_FILLED: 'M920-600q0 69-24.5 131.5T829-355q-12 14-30 15t-32-13q-13-13-12-31t12-33q30-38 46.5-85t16.5-98q0-51-16.5-97T767-781q-12-15-12.5-33t12.5-32q13-14 31.5-13.5T829-845q42 51 66.5 113.5T920-600Zm-182 0q0 32-10 61.5T700-484q-11 15-29.5 15.5T638-482q-13-13-13.5-31.5T633-549q6-11 9.5-24t3.5-27q0-14-3.5-27t-9.5-25q-9-17-8.5-35t13.5-31q14-14 32.5-13.5T700-716q18 25 28 54.5t10 61.5ZM360-440q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM40-200v-32q0-33 17-62t47-44q51-26 115-44t141-18q77 0 141 18t115 44q30 15 47 44t17 62v32q0 33-23.5 56.5T600-120H120q-33 0-56.5-23.5T40-200Z',
TUNE_FILLED: 'M480-120q-17 0-28.5-11.5T440-160v-160q0-17 11.5-28.5T480-360q17 0 28.5 11.5T520-320v40h280q17 0 28.5 11.5T840-240q0 17-11.5 28.5T800-200H520v40q0 17-11.5 28.5T480-120Zm-320-80q-17 0-28.5-11.5T120-240q0-17 11.5-28.5T160-280h160q17 0 28.5 11.5T360-240q0 17-11.5 28.5T320-200H160Zm160-160q-17 0-28.5-11.5T280-400v-40H160q-17 0-28.5-11.5T120-480q0-17 11.5-28.5T160-520h120v-40q0-17 11.5-28.5T320-600q17 0 28.5 11.5T360-560v160q0 17-11.5 28.5T320-360Zm160-80q-17 0-28.5-11.5T440-480q0-17 11.5-28.5T480-520h320q17 0 28.5 11.5T840-480q0 17-11.5 28.5T800-440H480Zm160-160q-17 0-28.5-11.5T600-640v-160q0-17 11.5-28.5T640-840q17 0 28.5 11.5T680-800v40h120q17 0 28.5 11.5T840-720q0 17-11.5 28.5T800-680H680v40q0 17-11.5 28.5T640-600Zm-480-80q-17 0-28.5-11.5T120-720q0-17 11.5-28.5T160-760h320q17 0 28.5 11.5T520-720q0 17-11.5 28.5T480-680H160Z',
RECTANGLE_DEFAULT: 'M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Z'
RECTANGLE_DEFAULT: 'M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Z',
TIMER_DEFAULT: 'M360-840v-80h240v80H360Zm80 440h80v-240h-80v240Zm40 320q-74 0-139.5-28.5T226-186q-49-49-77.5-114.5T120-440q0-74 28.5-139.5T226-694q49-49 114.5-77.5T480-800q62 0 119 20t107 58l56-56 56 56-56 56q38 50 58 107t20 119q0 74-28.5 139.5T734-186q-49 49-114.5 77.5T480-80Zm0-80q116 0 198-82t82-198q0-116-82-198t-198-82q-116 0-198 82t-82 198q0 116 82 198t198 82Zm0-280Z',
SHUTTER_SPEED_DEFAULT: 'M360-840v-80h240v80H360ZM480-80q-75 0-140.5-28.5T225-186q-49-49-77-114.5T120-440q0-74 28.5-139.5T226-694q49-49 114.5-77.5T480-800q63 0 120 21t104 59l58-58 56 56-56 58q36 47 57 104t21 120q0 74-28 139.5T735-186q-49 49-114.5 77.5T480-80Zm0-360Zm0-80h268q-18-62-61.5-109T584-700L480-520Zm-70 40 134-232q-59-15-121.5-2.5T306-660l104 180Zm-206 80h206L276-632q-42 47-62.5 106.5T204-400Zm172 220 104-180H212q18 62 61.5 109T376-180Zm40 12q66 17 128 1.5T654-220L550-400 416-168Zm268-80q44-48 63.5-107.5T756-480H550l134 232Z',
}

// Utils
Expand All @@ -246,13 +248,20 @@ const MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT = 4
// Displayed on the about page and used in the main.js file to only allow bitcoin URLs with this wallet address to be opened
const ABOUT_BITCOIN_ADDRESS = '1Lih7Ho5gnxb1CwPD4o59ss78pwo2T91eS'

const SilenceSkip = {
SILENCE_DETECTION_MULTIPLIER: 4, // Multiplier for silence detection. Higher = more sensitive
MIN_SILENCE_DURATION_MS: 150, // Min silence duration in ms. Higher for longer silence before skipping. Lower for faster reaction.
MIN_SOUND_DURATION_MS: 5, // Min sound duration in ms. Higher to avoid false positives for short sounds.
}

export {
IpcChannels,
DBActions,
SyncEvents,
DefaultFolderKind,
KeyboardShortcuts,
PlayerIcons,
SilenceSkip,
MAIN_PROFILE_ID,
MOBILE_WIDTH_THRESHOLD,
PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD,
Expand Down
16 changes: 16 additions & 0 deletions src/renderer/components/PlayerSettings/PlayerSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
:tooltip="t('Tooltips.Player Settings.Skip by Scrolling Over Video Player')"
@change="updateVideoSkipMouseScroll"
/>
<FtToggleSwitch
:label="t('Settings.Player Settings.Skip Silence Enabled')"
:compact="true"
:default-value="skipSilenceEnabled"
@change="updateSkipSilenceEnabled"
/>
</div>
<div class="switchColumn">
<FtToggleSwitch
Expand Down Expand Up @@ -383,6 +389,16 @@ function updateEnterFullscreenOnDisplayRotate(value) {
store.dispatch('updateEnterFullscreenOnDisplayRotate', value)
}

/** @type {import('vue').ComputedRef<boolean>} */
const skipSilenceEnabled = computed(() => store.getters.getSkipSilenceEnabled)

/**
* @param {boolean} value
*/
function updateSkipSilenceEnabled(value) {
store.dispatch('updateSkipSilenceEnabled', value)
}

/** @type {import('vue').ComputedRef<string>} */
const externalPlayer = computed(() => store.getters.getExternalPlayer)

Expand Down
168 changes: 162 additions & 6 deletions src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import shaka from 'shaka-player'
import { useI18n } from '../../composables/use-i18n-polyfill'

import store from '../../store/index'
import { DefaultFolderKind, KeyboardShortcuts } from '../../../constants'
import { DefaultFolderKind, KeyboardShortcuts, SilenceSkip } from '../../../constants'
import { AudioTrackSelection } from './player-components/AudioTrackSelection'
import { FullWindowButton } from './player-components/FullWindowButton'
import { LegacyQualitySelection } from './player-components/LegacyQualitySelection'
import { ScreenshotButton } from './player-components/ScreenshotButton'
import { StatsButton } from './player-components/StatsButton'
import { TheatreModeButton } from './player-components/TheatreModeButton'
import { AutoplayToggle } from './player-components/AutoplayToggle'
import { SkipSilenceButton } from './player-components/SkipSilenceButton'
import {
deduplicateAudioTracks,
findMostSimilarAudioBandwidth,
Expand Down Expand Up @@ -146,6 +147,10 @@ export default defineComponent({
type: Number,
default: 1
},
skipSilenceEnabled: {
type: Boolean,
default: false
}
},
emits: [
'error',
Expand All @@ -157,6 +162,7 @@ export default defineComponent({
'playback-rate-updated',
'skip-to-next',
'skip-to-prev',
'skip-silence-updated',
],
setup: function (props, { emit, expose }) {
const { locale, t } = useI18n()
Expand Down Expand Up @@ -193,6 +199,9 @@ export default defineComponent({
let startInFullscreen = props.startInFullscreen
let startInPip = props.startInPip

const isSilenceSkipEnabled = ref(false)
const trickPlayNormalSpeed = ref(props.currentPlaybackRate)

/**
* @type {{
* url: string,
Expand Down Expand Up @@ -817,6 +826,7 @@ export default defineComponent({
'captions',
'ft_audio_tracks',
'loop',
'ft_skip_silence_toggle',
'ft_screenshot',
'picture_in_picture',
'ft_full_window',
Expand Down Expand Up @@ -844,6 +854,7 @@ export default defineComponent({
'playback_rate',
props.format === 'legacy' ? 'ft_legacy_quality' : 'quality',
'loop',
'ft_skip_silence_toggle',
'recenter_vr',
'toggle_stereoscopic',
)
Expand Down Expand Up @@ -1207,6 +1218,119 @@ export default defineComponent({
}
}

/**
* Toggles and manages the silence skip functionality for the video player.
*
* When enabled, the function uses the Web Audio API to analyze the audio stream of the video element
* and detect silent segments. Silence detection is performed via an AnalyserNode connected in parallel
* to the audio output, allowing for volume analysis even when the output is muted during fast-forward.
*
* The detection logic calculates the maximum and average amplitude of the audio signal. If a silent segment
* is detected and persists for a defined minimum duration, the video is fast-forwarded and the output is smoothly
* muted using a GainNode to avoid click artifacts. When non-silent audio is detected and persists for a minimum duration,
* the output is smoothly unmuted and playback speed returns to normal, with a additional delay to further reduce audio clicks.
*
* The function continuously analyzes the audio stream using requestAnimationFrame, adapting playback and muting in real time.
* All transitions for muting and unmuting use smooth ramping via setTargetAtTime for click-free audio..
*/
function skipSilence() {
isSilenceSkipEnabled.value = !isSilenceSkipEnabled.value

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 gain = audioContext.createGain()
const analyser = audioContext.createAnalyser()
source.disconnect()
source.connect(gain)
source.connect(analyser)
gain.connect(audioContext.destination)

analyser.fftSize = 2048
const bufferLength = analyser.frequencyBinCount
const amplitudeArray = new Uint8Array(bufferLength)

let loopId = 0
let silenceStart = null
let soundStart = null
let isSkipping = false

trickPlayNormalSpeed.value = player.getPlaybackRate()
const trickPlayFastForwardSpeed = maxVideoPlaybackRate.value

function resetSkip() {
gain.gain.setTargetAtTime(1, audioContext.currentTime, 0.015)
player.trickPlay(trickPlayNormalSpeed.value)
isSkipping = false
silenceStart = null
soundStart = null
}

const loop = () => {
if (!player) {
cancelAnimationFrame(loopId)
return
}

const currentPlaybackRate = player.getPlaybackRate()
// Update the trick play speed, if the user changes the playback rate
if (trickPlayNormalSpeed.value !== currentPlaybackRate && trickPlayFastForwardSpeed !== currentPlaybackRate) {
trickPlayNormalSpeed.value = player.getPlaybackRate()
}

if (isSilenceSkipEnabled.value) {
analyser.getByteTimeDomainData(amplitudeArray)
const volumeValues = Array.from(amplitudeArray)
const filteredVolumes = volumeValues.map(v => v - 128).filter(v => v !== 0).map(Math.abs)
const maxVolume = filteredVolumes.length ? Math.max(...filteredVolumes) : 0
const averageVolume = filteredVolumes.length ? filteredVolumes.reduce((a, b) => a + b, 0) / filteredVolumes.length : 0
const silencePercentage = !isNaN(maxVolume) && !isNaN(averageVolume) ? (averageVolume / maxVolume) * SilenceSkip.SILENCE_DETECTION_MULTIPLIER : 0
const isSilent = (maxVolume <= averageVolume || maxVolume <= silencePercentage)

const now = performance.now()

if (isSilent && !isSkipping && !video_.paused && !video_.ended && !video_.muted) {
if (!silenceStart) silenceStart = now
if (now - silenceStart > SilenceSkip.MIN_SILENCE_DURATION_MS) {
gain.gain.setTargetAtTime(0, audioContext.currentTime, 0.025)
player.trickPlay(trickPlayFastForwardSpeed)
isSkipping = true
soundStart = null
}
} else if (!isSilent && isSkipping) {
if (!soundStart) soundStart = now
if (now - soundStart > SilenceSkip.MIN_SOUND_DURATION_MS) {
gain.gain.setTargetAtTime(1, audioContext.currentTime, 0.015)
setTimeout(() => {
resetSkip()
}, 25)
}
} else if (!isSilent && !isSkipping) {
resetSkip()
} else if (isSkipping && (video_.paused || video_.ended || video_.muted)) {
resetSkip()
}
} else {
resetSkip()
return
}

loopId = requestAnimationFrame(loop)
}

loop()
}
}

// #endregion video event handlers

// #region request/response filters
Expand Down Expand Up @@ -1712,6 +1836,25 @@ export default defineComponent({
shakaOverflowMenu.registerElement('ft_autoplay_toggle', new AutoplayToggleFactory())
}

function registerSkipSilenceToggle() {
events.addEventListener('toggleSkipSilence', () => {
emit('skip-silence-updated', !isSilenceSkipEnabled.value)
skipSilence()
})

/**
* @implements {shaka.extern.IUIElement.Factory}
*/
class SkipSilenceToggleFactory {
create(rootElement, controls) {
return new SkipSilenceButton(isSilenceSkipEnabled.value, events, rootElement, controls)
}
}

shakaControls.registerElement('ft_skip_silence_toggle', new SkipSilenceToggleFactory())
shakaOverflowMenu.registerElement('ft_skip_silence_toggle', new SkipSilenceToggleFactory())
}

function registerTheatreModeButton() {
events.addEventListener('toggleTheatreMode', () => {
emit('toggle-theatre-mode')
Expand Down Expand Up @@ -1863,6 +2006,9 @@ export default defineComponent({

shakaControls.registerElement('ft_screenshot', null)
shakaOverflowMenu.registerElement('ft_screenshot', null)

shakaControls.registerElement('ft_skip_silence_toggle', null)
shakaOverflowMenu.registerElement('ft_skip_silence_toggle', null)
}

// #endregion custom player controls
Expand Down Expand Up @@ -2165,6 +2311,8 @@ export default defineComponent({
return
}

const playbackRate = isSilenceSkipEnabled.value ? trickPlayNormalSpeed.value : player.getPlaybackRate()

switch (event.key.toLowerCase()) {
case ' ':
case 'spacebar': // older browsers might return spacebar instead of a space character
Expand All @@ -2176,12 +2324,12 @@ export default defineComponent({
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.LARGE_REWIND:
// Rewind by 2x the time-skip interval (in seconds)
event.preventDefault()
seekBySeconds(-defaultSkipInterval.value * player.getPlaybackRate() * 2, false, true)
seekBySeconds(-defaultSkipInterval.value * playbackRate * 2, false, true)
break
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.LARGE_FAST_FORWARD:
// Fast-Forward by 2x the time-skip interval (in seconds)
event.preventDefault()
seekBySeconds(defaultSkipInterval.value * player.getPlaybackRate() * 2, false, true)
seekBySeconds(defaultSkipInterval.value * playbackRate * 2, false, true)
break
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.DECREASE_VIDEO_SPEED:
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.DECREASE_VIDEO_SPEED_ALT:
Expand Down Expand Up @@ -2240,7 +2388,7 @@ export default defineComponent({
showOverlayControls()
} else {
// Rewind by the time-skip interval (in seconds)
seekBySeconds(-defaultSkipInterval.value * player.getPlaybackRate(), false, true)
seekBySeconds(-defaultSkipInterval.value * playbackRate, false, true)
}
break
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.SMALL_FAST_FORWARD:
Expand All @@ -2251,7 +2399,7 @@ export default defineComponent({
showOverlayControls()
} else {
// Fast-Forward by the time-skip interval (in seconds)
seekBySeconds(defaultSkipInterval.value * player.getPlaybackRate(), false, true)
seekBySeconds(defaultSkipInterval.value * playbackRate, false, true)
}
break
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.PICTURE_IN_PICTURE:
Expand Down Expand Up @@ -2577,6 +2725,7 @@ export default defineComponent({
registerFullWindowButton()
registerLegacyQualitySelection()
registerStatsButton()
registerSkipSilenceToggle()

if (ui.isMobile()) {
onlyUseOverFlowMenu.value = true
Expand Down Expand Up @@ -2631,6 +2780,13 @@ export default defineComponent({
player.addEventListener('ratechange', () => {
emit('playback-rate-updated', player.getPlaybackRate())
})

if (props.skipSilenceEnabled) {
skipSilence()
events.dispatchEvent(new CustomEvent('setSkipSilence', {
detail: isSilenceSkipEnabled.value
}))
}
})

async function performFirstLoad() {
Expand Down Expand Up @@ -3030,7 +3186,7 @@ export default defineComponent({
pause,
getCurrentTime,
setCurrentTime,
destroyPlayer
destroyPlayer,
})

// #endregion functions used by the watch page
Expand Down
Loading