mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-31 19:18:26 +01:00
feat(sync-play): squash feature/sync-play
This commit is contained in:
87
providers/SyncPlay/useSyncPlayWebSocket.ts
Normal file
87
providers/SyncPlay/useSyncPlayWebSocket.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* useSyncPlayWebSocket
|
||||
*
|
||||
* Hook that connects the SyncPlay manager to WebSocket messages.
|
||||
* Listens for SyncPlayCommand and SyncPlayGroupUpdate messages.
|
||||
*
|
||||
* IMPORTANT: We subscribe directly to the WebSocket via `addEventListener`
|
||||
* rather than reading WebSocketProvider's `lastMessage` state. That state
|
||||
* only holds the most recent message, so when the server emits bursts
|
||||
* after a join (GroupJoined + StateUpdate + UserJoined + PlayQueue, all
|
||||
* within a few ms), React's batching causes earlier messages to be
|
||||
* overwritten before our effect can read them — most notably the
|
||||
* GroupJoined message, which left the joining client thinking it hadn't
|
||||
* joined while other members already saw it as a participant.
|
||||
*
|
||||
* Listening on the raw socket guarantees we see every frame in order.
|
||||
*/
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useWebSocketContext } from "@/providers/WebSocketProvider";
|
||||
import type { SyncPlayManager } from "./Manager";
|
||||
import type { SendCommand } from "./types";
|
||||
|
||||
/**
|
||||
* Hook to connect SyncPlay manager to WebSocket
|
||||
*/
|
||||
export function useSyncPlayWebSocket(manager: SyncPlayManager | null): void {
|
||||
const { ws } = useWebSocketContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (!ws || !manager) return;
|
||||
|
||||
const handleMessage = (event: WebSocketMessageEvent) => {
|
||||
let parsed: { MessageType?: string; Data?: unknown };
|
||||
try {
|
||||
parsed = JSON.parse(event.data as string);
|
||||
} catch (error) {
|
||||
console.error("SyncPlay: failed to parse WebSocket message", error);
|
||||
return;
|
||||
}
|
||||
|
||||
const { MessageType, Data } = parsed;
|
||||
|
||||
// Only handle SyncPlay messages here; everything else is handled
|
||||
// elsewhere via WebSocketProvider's lastMessage.
|
||||
if (!MessageType?.startsWith("SyncPlay")) return;
|
||||
|
||||
console.log(
|
||||
`SyncPlay WebSocket [${MessageType}]:`,
|
||||
JSON.stringify(Data).substring(0, 300),
|
||||
);
|
||||
|
||||
switch (MessageType) {
|
||||
case "SyncPlayCommand": {
|
||||
const command = Data as SendCommand;
|
||||
console.log(
|
||||
`SyncPlay: COMMAND received - ${command.Command} at ${command.When}`,
|
||||
command.Command === "Seek"
|
||||
? `position=${command.PositionTicks}`
|
||||
: "",
|
||||
);
|
||||
|
||||
// Note: it's normal for controls to be missing here during the
|
||||
// join → navigate → load window. Manager stashes the command and
|
||||
// replays it on attach.
|
||||
manager.processCommand(command);
|
||||
break;
|
||||
}
|
||||
|
||||
case "SyncPlayGroupUpdate": {
|
||||
const update = Data as { Type?: string; Data?: unknown };
|
||||
console.debug("SyncPlay: group update -", update.Type);
|
||||
manager.processGroupUpdate(update);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.addEventListener("message", handleMessage);
|
||||
return () => {
|
||||
ws.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, [ws, manager]);
|
||||
}
|
||||
Reference in New Issue
Block a user