pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #721 from qtchaos/mediasession

Implement MediaSession support

authored by

William Oldham and committed by
GitHub
9409922e fcee7001

+184
+2
src/components/player/base/Container.tsx
··· 4 4 import { CastingInternal } from "@/components/player/internals/CastingInternal"; 5 5 import { HeadUpdater } from "@/components/player/internals/HeadUpdater"; 6 6 import { KeyboardEvents } from "@/components/player/internals/KeyboardEvents"; 7 + import { MediaSession } from "@/components/player/internals/MediaSession"; 7 8 import { MetaReporter } from "@/components/player/internals/MetaReporter"; 8 9 import { ProgressSaver } from "@/components/player/internals/ProgressSaver"; 9 10 import { ThumbnailScraper } from "@/components/player/internals/ThumbnailScraper"; ··· 91 92 <VideoContainer /> 92 93 <ProgressSaver /> 93 94 <KeyboardEvents /> 95 + <MediaSession /> 94 96 <div className="relative h-screen overflow-hidden"> 95 97 <VideoClickTarget showingControls={props.showingControls} /> 96 98 <HeadUpdater />
+182
src/components/player/internals/MediaSession.tsx
··· 1 + import { useCallback, useEffect, useRef } from "react"; 2 + 3 + import { usePlayerStore } from "@/stores/player/store"; 4 + 5 + import { usePlayerMeta } from "../hooks/usePlayerMeta"; 6 + 7 + export function MediaSession() { 8 + const { setDirectMeta } = usePlayerMeta(); 9 + const setShouldStartFromBeginning = usePlayerStore( 10 + (s) => s.setShouldStartFromBeginning, 11 + ); 12 + 13 + const shouldUpdatePositionState = useRef(false); 14 + const lastPlaybackPosition = useRef(0); 15 + 16 + const data = usePlayerStore.getState(); 17 + 18 + const changeEpisode = useCallback( 19 + (change: number) => { 20 + const nextEp = data.meta?.episodes?.find( 21 + (v) => v.number === (data.meta?.episode?.number ?? 0) + change, 22 + ); 23 + 24 + if (!data.meta || !nextEp) return; 25 + const metaCopy = { ...data.meta }; 26 + metaCopy.episode = nextEp; 27 + setShouldStartFromBeginning(true); 28 + setDirectMeta(metaCopy); 29 + }, 30 + [data.meta, setDirectMeta, setShouldStartFromBeginning], 31 + ); 32 + 33 + const updatePositionState = useCallback( 34 + (position: number) => { 35 + // If the updated position needs to be buffered, queue an update 36 + if (position > data.progress.buffered) { 37 + shouldUpdatePositionState.current = true; 38 + } 39 + if (position > data.progress.duration) return; 40 + 41 + lastPlaybackPosition.current = data.progress.time; 42 + navigator.mediaSession.setPositionState({ 43 + duration: data.progress.duration, 44 + playbackRate: data.mediaPlaying.playbackRate, 45 + position, 46 + }); 47 + }, 48 + [ 49 + data.mediaPlaying.playbackRate, 50 + data.progress.buffered, 51 + data.progress.duration, 52 + data.progress.time, 53 + ], 54 + ); 55 + 56 + useEffect(() => { 57 + if (!("mediaSession" in navigator)) return; 58 + 59 + // If the media is paused, update the navigator 60 + if (data.mediaPlaying.isPaused) { 61 + navigator.mediaSession.playbackState = "paused"; 62 + } else { 63 + navigator.mediaSession.playbackState = "playing"; 64 + } 65 + }, [data.mediaPlaying.isPaused]); 66 + 67 + useEffect(() => { 68 + if (!("mediaSession" in navigator)) return; 69 + 70 + updatePositionState(data.progress.time); 71 + }, [data.progress.time, data.mediaPlaying.playbackRate, updatePositionState]); 72 + 73 + useEffect(() => { 74 + if (!("mediaSession" in navigator)) return; 75 + // If not already updating the position state, and the media is loading, queue an update 76 + if (!shouldUpdatePositionState.current && data.mediaPlaying.isLoading) { 77 + shouldUpdatePositionState.current = true; 78 + } 79 + 80 + // If the user has skipped (or MediaSession desynced) by more than 5 seconds, queue an update 81 + if ( 82 + Math.abs(data.progress.time - lastPlaybackPosition.current) >= 5 && 83 + !data.mediaPlaying.isLoading && 84 + !shouldUpdatePositionState.current 85 + ) { 86 + shouldUpdatePositionState.current = true; 87 + } 88 + 89 + // If not loading and the position state is queued, update it 90 + if (shouldUpdatePositionState.current && !data.mediaPlaying.isLoading) { 91 + shouldUpdatePositionState.current = false; 92 + updatePositionState(data.progress.time); 93 + } 94 + 95 + lastPlaybackPosition.current = data.progress.time; 96 + }, [updatePositionState, data.progress.time, data.mediaPlaying.isLoading]); 97 + 98 + useEffect(() => { 99 + if ( 100 + !("mediaSession" in navigator) || 101 + (!data.mediaPlaying.isLoading && 102 + data.mediaPlaying.isPlaying && 103 + !data.display) 104 + ) 105 + return; 106 + 107 + let title: string | undefined; 108 + let artist: string | undefined; 109 + 110 + if (data.meta?.type === "movie") { 111 + title = data.meta?.title; 112 + } else if (data.meta?.type === "show") { 113 + artist = data.meta?.title; 114 + title = `S${data.meta?.season?.number} E${data.meta?.episode?.number}: ${data.meta?.episode?.title}`; 115 + } 116 + 117 + navigator.mediaSession.metadata = new MediaMetadata({ 118 + title, 119 + artist, 120 + artwork: [ 121 + { 122 + src: data.meta?.poster ?? "", 123 + sizes: "342x513", 124 + type: "image/png", 125 + }, 126 + ], 127 + }); 128 + 129 + navigator.mediaSession.setActionHandler("play", () => { 130 + if (data.mediaPlaying.isLoading) return; 131 + data.display?.play(); 132 + 133 + updatePositionState(data.progress.time); 134 + }); 135 + 136 + navigator.mediaSession.setActionHandler("pause", () => { 137 + if (data.mediaPlaying.isLoading) return; 138 + data.display?.pause(); 139 + 140 + updatePositionState(data.progress.time); 141 + }); 142 + 143 + navigator.mediaSession.setActionHandler("seekto", (e) => { 144 + if (!e.seekTime) return; 145 + data.display?.setTime(e.seekTime); 146 + updatePositionState(e.seekTime); 147 + }); 148 + 149 + if ((data.meta?.episode?.number ?? 1) !== 1) { 150 + navigator.mediaSession.setActionHandler("previoustrack", () => { 151 + changeEpisode(-1); 152 + }); 153 + } else { 154 + navigator.mediaSession.setActionHandler("previoustrack", null); 155 + } 156 + 157 + if (data.meta?.episode?.number !== data.meta?.episodes?.length) { 158 + navigator.mediaSession.setActionHandler("nexttrack", () => { 159 + changeEpisode(1); 160 + }); 161 + } else { 162 + navigator.mediaSession.setActionHandler("nexttrack", null); 163 + } 164 + }, [ 165 + changeEpisode, 166 + updatePositionState, 167 + data.mediaPlaying.hasPlayedOnce, 168 + data.mediaPlaying.isLoading, 169 + data.progress.duration, 170 + data.progress.time, 171 + data.meta?.episode?.number, 172 + data.meta?.episodes?.length, 173 + data.display, 174 + data.mediaPlaying, 175 + data.meta?.episode?.title, 176 + data.meta?.title, 177 + data.meta?.type, 178 + data.meta?.poster, 179 + data.meta?.season?.number, 180 + ]); 181 + return null; 182 + }