disable playback speed in watchparty

This commit is contained in:
Pas 2025-06-05 12:25:22 -06:00
parent 2b9680313c
commit e8e9352e88
5 changed files with 51 additions and 33 deletions

View file

@ -483,7 +483,8 @@
}, },
"playback": { "playback": {
"speedLabel": "Playback speed", "speedLabel": "Playback speed",
"title": "Playback settings" "title": "Playback settings",
"disabled": "(Disabled in watch party)"
}, },
"quality": { "quality": {
"automaticLabel": "Automatic quality", "automaticLabel": "Automatic quality",

View file

@ -1,5 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import { useCallback } from "react"; import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Toggle } from "@/components/buttons/Toggle"; import { Toggle } from "@/components/buttons/Toggle";
@ -7,11 +7,13 @@ import { Menu } from "@/components/player/internals/ContextMenu";
import { useOverlayRouter } from "@/hooks/useOverlayRouter"; import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences"; import { usePreferencesStore } from "@/stores/preferences";
import { useWatchPartyStore } from "@/stores/watchParty";
function ButtonList(props: { function ButtonList(props: {
options: number[]; options: number[];
selected: number; selected: number;
onClick: (v: any) => void; onClick: (v: any) => void;
disabled?: boolean;
}) { }) {
return ( return (
<div className="flex items-center bg-video-context-light/10 p-1 rounded-lg"> <div className="flex items-center bg-video-context-light/10 p-1 rounded-lg">
@ -19,11 +21,13 @@ function ButtonList(props: {
return ( return (
<button <button
type="button" type="button"
disabled={props.disabled}
className={classNames( className={classNames(
"w-full px-2 py-1 rounded-md tabbable", "w-full px-2 py-1 rounded-md tabbable",
props.selected === option props.selected === option
? "bg-video-context-light/20 text-white" ? "bg-video-context-light/20 text-white"
: null, : null,
props.disabled ? "opacity-50 cursor-not-allowed" : null,
)} )}
onClick={() => props.onClick(option)} onClick={() => props.onClick(option)}
key={option} key={option}
@ -43,14 +47,23 @@ export function PlaybackSettingsView({ id }: { id: string }) {
const display = usePlayerStore((s) => s.display); const display = usePlayerStore((s) => s.display);
const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails); const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails);
const setEnableThumbnails = usePreferencesStore((s) => s.setEnableThumbnails); const setEnableThumbnails = usePreferencesStore((s) => s.setEnableThumbnails);
const isInWatchParty = useWatchPartyStore((s) => s.enabled);
const setPlaybackRate = useCallback( const setPlaybackRate = useCallback(
(v: number) => { (v: number) => {
if (isInWatchParty) return; // Don't allow changes in watch party
display?.setPlaybackRate(v); display?.setPlaybackRate(v);
}, },
[display], [display, isInWatchParty],
); );
// Force 1x speed in watch party
useEffect(() => {
if (isInWatchParty && display && playbackRate !== 1) {
display.setPlaybackRate(1);
}
}, [isInWatchParty, display, playbackRate]);
const options = [0.25, 0.5, 1, 1.5, 2]; const options = [0.25, 0.5, 1, 1.5, 2];
return ( return (
@ -62,11 +75,17 @@ export function PlaybackSettingsView({ id }: { id: string }) {
<div className="space-y-4 mt-3"> <div className="space-y-4 mt-3">
<Menu.FieldTitle> <Menu.FieldTitle>
{t("player.menus.playback.speedLabel")} {t("player.menus.playback.speedLabel")}
{isInWatchParty && (
<span className="text-sm text-type-secondary ml-2">
{t("player.menus.playback.disabled")}
</span>
)}
</Menu.FieldTitle> </Menu.FieldTitle>
<ButtonList <ButtonList
options={options} options={options}
selected={playbackRate} selected={isInWatchParty ? 1 : playbackRate}
onClick={setPlaybackRate} onClick={setPlaybackRate}
disabled={isInWatchParty}
/> />
</div> </div>
</Menu.Section> </Menu.Section>

View file

@ -7,6 +7,7 @@ import { useOverlayStack } from "@/stores/interface/overlayStack";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
import { useSubtitleStore } from "@/stores/subtitles"; import { useSubtitleStore } from "@/stores/subtitles";
import { useEmpheralVolumeStore } from "@/stores/volume"; import { useEmpheralVolumeStore } from "@/stores/volume";
import { useWatchPartyStore } from "@/stores/watchParty";
export function KeyboardEvents() { export function KeyboardEvents() {
const router = useOverlayRouter(""); const router = useOverlayRouter("");
@ -16,6 +17,7 @@ export function KeyboardEvents() {
const mediaPlaying = usePlayerStore((s) => s.mediaPlaying); const mediaPlaying = usePlayerStore((s) => s.mediaPlaying);
const time = usePlayerStore((s) => s.progress.time); const time = usePlayerStore((s) => s.progress.time);
const { setVolume, toggleMute } = useVolume(); const { setVolume, toggleMute } = useVolume();
const isInWatchParty = useWatchPartyStore((s) => s.enabled);
const { toggleLastUsed } = useCaptions(); const { toggleLastUsed } = useCaptions();
const setShowVolume = useEmpheralVolumeStore((s) => s.setShowVolume); const setShowVolume = useEmpheralVolumeStore((s) => s.setShowVolume);
@ -48,7 +50,9 @@ export function KeyboardEvents() {
delay, delay,
setShowDelayIndicator, setShowDelayIndicator,
setCurrentOverlay, setCurrentOverlay,
isInWatchParty,
}); });
useEffect(() => { useEffect(() => {
dataRef.current = { dataRef.current = {
setShowVolume, setShowVolume,
@ -67,6 +71,7 @@ export function KeyboardEvents() {
delay, delay,
setShowDelayIndicator, setShowDelayIndicator,
setCurrentOverlay, setCurrentOverlay,
isInWatchParty,
}; };
}, [ }, [
setShowVolume, setShowVolume,
@ -85,6 +90,7 @@ export function KeyboardEvents() {
delay, delay,
setShowDelayIndicator, setShowDelayIndicator,
setCurrentOverlay, setCurrentOverlay,
isInWatchParty,
]); ]);
useEffect(() => { useEffect(() => {
@ -116,8 +122,8 @@ export function KeyboardEvents() {
); );
if (keyL === "m") dataRef.current.toggleMute(); if (keyL === "m") dataRef.current.toggleMute();
// Video playback speed // Video playback speed - disabled in watch party
if (k === ">" || k === "<") { if ((k === ">" || k === "<") && !dataRef.current.isInWatchParty) {
const options = [0.25, 0.5, 1, 1.5, 2]; const options = [0.25, 0.5, 1, 1.5, 2];
let idx = options.indexOf(dataRef.current.mediaPlaying?.playbackRate); let idx = options.indexOf(dataRef.current.mediaPlaying?.playbackRate);
if (idx === -1) idx = options.indexOf(1); if (idx === -1) idx = options.indexOf(1);

View file

@ -17,7 +17,6 @@ interface RoomUser {
isPaused: boolean; isPaused: boolean;
time: number; time: number;
duration: number; duration: number;
playbackRate: number;
}; };
content: { content: {
title: string; title: string;
@ -80,10 +79,6 @@ export function useWatchPartySync(
const display = usePlayerStore((s) => s.display); const display = usePlayerStore((s) => s.display);
const currentTime = usePlayerStore((s) => s.progress.time); const currentTime = usePlayerStore((s) => s.progress.time);
const isPlaying = usePlayerStore((s) => s.mediaPlaying.isPlaying); const isPlaying = usePlayerStore((s) => s.mediaPlaying.isPlaying);
const currentPlaybackRate = usePlayerStore(
(s) => s.mediaPlaying.playbackRate,
);
// Get watch party state // Get watch party state
const { roomCode, isHost, enabled, enableAsGuest } = useWatchPartyStore(); const { roomCode, isHost, enabled, enableAsGuest } = useWatchPartyStore();
@ -169,10 +164,6 @@ export function useWatchPartySync(
const predictedHostTime = getPredictedHostTime(); const predictedHostTime = getPredictedHostTime();
const difference = currentTime - predictedHostTime; const difference = currentTime - predictedHostTime;
// Handle playback rate sync
const needsPlaybackRateSync =
hostUser.player.playbackRate !== currentPlaybackRate;
// Handle time sync // Handle time sync
const activeThreshold = isPlaying ? 2 : 5; const activeThreshold = isPlaying ? 2 : 5;
const needsTimeSync = Math.abs(difference) > activeThreshold; const needsTimeSync = Math.abs(difference) > activeThreshold;
@ -188,21 +179,10 @@ export function useWatchPartySync(
Math.abs(hostUser.player.time - state.previousHostTime) > 5; Math.abs(hostUser.player.time - state.previousHostTime) > 5;
// Sync if needed // Sync if needed
if ( if ((needsTimeSync || needsPlayStateSync || needsJumpSync) && !isSyncing) {
(needsTimeSync ||
needsPlayStateSync ||
needsJumpSync ||
needsPlaybackRateSync) &&
!isSyncing
) {
state.syncInProgress = true; state.syncInProgress = true;
setIsSyncing(true); setIsSyncing(true);
// Sync playback rate first if needed
if (needsPlaybackRateSync) {
display.setPlaybackRate(hostUser.player.playbackRate);
}
// Sync time // Sync time
display.setTime(predictedHostTime); display.setTime(predictedHostTime);
@ -229,7 +209,6 @@ export function useWatchPartySync(
hostUser, hostUser,
isHost, isHost,
currentTime, currentTime,
currentPlaybackRate,
display, display,
isSyncing, isSyncing,
getPredictedHostTime, getPredictedHostTime,
@ -262,7 +241,6 @@ export function useWatchPartySync(
isPaused: latestStatus.player.isPaused, isPaused: latestStatus.player.isPaused,
time: latestStatus.player.time, time: latestStatus.player.time,
duration: latestStatus.player.duration, duration: latestStatus.player.duration,
playbackRate: latestStatus.player.playbackRate,
}, },
content: { content: {
title: latestStatus.content.title, title: latestStatus.content.title,

View file

@ -1,6 +1,8 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { usePlayerStore } from "@/stores/player/store";
interface WatchPartyStore { interface WatchPartyStore {
// Whether the watch party feature is enabled // Whether the watch party feature is enabled
enabled: boolean; enabled: boolean;
@ -27,6 +29,14 @@ const generateRoomCode = (): string => {
return Math.floor(1000 + Math.random() * 9000).toString(); return Math.floor(1000 + Math.random() * 9000).toString();
}; };
// Helper function to reset playback rate to 1x
const resetPlaybackRate = () => {
const display = usePlayerStore.getState().display;
if (display) {
display.setPlaybackRate(1);
}
};
export const useWatchPartyStore = create<WatchPartyStore>()( export const useWatchPartyStore = create<WatchPartyStore>()(
persist( persist(
(set) => ({ (set) => ({
@ -35,19 +45,23 @@ export const useWatchPartyStore = create<WatchPartyStore>()(
isHost: false, isHost: false,
showStatusOverlay: false, showStatusOverlay: false,
enableAsHost: () => enableAsHost: () => {
resetPlaybackRate();
set(() => ({ set(() => ({
enabled: true, enabled: true,
roomCode: generateRoomCode(), roomCode: generateRoomCode(),
isHost: true, isHost: true,
})), }));
},
enableAsGuest: (code: string) => enableAsGuest: (code: string) => {
resetPlaybackRate();
set(() => ({ set(() => ({
enabled: true, enabled: true,
roomCode: code, roomCode: code,
isHost: false, isHost: false,
})), }));
},
updateRoomCode: (code: string) => updateRoomCode: (code: string) =>
set((state) => ({ set((state) => ({