mid mobile webrtc integration

This commit is contained in:
balzack 2023-03-29 13:07:20 -07:00
parent ee474b5f73
commit 99e1aa88f5
4 changed files with 275 additions and 11 deletions

View File

@ -6,6 +6,18 @@ import { keepCall } from 'api/keepCall';
import { removeCall } from 'api/removeCall'; import { removeCall } from 'api/removeCall';
import { removeContactCall } from 'api/removeContactCall'; import { removeContactCall } from 'api/removeContactCall';
import {
ScreenCapturePickerView,
RTCPeerConnection,
RTCIceCandidate,
RTCSessionDescription,
RTCView,
MediaStream,
MediaStreamTrack,
mediaDevices,
registerGlobals
} from 'react-native-webrtc';
export function useRingContext() { export function useRingContext() {
const [state, setState] = useState({ const [state, setState] = useState({
ringing: new Map(), ringing: new Map(),
@ -31,6 +43,7 @@ export function useRingContext() {
const accessAudio = useRef(false); const accessAudio = useRef(false);
const videoTrack = useRef(); const videoTrack = useRef();
const audioTrack = useRef(); const audioTrack = useRef();
const candidates = useRef([]);
const iceServers = [ const iceServers = [
{ {
@ -96,6 +109,182 @@ export function useRingContext() {
} }
}, },
accept: async (cardId, callId, contactNode, contactToken, calleeToken) => { 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 () => { end: async () => {
}, },

View File

@ -25,6 +25,7 @@ import { ProfileIcon } from './profileIcon/ProfileIcon';
import { CardsIcon } from './cardsIcon/CardsIcon'; import { CardsIcon } from './cardsIcon/CardsIcon';
import { Logo } from 'utils/Logo'; import { Logo } from 'utils/Logo';
import splash from 'images/session.png'; import splash from 'images/session.png';
import { RTCView } from 'react-native-webrtc';
const ConversationStack = createStackNavigator(); const ConversationStack = createStackNavigator();
const ProfileStack = createStackNavigator(); const ProfileStack = createStackNavigator();
@ -323,13 +324,13 @@ export function Session() {
<View key={key} style={styles.ringEntry}> <View key={key} style={styles.ringEntry}>
<Logo src={img} width={40} height={40} radius={4} /> <Logo src={img} width={40} height={40} radius={4} />
<Text style={styles.ringName} numberOfLines={1} ellipsizeMode={'tail'}>{ label }</Text> <Text style={styles.ringName} numberOfLines={1} ellipsizeMode={'tail'}>{ label }</Text>
<TouchableOpacity style={styles.ringIgnore} onPress={() => actions.ignore(cardId, callId)}> <TouchableOpacity style={styles.ringIgnore} onPress={() => actions.ignore({ cardId, callId })}>
<MatIcons name={'eye-off-outline'} size={20} color={Colors.text} /> <MatIcons name={'eye-off-outline'} size={20} color={Colors.text} />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.ringDecline} onPress={() => actions.decline(cardId, contactNode, contactToken, callId)}> <TouchableOpacity style={styles.ringDecline} onPress={() => actions.decline({ cardId, contactNode, contactToken, callId })}>
<MatIcons name={'phone-hangup'} size={20} color={Colors.alert} /> <MatIcons name={'phone-hangup'} size={20} color={Colors.alert} />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.ringAccept} onPress={() => actions.accept(cardId, callId, contactNode, contactToken, calleeToken)}> <TouchableOpacity style={styles.ringAccept} onPress={() => actions.accept({ cardId, callId, contactNode, contactToken, calleeToken })}>
<MatIcons name={'phone'} size={20} color={Colors.primary} /> <MatIcons name={'phone'} size={20} color={Colors.primary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -410,7 +411,7 @@ export function Session() {
<Modal <Modal
animationType="fade" animationType="fade"
transparent={true} transparent={true}
visible={ringing.length > 0} visible={ringing.length > 0 && state.callStatus == null}
supportedOrientations={['portrait', 'landscape']} supportedOrientations={['portrait', 'landscape']}
> >
<View style={styles.ringBase}> <View style={styles.ringBase}>
@ -419,6 +420,28 @@ export function Session() {
</View> </View>
</View> </View>
</Modal> </Modal>
<Modal
animationType="fade"
transparent={true}
visible={state.callStatus != null}
supportedOrientations={['portrait', 'landscape']}
>
<View style={styles.callBase}>
<View style={styles.callFrame}>
{ state.remoteStream && (
<View style={styles.callRemote}>
<RTCView
mirror={true}
objectFit={'cover'}
streamURL={state.remoteStream.toURL()}
zOrder={0}
/>
</View>
)}
<Text>{ JSON.stringify(state.callStatus == null) }</Text>
</View>
</View>
</Modal>
</NavigationContainer> </NavigationContainer>
); );
} }

View File

@ -186,5 +186,26 @@ export const styles = StyleSheet.create({
padding: 6, padding: 6,
marginLeft: 4, 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',
},
}); });

View File

@ -9,13 +9,21 @@ import { RingContext } from 'context/RingContext';
export function useSession() { export function useSession() {
const [state, setState] = useState({ const [state, setState] = useState({
ringing: [],
tabbled: null, tabbled: null,
subWidth: '50%', subWidth: '50%',
baseWidth: '50%', baseWidth: '50%',
cardId: null, cardId: null,
converstaionId: null, converstaionId: null,
firstRun: null, firstRun: null,
ringing: [],
callStatus: null,
callLogo: null,
localStream: null,
localVideo: false,
localAudio: false,
remoteStream: null,
remoteVideo: false,
remoteAudio: false,
}); });
const ring = useContext(RingContext); 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; 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]); }, [ring.state]);
useEffect(() => { useEffect(() => {
@ -81,16 +96,32 @@ export function useSession() {
updateState({ firstRun: false }); updateState({ firstRun: false });
store.actions.setFirstRun(); store.actions.setFirstRun();
}, },
ignore: async (cardId, callId) => { ignore: (call) => {
await ring.actions.ignore(cardId, callId); 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); 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); 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 }; return { state, actions };