From 99e1aa88f5f95f9e5d0f1305e7fac4e30c92c5fa Mon Sep 17 00:00:00 2001 From: balzack Date: Wed, 29 Mar 2023 13:07:20 -0700 Subject: [PATCH] mid mobile webrtc integration --- app/mobile/src/context/useRingContext.hook.js | 189 ++++++++++++++++++ app/mobile/src/session/Session.jsx | 31 ++- app/mobile/src/session/Session.styled.js | 21 ++ app/mobile/src/session/useSession.hook.js | 45 ++++- 4 files changed, 275 insertions(+), 11 deletions(-) diff --git a/app/mobile/src/context/useRingContext.hook.js b/app/mobile/src/context/useRingContext.hook.js index 7f53f7d3..3c3d727f 100644 --- a/app/mobile/src/context/useRingContext.hook.js +++ b/app/mobile/src/context/useRingContext.hook.js @@ -6,6 +6,18 @@ 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(), @@ -31,6 +43,7 @@ export function useRingContext() { const accessAudio = useRef(false); const videoTrack = useRef(); const audioTrack = useRef(); + const candidates = useRef([]); const iceServers = [ { @@ -96,6 +109,182 @@ export function useRingContext() { } }, 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 }); + + // connect signal socket + candidates.current = []; + calling.current = { state: "connecting", callId, contactNode, contactToken, host: false }; + updateState({ callStatus: "connecting", cardId, remoteVideo: false, remoteAudio: false }); + + 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); + console.log("ICE NEGOTIATION", event); + console.log("ICE NEGOTIATION", event); + console.log("ICE NEGOTIATION", event); + console.log("ICE NEGOTIATION", event); + console.log("ICE NEGOTIATION", event); + console.log("ICE NEGOTIATION", event); + console.log("ICE NEGOTIATION", event); + console.log("ICE NEGOTIATION", event); + console.log("ICE NEGOTIATION", event); + console.log("ICE NEGOTIATION", event); + console.log("ICE NEGOTIATION", event); + console.log("ICE NEGOTIATION", event); + } ); + pc.current.addEventListener( 'signalingstatechange', event => { + console.log("ICE SIGNALING", event); + } ); + pc.current.addEventListener( 'track', event => { + + console.log("ICE TRACK", event.track); + if (stream.current == null) { +console.log("NEW STREAM."); +console.log("NEW STREAM."); +console.log("NEW STREAM."); +console.log("NEW STREAM."); +console.log("NEW STREAM."); +console.log("NEW STREAM."); +console.log("NEW STREAM."); +console.log("NEW STREAM."); +console.log("NEW STREAM."); + stream.current = new MediaStream(); + updateState({ remoteStream: stream.current }); + } + stream.current.addTrack(event.track, stream.current); + } ); + + + + + ws.current = createWebsocket(`wss://${contactNode}/signal`); + ws.current.onmessage = async (ev) => { + // handle messages [impolite] + try { + const signal = JSON.parse(ev.data); + if (signal.status === 'closed') { + ws.current.close(); + } + else if (signal.description) { +console.log("DESCRIPTION", signal.description); + + + + stream.current = null; + if (signal.description.type === 'offer' && pc.current.signalingState !== 'stable') { +console.log("IGNORING OFFER!"); + return; //rudely ignore + } + + const offer = new RTCSessionDescription(signal.description); + await pc.current.setRemoteDescription(offer); + + if (signal.description.type === 'offer') { + const answer = await pc.current.createAnswer(); + await pc.current.setLocalDescription(answer); + ws.current.send(JSON.stringify({ description: answer })); + } + +console.log("STATE:", pc.current.signalingState); + + const adding = candidates.current; + candidates.current = []; + for (let i = 0; i < adding.length; i++) { + try { + const candidate = new RTCIceCandidate(adding[i]); + await pc.current.addIceCandidate(candidate); + console.log("success:", adding[i]); + } + catch (err) { + console.log(err); + console.log(adding[i]); + } + }; + } + else if (signal.candidate) { + if (pc.current.remoteDescription == null) { + candidates.current.push(signal.candidate); + 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(); + 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 () => { + calling.current.state = "connected" + updateState({ callStatus: "connected" }); + ws.current.send(JSON.stringify({ AppToken: calleeToken })) + + try { + const constraints = { + mandatory: { + OfferToReceiveAudio: true, + OfferToReceiveVideo: true, + VoiceActivityDetection: true + } + }; + const offer = await pc.current.createOffer(constraints); + await pc.current.setLocalDescription(offer); + ws.current.send(JSON.stringify({ description: offer })); +console.log("OPENING OFFER"); +console.log("OPENING OFFER"); +console.log("OPENING OFFER"); +console.log("OPENING OFFER"); +console.log("OPENING OFFER"); +console.log("OPENING OFFER"); +console.log("OPENING OFFER"); +console.log("OPENING OFFER"); + + } + catch(err) { + console.log(err); + } + } + ws.current.error = (e) => { + console.log(e) + ws.current.close(); + } + + } }, end: async () => { }, diff --git a/app/mobile/src/session/Session.jsx b/app/mobile/src/session/Session.jsx index b2c6db44..6469bdc2 100644 --- a/app/mobile/src/session/Session.jsx +++ b/app/mobile/src/session/Session.jsx @@ -25,6 +25,7 @@ import { ProfileIcon } from './profileIcon/ProfileIcon'; import { CardsIcon } from './cardsIcon/CardsIcon'; import { Logo } from 'utils/Logo'; import splash from 'images/session.png'; +import { RTCView } from 'react-native-webrtc'; const ConversationStack = createStackNavigator(); const ProfileStack = createStackNavigator(); @@ -323,13 +324,13 @@ export function Session() { { label } - actions.ignore(cardId, callId)}> + actions.ignore({ cardId, callId })}> - actions.decline(cardId, contactNode, contactToken, callId)}> + actions.decline({ cardId, contactNode, contactToken, callId })}> - actions.accept(cardId, callId, contactNode, contactToken, calleeToken)}> + actions.accept({ cardId, callId, contactNode, contactToken, calleeToken })}> @@ -410,7 +411,7 @@ export function Session() { 0} + visible={ringing.length > 0 && state.callStatus == null} supportedOrientations={['portrait', 'landscape']} > @@ -419,6 +420,28 @@ export function Session() { + + + + { state.remoteStream && ( + + + + )} + { JSON.stringify(state.callStatus == null) } + + + ); } diff --git a/app/mobile/src/session/Session.styled.js b/app/mobile/src/session/Session.styled.js index 2f36f45e..e8878c55 100644 --- a/app/mobile/src/session/Session.styled.js +++ b/app/mobile/src/session/Session.styled.js @@ -186,5 +186,26 @@ export const styles = StyleSheet.create({ padding: 6, marginLeft: 4, }, + callBase: { + display: 'flex', + width: '100%', + height: '100%', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(52, 52, 52, 0.8)' + }, + callFrame: { + backgroundColor: Colors.formBackground, + padding: 16, + width: '90%', + maxWidth: 400, + borderRadius: 4, + }, + callRemote: { + width: 256, + height: 256, + backgroundColor: 'yellow', + }, + }); diff --git a/app/mobile/src/session/useSession.hook.js b/app/mobile/src/session/useSession.hook.js index a6dcceb4..4231d9a1 100644 --- a/app/mobile/src/session/useSession.hook.js +++ b/app/mobile/src/session/useSession.hook.js @@ -9,13 +9,21 @@ import { RingContext } from 'context/RingContext'; export function useSession() { const [state, setState] = useState({ - ringing: [], tabbled: null, subWidth: '50%', baseWidth: '50%', cardId: null, converstaionId: null, firstRun: null, + ringing: [], + callStatus: null, + callLogo: null, + localStream: null, + localVideo: false, + localAudio: false, + remoteStream: null, + remoteVideo: false, + remoteAudio: false, }); const ring = useContext(RingContext); @@ -46,8 +54,15 @@ export function useSession() { } }); + let callLogo = null; + const contact = card.state.cards.get(ring.state.cardId); + if (contact) { + const { imageSet } = contact.card?.profile || {}; + callLogo = imageSet ? card.actions.getCardImageUrl(ring.state.cardId) : null; + } + const { callStatus, localStream, localVideo, localAudio, remoteStream, remoteVideo, remoteAudio } = ring.state; - updateState({ ringing, callStatus, localStream, localVideo, localAudio, remoteStream, remoteVideo, remoteAudio }); + updateState({ ringing, callStatus, callLogo, localStream, localVideo, localAudio, remoteStream, remoteVideo, remoteAudio }); }, [ring.state]); useEffect(() => { @@ -81,16 +96,32 @@ export function useSession() { updateState({ firstRun: false }); store.actions.setFirstRun(); }, - ignore: async (cardId, callId) => { - await ring.actions.ignore(cardId, callId); + ignore: (call) => { + ring.actions.ignore(call.cardId, call.callId); }, - decline: async (cardId, contactNode, contactToken, callId) => { + decline: async (call) => { + const { cardId, contactNode, contactToken, callId } = call; await ring.actions.decline(cardId, contactNode, contactToken, callId); }, - accept: async (cardId, callId, contactNode, contactToken, calleeToken) => { + accept: async (call) => { + const { cardId, callId, contactNode, contactToken, calleeToken } = call; await ring.actions.accept(cardId, callId, contactNode, contactToken, calleeToken); }, - + end: async () => { + await ring.actions.end(); + }, + enableVideo: async () => { + await ring.actions.enableVideo(); + }, + disableVideo: async () => { + await ring.actions.disableVideo(); + }, + enableAudio: async () => { + await ring.actions.enableAudio(); + }, + disableAudio: async () => { + await ring.actions.disableAudio(); + }, }; return { state, actions };