From 8adf956ab1d3042859b7869b52088a4eff0db2e4 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Mon, 22 Dec 2025 06:30:44 +1300 Subject: [PATCH] Progress on custom player ui --- materialious/src/lib/components/Player.svelte | 395 ++++++++++++++---- materialious/src/lib/css/global.css | 1 + materialious/src/lib/i18n/locales/en.json | 12 +- materialious/src/routes/(app)/+layout.svelte | 3 +- .../routes/(no-layout)/tv/[slug]/+page.svelte | 2 +- 5 files changed, 326 insertions(+), 87 deletions(-) diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index 6590999f..c7d8f465 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,7 +32,6 @@ playerProxyVideosStore, playerSavePlaybackPositionStore, playerState, - playerStatisticsByDefault, playertheatreModeIsActive, playerYouTubeJsFallback, sponsorBlockCategoriesStore, @@ -42,24 +39,30 @@ 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[] = []; @@ -70,24 +73,21 @@ 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 playerIsSeeking = $state(false); + let playerIsBuffering = $state(false); + let playerVolume = $state(0); + let playerSettings: 'quality' | 'speed' | 'language' | 'root' = $state('root'); + 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; - await tick(); - shakaUi.configure({ - seekBarColors: { - played: (await getDynamicTheme())['--primary'] - } - }); - } - - themeColorStore.subscribe(updateSeekBarTheme); - darkModeStore.subscribe(updateSeekBarTheme); playertheatreModeIsActive.subscribe(async () => { await tick(); updateVideoPlayerHeight(); @@ -115,8 +115,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()); } @@ -124,6 +140,7 @@ const savedVolume = localStorage.getItem(STORAGE_KEY_VOLUME); if (savedVolume && playerElement) { playerElement.volume = parseFloat(savedVolume); + playerVolume = playerElement.volume; } } @@ -304,20 +321,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(); @@ -427,14 +450,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() { @@ -443,6 +458,14 @@ 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. @@ -482,6 +505,8 @@ }); playerElement = document.getElementById('player') as HTMLMediaElement; + playerElement.loop = playerLoop; + if ($playerState) { playerState.set({ ...$playerState, playerElement: playerElement }); } @@ -489,13 +514,11 @@ // Change instantly to stop video from being loud for a second restoreVolumePreference(); - playerContainer = document.getElementById('shaka-container') as HTMLElement; + playerContainer = document.getElementById('player-container') as HTMLElement; window.addEventListener('resize', updateVideoPlayerHeight); updateVideoPlayerHeight(); - shakaUi = new shaka.ui.Overlay(player, playerContainer, playerElement); - await player.attach(playerElement); shaka.ui.Controls.registerElement('end_time', { @@ -504,33 +527,6 @@ } }); - 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); @@ -715,17 +711,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; @@ -735,7 +720,7 @@ if (document.fullscreenElement) { document.exitFullscreen(); } else { - shakaUi.getControls()?.toggleFullScreen(); + // TODO } return false; }); @@ -771,13 +756,32 @@ }); 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; + }); + try { await loadVideo(); } catch (error: unknown) { @@ -893,19 +897,13 @@ player.unload(); player.destroy(); } - - if (shakaUi) { - shakaUi.destroy(); - } });
{#if showVideoRetry} @@ -948,6 +1140,41 @@ {/if}