diff --git a/README.md b/README.md index a44fde2a..23a592de 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ - Sync your watch progress between Invidious sessions. - Watch sync parties! - Mini player. +- Silence skipper (Experimental) - Preview video on hover. - Sponsorblock built-in. - Return YouTube dislikes built-in. diff --git a/materialious/src/lib/Player.svelte b/materialious/src/lib/Player.svelte index 8eb5c41a..14d9d447 100644 --- a/materialious/src/lib/Player.svelte +++ b/materialious/src/lib/Player.svelte @@ -4,7 +4,6 @@ import { page } from '$app/stores'; import { Capacitor } from '@capacitor/core'; import type { Page } from '@sveltejs/kit'; - import { type ParsedCaptionsResult } from 'media-captions'; import { SponsorBlock, type Category } from 'sponsorblock-api'; import { onDestroy, onMount } from 'svelte'; import { _ } from 'svelte-i18n'; @@ -23,6 +22,7 @@ playerDashStore, playerProxyVideosStore, playerSavePlaybackPositionStore, + silenceSkipperStore, sponsorBlockCategoriesStore, sponsorBlockDisplayToastStore, sponsorBlockStore, @@ -43,7 +43,10 @@ let playerIsLive = false; let playerPosSet = false; - let transcript: ParsedCaptionsResult; + let userVideoSpeed = 1; + let silenceIsFastForwarding = false; + + let silenceSkipperInterval: NodeJS.Timeout; function loadTimeFromUrl(page: Page): boolean { if (player) { @@ -59,8 +62,74 @@ page.subscribe((pageUpdate) => loadTimeFromUrl(pageUpdate)); + let initStoreSilence = true; + silenceSkipperStore.subscribe((value) => { + if (initStoreSilence) { + initStoreSilence = false; + return; + } + + if (typeof silenceSkipperInterval !== 'undefined') { + clearInterval(silenceSkipperInterval); + } + + if (value) { + initSilenceSkipper(); + } + }); + const proxyVideos = get(playerProxyVideosStore); + function fastForwardToSound( + videoElement: HTMLAudioElement, + threshold: number = 0.01, + checkInterval: number = 100, + fastForwardRate: number = 4 + ): void { + if (!(videoElement instanceof HTMLMediaElement)) { + console.error('Provided element is not a valid HTMLMediaElement'); + return; + } + + const audioContext = new AudioContext(); + const source = audioContext.createMediaElementSource(videoElement); + const analyser = audioContext.createAnalyser(); + + source.connect(analyser); + analyser.connect(audioContext.destination); + analyser.fftSize = 256; + + const dataArray = new Uint8Array(analyser.frequencyBinCount); + + const checkForSound = () => { + analyser.getByteFrequencyData(dataArray); + + const averageVolume = dataArray.reduce((a, b) => a + b, 0) / dataArray.length; + const volume = averageVolume / 255; + + if (volume < threshold) { + if (!silenceIsFastForwarding) { + videoElement.playbackRate = fastForwardRate; + silenceIsFastForwarding = true; + } + } else { + if (silenceIsFastForwarding) { + videoElement.playbackRate = userVideoSpeed; + silenceIsFastForwarding = false; + } + } + }; + + silenceSkipperInterval = setInterval(checkForSound, checkInterval); + } + + function initSilenceSkipper() { + const videoContainer = document.getElementById('video') as HTMLElement; + const videoElement = videoContainer.querySelector('video') as HTMLMediaElement; + + fastForwardToSound(videoElement); + } + onMount(async () => { miniPlayerSrcStore.set(null); @@ -105,10 +174,35 @@ player.addEventListener('pause', () => { savePlayerPos(); + if (get(silenceSkipperStore)) { + clearInterval(silenceSkipperInterval); + } }); player.addEventListener('end', () => { savePlayerPos(); + if (get(silenceSkipperStore)) { + clearInterval(silenceSkipperInterval); + } + }); + + player.addEventListener('play', () => { + if (get(silenceSkipperStore)) { + initSilenceSkipper(); + } + }); + + player.addEventListener('seeked', () => { + if (get(silenceSkipperStore)) { + clearInterval(silenceSkipperInterval); + + initSilenceSkipper(); + } + }); + + player.addEventListener('rate-change', () => { + if (silenceIsFastForwarding) return; + userVideoSpeed = player.playbackRate; }); if (get(sponsorBlockStore) && get(sponsorBlockCategoriesStore)) { @@ -266,6 +360,9 @@ } onDestroy(() => { + if (typeof silenceSkipperInterval !== 'undefined') { + clearInterval(silenceSkipperInterval); + } savePlayerPos(); player.pause(); player.destroy(); @@ -279,6 +376,7 @@ -
Interface
+
{$_('layout.interface')}
{/if} + +
+ +
diff --git a/materialious/src/lib/externalSettings.ts b/materialious/src/lib/externalSettings.ts index e8d34734..70678614 100644 --- a/materialious/src/lib/externalSettings.ts +++ b/materialious/src/lib/externalSettings.ts @@ -22,6 +22,7 @@ import { playerTheatreModeByDefaultStore, returnYTDislikesInstanceStore, returnYtDislikesStore, + silenceSkipperStore, sponsorBlockCategoriesStore, sponsorBlockDisplayToastStore, sponsorBlockStore, @@ -177,6 +178,11 @@ const persistedStores = [ name: 'sponsorBlockDisplayToast', store: sponsorBlockDisplayToastStore, type: 'boolean' + }, + { + name: 'silenceSkipper', + store: silenceSkipperStore, + type: 'boolean' } ]; diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index 7eef6fe5..742df5a6 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -75,6 +75,7 @@ "addToPlaylist": "Add to playlist" }, "layout": { + "interface": "Interface", "star": "Star us on Github!", "syncParty": "Sync party", "syncPartyWarning": "Please note your IP will be visible to users you invite.", @@ -111,7 +112,8 @@ "miniPlayer": "Mini player", "proxyVideos": "Proxy videos", "backgroundPlay": "Play audio in background", - "dash": "Dash" + "dash": "Dash", + "silenceSkipper": "Silence skipper (Experimental)" }, "bookmarklet": "Bookmarklet", "instanceUrl": "Instance URL", diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index 9575f53f..1968e8ee 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -6,7 +6,6 @@ import { persisted } from 'svelte-persisted-store'; import { writable, type Writable } from 'svelte/store'; import type { TitleCase } from './misc'; - function platformDependentDefault(givenValue: any, defaultValue: any): any { if (typeof givenValue !== 'undefined' && typeof givenValue !== null) { return givenValue; @@ -15,7 +14,6 @@ function platformDependentDefault(givenValue: any, defaultValue: any): any { } } - export const instanceStore: Writable = persisted( "invidiousInstance", platformDependentDefault(import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE, 'https://invidious.materialio.us') @@ -95,3 +93,5 @@ export const playlistSettingsStore: Writable = writable(null); + +export const silenceSkipperStore: Writable = persisted('silenceSkipper', false);