Moved to simple peer

This commit is contained in:
Mitchell McCaffrey
2020-04-05 22:20:34 +10:00
parent 7ac13c1145
commit 8361d6eb0b
11 changed files with 475 additions and 443 deletions
+3 -8
View File
@@ -1,7 +1,6 @@
import React from "react";
import { useRoutes } from "hookrouter";
import { ThemeProvider } from "theme-ui";
import { GameProvider } from "./contexts/GameContext";
import theme from "./theme.js";
import Home from "./routes/Home";
@@ -10,17 +9,13 @@ import Join from "./routes/Join";
const routes = {
"/": () => <Home />,
"/game": () => <Game />,
"/join": () => <Join />
"/game/:id": ({ id }) => <Game gameId={id} />,
"/join": () => <Join />,
};
function App() {
const route = useRoutes(routes);
return (
<ThemeProvider theme={theme}>
<GameProvider>{route}</GameProvider>
</ThemeProvider>
);
return <ThemeProvider theme={theme}>{route}</ThemeProvider>;
}
export default App;
+2 -2
View File
@@ -3,7 +3,7 @@ import { IconButton, Flex, Box, Label, Text } from "theme-ui";
import Modal from "./Modal";
function AddPartyMemberButton({ peerId }) {
function AddPartyMemberButton({ gameId }) {
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
function openModal() {
setIsAddModalOpen(true);
@@ -32,7 +32,7 @@ function AddPartyMemberButton({ peerId }) {
<Box>
<Label p={2}>Other people can join using your ID ʕʔ</Label>
<Box p={2} bg="hsla(230, 20%, 0%, 20%)">
<Text>{peerId}</Text>
<Text>{gameId}</Text>
</Box>
</Box>
</Modal>
+10 -7
View File
@@ -3,7 +3,7 @@ import { Flex, Box, Text } from "theme-ui";
import AddPartyMemberButton from "./AddPartyMemberButton";
function Party({ nicknames, peerId, onNicknameChange }) {
function Party({ nickname, partyNicknames, gameId, onNicknameChange }) {
return (
<Flex
p={3}
@@ -13,12 +13,12 @@ function Party({ nicknames, peerId, onNicknameChange }) {
width: "96px",
minWidth: "96px",
overflowY: "auto",
alignItems: "center"
alignItems: "center",
}}
>
<Box
sx={{
width: "100%"
width: "100%",
}}
>
<Text mb={1} variant="heading">
@@ -28,17 +28,20 @@ function Party({ nicknames, peerId, onNicknameChange }) {
<Box
sx={{
flexGrow: 1,
width: "100%"
width: "100%",
}}
>
{Object.entries(nicknames).map(([id, nickname]) => (
<Text my={1} variant="caption" sx={{ fontSize: 10 }}>
{nickname || ""} (you)
</Text>
{Object.entries(partyNicknames).map(([id, partyNickname]) => (
<Text my={1} variant="caption" sx={{ fontSize: 10 }} key={id}>
{nickname} {id === peerId ? "(you)" : ""}
{partyNickname}
</Text>
))}
</Box>
<Box>
<AddPartyMemberButton peerId={peerId} />
<AddPartyMemberButton gameId={gameId} />
</Box>
</Flex>
);
-11
View File
@@ -1,11 +0,0 @@
import React, { useState } from "react";
const GameContext = React.createContext();
export function GameProvider({ children }) {
const [gameId, setGameId] = useState(null);
const value = { gameId, setGameId };
return <GameContext.Provider value={value}>{children}</GameContext.Provider>;
}
export default GameContext;
+97
View File
@@ -0,0 +1,97 @@
import SimplePeer from "simple-peer";
import BinaryPack from "js-binarypack";
import toBuffer from "blob-to-buffer";
import shortid from "shortid";
// Limit buffer size to 16kb to avoid issues with chrome packet size
// http://viblast.com/blog/2015/2/5/webrtc-data-channel-message-size/
const MAX_BUFFER_SIZE = 16000;
class Peer extends SimplePeer {
constructor(props) {
super(props);
this.currentChunks = {};
this.on("data", (packed) => {
const unpacked = BinaryPack.unpack(packed);
// If the special property __chunked is set and true
// The data is a partial chunk of the a larger file
// So wait until all chunks are collected and assembled
// before emitting the dataComplete event
if (unpacked.__chunked) {
let chunk = this.currentChunks[unpacked.id] || {
data: [],
count: 0,
total: unpacked.total,
};
chunk.data[unpacked.index] = unpacked.data;
chunk.count++;
this.currentChunks[unpacked.id] = chunk;
// All chunks have been loaded
if (chunk.count === chunk.total) {
const merged = BinaryPack.unpack(Buffer.concat(chunk.data));
this.emit("dataComplete", merged);
delete this.currentChunks[unpacked.id];
}
} else {
this.emit("dataComplete", unpacked);
}
});
}
sendPackedData(packedData) {
toBuffer(packedData, (error, buffer) => {
if (error) {
throw error;
}
super.send(buffer);
});
}
send(data) {
const packedData = BinaryPack.pack(data);
if (packedData.size > MAX_BUFFER_SIZE) {
const chunks = this.chunk(packedData);
for (let chunk of chunks) {
this.sendPackedData(BinaryPack.pack(chunk));
}
return;
} else {
this.sendPackedData(packedData);
}
}
// Converted from https://github.com/peers/peerjs/
chunk(blob) {
const chunks = [];
const size = blob.size;
const total = Math.ceil(size / MAX_BUFFER_SIZE);
const id = shortid.generate();
let index = 0;
let start = 0;
while (start < size) {
const end = Math.min(size, start + MAX_BUFFER_SIZE);
const slice = blob.slice(start, end);
const chunk = {
__chunked: true,
data: slice,
id,
index,
total,
};
chunks.push(chunk);
start = end;
index++;
}
return chunks;
}
}
export default Peer;
+64 -110
View File
@@ -1,132 +1,86 @@
import { useEffect, useState } from "react";
import Peer from "peerjs";
import io from "socket.io-client";
function useSession(onConnectionOpen, onConnectionSync) {
const [peerId, setPeerId] = useState(null);
const [peer, setPeer] = useState(null);
const [connections, setConnections] = useState({});
import { omit } from "../helpers/shared";
import Peer from "../helpers/Peer";
function addConnection(connection) {
console.log("Adding connection", connection.peer);
setConnections(prevConnnections => {
console.log("Connections", {
...prevConnnections,
[connection.peer]: connection
const socket = io("http://localhost:9000");
function useSession(partyId, onPeerConnected, onPeerDisconnected, onPeerData) {
useEffect(() => {
socket.emit("join party", partyId);
}, [partyId]);
const [peers, setPeers] = useState({});
useEffect(() => {
function addPeer(id, initiator) {
const peer = new Peer({ initiator, trickle: false });
peer.on("signal", (signal) => {
socket.emit("signal", JSON.stringify({ to: id, signal }));
});
return {
...prevConnnections,
[connection.peer]: connection
};
});
}
useEffect(() => {
console.log("Creating peer");
setPeer(new Peer());
}, []);
peer.on("connect", () => {
onPeerConnected && onPeerConnected({ id, peer, initiator });
});
useEffect(() => {
function handleOpen(id) {
console.log("Peer open", id);
setPeerId(id);
peer.on("dataComplete", (data) => {
onPeerData && onPeerData({ id, peer, data });
});
peer.on("close", () => {
onPeerDisconnected && onPeerDisconnected(id);
});
peer.on("error", (err) => {
onPeerDisconnected && onPeerDisconnected(id);
console.error("error", err);
});
setPeers((prevPeers) => ({
...prevPeers,
[id]: peer,
}));
}
function handleConnection(connection) {
connection.on("open", () => {
console.log("incoming connection added", connection.peer);
const metadata = connection.metadata;
if (metadata.sync) {
connection.send({
id: "sync",
data: { connections: Object.keys(connections) }
});
if (onConnectionSync) {
onConnectionSync(connection);
}
}
function handlePartyMemberJoined(id) {
addPeer(id, false);
}
addConnection(connection);
if (onConnectionOpen) {
onConnectionOpen(connection);
}
});
function removeConnection() {
console.log("removing connection", connection.peer);
setConnections(prevConnections => {
const { [connection.peer]: old, ...rest } = prevConnections;
return rest;
});
function handlePartyMemberLeft(id) {
if (id in peers) {
peers[id].destroy();
}
connection.on("close", removeConnection);
connection.on("error", error => {
console.error("Data Connection error", error);
removeConnection();
});
setPeers((prevPeers) => omit(prevPeers, [id]));
}
function handleError(error) {
console.error("Peer error", error);
function handleJoinedParty(otherIds) {
for (let id of otherIds) {
addPeer(id, true);
}
}
if (!peer) {
return;
function handleSignal(data) {
const { from, signal } = JSON.parse(data);
if (from in peers) {
peers[from].signal(signal);
}
}
peer.on("open", handleOpen);
peer.on("connection", handleConnection);
peer.on("error", handleError);
socket.on("party member joined", handlePartyMemberJoined);
socket.on("party member left", handlePartyMemberLeft);
socket.on("joined party", handleJoinedParty);
socket.on("signal", handleSignal);
return () => {
peer.removeListener("open", handleOpen);
peer.removeListener("connection", handleConnection);
peer.removeListener("error", handleError);
socket.removeListener("party member joined", handlePartyMemberJoined);
socket.removeListener("party member left", handlePartyMemberLeft);
socket.removeListener("joined party", handleJoinedParty);
socket.removeListener("signal", handleSignal);
};
}, [peer, peerId, connections, onConnectionOpen, onConnectionSync]);
}, [peers, onPeerConnected, onPeerDisconnected, onPeerData]);
function connectTo(connectionId, payload) {
console.log("Connecting to", connectionId);
if (connectionId in connections) {
return;
}
const connection = peer.connect(connectionId, {
metadata: { sync: true }
});
addConnection(connection);
connection.on("open", () => {
connection.on("data", data => {
if (data.id === "sync") {
const { connections: syncConnections } = data.data;
for (let syncId of syncConnections) {
console.log("Syncing to", syncId);
if (connectionId === syncId || syncId in connections) {
continue;
}
const syncConnection = peer.connect(syncId, {
metadata: { sync: false }
});
addConnection(syncConnection);
syncConnection.on("open", () => {
if (onConnectionOpen) {
onConnectionOpen(syncConnection);
}
if (payload) {
syncConnection.send(payload);
}
});
}
}
});
if (onConnectionOpen) {
onConnectionOpen(connection);
}
if (payload) {
connection.send(payload);
}
});
}
return { peer, peerId, connections, connectTo };
return { peers, id: socket.id };
}
export default useSession;
+59 -83
View File
@@ -1,15 +1,8 @@
import React, {
useContext,
useEffect,
useState,
useRef,
useCallback
} from "react";
import React, { useState, useRef } from "react";
import { Flex } from "theme-ui";
import { omit } from "../helpers/shared";
import GameContext from "../contexts/GameContext";
import useSession from "../helpers/useSession";
import { getRandomMonster } from "../helpers/monsters";
@@ -17,40 +10,22 @@ import Party from "../components/Party";
import Tokens from "../components/Tokens";
import Map from "../components/Map";
const defaultNickname = getRandomMonster();
function Game() {
const { gameId } = useContext(GameContext);
const handleConnectionOpenCallback = useCallback(handleConnectionOpen);
const handleConnectionSyncCallback = useCallback(handleConnectionSync);
const { peerId, connections, connectTo } = useSession(
handleConnectionOpenCallback,
handleConnectionSyncCallback
function Game({ gameId }) {
const { peers, id } = useSession(
gameId,
handlePeerConnected,
handlePeerDisconnected,
handlePeerData
);
const [nicknames, setNicknames] = useState({});
useEffect(() => {
// Initialize nickname state
if (peerId !== null && !(peerId in nicknames)) {
setNicknames({ [peerId]: defaultNickname });
}
if (gameId !== null && peerId !== null && !(gameId in connections)) {
connectTo(gameId, {
id: "nickname",
data: { [peerId]: nicknames[peerId] || defaultNickname }
});
}
}, [gameId, peerId, connectTo, connections, nicknames]);
const [mapSource, setMapSource] = useState(null);
const mapDataRef = useRef(null);
function handleMapChanged(mapData, mapSource) {
mapDataRef.current = mapData;
setMapSource(mapSource);
for (let connection of Object.values(connections)) {
connection.send({ id: "map", data: mapDataRef.current });
for (let peer of Object.values(peers)) {
peer.send({ id: "map", data: mapDataRef.current });
}
}
@@ -60,75 +35,75 @@ function Game() {
if (!mapSource) {
return;
}
setMapTokens(prevMapTokens => ({
setMapTokens((prevMapTokens) => ({
...prevMapTokens,
[token.id]: token
[token.id]: token,
}));
for (let connection of Object.values(connections)) {
for (let peer of Object.values(peers)) {
const data = { [token.id]: token };
connection.send({ id: "tokenEdit", data });
peer.send({ id: "tokenEdit", data });
}
}
function handleRemoveMapToken(token) {
setMapTokens(prevMapTokens => {
setMapTokens((prevMapTokens) => {
const { [token.id]: old, ...rest } = prevMapTokens;
return rest;
});
for (let connection of Object.values(connections)) {
for (let peer of Object.values(peers)) {
const data = { [token.id]: token };
connection.send({ id: "tokenRemove", data });
peer.send({ id: "tokenRemove", data });
}
}
const [nickname, setNickname] = useState(getRandomMonster());
const [partyNicknames, setPartyNicknames] = useState({});
function handleNicknameChange(nickname) {
setNicknames(prevNicknames => ({
...prevNicknames,
[peerId]: nickname
}));
for (let connection of Object.values(connections)) {
const data = { [peerId]: nickname };
connection.send({ id: "nickname", data });
setNickname(nickname);
for (let peer of Object.values(peers)) {
const data = { [id]: nickname };
peer.send({ id: "nickname", data });
}
}
function handleConnectionOpen(connection) {
connection.on("data", data => {
if (data.id === "map") {
const blob = new Blob([data.data.file]);
mapDataRef.current = { ...data.data, file: blob };
setMapSource(URL.createObjectURL(mapDataRef.current.file));
function handlePeerConnected({ id, peer, initiator }) {
if (!initiator) {
if (mapSource) {
peer.send({ id: "map", data: mapDataRef.current });
}
if (data.id === "tokenEdit") {
setMapTokens(prevMapTokens => ({
...prevMapTokens,
...data.data
}));
}
if (data.id === "tokenRemove") {
setMapTokens(prevMapTokens =>
omit(prevMapTokens, Object.keys(data.data))
);
}
if (data.id === "nickname") {
setNicknames(prevNicknames => ({
...prevNicknames,
...data.data
}));
}
});
connection.on("error", error => {
console.error("Data Connection error", error);
setNicknames(prevNicknames => omit(prevNicknames, [connection.peer]));
});
peer.send({ id: "tokenEdit", data: mapTokens });
}
peer.send({ id: "nickname", data: { [id]: nickname } });
}
function handleConnectionSync(connection) {
if (mapSource) {
connection.send({ id: "map", data: mapDataRef.current });
function handlePeerData({ id, peer, data }) {
if (data.id === "map") {
const blob = new Blob([data.data.file]);
mapDataRef.current = { ...data.data, file: blob };
setMapSource(URL.createObjectURL(mapDataRef.current.file));
}
connection.send({ id: "tokenEdit", data: mapTokens });
connection.send({ id: "nickname", data: nicknames });
if (data.id === "tokenEdit") {
setMapTokens((prevMapTokens) => ({
...prevMapTokens,
...data.data,
}));
}
if (data.id === "tokenRemove") {
setMapTokens((prevMapTokens) =>
omit(prevMapTokens, Object.keys(data.data))
);
}
if (data.id === "nickname") {
setPartyNicknames((prevNicknames) => ({
...prevNicknames,
...data.data,
}));
}
}
function handlePeerDisconnected(disconnectedId) {
setPartyNicknames((prevNicknames) => omit(prevNicknames, [disconnectedId]));
}
return (
@@ -137,8 +112,9 @@ function Game() {
sx={{ justifyContent: "space-between", flexGrow: 1, height: "100%" }}
>
<Party
nicknames={nicknames}
peerId={peerId}
nickname={nickname}
partyNicknames={partyNicknames}
gameId={gameId}
onNicknameChange={handleNicknameChange}
/>
<Map
+4 -8
View File
@@ -1,17 +1,13 @@
import React, { useContext } from "react";
import React from "react";
import { navigate } from "hookrouter";
import { Container, Flex, Button, Image, Text } from "theme-ui";
import GameContext from "../contexts/GameContext";
import shortid from "shortid";
import owlington from "../images/Owlington.png";
function Home() {
const { setGameId } = useContext(GameContext);
function handleStartGame() {
setGameId(null);
navigate("/game");
navigate(`/game/${shortid.generate()}`);
}
function handleJoinGame() {
@@ -24,7 +20,7 @@ function Home() {
sx={{
flexDirection: "column",
height: "100%",
justifyContent: "center"
justifyContent: "center",
}}
>
<Text variant="display" sx={{ textAlign: "center" }}>
+4 -6
View File
@@ -1,11 +1,9 @@
import React, { useContext } from "react";
import React, { useState } from "react";
import { navigate } from "hookrouter";
import { Container, Box, Label, Input, Button, Flex } from "theme-ui";
import GameContext from "../contexts/GameContext";
function Join() {
const { gameId, setGameId } = useContext(GameContext);
const [gameId, setGameId] = useState("");
function handleChange(event) {
setGameId(event.target.value);
@@ -13,7 +11,7 @@ function Join() {
function handleSubmit(event) {
event.preventDefault();
navigate("/game");
navigate(`/game/${gameId}`);
}
return (
@@ -22,7 +20,7 @@ function Join() {
sx={{
flexDirection: "column",
height: "100%",
justifyContent: "center"
justifyContent: "center",
}}
>
<Box as="form" onSubmit={handleSubmit}>