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.

disable playback speed in watchparty

Pas e8e9352e 2b968031

+51 -33
+2 -1
src/assets/locales/en.json
··· 483 483 }, 484 484 "playback": { 485 485 "speedLabel": "Playback speed", 486 - "title": "Playback settings" 486 + "title": "Playback settings", 487 + "disabled": "(Disabled in watch party)" 487 488 }, 488 489 "quality": { 489 490 "automaticLabel": "Automatic quality",
+22 -3
src/components/player/atoms/settings/PlaybackSettingsView.tsx
··· 1 1 import classNames from "classnames"; 2 - import { useCallback } from "react"; 2 + import { useCallback, useEffect } from "react"; 3 3 import { useTranslation } from "react-i18next"; 4 4 5 5 import { Toggle } from "@/components/buttons/Toggle"; ··· 7 7 import { useOverlayRouter } from "@/hooks/useOverlayRouter"; 8 8 import { usePlayerStore } from "@/stores/player/store"; 9 9 import { usePreferencesStore } from "@/stores/preferences"; 10 + import { useWatchPartyStore } from "@/stores/watchParty"; 10 11 11 12 function ButtonList(props: { 12 13 options: number[]; 13 14 selected: number; 14 15 onClick: (v: any) => void; 16 + disabled?: boolean; 15 17 }) { 16 18 return ( 17 19 <div className="flex items-center bg-video-context-light/10 p-1 rounded-lg"> ··· 19 21 return ( 20 22 <button 21 23 type="button" 24 + disabled={props.disabled} 22 25 className={classNames( 23 26 "w-full px-2 py-1 rounded-md tabbable", 24 27 props.selected === option 25 28 ? "bg-video-context-light/20 text-white" 26 29 : null, 30 + props.disabled ? "opacity-50 cursor-not-allowed" : null, 27 31 )} 28 32 onClick={() => props.onClick(option)} 29 33 key={option} ··· 43 47 const display = usePlayerStore((s) => s.display); 44 48 const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails); 45 49 const setEnableThumbnails = usePreferencesStore((s) => s.setEnableThumbnails); 50 + const isInWatchParty = useWatchPartyStore((s) => s.enabled); 46 51 47 52 const setPlaybackRate = useCallback( 48 53 (v: number) => { 54 + if (isInWatchParty) return; // Don't allow changes in watch party 49 55 display?.setPlaybackRate(v); 50 56 }, 51 - [display], 57 + [display, isInWatchParty], 52 58 ); 53 59 60 + // Force 1x speed in watch party 61 + useEffect(() => { 62 + if (isInWatchParty && display && playbackRate !== 1) { 63 + display.setPlaybackRate(1); 64 + } 65 + }, [isInWatchParty, display, playbackRate]); 66 + 54 67 const options = [0.25, 0.5, 1, 1.5, 2]; 55 68 56 69 return ( ··· 62 75 <div className="space-y-4 mt-3"> 63 76 <Menu.FieldTitle> 64 77 {t("player.menus.playback.speedLabel")} 78 + {isInWatchParty && ( 79 + <span className="text-sm text-type-secondary ml-2"> 80 + {t("player.menus.playback.disabled")} 81 + </span> 82 + )} 65 83 </Menu.FieldTitle> 66 84 <ButtonList 67 85 options={options} 68 - selected={playbackRate} 86 + selected={isInWatchParty ? 1 : playbackRate} 69 87 onClick={setPlaybackRate} 88 + disabled={isInWatchParty} 70 89 /> 71 90 </div> 72 91 </Menu.Section>
+8 -2
src/components/player/internals/KeyboardEvents.tsx
··· 7 7 import { usePlayerStore } from "@/stores/player/store"; 8 8 import { useSubtitleStore } from "@/stores/subtitles"; 9 9 import { useEmpheralVolumeStore } from "@/stores/volume"; 10 + import { useWatchPartyStore } from "@/stores/watchParty"; 10 11 11 12 export function KeyboardEvents() { 12 13 const router = useOverlayRouter(""); ··· 16 17 const mediaPlaying = usePlayerStore((s) => s.mediaPlaying); 17 18 const time = usePlayerStore((s) => s.progress.time); 18 19 const { setVolume, toggleMute } = useVolume(); 20 + const isInWatchParty = useWatchPartyStore((s) => s.enabled); 19 21 20 22 const { toggleLastUsed } = useCaptions(); 21 23 const setShowVolume = useEmpheralVolumeStore((s) => s.setShowVolume); ··· 48 50 delay, 49 51 setShowDelayIndicator, 50 52 setCurrentOverlay, 53 + isInWatchParty, 51 54 }); 55 + 52 56 useEffect(() => { 53 57 dataRef.current = { 54 58 setShowVolume, ··· 67 71 delay, 68 72 setShowDelayIndicator, 69 73 setCurrentOverlay, 74 + isInWatchParty, 70 75 }; 71 76 }, [ 72 77 setShowVolume, ··· 85 90 delay, 86 91 setShowDelayIndicator, 87 92 setCurrentOverlay, 93 + isInWatchParty, 88 94 ]); 89 95 90 96 useEffect(() => { ··· 116 122 ); 117 123 if (keyL === "m") dataRef.current.toggleMute(); 118 124 119 - // Video playback speed 120 - if (k === ">" || k === "<") { 125 + // Video playback speed - disabled in watch party 126 + if ((k === ">" || k === "<") && !dataRef.current.isInWatchParty) { 121 127 const options = [0.25, 0.5, 1, 1.5, 2]; 122 128 let idx = options.indexOf(dataRef.current.mediaPlaying?.playbackRate); 123 129 if (idx === -1) idx = options.indexOf(1);
+1 -23
src/hooks/useWatchPartySync.ts
··· 17 17 isPaused: boolean; 18 18 time: number; 19 19 duration: number; 20 - playbackRate: number; 21 20 }; 22 21 content: { 23 22 title: string; ··· 80 79 const display = usePlayerStore((s) => s.display); 81 80 const currentTime = usePlayerStore((s) => s.progress.time); 82 81 const isPlaying = usePlayerStore((s) => s.mediaPlaying.isPlaying); 83 - const currentPlaybackRate = usePlayerStore( 84 - (s) => s.mediaPlaying.playbackRate, 85 - ); 86 - 87 82 // Get watch party state 88 83 const { roomCode, isHost, enabled, enableAsGuest } = useWatchPartyStore(); 89 84 ··· 169 164 const predictedHostTime = getPredictedHostTime(); 170 165 const difference = currentTime - predictedHostTime; 171 166 172 - // Handle playback rate sync 173 - const needsPlaybackRateSync = 174 - hostUser.player.playbackRate !== currentPlaybackRate; 175 - 176 167 // Handle time sync 177 168 const activeThreshold = isPlaying ? 2 : 5; 178 169 const needsTimeSync = Math.abs(difference) > activeThreshold; ··· 188 179 Math.abs(hostUser.player.time - state.previousHostTime) > 5; 189 180 190 181 // Sync if needed 191 - if ( 192 - (needsTimeSync || 193 - needsPlayStateSync || 194 - needsJumpSync || 195 - needsPlaybackRateSync) && 196 - !isSyncing 197 - ) { 182 + if ((needsTimeSync || needsPlayStateSync || needsJumpSync) && !isSyncing) { 198 183 state.syncInProgress = true; 199 184 setIsSyncing(true); 200 185 201 - // Sync playback rate first if needed 202 - if (needsPlaybackRateSync) { 203 - display.setPlaybackRate(hostUser.player.playbackRate); 204 - } 205 - 206 186 // Sync time 207 187 display.setTime(predictedHostTime); 208 188 ··· 229 209 hostUser, 230 210 isHost, 231 211 currentTime, 232 - currentPlaybackRate, 233 212 display, 234 213 isSyncing, 235 214 getPredictedHostTime, ··· 262 241 isPaused: latestStatus.player.isPaused, 263 242 time: latestStatus.player.time, 264 243 duration: latestStatus.player.duration, 265 - playbackRate: latestStatus.player.playbackRate, 266 244 }, 267 245 content: { 268 246 title: latestStatus.content.title,
+18 -4
src/stores/watchParty.ts
··· 1 1 import { create } from "zustand"; 2 2 import { persist } from "zustand/middleware"; 3 3 4 + import { usePlayerStore } from "@/stores/player/store"; 5 + 4 6 interface WatchPartyStore { 5 7 // Whether the watch party feature is enabled 6 8 enabled: boolean; ··· 27 29 return Math.floor(1000 + Math.random() * 9000).toString(); 28 30 }; 29 31 32 + // Helper function to reset playback rate to 1x 33 + const resetPlaybackRate = () => { 34 + const display = usePlayerStore.getState().display; 35 + if (display) { 36 + display.setPlaybackRate(1); 37 + } 38 + }; 39 + 30 40 export const useWatchPartyStore = create<WatchPartyStore>()( 31 41 persist( 32 42 (set) => ({ ··· 35 45 isHost: false, 36 46 showStatusOverlay: false, 37 47 38 - enableAsHost: () => 48 + enableAsHost: () => { 49 + resetPlaybackRate(); 39 50 set(() => ({ 40 51 enabled: true, 41 52 roomCode: generateRoomCode(), 42 53 isHost: true, 43 - })), 54 + })); 55 + }, 44 56 45 - enableAsGuest: (code: string) => 57 + enableAsGuest: (code: string) => { 58 + resetPlaybackRate(); 46 59 set(() => ({ 47 60 enabled: true, 48 61 roomCode: code, 49 62 isHost: false, 50 - })), 63 + })); 64 + }, 51 65 52 66 updateRoomCode: (code: string) => 53 67 set((state) => ({