mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-20 22:42:09 +00:00
disable playback speed in watchparty
This commit is contained in:
parent
2b9680313c
commit
e8e9352e88
5 changed files with 51 additions and 33 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) => ({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue