From 99ccd86ac6443e8e59abcb18b5809fe18f536058 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Thu, 6 Apr 2023 14:45:09 -0700 Subject: [PATCH] fixing connectivity issues --- app/mobile/src/context/REFACTORFAIL | 469 ++++++++++++++++++ app/mobile/src/context/useRingContext.hook.js | 107 ++-- 2 files changed, 522 insertions(+), 54 deletions(-) create mode 100644 app/mobile/src/context/REFACTORFAIL diff --git a/app/mobile/src/context/REFACTORFAIL b/app/mobile/src/context/REFACTORFAIL new file mode 100644 index 00000000..785be5de --- /dev/null +++ b/app/mobile/src/context/REFACTORFAIL @@ -0,0 +1,469 @@ +import { useEffect, useContext, useState, useRef } from 'react'; +import { Alert } from 'react-native'; +import { createWebsocket } from 'api/fetchUtil'; +import { addContactRing } from 'api/addContactRing'; +import { addCall } from 'api/addCall'; +import { keepCall } from 'api/keepCall'; +import { removeCall } from 'api/removeCall'; +import { removeContactCall } from 'api/removeContactCall'; + +import { + ScreenCapturePickerView, + RTCPeerConnection, + RTCIceCandidate, + RTCSessionDescription, + RTCView, + MediaStream, + MediaStreamTrack, + mediaDevices, + registerGlobals +} from 'react-native-webrtc'; + +export function useRingContext() { + const [state, setState] = useState({ + ringing: new Map(), + callStatus: null, + cardId: null, + localStream: null, + localVideo: false, + localAudio: false, + remoteStream: null, + removeVideo: false, + removeAudio: false, + }); + const access = useRef(null); + + const EXPIRE = 3000 + const RING = 2000 + const ringing = useRef(new Map()); + const calling = useRef(null); + const ws = useRef(null); + const pc = useRef(null); + const stream = useRef(null); + const accessVideo = useRef(false); + const accessAudio = useRef(false); + const videoTrack = useRef(); + const audioTrack = useRef(); + const candidates = useRef([]); + const offers = useRef([]); + const processing = useRef(false); + const connected = useRef(false); + + const iceServers = [ + { + urls: 'stun:35.165.123.117:5001?transport=udp', + username: 'user', + credential: 'pass' + }, + { + urls: 'turn:35.165.123.117:5001?transport=udp', + username: 'user', + credential: 'pass' + } + ]; + + const constraints = { + mandatory: { + OfferToReceiveAudio: true, + OfferToReceiveVideo: true, + VoiceActivityDetection: true + } + }; + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })) + } + + const polite = async () => { + if (processing.current || !connected.current) { + return; + } + + processing.current = true; + + while (offers.current.length > 0) { + descriptions = offers.current; + offers.current = []; + + try { + for (let i = 0; i < descriptions.length; i++) { + const description = descriptions[i]; + stream.current = null; + + if (description == null) { + const offer = await pc.current.createOffer(constraints); + await pc.current.setLocalDescription(offer); + ws.current.send(JSON.stringify({ description: offer })); + } + else { + if (description.type === 'offer' && pc.current.signalingState !== 'stable') { + const rollback = new RTCSessionDescription({ type: "rollback" }); + await pc.current.setLocalDescription(rollback); + } + const offer = new RTCSessionDescription(description); + await pc.current.setRemoteDescription(offer); + if (description.type === 'offer') { + const answer = await pc.current.createAnswer(); + await pc.current.setLocalDescription(answer); + ws.current.send(JSON.stringify({ description: answer })); + } + } + } + } + catch (err) { + Alert.alert('webrtc error', err.toString()); + } + } + + processing.current = false; + } + + const impolite = async () => { + if (processing.current || !connected.current) { + return; + } + + processing.current = true; + while (offers.current.length > 0) { + descriptions = offers.current; + offers.current = []; + + for (let i = 0; i < descriptions.length; i++) { + const description = descriptions[i]; + stream.current = null; + + try { + if (description == null) { + const offer = await pc.current.createOffer(constraints); + await pc.current.setLocalDescription(offer); + ws.current.send(JSON.stringify({ description: offer })); + } + else { + if (description.type === 'offer' && pc.current.signalingState !== 'stable') { + continue; + } + if (description.type === 'answer' && pc.current.signalingState === 'stable') { + const offer = await pc.current.createOffer(constraints); + await pc.current.setLocalDescription(offer); + ws.current.send(JSON.stringify({ description: offer })); + continue; + } + + const offer = new RTCSessionDescription(description); + await pc.current.setRemoteDescription(offer); + + if (description.type === 'offer') { + const answer = await pc.current.createAnswer(); + await pc.current.setLocalDescription(answer); + ws.current.send(JSON.stringify({ description: answer })); + } + } + } + catch (err) { + Alert.alert('webrtc error', err.toString()); + } + } + } + processing.current = false; + } + + const connect = async (politePolicy, node, token, clearRing, clearAlive) => { + + // connect signal socket + candidates.current = []; + connected.current = false; + updateState({ remoteVideo: false, remoteAudio: false, remoteStream: null, localVideo: false, localAudio: false, localStream: null }); + + pc.current = new RTCPeerConnection({ iceServers }); + pc.current.addEventListener( 'connectionstatechange', event => { + console.log("CONNECTION STATE", event); + } ); + pc.current.addEventListener( 'icecandidate', event => { + ws.current.send(JSON.stringify({ candidate: event.candidate })); + } ); + pc.current.addEventListener( 'icecandidateerror', event => { + console.log("ICE ERROR"); + } ); + pc.current.addEventListener( 'iceconnectionstatechange', event => { + console.log("ICE STATE CHANGE", event); + } ); + pc.current.addEventListener( 'negotiationneeded', async (ev) => { + offers.current.push(null); + if (politePolicy) { + policy(); + } + else { + impolite(); + } + } ); + pc.current.addEventListener( 'signalingstatechange', event => { + console.log("ICE SIGNALING", event); + } ); + pc.current.addEventListener( 'track', event => { + if (stream.current == null) { + stream.current = new MediaStream(); + updateState({ remoteStream: stream.current }); + } + if (event.track.kind === 'audio') { + updateState({ remoteAudio: true }); + } + if (event.track.kind === 'video') { + updateState({ remoteVideo: true }); + } + stream.current.addTrack(event.track, stream.current); + } ); + + videoTrack.current = false; + audioTrack.current = false; + accessVideo.current = false; + accessAudio.current = false; + try { + const stream = await mediaDevices.getUserMedia({ + video: false, + audio: true, + }); + accessAudio.current = true; + updateState({ localAudio: true }); + for (const track of stream.getTracks()) { + if (track.kind === 'audio') { + audioTrack.current = track; + } + pc.current.addTrack(track); + } + } + catch (err) { + console.log(err); + } + + ws.current = createWebsocket(`wss://${node}/signal`); + ws.current.onmessage = async (ev) => { + // handle messages [impolite] + try { + const signal = JSON.parse(ev.data); + if (signal.status === 'connected') { + clearRing(); + updateState({ callStatus: "connected" }); + if (!politePolicy) { + connected.current = true; + impolite(); + } + } + else if (signal.status === 'closed') { + ws.current.close(); + } + else if (signal.description) { + offers.current.push(signal.description); + impolite(); + } + else if (signal.candidate) { + if (pc.current.remoteDescription == null) { + return; + } + const candidate = new RTCIceCandidate(signal.candidate); + await pc.current.addIceCandidate(candidate); + } + } + catch (err) { + console.log(err); + } + } + ws.current.onclose = (e) => { + // update state to disconnected + pc.current.close(); + clearRing(); + clearAlive(); + calling.current = null; + if (videoTrack.current) { + videoTrack.current.stop(); + videoTrack.current = null; + } + if (audioTrack.current) { + audioTrack.current.stop(); + audioTrack.current = null; + } + updateState({ callStatus: null }); + } + ws.current.onopen = async () => { + ws.current.send(JSON.stringify({ AppToken: token })); + if (politePolicy) { + connected.current = true; + polite(); + } + } + ws.current.error = (e) => { + console.log(e) + ws.current.close(); + } + } + + const actions = { + setSession: (token) => { + + if (access.current) { + throw new Error("invalid ring state"); + } + access.current = token; + ringing.current = new Map(); + calling.current = null; + updateState({ callStatus: null, ringing: ringing.current }); + }, + clearSession: () => { + access.current = null; + }, + ring: (cardId, callId, calleeToken) => { + const key = `${cardId}:${callId}` + const call = ringing.current.get(key) || { cardId, calleeToken, callId } + call.expires = Date.now() + EXPIRE; + ringing.current.set(key, call); + updateState({ ringing: ringing.current }); + setTimeout(() => { + updateState({ ringing: ringing.current }); + }, EXPIRE); + }, + ignore: async (cardId, callId) => { + const key = `${cardId}:${callId}` + const call = ringing.current.get(key); + if (call) { + call.status = 'ignored' + ringing.current.set(key, call); + updateState({ ringing: ringing.current }); + } + }, + decline: async (cardId, contactNode, contactToken, callId) => { + const key = `${cardId}:${callId}` + const call = ringing.current.get(key); + if (call) { + call.status = 'declined' + ringing.current.set(key, call); + updateState({ ringing: ringing.current }); + try { + await removeContactCall(contactNode, contactToken, callId); + } + catch (err) { + console.log(err); + } + } + }, + accept: async (cardId, callId, contactNode, contactToken, calleeToken) => { + if (calling.current) { + throw new Error("active session"); + } + + const key = `${cardId}:${callId}` + const call = ringing.current.get(key); + if (call) { + call.status = 'accepted' + ringing.current.set(key, call); + updateState({ ringing: ringing.current, callStatus: "connecting", cardId }); + + calling.current = { callId, contactNode, contactToken, host: false }; + await connect(impolite, contactNode, calleeToken, () => {}, () => {}); + } + }, + end: async () => { + if (!calling.current) { + throw new Error('inactive session'); + } + try { + const { host, callId, contactNode, contactToken } = calling.current; + if (host) { + const { server, token } = access.current; + await removeCall(server, token, callId); + } + else { + await removeContactCall(contactNode, contactToken, callId); + } + } + catch (err) { + console.log(err); + } + ws.current.close(); + }, + call: async (cardId, contactNode, contactToken) => { + if (calling.current) { + throw new Error("active session"); + } + + // create call + const { server, token } = access.current; + const call = await addCall(server, token, cardId); + const { id, keepAlive, callerToken, calleeToken } = call; + try { + await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken }); + } + catch (err) { + console.log(err); + } + const aliveInterval = setInterval(async () => { + try { + await keepCall(server, token, id); + } + catch (err) { + console.log(err); + } + }, keepAlive * 1000); + let index = 0; + const ringInterval = setInterval(async () => { + try { + await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken }); + index += 1; + } + catch (err) { + console.log(err); + } + }, RING); + + updateState({ callStatus: "ringing", cardId }); + calling.current = { callId: id, host: true }; + await connect(polite, server, callerToken, () => clearInterval(ringInterval), () => clearInterval(aliveInterval)); + }, + enableVideo: async () => { + if (!accessVideo.current) { + const stream = await mediaDevices.getUserMedia({ + audio: true, + video: { + frameRate: 30, + facingMode: 'user' + } + }); + accessVideo.current = true; + accessAudio.current = true; + updateState({ localStream: stream }); + for (const track of stream.getTracks()) { + if (track.kind === 'audio') { + audioTrack.current = track; + } + if (track.kind === 'video') { + videoTrack.current = track; + } + pc.current.addTrack(track); + } + } + else { + videoTrack.current.enabled = true; + } + updateState({ localVideo: true, localAudio: true }); + }, + disableVideo: async () => { + if (videoTrack.current) { + videoTrack.current.enabled = false; + } + updateState({ localVideo: false }); + }, + enableAudio: async () => { + if (accessAudio.current) { + audioTrack.current.enabled = true; + updateState({ localAudio: true }); + } + }, + disableAudio: async () => { + if (accessAudio.current) { + audioTrack.current.enabled = false; + updateState({ localAudio: false }); + } + }, + } + + return { state, actions } +} + diff --git a/app/mobile/src/context/useRingContext.hook.js b/app/mobile/src/context/useRingContext.hook.js index cc1aa402..a4d060d0 100644 --- a/app/mobile/src/context/useRingContext.hook.js +++ b/app/mobile/src/context/useRingContext.hook.js @@ -110,8 +110,9 @@ export function useRingContext() { const servers = candidates.current; candidates.current = []; for (let i = 0; i < servers.length; i++) { - const server = servers[i]; - ws.current.send(JSON.stringify(server)); +console.log("FLUSHING:", i); + const candidate = new RTCIceCandidate(servers[i]); + await pc.current.addIceCandidate(candidate); } } } @@ -160,8 +161,9 @@ export function useRingContext() { const servers = candidates.current; candidates.current = []; for (let i = 0; i < servers.length; i++) { - const server = servers[i]; - ws.current.send(JSON.stringify(server)); +console.log("FLUSHING:", i); + const candidate = new RTCIceCandidate(servers[i]); + await pc.current.addIceCandidate(candidate); } } } @@ -173,25 +175,14 @@ export function useRingContext() { processing.current = false; } - const connect = async (policy, node, token, clearRing, clearAlive) => { - - // connect signal socket - connected.current = false; - candidates.current = []; - updateState({ remoteVideo: false, remoteAudio: false, remoteStream: null, localVideo: false, localAudio: false, localStream: null }); + const transmit = async (policy) => { pc.current = new RTCPeerConnection({ iceServers }); pc.current.addEventListener( 'connectionstatechange', event => { console.log("CONNECTION STATE", event); } ); pc.current.addEventListener( 'icecandidate', event => { - if (pc.current.remoteDescription == null) { - console.log("QUEING ICE"); - candidates.current.push({ candidate: event.candidate }); - } - else { - ws.current.send(JSON.stringify({ candidate: event.candidate })); - } + ws.current.send(JSON.stringify({ candidate: event.candidate })); } ); pc.current.addEventListener( 'icecandidateerror', event => { console.log("ICE ERROR"); @@ -225,28 +216,45 @@ export function useRingContext() { stream.current.addTrack(event.track, stream.current); } ); - videoTrack.current = false; - audioTrack.current = false; - accessVideo.current = false; - accessAudio.current = false; try { const stream = await mediaDevices.getUserMedia({ - video: false, audio: true, + video: { + frameRate: 30, + facingMode: 'user' + } }); - accessAudio.current = true; - updateState({ localAudio: true }); for (const track of stream.getTracks()) { if (track.kind === 'audio') { + accessAudio.current = true; audioTrack.current = track; + pc.current.addTrack(track, stream); + updateState({ localAudio: true }); + } + if (track.kind === 'video') { + accessVideo.current = true; + videoTrack.current = track; + pc.current.addTrack(track, stream); + updateState({ localVideo: true }); } - pc.current.addTrack(track); } } catch (err) { console.log(err); - Alert.alert('Media Access', err.toString()); } + } + + const connect = async (policy, node, token, clearRing, clearAlive) => { + + // connect signal socket + connected.current = false; + candidates.current = []; + updateState({ remoteVideo: false, remoteAudio: false, remoteStream: null, localVideo: false, localAudio: false, localStream: null }); + + videoTrack.current = false; + audioTrack.current = false; + accessVideo.current = false; + accessAudio.current = false; ws.current = createWebsocket(`wss://${node}/signal`); ws.current.onmessage = async (ev) => { @@ -258,6 +266,7 @@ export function useRingContext() { updateState({ callStatus: "connected" }); if (policy === 'polite') { connected.current = true; + transmit('polite'); polite(); } } @@ -275,10 +284,12 @@ export function useRingContext() { } else if (signal.candidate) { if (pc.current.remoteDescription == null) { - return; + candidates.current.push(signal.candidate); + } + else { + const candidate = new RTCIceCandidate(signal.candidate); + await pc.current.addIceCandidate(candidate); } - const candidate = new RTCIceCandidate(signal.candidate); - await pc.current.addIceCandidate(candidate); } } catch (err) { @@ -305,6 +316,7 @@ export function useRingContext() { ws.current.send(JSON.stringify({ AppToken: token })); if (policy === 'impolite') { connected.current = true; + transmit('impolite'); impolite(); } } @@ -436,46 +448,33 @@ export function useRingContext() { await connect('polite', server, callerToken, () => clearInterval(ringInterval), () => clearInterval(aliveInterval)); }, enableVideo: async () => { - if (!accessVideo.current) { - const stream = await mediaDevices.getUserMedia({ - audio: true, - video: { - frameRate: 30, - facingMode: 'user' - } - }); - accessVideo.current = true; - accessAudio.current = true; - updateState({ localStream: stream }); - for (const track of stream.getTracks()) { - if (track.kind === 'audio') { - audioTrack.current = track; - } - if (track.kind === 'video') { - videoTrack.current = track; - } - pc.current.addTrack(track); + if (videoTrack.current) { + if (!accessVideo.current) { + accessVideo.current = true; + pc.current.addTrack(videoTrack.current); } - } - else { videoTrack.current.enabled = true; } - updateState({ localVideo: true, localAudio: true }); + updateState({ localVideo: true }); }, disableVideo: async () => { if (videoTrack.current) { videoTrack.current.enabled = false; + updateState({ localVideo: false }); } - updateState({ localVideo: false }); }, enableAudio: async () => { - if (accessAudio.current) { + if (audioTrack.current) { + if (!accessAudio.current) { + accessAudio.current = true; + pc.current.addTrack(audioTrack.current); + } audioTrack.current.enabled = true; updateState({ localAudio: true }); } }, disableAudio: async () => { - if (accessAudio.current) { + if (audioTrack.current) { audioTrack.current.enabled = false; updateState({ localAudio: false }); }