databag/app/client/mobile/src/context/useRingContext.hook.ts
2025-02-05 14:56:23 -08:00

369 lines
12 KiB
TypeScript

import { useState, useContext, useEffect, useRef } from 'react'
import { DisplayContext } from '../context/DisplayContext';
import { AppContext } from '../context/AppContext'
import { ContextType } from '../context/ContextType'
import { Link, type Card } from 'databag-client-sdk';
import InCallManager from 'react-native-incall-manager';
import {
ScreenCapturePickerView,
RTCPeerConnection,
RTCIceCandidate,
RTCSessionDescription,
RTCView,
MediaStream,
MediaiStreamTrack,
mediaDevices,
registerGlobals
} from 'react-native-webrtc';
const CLOSE_POLL_MS = 100;
export function useRingContext() {
const app = useContext(AppContext) as ContextType;
const display = useContext(DisplayContext) as ContextType;
const call = useRef(null as { peer: RTCPeerConnection, link: Link, candidates: RTCIceCandidate[] } | null);
const sourceStream = useRef(null as null|MediaStream);
const localStream = useRef(null as null|MediaStream);
const localAudio = useRef(null as null|MediaStreamTrack);
const localVideo = useRef(null as null|MediaStreamTrack);
const localAudioAdded = useRef(false);
const localVideoAdded = useRef(false);
const remoteStream = useRef(null as null|MediaStream);
const updatingPeer = useRef(false);
const peerUpdate = useRef([] as {type: string, data?: any}[]);
const connecting = useRef(false);
const closing = useRef(false);
const [ringing, setRinging] = useState([] as { cardId: string, callId: string }[]);
const [cards, setCards] = useState([] as Card[]);
const [state, setState] = useState({
calls: [] as { callId: string, cardId: string}[],
calling: null as null | Card,
localStream: null as null|MediaStream,
remoteStream: null as null|MediaStream,
localVideo: false,
remoteVideo: false,
audioEnabled: false,
videoEnabled: false,
connected: false,
connectedTime: 0,
failed: false,
fullscreen: false,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateState = (value: any) => {
setState((s) => ({ ...s, ...value }))
}
useEffect(() => {
const calls = ringing.map(ring => ({ callId: ring.callId, card: cards.find(card => ring.cardId === card.cardId) }))
.filter(ring => (ring.card && !ring.card.blocked));
updateState({ calls });
}, [ringing, cards]);
const constraints = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: false,
VoiceActivityDetection: true
}
};
const linkStatus = async (status: string) => {
if (call.current) {
const { peer, link } = call.current;
if (status === 'connected') {
const now = new Date();
const connectedTime = Math.floor(now.getTime() / 1000);
console.log("CONTEXT CONNECTED: ", connectedTime);
updateState({ connected: true, connectedTime });
await actions.enableAudio();
} else if (status === 'closed') {
await cleanup();
}
}
}
const updatePeer = async (type: string, data?: any) => {
peerUpdate.current.push({ type, data });
if (!updatingPeer.current) {
updatingPeer.current = true;
while (!closing.current && call.current && peerUpdate.current.length > 0) {
const { peer, link, candidates } = call.current;
const { type, data } = peerUpdate.current.shift() || { type: '' };
try {
switch (type) {
case 'negotiate':
const description = await peer.createOffer();
await peer.setLocalDescription(description);
await link.sendMessage({ description });
break;
case 'candidate':
await link.sendMessage({ data });
break;
case 'message':
if (data.description) {
const offer = new RTCSessionDescription(data.description);
await peer.setRemoteDescription(offer);
if (data.description.type === 'offer') {
const description = await peer.createAnswer();
await peer.setLocalDescription(description);
link.sendMessage({ description });
}
for (const candidate of candidates) {
await peer.addIceCandidate(candidate);
};
call.current.candidates = [];
} else if (data.candidate) {
const candidate = new RTCIceCandidate(data.candidate);
if (peer.remoteDescription == null) {
candidates.push(candidate);
} else {
await peer.addIceCandidate(candidate);
}
}
break;
case 'remote_track':
if (remoteStream.current) {
remoteStream.current.addTrack(data);
if (data.kind === 'video') {
InCallManager.setForceSpeakerphoneOn(true);
updateState({ remoteVideo: true });
}
}
break;
case 'local_track':
peer.addTrack(data, sourceStream.current);
if (data.kind === 'video') {
InCallManager.setForceSpeakerphoneOn(true);
updateState({ localVideo: true })
}
break;
default:
console.log('unknown event');
break;
}
} catch (err) {
console.log(err);
updateState({ failed: true });
}
}
updatingPeer.current = false;
}
}
const setup = async (link: Link, card: Card) => {
remoteStream.current = new MediaStream();
localStream.current = new MediaStream();
sourceStream.current = await mediaDevices.getUserMedia({
audio: true,
video: {
frameRate: 30,
facingMode: 'user'
}
});
InCallManager.start({media: 'audio'});
localAudio.current = sourceStream.current.getTracks().find(track => track.kind === 'audio');
localVideo.current = sourceStream.current.getTracks().find(track => track.kind === 'video');
if (localAudio.current) {
localAudio.current.enabled = false;
}
if (localVideo.current) {
localVideo.current.enabled = false;
}
const ice = link.getIce();
const peer = transmit(ice);
const candidates = [] as RTCIceCandidate[];
call.current = { peer, link, candidates };
link.setStatusListener(linkStatus);
link.setMessageListener((msg: any) => updatePeer('message', msg));
updateState({ calling: card, failed: false, connected: false, connectedTime: 0,
audioEnabled: false, videoEnabled: false, localVideo: false, remoteVideo: false,
localStream: localStream.current, remoteStream: remoteStream.current });
}
const cleanup = async () => {
closing.current = true;
while (updatingPeer.current || connecting.current) {
await new Promise((r) => setTimeout(r, CLOSE_POLL_MS));
}
if (call.current) {
const { peer, link } = call.current;
peer.close();
link.close();
call.current = null;
}
if (localVideo.current) {
localVideo.current.stop();
localVideo.current = null;
localVideoAdded.current = false;
}
if (localAudio.current) {
localAudio.current.stop();
localAudio.current = null;
localAudioAdded.current = false;
}
localStream.current = null;
remoteStream.current = null;
sourceStream.current = null;
peerUpdate.current = [];
InCallManager.stop();
updateState({ calling: null, connected: false, connectedTime: 0, fullscreen: false, failed: false,
localStream: null, remoteStream: null, localVideo: false, remoteVideo: false });
closing.current = false;
}
const transmit = (ice: { urls: string; username: string; credential: string }[]) => {
const peerConnection = new RTCPeerConnection({ iceServers: ice });
peerConnection.addEventListener( 'connectionstatechange', event => {
console.log("CONNECTION STATE", event);
});
peerConnection.addEventListener( 'icecandidate', event => {
updatePeer('candidate', event.candidate);
});
peerConnection.addEventListener( 'icecandidateerror', event => {
console.log("ICE ERROR");
});
peerConnection.addEventListener( 'iceconnectionstatechange', event => {
console.log("ICE STATE CHANGE", event);
});
peerConnection.addEventListener( 'negotiationneeded', event => {
updatePeer('negotiate');
});
peerConnection.addEventListener( 'signalingstatechange', event => {
console.log("ICE SIGNALING", event);
});
peerConnection.addEventListener( 'track', event => {
updatePeer('remote_track', event.track);
});
return peerConnection;
}
useEffect(() => {
if (app.state.session) {
const setRing = (ringing: { cardId: string, callId: string }[]) => {
setRinging(ringing);
}
const setContacts = (cards: Card[]) => {
setCards(cards);
}
const ring = app.state.session.getRing();
ring.addRingingListener(setRinging);
const contact = app.state.session.getContact();
contact.addCardListener(setContacts);
return () => {
ring.removeRingingListener(setRing);
contact.removeCardListener(setContacts);
cleanup();
}
}
}, [app.state.session]);
const actions = {
setFullscreen: (fullscreen: boolean) => {
updateState({ fullscreen });
},
ignore: async (callId: string, card: Card) => {
const ring = app.state.session.getRing();
await ring.ignore(card.cardId, callId);
},
decline: async (callId: string, card: Card) => {
const ring = app.state.session.getRing();
await ring.decline(card.cardId, callId);
},
end: async () => {
await cleanup();
},
accept: async (callId: string, card: Card) => {
if (connecting.current || closing.current || call.current) {
throw new Error('not ready to accept calls');
}
try {
connecting.current = true;
const { cardId, node } = card;
const ring = app.state.session.getRing();
const link = await ring.accept(cardId, callId, node);
await setup(link, card);
connecting.current = false;
} catch (err) {
connecting.current = false;
throw err;
}
},
call: async (card: Card) => {
if (connecting.current || closing.current || call.current) {
throw new Error('not ready make calls');
}
try {
connecting.current = true;
const contact = app.state.session.getContact();
const link = await contact.callCard(card.cardId);
await setup(link, card);
connecting.current = false;
} catch (err) {
connecting.current = false;
throw err;
}
},
enableAudio: async () => {
if (connecting.current || closing.current || !call.current) {
throw new Error('cannot unmute audio');
}
if (!localAudio.current) {
throw new Error('audio not available');
} else {
if (!localAudioAdded.current) {
localAudioAdded.current = true;
updatePeer('local_track', localAudio.current);
}
localAudio.current.enabled = true;
}
updateState({ audioEnabled: true });
},
disableAudio: async () => {
if (!call.current) {
throw new Error('cannot mute audio');
}
if (localAudio.current) {
localAudio.current.enabled = false;
}
updateState({ audioEnabled: false });
},
enableVideo: async () => {
if (connecting.current || closing.current || !call.current) {
throw new Error('cannot start video');
}
if (!localVideo.current) {
throw new Error('video not available');
} else {
if (!localVideoAdded.current) {
localVideoAdded.current = true;
localStream.current.addTrack(localVideo.current, localStream.current);
updatePeer('local_track', localVideo.current);
}
localVideo.current.enabled = true;
}
updateState({ videoEnabled: true });
},
disableVideo: async () => {
if (!call.current) {
throw new Error('cannot stop video');
}
if (localVideo.current) {
localVideo.current.enabled = false;
}
updateState({ videoEnabled: false });
},
}
return { state, actions }
}