2.3.4.2 Client Logic (React Example)

// frontend/src/features/voice/VoiceChat.jsx
import React, { useRef, useEffect, useState } from 'react';
import { getSocket } from '../../services/socket';

function VoiceChat({ targetUserId }) {
  const localVideoRef = useRef(null);
  const remoteVideoRef = useRef(null);
  const pcRef = useRef(null);
  const [socket, setSocket] = useState(null);

  useEffect(() => {
    const s = getSocket();
    setSocket(s);
    
    pcRef.current = new RTCPeerConnection();

    // On receiving new ICE candidate
    s.on('iceCandidate', ({ fromUserId, candidate }) => {
      pcRef.current.addIceCandidate(new RTCIceCandidate(candidate));
    });

    // On receiving an offer
    s.on('offer', async ({ fromUserId, sdp }) => {
      await pcRef.current.setRemoteDescription(new RTCSessionDescription(sdp));
      const answer = await pcRef.current.createAnswer();
      await pcRef.current.setLocalDescription(answer);
      s.emit('answer', { toUserId: fromUserId, sdp: answer });
    });

    // On receiving an answer
    s.on('answer', async ({ fromUserId, sdp }) => {
      await pcRef.current.setRemoteDescription(new RTCSessionDescription(sdp));
    });

    // ICE candidate generation
    pcRef.current.onicecandidate = (event) => {
      if (event.candidate) {
        s.emit('iceCandidate', {
          toUserId: targetUserId,
          candidate: event.candidate
        });
      }
    };

    // Streams
    pcRef.current.ontrack = (event) => {
      remoteVideoRef.current.srcObject = event.streams[0];
    };

    // Get user media
    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
      .then((stream) => {
        localVideoRef.current.srcObject = stream;
        stream.getTracks().forEach((track) => {
          pcRef.current.addTrack(track, stream);
        });
      })
      .catch(console.error);

  }, [targetUserId]);

  const initiateCall = async () => {
    const offer = await pcRef.current.createOffer();
    await pcRef.current.setLocalDescription(offer);
    socket.emit('offer', { toUserId: targetUserId, sdp: offer });
  };

  return (
    <div>
      <video ref={localVideoRef} autoPlay muted />
      <video ref={remoteVideoRef} autoPlay />
      <button onClick={initiateCall}>Call</button>
    </div>
  );
}

export default VoiceChat;

Note:

• In production, we’ll need TURN servers (e.g., coturn) for relaying media when direct P2P fails.

• Additional features like group calls require more complex logic or a media server (e.g., Jitsi, Janus).

Last updated