218 lines
7.0 KiB
TypeScript
218 lines
7.0 KiB
TypeScript
import React, { useState, useEffect, useCallback } from "react";
|
|
import Paper from "@mui/material/Paper";
|
|
import List from "@mui/material/List";
|
|
import "./PlayerList.css";
|
|
import { MediaControl, MediaAgent, Peer } from "./MediaControl";
|
|
import Box from "@mui/material/Box";
|
|
import { Session, Room } from "./GlobalContext";
|
|
import useWebSocket from "react-use-websocket";
|
|
|
|
type Player = {
|
|
name: string;
|
|
session_id: string;
|
|
live: boolean;
|
|
local: boolean /* Client side variable */;
|
|
protected?: boolean;
|
|
has_media?: boolean; // Whether this Player provides audio/video streams
|
|
bot_run_id?: string;
|
|
bot_provider_id?: string;
|
|
bot_instance_id?: string; // For bot instances
|
|
muted?: boolean;
|
|
video_on?: boolean;
|
|
};
|
|
|
|
type PlayerListProps = {
|
|
socketUrl: string;
|
|
session: Session;
|
|
roomId: string;
|
|
};
|
|
|
|
const PlayerList: React.FC<PlayerListProps> = (props: PlayerListProps) => {
|
|
const { socketUrl, session, roomId } = props;
|
|
const [Players, setPlayers] = useState<Player[] | null>(null);
|
|
const [peers, setPeers] = useState<Record<string, Peer>>({});
|
|
|
|
const sortPlayers = useCallback(
|
|
(A: any, B: any) => {
|
|
if (!session) {
|
|
return 0;
|
|
}
|
|
/* active Player first */
|
|
if (A.name === session.name) {
|
|
return -1;
|
|
}
|
|
if (B.name === session.name) {
|
|
return +1;
|
|
}
|
|
/* Sort active Players first */
|
|
if (A.name && !B.name) {
|
|
return -1;
|
|
}
|
|
if (B.name && !A.name) {
|
|
return +1;
|
|
}
|
|
/* Otherwise, sort by color */
|
|
if (A.color && B.color) {
|
|
return A.color.localeCompare(B.color);
|
|
}
|
|
return 0;
|
|
},
|
|
[session]
|
|
);
|
|
|
|
// Use the WebSocket hook for room events with automatic reconnection
|
|
const { sendJsonMessage } = useWebSocket(socketUrl, {
|
|
share: true,
|
|
shouldReconnect: (closeEvent) => true, // Auto-reconnect on connection loss
|
|
reconnectInterval: 5000,
|
|
onMessage: (event: MessageEvent) => {
|
|
if (!session) {
|
|
return;
|
|
}
|
|
const message = JSON.parse(event.data);
|
|
const data: any = message.data;
|
|
switch (message.type) {
|
|
case "room_state": {
|
|
type RoomStateData = {
|
|
participants: Player[];
|
|
};
|
|
const room_state = data as RoomStateData;
|
|
console.log(`Players - room_state`, room_state.participants);
|
|
room_state.participants.forEach((Player) => {
|
|
Player.local = Player.session_id === session.id;
|
|
});
|
|
room_state.participants.sort(sortPlayers);
|
|
setPlayers(room_state.participants);
|
|
// Initialize peers with remote mute/video state
|
|
setPeers((prevPeers) => {
|
|
const updated: Record<string, Peer> = { ...prevPeers };
|
|
room_state.participants.forEach((Player) => {
|
|
// Only update remote peers, never overwrite local peer object
|
|
if (!Player.local && updated[Player.session_id]) {
|
|
updated[Player.session_id] = {
|
|
...updated[Player.session_id],
|
|
muted: Player.muted ?? false,
|
|
video_on: Player.video_on ?? true,
|
|
};
|
|
}
|
|
});
|
|
return updated;
|
|
});
|
|
break;
|
|
}
|
|
case "update_name": {
|
|
// Update local session name immediately
|
|
if (data && typeof data.name === "string") {
|
|
session.name = data.name;
|
|
}
|
|
break;
|
|
}
|
|
case "peer_state_update": {
|
|
// Update peer state in peers, but do not override local mute
|
|
setPeers((prevPeers) => {
|
|
const updated = { ...prevPeers };
|
|
const peerId = data.peer_id;
|
|
if (peerId && updated[peerId]) {
|
|
updated[peerId] = {
|
|
...updated[peerId],
|
|
muted: data.muted,
|
|
video_on: data.video_on,
|
|
};
|
|
}
|
|
return updated;
|
|
});
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (Players !== null) {
|
|
return;
|
|
}
|
|
sendJsonMessage({
|
|
type: "list_Players",
|
|
});
|
|
}, [Players, sendJsonMessage]);
|
|
|
|
return (
|
|
<Box sx={{ position: "relative", width: "100%" }}>
|
|
<Paper
|
|
className={`PlayerList Medium`}
|
|
sx={{
|
|
maxWidth: { xs: "100%", sm: 500 },
|
|
p: { xs: 1, sm: 2 },
|
|
m: { xs: 0, sm: 2 },
|
|
}}
|
|
>
|
|
<MediaAgent {...{ session, socketUrl, peers, setPeers }} />
|
|
<List className="PlayerSelector">
|
|
{Players?.map((Player) => (
|
|
<Box
|
|
key={Player.session_id}
|
|
sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}
|
|
className={`PlayerEntry ${Player.local ? "PlayerSelf" : ""}`}
|
|
>
|
|
<Box>
|
|
<Box style={{ display: "flex-wrap", alignItems: "center", justifyContent: "space-between" }}>
|
|
<Box style={{ display: "flex-wrap", alignItems: "center" }}>
|
|
<div className="Name">{Player.name ? Player.name : Player.session_id}</div>
|
|
{Player.protected && (
|
|
<div
|
|
style={{ marginLeft: 8, fontSize: "0.8em", color: "#a00" }}
|
|
title="This name is protected with a password"
|
|
>
|
|
🔒
|
|
</div>
|
|
)}
|
|
{Player.bot_instance_id && (
|
|
<div style={{ marginLeft: 8, fontSize: "0.8em", color: "#00a" }} title="This is a bot">
|
|
🤖
|
|
</div>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
{Player.name && !Player.live && <div className="NoNetwork"></div>}
|
|
</Box>
|
|
{Player.name && Player.live && peers[Player.session_id] && (Player.local || Player.has_media !== false) ? (
|
|
<MediaControl
|
|
className="Medium"
|
|
key={Player.session_id}
|
|
peer={peers[Player.session_id]}
|
|
isSelf={Player.local}
|
|
sendJsonMessage={Player.local ? sendJsonMessage : undefined}
|
|
remoteAudioMuted={peers[Player.session_id].muted}
|
|
remoteVideoOff={peers[Player.session_id].video_on === false}
|
|
/>
|
|
) : Player.name && Player.live && Player.has_media === false ? (
|
|
<div
|
|
className="Video fade-in"
|
|
style={{
|
|
background: "#333",
|
|
color: "#fff",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
width: "100%",
|
|
height: "100%",
|
|
fontSize: "14px",
|
|
}}
|
|
>
|
|
💬 Chat Only
|
|
</div>
|
|
) : (
|
|
<video className="Video"></video>
|
|
)}
|
|
</Box>
|
|
))}
|
|
</List>
|
|
</Paper>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export { PlayerList };
|