wip testing voice and video

This commit is contained in:
pb-coding 2023-10-13 11:37:51 +02:00
parent 02f86b71a8
commit 3586a1285c
5 changed files with 182 additions and 51 deletions

1
.gitignore vendored
View file

@ -11,6 +11,7 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
cert
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*

View file

@ -1,6 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { socket } from "./socket"; import { socket } from "./socket";
import SimplePeer from "simple-peer";
import GameCanvas from "./components/GameCanvas"; import GameCanvas from "./components/GameCanvas";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
@ -54,50 +53,6 @@ export default function App() {
socket.on("clients-in-session", onClientsInRoomUpdate); socket.on("clients-in-session", onClientsInRoomUpdate);
socket.on("game-update", onGameUpdate); socket.on("game-update", onGameUpdate);
const peers = new Map<string, SimplePeer.Instance>();
navigator.mediaDevices
.getUserMedia({ audio: true })
.then((stream) => {
socket.on("initiate-voice-chat", (data) => {
const { to } = data;
console.log("Initiating voice chat with", to);
const peer = new SimplePeer({ initiator: true, stream });
peers.set(to, peer);
peer.on("signal", (signalData) => {
socket.emit("signal", { to, signalData });
});
});
socket.on("signal", (data) => {
const { from, signalData } = data;
let peer = peers.get(from);
if (!peer) {
peer = new SimplePeer({ initiator: false, stream });
peers.set(from, peer);
peer.on("signal", (signalData) => {
socket.emit("signal", { to: from, signalData });
});
peer.on("stream", (remoteStream) => {
const audioEl = new Audio();
audioEl.srcObject = remoteStream;
audioEl.play();
});
}
peer.signal(signalData);
});
})
.catch((err) => {
console.log("Error getting user media", err);
});
return () => { return () => {
socket.off("connect", onConnect); socket.off("connect", onConnect);
socket.off("disconnect", onDisconnect); socket.off("disconnect", onDisconnect);

View file

@ -3,6 +3,7 @@ import { socket } from "../socket";
import { ConnectedIndicator, DisconnectedIndicator } from "./Indicators"; import { ConnectedIndicator, DisconnectedIndicator } from "./Indicators";
import Button from "../global/Button"; import Button from "../global/Button";
import VoiceChat from "./VoiceChat";
type SessionManagerProps = { type SessionManagerProps = {
isConnected: boolean; isConnected: boolean;
@ -100,6 +101,7 @@ export const SessionManager: FC<SessionManagerProps> = ({
<span className="text-gray-200">Game Server: </span> <span className="text-gray-200">Game Server: </span>
{isConnected ? <ConnectedIndicator /> : <DisconnectedIndicator />} {isConnected ? <ConnectedIndicator /> : <DisconnectedIndicator />}
</div> </div>
<VoiceChat session={session} />
</div> </div>
</div> </div>
</section> </section>

View file

@ -0,0 +1,178 @@
import { FC, useEffect, useState, useRef } from "react";
import { socket } from "../socket";
type VoiceChatProps = {
session: string;
};
const VoiceChat: FC<VoiceChatProps> = ({ session }) => {
const servers = {
iceServers: [
{
urls: [
"stun:stun1.l.google.com:19302",
"stun:stun2.l.google.com:19302",
],
},
],
iceCandidatePoolSize: 10,
};
const localVideoRef = useRef<HTMLVideoElement>(null);
const remoteVideoRef = useRef<HTMLVideoElement>(null);
const localStreamRef = useRef<MediaStream | null>(null);
const remoteStreamRef = useRef<MediaStream>(new MediaStream());
const pc = useRef(new RTCPeerConnection(servers));
const sessionRef = useRef<string>(session);
useEffect(() => {
sessionRef.current = session;
}, [session]);
const [isWebcamSetup, setIsWebcamSetup] = useState(false);
// Setup media sources
const setupWebcam = async () => {
localStreamRef.current = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
if (localVideoRef.current) {
localVideoRef.current.srcObject = localStreamRef.current;
}
localStreamRef.current.getTracks().forEach((track) => {
if (!localStreamRef.current) return;
pc.current.addTrack(track, localStreamRef.current);
});
};
// Create an offer
const createOffer = async () => {
const offerDescription = await pc.current.createOffer();
await pc.current.setLocalDescription(offerDescription);
console.log("session (create offer):", session);
socket.emit("create-offer", { offerDescription, sessionName: session });
};
// Answer the call
const answerCall = async (offer: RTCSessionDescriptionInit) => {
console.log("Answer call triggered");
await pc.current.setRemoteDescription(new RTCSessionDescription(offer));
const answerDescription = await pc.current.createAnswer();
await pc.current.setLocalDescription(answerDescription);
console.log("session (answer call):", sessionRef.current);
socket.emit("answer-call", {
answerDescription,
sessionName: sessionRef.current,
});
};
useEffect(() => {
if (localVideoRef.current && localStreamRef.current) {
localVideoRef.current.srcObject = localStreamRef.current;
}
if (remoteVideoRef.current) {
remoteVideoRef.current.srcObject = remoteStreamRef.current;
}
socket.on("offer-made", async (offer) => {
console.log("Offer received:", offer);
answerCall(offer);
});
socket.on("answer-made", async (answer) => {
console.log("Answer received:", answer);
await pc.current.setRemoteDescription(new RTCSessionDescription(answer));
console.log("set remote description", pc.current);
console.log("set");
});
socket.on("add-ice-candidate", (candidate) => {
console.log("Ice candidate received:", candidate);
pc.current.addIceCandidate(new RTCIceCandidate(candidate));
});
pc.current.onicecandidate = (event) => {
if (event.candidate) {
const candidate = event.candidate.toJSON();
console.log("Emit ICE-candidate:", candidate);
socket.emit("ice-candidate", {
candidate,
sessionName: sessionRef.current,
});
}
};
pc.current.ontrack = (event) => {
event.streams[0].getTracks().forEach((track) => {
remoteStreamRef.current.addTrack(track);
});
console.log("remote stream", remoteStreamRef.current);
if (remoteVideoRef.current) {
console.log("setting remote video ref");
remoteVideoRef.current.srcObject = remoteStreamRef.current;
}
};
}, []);
const startWebcam = async () => {
await setupWebcam();
setIsWebcamSetup(true);
};
const initiateCall = async () => {
if (isWebcamSetup) {
// Only allow calls if the webcam is set up
await createOffer();
} else {
console.log("Please set up your webcam first.");
}
};
return (
<div style={{ zIndex: 100 }}>
<h2>Webcam</h2>
<div className="videos">
<span>
<h3>Local Stream</h3>
<video ref={localVideoRef} autoPlay muted></video>
</span>
<span>
<h3>Remote Stream</h3>
<video ref={remoteVideoRef} autoPlay></video>
</span>
</div>
<button style={{ zIndex: 100 }} onClick={startWebcam}>
Start Webcam
</button>
<h2>2. Create a new Call</h2>
<button onClick={initiateCall}>Initiate Call</button>
<h2>3. Join a Call</h2>
<p>Answer the call from a different browser window or device</p>
<input id="callInput" />
<button id="answerButton" disabled>
Answer
</button>
<h2>4. Hangup</h2>
<button id="hangupButton" disabled>
Hangup
</button>
</div>
);
};
export default VoiceChat;

View file

@ -1,10 +1,5 @@
import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App.tsx"; import App from "./App.tsx";
import "./index.css"; import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
<React.StrictMode>
<App />
</React.StrictMode>
);