diff --git a/src/constants.js b/src/constants.js
index e6c8aff4fde4e..dcdc98d2162e2 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -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
@@ -246,6 +248,12 @@ 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,
@@ -253,6 +261,7 @@ export {
DefaultFolderKind,
KeyboardShortcuts,
PlayerIcons,
+ SilenceSkip,
MAIN_PROFILE_ID,
MOBILE_WIDTH_THRESHOLD,
PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD,
diff --git a/src/renderer/components/PlayerSettings/PlayerSettings.vue b/src/renderer/components/PlayerSettings/PlayerSettings.vue
index cbf48e59e26c1..e86267cfef65e 100644
--- a/src/renderer/components/PlayerSettings/PlayerSettings.vue
+++ b/src/renderer/components/PlayerSettings/PlayerSettings.vue
@@ -40,6 +40,12 @@
:tooltip="t('Tooltips.Player Settings.Skip by Scrolling Over Video Player')"
@change="updateVideoSkipMouseScroll"
/>
+
} */
+const skipSilenceEnabled = computed(() => store.getters.getSkipSilenceEnabled)
+
+/**
+ * @param {boolean} value
+ */
+function updateSkipSilenceEnabled(value) {
+ store.dispatch('updateSkipSilenceEnabled', value)
+}
+
/** @type {import('vue').ComputedRef} */
const externalPlayer = computed(() => store.getters.getExternalPlayer)
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 e4b52d37b4097..09c9e0d03bc5b 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
@@ -3,7 +3,7 @@ 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'
@@ -11,6 +11,7 @@ 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,
@@ -146,6 +147,10 @@ export default defineComponent({
type: Number,
default: 1
},
+ skipSilenceEnabled: {
+ type: Boolean,
+ default: false
+ }
},
emits: [
'error',
@@ -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()
@@ -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,
@@ -817,6 +826,7 @@ export default defineComponent({
'captions',
'ft_audio_tracks',
'loop',
+ 'ft_skip_silence_toggle',
'ft_screenshot',
'picture_in_picture',
'ft_full_window',
@@ -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',
)
@@ -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
@@ -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')
@@ -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
@@ -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
@@ -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:
@@ -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:
@@ -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:
@@ -2577,6 +2725,7 @@ export default defineComponent({
registerFullWindowButton()
registerLegacyQualitySelection()
registerStatsButton()
+ registerSkipSilenceToggle()
if (ui.isMobile()) {
onlyUseOverFlowMenu.value = true
@@ -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() {
@@ -3030,7 +3186,7 @@ export default defineComponent({
pause,
getCurrentTime,
setCurrentTime,
- destroyPlayer
+ destroyPlayer,
})
// #endregion functions used by the watch page
diff --git a/src/renderer/components/ft-shaka-video-player/player-components/SkipSilenceButton.js b/src/renderer/components/ft-shaka-video-player/player-components/SkipSilenceButton.js
new file mode 100644
index 0000000000000..a521af2559ab0
--- /dev/null
+++ b/src/renderer/components/ft-shaka-video-player/player-components/SkipSilenceButton.js
@@ -0,0 +1,72 @@
+import shaka from 'shaka-player'
+
+import i18n from '../../../i18n/index'
+import { PlayerIcons } from '../../../../constants'
+
+export class SkipSilenceButton extends shaka.ui.Element {
+ /**
+ * @param {boolean} skipSilenceEnabled
+ * @param {EventTarget} events
+ * @param {HTMLElement} parent
+ * @param {shaka.ui.Controls} controls
+ */
+ constructor(skipSilenceEnabled, events, parent, controls) {
+ super(parent, controls)
+
+ /** @private */
+ this.button_ = document.createElement('button')
+ this.button_.classList.add('skip-silence-button', 'shaka-tooltip')
+
+ /** @private */
+ this.icon_ = new shaka.ui.MaterialSVGIcon(this.button_, PlayerIcons.TIMER_DEFAULT)
+
+ const label = document.createElement('label')
+ label.classList.add(
+ 'shaka-overflow-button-label',
+ 'shaka-overflow-menu-only',
+ 'shaka-simple-overflow-button-label-inline'
+ )
+
+ /** @private */
+ this.nameSpan_ = document.createElement('span')
+ label.appendChild(this.nameSpan_)
+
+ /** @private */
+ this.currentState_ = document.createElement('span')
+ this.currentState_.classList.add('shaka-current-selection-span')
+ label.appendChild(this.currentState_)
+
+ this.button_.appendChild(label)
+
+ this.parent.appendChild(this.button_)
+
+ /** @private */
+ this.skipSilenceEnabled_ = skipSilenceEnabled
+
+ // listeners
+
+ this.eventManager.listen(this.button_, 'click', () => {
+ events.dispatchEvent(new CustomEvent('toggleSkipSilence'))
+ this.skipSilenceEnabled_ = !this.skipSilenceEnabled_
+ this.updateLocalisedStrings_()
+ })
+
+ this.eventManager.listen(events, 'setSkipSilence', (event) => {
+ this.skipSilenceEnabled_ = event.detail
+ this.updateLocalisedStrings_()
+ })
+
+ this.eventManager.listen(events, 'localeChanged', () => {
+ this.updateLocalisedStrings_()
+ })
+
+ this.updateLocalisedStrings_()
+ }
+
+ /** @private */
+ updateLocalisedStrings_() {
+ this.nameSpan_.textContent = this.button_.ariaLabel = i18n.global.t('Video.Player.Skip Silence')
+ this.icon_.use(this.skipSilenceEnabled_ ? PlayerIcons.SHUTTER_SPEED_DEFAULT : PlayerIcons.TIMER_DEFAULT)
+ this.currentState_.textContent = this.localization.resolve(this.skipSilenceEnabled_ ? 'ON' : 'OFF')
+ }
+}
diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js
index 4339079b7962b..1851d0eeaf1ad 100644
--- a/src/renderer/store/modules/settings.js
+++ b/src/renderer/store/modules/settings.js
@@ -306,6 +306,7 @@ const state = {
quickBookmarkTargetPlaylistId: 'favorites',
generalAutoLoadMorePaginatedItemsEnabled: false,
hideToTrayOnMinimize: false,
+ skipSilenceEnabled: false,
// The settings below have side effects
currentLocale: 'system',
diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js
index 42ee8d687a0a2..b39efa9009ef5 100644
--- a/src/renderer/views/Watch/Watch.js
+++ b/src/renderer/views/Watch/Watch.js
@@ -143,6 +143,7 @@ export default defineComponent({
/** @type {Date|null} */
streamingDataExpiryDate: null,
currentPlaybackRate: null,
+ startNextVideoWithSkipSilenceEnabled: false
}
},
computed: {
@@ -339,6 +340,7 @@ export default defineComponent({
this.checkIfTimestamp()
this.currentPlaybackRate = this.$store.getters.getDefaultPlayback
+ this.startNextVideoWithSkipSilenceEnabled = this.$store.getters.getSkipSilenceEnabled
},
mounted: function () {
this.onMountedDependOnLocalStateLoading()
@@ -1780,6 +1782,9 @@ export default defineComponent({
updatePlaybackRate(newRate) {
this.currentPlaybackRate = newRate
},
+ updateSkipSilence(newState) {
+ this.startNextVideoWithSkipSilenceEnabled = newState
+ },
destroyPlayer: async function() {
const uiState = await this.$refs.player.destroyPlayer()
diff --git a/src/renderer/views/Watch/Watch.vue b/src/renderer/views/Watch/Watch.vue
index bbac535bb25f8..3f67165b9599c 100644
--- a/src/renderer/views/Watch/Watch.vue
+++ b/src/renderer/views/Watch/Watch.vue
@@ -40,6 +40,7 @@
:start-in-fullwindow="startNextVideoInFullwindow"
:start-in-pip="startNextVideoInPip"
:current-playback-rate="currentPlaybackRate"
+ :skip-silence-enabled="startNextVideoWithSkipSilenceEnabled"
class="videoPlayer"
@error="handlePlayerError"
@loaded="handleVideoLoaded"
@@ -50,6 +51,7 @@
@playback-rate-updated="updatePlaybackRate"
@skip-to-next="handleSkipToNext"
@skip-to-prev="handleSkipToPrev"
+ @skip-silence-updated="updateSkipSilence"
/>