mirror of
https://github.com/balzack/databag.git
synced 2025-04-26 11:35:19 +00:00
369 lines
12 KiB
TypeScript
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 }
|
|
}
|
|
|