176 lines
6.7 KiB
TypeScript
176 lines
6.7 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
'use client'
|
|
import React, { useRef } from 'react'
|
|
|
|
export default function ChatNoServerWidget() {
|
|
const localVideo = useRef<HTMLVideoElement | null>(null)
|
|
const remoteVideo = useRef<HTMLVideoElement | null>(null)
|
|
const localStream = useRef<MediaStream>(null)
|
|
const pc1 = useRef<RTCPeerConnection>(null)
|
|
const pc2 = useRef<RTCPeerConnection>(null)
|
|
|
|
|
|
const handleStart = async () => {
|
|
console.log('Requesting local stream');
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
|
console.log('Received local stream');
|
|
localVideo.current!.srcObject = stream;
|
|
localStream.current = stream;
|
|
} catch (e) {
|
|
console.error(e)
|
|
alert(`getUserMedia() error: ${(e as Error).name}`);
|
|
}
|
|
}
|
|
|
|
|
|
function getName(pc: RTCPeerConnection) {
|
|
return (pc === pc1.current) ? 'pc1' : 'pc2';
|
|
}
|
|
|
|
function getOtherPc(pc: RTCPeerConnection) {
|
|
return (pc === pc1.current) ? pc2.current : pc1.current;
|
|
}
|
|
|
|
async function onIceCandidate(pc: RTCPeerConnection, event: RTCPeerConnectionIceEvent) {
|
|
try {
|
|
await (getOtherPc(pc)!.addIceCandidate(event.candidate));
|
|
onAddIceCandidateSuccess(pc);
|
|
} catch (e) {
|
|
onAddIceCandidateError(pc, e);
|
|
}
|
|
console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
|
|
}
|
|
function onAddIceCandidateSuccess(pc: RTCPeerConnection) {
|
|
console.log(`${getName(pc)} addIceCandidate success`);
|
|
}
|
|
|
|
function onAddIceCandidateError(pc: RTCPeerConnection, error: any) {
|
|
console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`);
|
|
}
|
|
|
|
function onIceStateChange(pc: RTCPeerConnection, event: Event) {
|
|
if (pc) {
|
|
console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`);
|
|
console.log('ICE state change event: ', event);
|
|
}
|
|
}
|
|
|
|
function gotRemoteStream(e: RTCTrackEvent) {
|
|
if (remoteVideo.current!.srcObject !== e.streams[0]) {
|
|
remoteVideo.current!.srcObject = e.streams[0];
|
|
console.log('pc2 received remote stream');
|
|
}
|
|
}
|
|
async function call() {
|
|
console.log('Starting call');
|
|
|
|
const videoTracks = localStream.current!.getVideoTracks();
|
|
const audioTracks = localStream.current!.getAudioTracks();
|
|
if (videoTracks.length > 0) {
|
|
console.log(`Using video device: ${videoTracks[0].label}`);
|
|
}
|
|
if (audioTracks.length > 0) {
|
|
console.log(`Using audio device: ${audioTracks[0].label}`);
|
|
}
|
|
const configuration = {};
|
|
console.log('RTCPeerConnection configuration:', configuration);
|
|
pc1.current = new RTCPeerConnection(configuration);
|
|
console.log('Created local peer connection object pc1');
|
|
pc1.current.addEventListener('icecandidate', e => onIceCandidate(pc1.current!, e));
|
|
pc2.current = new RTCPeerConnection(configuration);
|
|
console.log('Created remote peer connection object pc2');
|
|
pc2.current.addEventListener('icecandidate', e => onIceCandidate(pc2.current!, e));
|
|
pc1.current.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1.current!, e));
|
|
pc2.current.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2.current!, e));
|
|
pc2.current.addEventListener('track', gotRemoteStream);
|
|
|
|
localStream.current!.getTracks().forEach(track => pc1.current!.addTrack(track, localStream.current!));
|
|
console.log('Added local stream to pc1');
|
|
|
|
try {
|
|
console.log('pc1 createOffer start');
|
|
const offer = await pc1.current.createOffer();
|
|
// const offer = await pc1.current.createOffer(offerOptions);
|
|
await onCreateOfferSuccess(offer);
|
|
} catch (e) {
|
|
onCreateSessionDescriptionError(e);
|
|
}
|
|
}
|
|
|
|
function onSetLocalSuccess(pc: RTCPeerConnection) {
|
|
console.log(`${getName(pc)} setLocalDescription complete`);
|
|
}
|
|
|
|
function onSetRemoteSuccess(pc: RTCPeerConnection) {
|
|
console.log(`${getName(pc)} setRemoteDescription complete`);
|
|
}
|
|
|
|
function onSetSessionDescriptionError(error: any) {
|
|
console.log(`Failed to set session description: ${error.toString()}`);
|
|
}
|
|
|
|
async function onCreateOfferSuccess(desc: RTCSessionDescriptionInit) {
|
|
console.log(`Offer from pc1\n${desc.sdp}`);
|
|
console.log('pc1 setLocalDescription start');
|
|
try {
|
|
await pc1.current!.setLocalDescription(desc);
|
|
onSetLocalSuccess(pc1.current!);
|
|
} catch (e) {
|
|
onSetSessionDescriptionError(e);
|
|
}
|
|
|
|
console.log('pc2 setRemoteDescription start');
|
|
try {
|
|
await pc2.current!.setRemoteDescription(desc);
|
|
onSetRemoteSuccess(pc2.current!);
|
|
} catch (e) {
|
|
onSetSessionDescriptionError(e);
|
|
}
|
|
|
|
console.log('pc2 createAnswer start');
|
|
// Since the 'remote' side has no media stream we need
|
|
// to pass in the right constraints in order for it to
|
|
// accept the incoming offer of audio and video.
|
|
try {
|
|
const answer = await pc2.current!.createAnswer();
|
|
await onCreateAnswerSuccess(answer);
|
|
} catch (e) {
|
|
onCreateSessionDescriptionError(e);
|
|
}
|
|
}
|
|
async function onCreateAnswerSuccess(desc: RTCSessionDescriptionInit) {
|
|
console.log(`Answer from pc2:\n${desc.sdp}`);
|
|
console.log('pc2 setLocalDescription start');
|
|
try {
|
|
await pc2.current!.setLocalDescription(desc);
|
|
onSetLocalSuccess(pc2.current!);
|
|
} catch (e) {
|
|
onSetSessionDescriptionError(e);
|
|
}
|
|
console.log('pc1 setRemoteDescription start');
|
|
try {
|
|
await pc1.current!.setRemoteDescription(desc);
|
|
onSetRemoteSuccess(pc1.current!);
|
|
} catch (e) {
|
|
onSetSessionDescriptionError(e);
|
|
}
|
|
}
|
|
|
|
function onCreateSessionDescriptionError(error: any) {
|
|
console.log(`Failed to create session description: ${error.toString()}`);
|
|
}
|
|
return (
|
|
<div>
|
|
<video ref={localVideo} id="localVideo" playsInline autoPlay muted></video>
|
|
<video ref={remoteVideo} id="remoteVideo" playsInline autoPlay></video>
|
|
|
|
<div className="box">
|
|
<button onClick={handleStart} id="startButton">Start</button>
|
|
<button onClick={() => call()} id="callButton">Call</button>
|
|
<button id="hangupButton">Hang Up</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|