diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index d4b619ab..dcbeaf08 100644 --- a/materialious/android/app/build.gradle +++ b/materialious/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "us.materialio.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 177 - versionName "1.12.5" + versionCode 178 + versionName "1.12.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/materialious/electron/materialious.metainfo.xml b/materialious/electron/materialious.metainfo.xml index 3e720ab4..678490f0 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -74,7 +74,11 @@ - + + + https://github.com/Materialious/Materialious/releases/tag/1.12.6 + + https://github.com/Materialious/Materialious/releases/tag/1.12.5 diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index 5297bbb1..54131c68 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "Materialious", - "version": "1.12.5", + "version": "1.12.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.12.5", + "version": "1.12.6", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", diff --git a/materialious/electron/package.json b/materialious/electron/package.json index d4cfc01d..d5df0fe8 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.12.5", + "version": "1.12.6", "description": "Modern material design for Invidious.", "author": { "name": "Ward Pearce", diff --git a/materialious/package.json b/materialious/package.json index 06e17316..0e8a6af8 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.12.5", + "version": "1.12.6", "private": true, "scripts": { "dev": "vite dev", @@ -75,4 +75,4 @@ "svelte-infinite-loading": "^1.4.0", "youtubei.js": "^16.0.1" } -} +} \ No newline at end of file diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index e816bb6c..0f00bdbe 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -10,8 +10,6 @@ import ui from 'beercss'; import ISO6391 from 'iso-639-1'; import Mousetrap from 'mousetrap'; - import 'shaka-player/dist/controls.css'; - import '$lib/css/shaka-player-theme.css'; import { CapacitorMusicControls } from 'capacitor-music-controls-plugin'; import shaka from 'shaka-player/dist/shaka-player.ui'; import { SponsorBlock, type Category, type Segment } from 'sponsorblock-api'; @@ -22,9 +20,9 @@ import type { VideoPlay } from '../api/model'; import { authStore, - darkModeStore, instanceStore, isAndroidTvStore, + playerAlwaysLoopStore, playerAndroidLockOrientation, playerAutoPlayStore, playerCCByDefault, @@ -34,31 +32,36 @@ playerProxyVideosStore, playerSavePlaybackPositionStore, playerState, - playerStatisticsByDefault, + playertheatreModeIsActive, playerYouTubeJsFallback, sponsorBlockCategoriesStore, sponsorBlockDisplayToastStore, sponsorBlockStore, sponsorBlockUrlStore, synciousInstanceStore, - synciousStore, - themeColorStore + synciousStore } from '../store'; - import { getDynamicTheme, setStatusBarColor } from '../theme'; + import { setStatusBarColor } from '../theme'; import { patchYoutubeJs } from '$lib/patches/youtubejs'; import { goToNextVideo, goToPreviousVideo, playbackRates } from '$lib/player'; - import { EndTimeElement } from '$lib/shaka-elements/endTime'; import { dashManifestDomainInclusion } from '$lib/android/youtube/dash'; import { injectSabr } from '$lib/sabr'; import type { SabrStreamingAdapter } from 'googlevideo/sabr-streaming-adapter'; + import { SvelteSet } from 'svelte/reactivity'; interface Props { data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null }; isEmbed?: boolean; playerElement?: HTMLMediaElement | undefined; + hideControls?: boolean; } - let { data, isEmbed = false, playerElement = $bindable(undefined) }: Props = $props(); + let { + data, + isEmbed = false, + playerElement = $bindable(undefined), + hideControls = false + }: Props = $props(); let segments: Segment[] = []; @@ -69,23 +72,27 @@ let showVideoRetry = $state(false); let player: shaka.Player; - let shakaUi: shaka.ui.Overlay; let sabrAdapter: SabrStreamingAdapter | null; + let playerContainer: HTMLElement; + + let playerCurrentPlaybackState = $state(false); + let playerCurrentTime = $state(0); + let playerMaxKnownTime = $state(0); + let playerIsSeeking = $state(false); + let playerIsBuffering = $state(false); + let playerVolume = $state(0); + let playerSettings: 'quality' | 'speed' | 'language' | 'root' = $state('root'); + let playerTextTracks: shaka.extern.TextTrack[] | undefined = $state(undefined); + let playerCurrentVideoTrack: shaka.extern.VideoTrack | undefined = $state(undefined); + let playerCurrentAudioTrack: shaka.extern.AudioTrack | undefined = $state(undefined); + let playerLoop = $state($playerAlwaysLoopStore); const STORAGE_KEY_VOLUME = 'shaka-preferred-volume'; - async function updateSeekBarTheme() { - if (!shakaUi) return; + playertheatreModeIsActive.subscribe(async () => { await tick(); - shakaUi.configure({ - seekBarColors: { - played: (await getDynamicTheme())['--primary'] - } - }); - } - - themeColorStore.subscribe(updateSeekBarTheme); - darkModeStore.subscribe(updateSeekBarTheme); + updateVideoPlayerHeight(); + }); function restoreDefaultLanguage() { if (!$playerDefaultLanguage || $playerDefaultLanguage === 'original') { @@ -109,8 +116,24 @@ } } + function filterUniqueAudioTracks(tracks: shaka.extern.AudioTrack[]): shaka.extern.AudioTrack[] { + const uniqueTracks: shaka.extern.AudioTrack[] = []; + const seen = new SvelteSet(); + + for (const track of tracks) { + const identifier = `${track.language}-${track.label || 'No Label'}`; + if (!seen.has(identifier)) { + seen.add(identifier); + uniqueTracks.push(track); + } + } + + return uniqueTracks; + } + function saveVolumePreference() { if (!playerElement) return; + playerVolume = playerElement.volume; localStorage.setItem(STORAGE_KEY_VOLUME, playerElement.volume.toString()); } @@ -118,6 +141,7 @@ const savedVolume = localStorage.getItem(STORAGE_KEY_VOLUME); if (savedVolume && playerElement) { playerElement.volume = parseFloat(savedVolume); + playerVolume = playerElement.volume; } } @@ -298,20 +322,26 @@ page.subscribe((pageUpdate) => loadTimeFromUrl(pageUpdate)); + function setActiveVideoTrack() { + const videoTracks = player.getVideoTracks(); + playerCurrentVideoTrack = videoTracks.find((track) => track.active); + } + + function setActiveAudioTrack() { + const audioTracks = player.getAudioTracks(); + playerCurrentAudioTrack = audioTracks.find((track) => track.active); + } + async function loadVideo() { showVideoRetry = false; sabrAdapter = await injectSabr(data.video, player); - try { - document.getElementsByClassName('shaka-ad-info')[0].remove(); - } catch { - // Continue regardless of error - } - player.addEventListener('loaded', () => { restoreQualityPreference(); restoreDefaultLanguage(); + setActiveVideoTrack(); + setActiveAudioTrack(); if ($playerCCByDefault) { toggleSubtitles(); @@ -421,14 +451,6 @@ if ($playerDefaultPlaybackSpeed && playerElement) { playerElement.playbackRate = $playerDefaultPlaybackSpeed; } - - if ($playerStatisticsByDefault) { - // Appears to be no native way in shaka to toggle statistics on and off - const shakaStatisticsButton = document.querySelector('.shaka-statistics-button') as - | HTMLButtonElement - | undefined; - shakaStatisticsButton?.click(); - } } async function reloadVideo() { @@ -437,6 +459,24 @@ await loadVideo(); } + function toggleVideoPlaybackStatus() { + if (playerCurrentPlaybackState) { + playerElement?.pause(); + } else { + playerElement?.play(); + } + } + + // Due to how our player is rendered in layout for stateful pip + // we calaculate player height to then allow children pages + // to wrap around it. + function updateVideoPlayerHeight() { + if (playerContainer) { + const height = playerContainer.getBoundingClientRect().height; + document.documentElement.style.setProperty('--video-player-height', `${height + 10}px`); + } + } + onMount(async () => { shaka.polyfill.installAll(); if (!shaka.Player.isBrowserSupported()) { @@ -466,66 +506,22 @@ }); playerElement = document.getElementById('player') as HTMLMediaElement; + playerElement.loop = playerLoop; + if ($playerState) { playerState.set({ ...$playerState, playerElement: playerElement }); } - // Due to how our player is rendered in layout for stateful pip - // we calaculate player height to then allow children pages - // to wrap around it. - function updateVideoPlayerHeight() { - const container = document.getElementById('shaka-container') as HTMLElement; - - if (container) { - const height = container.getBoundingClientRect().height; - document.documentElement.style.setProperty('--video-player-height', `${height + 10}px`); - } - } - window.addEventListener('resize', updateVideoPlayerHeight); - updateVideoPlayerHeight(); - // Change instantly to stop video from being loud for a second restoreVolumePreference(); - const playerContainer = document.getElementById('shaka-container') as HTMLElement; + playerContainer = document.getElementById('player-container') as HTMLElement; - shakaUi = new shaka.ui.Overlay(player, playerContainer, playerElement); + window.addEventListener('resize', updateVideoPlayerHeight); + updateVideoPlayerHeight(); await player.attach(playerElement); - shaka.ui.Controls.registerElement('end_time', { - create: (parent: HTMLElement, controls: shaka.ui.Controls) => { - return new EndTimeElement(parent, controls); - } - }); - - shakaUi.configure({ - addBigPlayButton: Capacitor.getPlatform() === 'android', - controlPanelElements: [ - 'play_pause', - Capacitor.getPlatform() === 'android' ? '' : 'volume', - 'spacer', - 'time_and_duration', - data.video.liveNow || data.video.lengthSeconds < 240 ? '' : 'end_time', - 'captions', - 'overflow_menu', - 'fullscreen' - ], - overflowMenuButtons: [ - 'cast', - 'airplay', - 'quality', - 'playback_rate', - 'loop', - 'language', - 'statistics' - ], - playbackRates: playbackRates, - enableTooltips: false - }); - - updateSeekBarTheme(); - player?.addEventListener('error', (event) => { const error = (event as CustomEvent).detail as shaka.util.Error; console.error('Player error:', error); @@ -557,16 +553,6 @@ } }); - // Required to stop buttons from being still selected when fullscreening - document?.addEventListener('fullscreenchange', async () => { - const buttons = document.querySelectorAll('.shaka-controls-button-panel button'); - buttons.forEach((button) => { - // Reset the button's focus and active states - (button as HTMLElement).blur(); // Remove focus from the button - button.removeAttribute('aria-pressed'); // Reset any ARIA attributes that might indicate selection - }); - }); - playerElement?.addEventListener('volumechange', saveVolumePreference); await androidHandleRotate(); @@ -710,17 +696,6 @@ }); } - const volumeContainer = playerContainer.getElementsByClassName('shaka-volume-bar-container'); - volumeContainer[0]?.addEventListener('mousewheel', (event) => { - event.preventDefault(); - const delta = Math.sign((event as any).deltaY); - const newVolume = Math.max( - 0, - Math.min(1, (playerElement as HTMLMediaElement).volume - delta * 0.05) - ); - (playerElement as HTMLMediaElement).volume = newVolume; - }); - Mousetrap.bind('c', () => { toggleSubtitles(); return false; @@ -730,7 +705,7 @@ if (document.fullscreenElement) { document.exitFullscreen(); } else { - shakaUi.getControls()?.toggleFullScreen(); + playerContainer.requestFullscreen(); } return false; }); @@ -765,14 +740,49 @@ playerElement.currentTime += frameTime; }); + const volumeContainer = document.getElementById('volume-slider'); + volumeContainer?.addEventListener('mousewheel', (event) => { + event.preventDefault(); + const delta = Math.sign((event as any).deltaY); + const newVolume = Math.max( + 0, + Math.min(1, (playerElement as HTMLMediaElement).volume - delta * 0.05) + ); + (playerElement as HTMLMediaElement).volume = newVolume; + playerVolume = newVolume; + }); + playerElement?.addEventListener('pause', async () => { + playerCurrentPlaybackState = false; + playerIsBuffering = false; savePlayerPos(); }); playerElement?.addEventListener('ended', async () => { + playerCurrentPlaybackState = false; + playerIsBuffering = false; await goToNextVideo(data.video, data.playlistId); }); + playerElement?.addEventListener('playing', () => { + playerCurrentPlaybackState = true; + playerIsBuffering = false; + }); + + playerElement?.addEventListener('waiting', () => { + playerCurrentPlaybackState = false; + playerIsBuffering = true; + }); + + playerElement?.addEventListener('timeupdate', () => { + if (playerIsSeeking) return; + playerCurrentTime = playerElement?.currentTime ?? 0; + + if (playerMaxKnownTime === 0 || playerCurrentTime > playerMaxKnownTime) { + playerMaxKnownTime = Number(playerElement?.currentTime); + } + }); + try { await loadVideo(); } catch (error: unknown) { @@ -788,6 +798,11 @@ await reloadVideo(); } } + + // Update video player height again on video loaded. + updateVideoPlayerHeight(); + + playerTextTracks = player.getTextTracks(); }); async function getLastPlayPos(): Promise { @@ -869,6 +884,8 @@ // Continue regardless of error } + window.removeEventListener('resize', updateVideoPlayerHeight); + Mousetrap.unbind(['left', 'right', 'space', 'c', 'f', 'shift+left', 'shift+right']); if (watchProgressTimeout) { @@ -883,20 +900,24 @@ player.unload(); player.destroy(); } - - if (shakaUi) { - shakaUi.destroy(); - } }); {#if showVideoRetry} @@ -938,6 +1229,59 @@ {/if}