From 87a6ba4de9a940a854a2b1a3e1ec958b18585928 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Wed, 29 Mar 2023 16:15:52 -0700 Subject: [PATCH] adding webrtc calling from mobile --- app/mobile/src/context/useRingContext.hook.js | 224 +++++++++++++++++- app/mobile/src/session/Session.jsx | 4 +- app/mobile/src/session/contact/Contact.jsx | 4 + .../src/session/contact/useContact.hook.js | 9 + app/mobile/src/session/useSession.hook.js | 38 +-- .../src/session/contact/useContact.hook.js | 1 - 6 files changed, 239 insertions(+), 41 deletions(-) diff --git a/app/mobile/src/context/useRingContext.hook.js b/app/mobile/src/context/useRingContext.hook.js index c65cb032..119f1830 100644 --- a/app/mobile/src/context/useRingContext.hook.js +++ b/app/mobile/src/context/useRingContext.hook.js @@ -65,6 +65,7 @@ export function useRingContext() { const actions = { setSession: (token) => { + if (access.current) { throw new Error("invalid ring state"); } @@ -160,7 +161,7 @@ export function useRingContext() { stream.current.addTrack(event.track, stream.current); } ); - const processOffers = async () => { + const impolite = async () => { if (processing.current) { return; } @@ -225,11 +226,10 @@ export function useRingContext() { } else if (signal.description) { offers.current.push(signal.description); - processOffers(); + impolite(); } else if (signal.candidate) { if (pc.current.remoteDescription == null) { - console.log("IGNOREING CANDIDATE"); return; } const candidate = new RTCIceCandidate(signal.candidate); @@ -288,7 +288,8 @@ export function useRingContext() { try { const { host, callId, contactNode, contactToken } = calling.current; if (host) { - await removeCall(access.current, callId); + const { server, token } = access.current; + await removeCall(server, token, callId); } else { await removeContactCall(contactNode, contactToken, callId); @@ -308,14 +309,229 @@ export function useRingContext() { } }, 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); + + calling.current = { state: "connecting", callId: id, host: true }; + updateState({ callStatus: "connecting", cardId, remoteVideo: false, remoteAudio: false }); + + // form peer connection + 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', event => { + console.log("ICE NEGOTIATION", event); + } ); + 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; + try { + const stream = await mediaDevices.getUserMedia({ + video: false, + audio: true, + }); + accessAudio.current = true; + updateState({ localVideo: false, localAudio: true, 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); + } + } + catch (err) { + console.log(err); + } + + const polite = async () => { + if (processing.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; + + if (description.type === 'offer' && pc.current.signalingState !== 'stable') { + const rollback = new RTCSessionDescription({ type: "rollback" }); + await pc.current.setLocalDescription(reollback); + } + 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 })); + } + } + } + + processing.current = false; + } + + ws.current = createWebsocket(`wss://${server}/signal`); + ws.current.onmessage = async (ev) => { + // handle messages [polite] + try { + const signal = JSON.parse(ev.data); + if (signal.status) { + if (calling.current.state !== 'connected' && signal.status === 'connected') { + clearInterval(ringInterval); + calling.current.state = 'connected'; + updateState({ callStatus: "connected" }); + } + if (signal.status === 'closed') { + ws.current.close(); + } + } + else if (signal.description) { + offers.current.push(signal.description); + polite(); + } + 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) => { + pc.current.close(); + clearInterval(ringInterval); + clearInterval(aliveInterval); + 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 = () => { + calling.current.state = "ringing"; + updateState({ callStatus: "ringing" }); + ws.current.send(JSON.stringify({ AppToken: callerToken })) + } + ws.current.error = (e) => { + console.log(e) + ws.current.close(); + } }, enableVideo: async () => { + if (!accessVideo.current) { + const stream = await mediaDevices.getUserMedia({ + video: true, + audio: true, + }); + 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 }); + } }, } diff --git a/app/mobile/src/session/Session.jsx b/app/mobile/src/session/Session.jsx index a9fda2bf..72db8e75 100644 --- a/app/mobile/src/session/Session.jsx +++ b/app/mobile/src/session/Session.jsx @@ -475,12 +475,12 @@ export function Session() { )} { !state.localAudio && ( - + )} { state.localAudio && ( - + )} diff --git a/app/mobile/src/session/contact/Contact.jsx b/app/mobile/src/session/contact/Contact.jsx index 6670342a..3046e771 100644 --- a/app/mobile/src/session/contact/Contact.jsx +++ b/app/mobile/src/session/contact/Contact.jsx @@ -198,6 +198,10 @@ export function ContactBody({ contact }) { Report Contact + + + Call Contact + )} { state.status === 'connecting' && ( diff --git a/app/mobile/src/session/contact/useContact.hook.js b/app/mobile/src/session/contact/useContact.hook.js index 92af0fe7..df6f2f37 100644 --- a/app/mobile/src/session/contact/useContact.hook.js +++ b/app/mobile/src/session/contact/useContact.hook.js @@ -1,5 +1,6 @@ import { useState, useEffect, useRef, useContext } from 'react'; import { CardContext } from 'context/CardContext'; +import { RingContext } from 'context/RingContext'; import { getListingMessage } from 'api/getListingMessage'; import { getListingImageUrl } from 'api/getListingImageUrl'; import { addFlag } from 'api/addFlag'; @@ -22,6 +23,7 @@ export function useContact(contact) { }); const card = useContext(CardContext); + const ring = useContext(RingContext); const updateState = (value) => { setState((s) => ({ ...s, ...value })); @@ -151,6 +153,13 @@ export function useContact(contact) { resync: () => { card.actions.resync(contact.card); }, + ring: async () => { + console.log("calling!!"); + const contact = card.state.cards.get(state.cardId); + const { node, guid } = contact.card?.profile || {} + const { token } = contact.card?.detail || {} + await ring.actions.call(state.cardId, node, `${guid}.${token}`); + }, }; return { state, actions }; diff --git a/app/mobile/src/session/useSession.hook.js b/app/mobile/src/session/useSession.hook.js index 97e19537..4231d9a1 100644 --- a/app/mobile/src/session/useSession.hook.js +++ b/app/mobile/src/session/useSession.hook.js @@ -111,46 +111,16 @@ export function useSession() { await ring.actions.end(); }, enableVideo: async () => { - if (!accessVideo.current) { - const stream = await mediaDevices.getUserMedia({ - video: true, - audio: true, - }); - 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 }); + await ring.actions.enableVideo(); }, disableVideo: async () => { - if (videoTrack.current) { - videoTrack.current.enabled = false; - } - updateState({ localVideo: false }); + await ring.actions.disableVideo(); }, enableAudio: async () => { - if (accessAudio.current) { - audioTrack.current.enabled = true; - updateState({ localAudio: true }); - } + await ring.actions.enableAudio(); }, disableAudio: async () => { - if (accessAudio.current) { - audioTrack.current.enabled = false; - updateState({ localAudio: false }); - } + await ring.actions.disableAudio(); }, }; diff --git a/net/web/src/session/contact/useContact.hook.js b/net/web/src/session/contact/useContact.hook.js index 1354c02f..8121e666 100644 --- a/net/web/src/session/contact/useContact.hook.js +++ b/net/web/src/session/contact/useContact.hook.js @@ -164,7 +164,6 @@ export function useContact(guid, listing, close) { const { node, guid } = contact.data.cardProfile; const { token } = contact.data.cardDetail; await ring.actions.call(state.cardId, node, `${guid}.${token}`); - //await addContactRing(node, `${guid}.${token}`, { index: 0, callId: 'abc', calleeToken: '123' }); }, };