From 45b20948dcc7c56123f22681c3764f934f44e9cf Mon Sep 17 00:00:00 2001 From: balzack Date: Mon, 3 Feb 2025 18:02:26 -0800 Subject: [PATCH] ring context cleanup --- app/client/web/src/calling/Calling.tsx | 23 +- app/client/web/src/calling/useCalling.hook.ts | 364 +----------------- app/client/web/src/contacts/Contacts.tsx | 9 +- .../web/src/context/useRingContext.hook.ts | 34 +- app/client/web/src/session/Session.tsx | 8 +- 5 files changed, 44 insertions(+), 394 deletions(-) diff --git a/app/client/web/src/calling/Calling.tsx b/app/client/web/src/calling/Calling.tsx index 5df66458..f67108ef 100644 --- a/app/client/web/src/calling/Calling.tsx +++ b/app/client/web/src/calling/Calling.tsx @@ -1,13 +1,14 @@ import React, { useEffect, useRef, useState } from 'react'; import classes from './Calling.module.css' -import { useCalling, type Ring } from './useCalling.hook'; -import { Card } from '../card/Card'; +import { type Card } from 'databag-client-sdk' +import { useCalling } from './useCalling.hook'; +import { Card as Contact } from '../card/Card'; import { Loader, Image, Text, ActionIcon } from '@mantine/core' import { IconEyeX, IconPhone, IconPhoneOff, IconMicrophone, IconMicrophoneOff, IconVideo, IconVideoOff } from '@tabler/icons-react' import { modals } from '@mantine/modals' import { Colors } from '../constants/Colors' -export function Calling({ callCard }: { callCard: { cardId: null|string }}) { +export function Calling({ callCard }: { callCard: { card: null|Card }}) { const [connecting, setConnecting] = useState(false); const [ending, setEnding] = useState(false); const [applyingVideo, setApplyingVideo] = useState(false); @@ -80,11 +81,11 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) { } } - const call = async (cardId: string) => { + const call = async (card: Card) => { if (!connecting) { setConnecting(true); try { - await actions.call(cardId); + await actions.call(card); } catch (err) { console.log(err); showError(); @@ -93,7 +94,7 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) { } } - const accept = async (ring: Ring) => { + const accept = async (ring: { callId: string, card: Card }) => { if (!accepting) { setAccepting(ring.callId); try { @@ -106,7 +107,7 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) { } } - const ignore = async (ring: Ring) => { + const ignore = async (ring: { callId: string, card: Card }) => { if (!ignoring) { setIgnoring(ring.callId); try { @@ -119,7 +120,7 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) { } } - const decline = async (ring: Ring) => { + const decline = async (ring: { callId: string, card: Card }) => { if (!declining) { setDeclining(ring.callId); try { @@ -133,8 +134,8 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) { } useEffect(() => { - if (callCard?.cardId) { - call(callCard.cardId); + if (callCard?.card) { + call(callCard.card); } }, [callCard]); @@ -162,7 +163,7 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) { return (
- +
) }); diff --git a/app/client/web/src/calling/useCalling.hook.ts b/app/client/web/src/calling/useCalling.hook.ts index 03bd2caa..dcca9085 100644 --- a/app/client/web/src/calling/useCalling.hook.ts +++ b/app/client/web/src/calling/useCalling.hook.ts @@ -1,33 +1,17 @@ import { useState, useContext, useEffect, useRef } from 'react' -import { DisplayContext } from '../context/DisplayContext'; -import { AppContext } from '../context/AppContext' +import { RingContext } from '../context/RingContext' +import { DisplayContext } from '../context/DisplayContext' import { ContextType } from '../context/ContextType' -import { Link, type Card } from 'databag-client-sdk'; - -export type Ring = { - callId: string, - card: Card, -} +import { Card } from 'databag-client-sdk'; export function useCalling() { - const app = useContext(AppContext) as ContextType; + const ring = useContext(RingContext) as ContextType; const display = useContext(DisplayContext) as ContextType; - const call = useRef(null as { policy: string, peer: RTCPeerConnection, link: Link, candidates: RTCIceCandidate[] } | null); - const localStream = useRef(null as null|MediaStream); - const localAudio = useRef(null as null|MediaStreamTrack); - const localVideo = useRef(null as null|MediaStreamTrack); - const remoteStream = useRef(null as null|MediaStream); - const updatingPeer = useRef(false); - const peerUpdate = useRef([] as {type: string, data?: any}[]); const [state, setState] = useState({ strings: display.state.strings, - ringing: [] as { cardId: string, callId: string }[], - calls: [] as Ring[], - cards: [] as Card[], + calls: [] as { callId: string, card: Card }[], calling: null as null | Card, - failed: false, - loaded: false, localStream: null as null|MediaStream, remoteStream: null as null|MediaStream, localVideo: false, @@ -35,6 +19,7 @@ export function useCalling() { audioEnabled: false, videoEnabled: false, connected: false, + failed: false, }) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -43,337 +28,10 @@ export function useCalling() { } useEffect(() => { - const calls = state.ringing - .map(ring => ({ callId: ring.callId, card: state.cards.find(card => ring.cardId === card.cardId) }) ) - .filter(ring => (ring.card && !ring.card.blocked)); - updateState({ calls }); - }, [state.ringing, state.cards]); + const { calls, calling, localStream, remoteStream, localVideo, remoteVideo, audioEnabled, videoEnabled, connected, failed } = ring.state; + updateState({ calls, calling, localStream, remoteStream, localVideo, remoteVideo, audioEnabled, videoEnabled, connected, failed }); + }, [ring.state]); - useEffect(() => { - const { strings } = display.state; - updateState({ strings }); - }, [display.state]); - - const getAudioStream = async (audioId: null|string) => { - try { - if (audioId) { - return await navigator.mediaDevices.getUserMedia({ video: false, audio: { deviceId: audioId } }); - } - } - catch (err) { - console.log(err); - } - return await navigator.mediaDevices.getUserMedia({ video: false, audio: true }); - } - - const getVideoStream = async (videoId: null|string) => { - try { - if (videoId) { - return await navigator.mediaDevices.getUserMedia({ video: { deviceId: videoId }, audio: false }); - } - } - catch (err) { - console.log(err); - } - return await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); - } - - const linkStatus = async (status: string) => { - if (call.current) { - const { policy, peer, link } = call.current; - if (status === 'connected') { - localVideo.current = null; - localStream.current = null; - remoteStream.current = new MediaStream(); - updateState({ localStream: localStream.current, remoteStream: remoteStream.current, - audioEnabled: false, videoEnabled: false, localVideo: false, remoteVideo: false, connected: true }); - - try { - const audioStream = await getAudioStream(null); - const audioTrack = audioStream.getTracks().find((track: MediaStreamTrack) => track.kind === 'audio'); - if (audioTrack) { - localAudio.current = audioTrack; - } - if (localAudio.current) { - localAudio.current.enabled = true; - await updatePeer('local_track', { track: audioTrack, stream: audioStream }); - updateState({ audioEnabled: true }); - } - } catch (err) { - console.log(err); - } - } else if (status === 'closed') { - updatePeer('close'); - } - } - } - - const linkMessage = async (message: any) => { - if (call.current) { - const { peer, link, policy, candidates } = call.current; - try { - if (message.description) { - const offer = new RTCSessionDescription(message.description); - await peer.setRemoteDescription(offer); - if (message.description.type === 'offer') { - const description = await peer.createAnswer(); - await peer.setLocalDescription(description); - link.sendMessage({ description }); - } - - if (call.current) { - for (const candidate of candidates) { - await peer.addIceCandidate(candidate); - }; - call.current.candidates = []; - } - } else if (message.candidate) { - const candidate = new RTCIceCandidate(message.candidate); - if (peer.remoteDescription == null) { - candidates.push(candidate); - } else { - await peer.addIceCandidate(candidate); - } - } - } catch (err) { - console.log(err); - updateState({ failed: true }); - } - } - } - - const peerCandidate = async (candidate: RTCIceCandidate) => { - if (call.current && candidate) { - const { link } = call.current; - await link.sendMessage({ candidate }); - } - } - - const peerNegotiate = async () => { - if (call.current) { - try { - const { peer, link } = call.current; - const description = await peer.createOffer(); - await peer.setLocalDescription(description); - await link.sendMessage({ description }); - } catch (err) { - console.log(err); - } - } - } - - const peerTrack = async (track: MediaStreamTrack, stream: MediaStream) => { - if (call.current && localStream.current) { - try { - const { peer } = call.current; - peer.addTrack(track, stream); - } catch (err) { - console.log(err); - } - } - } - - const updatePeer = async (type: string, data?: any) => { - peerUpdate.current.push({ type, data }); - - if (!updatingPeer.current) { - updatingPeer.current = true; - while (peerUpdate.current.length > 0) { - const { type, data } = peerUpdate.current.shift() || { type: '' }; - if (type === 'negotiate') { - await peerNegotiate(); - } else if (type === 'candidate') { - await peerCandidate(data); - } else if (type === 'message') { - await linkMessage(data); - } else if (type === 'remote_track') { - if (remoteStream.current) { - remoteStream.current.addTrack(data); - if (data.kind === 'video') { - updateState({ remoteVideo: true }); - } - } - } else if (type === 'local_track') { - await peerTrack(data.track, data.stream); - } else if (type === 'close' && call.current) { - peerUpdate.current = []; - const { peer, link } = call.current; - call.current = null; - try { - peer.close(); - link.close(); - } catch (err) { - console.log(err); - } - if (localVideo.current) { - localVideo.current.stop(); - localVideo.current = null; - } - if (localAudio.current) { - localAudio.current.stop(); - localAudio.current = null; - } - localStream.current = null; - remoteStream.current = null, - updateState({ calling: null, failed: false, localStream: null, remoteStream: null, localVideo: false, remoteVideo: false }); - } - } - updatingPeer.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 setRinging = (ringing: { cardId: string, callId: string }[]) => { - updateState({ ringing }); - } - const setContacts = (cards: Card[]) => { - updateState({ cards }); - } - const ring = app.state.session.getRing(); - ring.addRingingListener(setRinging); - const contact = app.state.session.getContact(); - contact.addCardListener(setContacts); - return () => { - ring.removeRingingListener(setRinging); - contact.removeCardListener(setContacts); - } - } - }, [app.state.session]); - - const actions = { - 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 () => { - if (!call.current) { - throw new Error('no active call'); - } - await updatePeer('close'); - }, - accept: async (callId: string, card: Card) => { - if (call.current) { - throw new Error('active call in progress'); - } - const { cardId, node } = card; - const ring = app.state.session.getRing(); - const link = await ring.accept(cardId, callId, node); - const ice = link.getIce(); - const peer = transmit(ice); - const policy = 'impolite'; - const candidates = [] as RTCIceCandidate[]; - call.current = { policy, peer, link, candidates }; - link.setStatusListener(linkStatus); - link.setMessageListener((msg: any) => updatePeer('message', msg)); - updateState({ calling: card, connected: false }); - }, - call: async (cardId: string) => { - if (call.current) { - throw new Error('active call in proegress'); - } - const card = state.cards.find(contact => contact.cardId === cardId); - if (!card) { - throw new Error('calling contact not found'); - } - const contact = app.state.session.getContact(); - const link = await contact.callCard(cardId); - const ice = link.getIce(); - const peer = transmit(ice); - const policy = 'polite'; - const candidates = [] as RTCIceCandidate[]; - call.current = { policy, peer, link, candidates }; - link.setStatusListener(linkStatus); - link.setMessageListener((msg: any) => updatePeer('message', msg)); - updateState({ calling: card, connected: false }); - }, - enableAudio: async () => { - if (!call.current) { - throw new Error('cannot unmute audio'); - } - if (!localAudio.current) { - const audioStream = await getAudioStream(null); - const audioTrack = audioStream.getTracks().find((track: MediaStreamTrack) => track.kind === 'audio'); - if (!audioTrack) { - throw new Error('no available audio track'); - } - localAudio.current = audioTrack; - localStream.current = audioStream; - updatePeer('local_track', { track: audioTrack, stream: audioStream }); - updateState({ localAudio: true, localStream: audioStream, audioEnabled: true }); - } else { - 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 (!call.current) { - throw new Error('cannot start video'); - } - if (!localVideo.current) { - const videoStream = await getVideoStream(null); - const videoTrack = videoStream.getTracks().find((track: MediaStreamTrack) => track.kind === 'video'); - if (videoTrack) { - localVideo.current = videoTrack; - localStream.current = videoStream; - updatePeer('local_track', { track: videoTrack, stream: videoStream }); - updateState({ localVideo: true, localStream: videoStream }); - } - } - if (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 } + const actions = ring.actions; + return { state, actions }; } diff --git a/app/client/web/src/contacts/Contacts.tsx b/app/client/web/src/contacts/Contacts.tsx index d05ecb6c..cec3e93b 100644 --- a/app/client/web/src/contacts/Contacts.tsx +++ b/app/client/web/src/contacts/Contacts.tsx @@ -3,7 +3,8 @@ import { useContacts } from './useContacts.hook' import { Text, ActionIcon, TextInput, Button } from '@mantine/core' import { IconUserCheck, IconCancel, IconRefresh, IconSearch, IconUserPlus, IconSortAscending, IconSortDescending, IconMessage2, IconPhone } from '@tabler/icons-react' import classes from './Contacts.module.css' -import { Card } from '../card/Card' +import { type Card } from 'databag-client-sdk'; +import { Card as Contact } from '../card/Card' import { ProfileParams } from '../profile/Profile' import { Colors } from '../constants/Colors' import { modals } from '@mantine/modals' @@ -37,7 +38,7 @@ function Action({ icon, color, strings, select }: { icon: ReactNode; color: stri ) } -export function Contacts({ openRegistry, openContact, textContact, callContact }: { openRegistry: ()=>void; openContact: (params: ProfileParams)=>void, textContact: (cardId: string)=>void, callContact: (cardId: string)=>void }) { +export function Contacts({ openRegistry, openContact, textContact, callContact }: { openRegistry: ()=>void; openContact: (params: ProfileParams)=>void, textContact: (cardId: string)=>void, callContact: (card: Card)=>void }) { const { state, actions } = useContacts() const cards = state.filtered.map((card, idx) => { @@ -47,7 +48,7 @@ export function Contacts({ openRegistry, openContact, textContact, callContact } const phone = const text = return [ - callContact(card.cardId)} strings={state.strings} />, + callContact(card)} strings={state.strings} />, textContact(card.cardId)} strings={state.strings} />, ] } else if (status === 'offsync') { @@ -75,7 +76,7 @@ export function Contacts({ openRegistry, openContact, textContact, callContact } } return ( - + ) }) diff --git a/app/client/web/src/context/useRingContext.hook.ts b/app/client/web/src/context/useRingContext.hook.ts index 064837c3..aa757b70 100644 --- a/app/client/web/src/context/useRingContext.hook.ts +++ b/app/client/web/src/context/useRingContext.hook.ts @@ -18,11 +18,11 @@ export function useRingContext() { 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({ - ringing: [] as { cardId: string, callId: string }[], calls: [] as { callId: string, cardId: string}[], - cards: [] as Card[], calling: null as null | Card, localStream: null as null|MediaStream, remoteStream: null as null|MediaStream, @@ -40,11 +40,10 @@ export function useRingContext() { } useEffect(() => { - const calls = state.ringing - .map(ring => ({ callId: ring.callId, card: state.cards.find(card => ring.cardId === card.cardId) }) ) + 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 }); - }, [state.ringing, state.cards]); + }, [ringing, cards]); const getAudioStream = async (audioId: null|string) => { try { @@ -74,10 +73,10 @@ export function useRingContext() { if (call.current) { const { peer, link } = call.current; if (status === 'connected') { - await updatePeer('open'); + updateState({ connected: true }); await actions.enableAudio(); } else if (status === 'closed') { - await updatePeer('close'); + await cleanup; } } } @@ -141,11 +140,6 @@ export function useRingContext() { updateState({ localVideo: true, localStream: localStream.current }) } break; - case 'open': - updateState({ connected: true }); - case 'close': - await cleanup(); - break; default: console.log('unknown event'); break; @@ -229,18 +223,18 @@ export function useRingContext() { useEffect(() => { if (app.state.session) { - const setRinging = (ringing: { cardId: string, callId: string }[]) => { - updateState({ ringing }); + const setRing = (ringing: { cardId: string, callId: string }[]) => { + setRinging(ringing); } const setContacts = (cards: Card[]) => { - updateState({ cards }); + setCards(cards); } const ring = app.state.session.getRing(); ring.addRingingListener(setRinging); const contact = app.state.session.getContact(); contact.addCardListener(setContacts); return () => { - ring.removeRingingListener(setRinging); + ring.removeRingingListener(setRing); contact.removeCardListener(setContacts); cleanup(); } @@ -275,18 +269,14 @@ export function useRingContext() { throw err; } }, - call: async (cardId: string) => { + call: async (card: Card) => { if (connecting.current || closing.current || call.current) { throw new Error('not ready make calls'); } try { connecting.current = true; - const card = state.cards.find(contact => contact.cardId === cardId); - if (!card) { - throw new Error('calling contact not found'); - } const contact = app.state.session.getContact(); - const link = await contact.callCard(cardId); + const link = await contact.callCard(card.cardId); await setup(link, card); connecting.current = false; } catch (err) { diff --git a/app/client/web/src/session/Session.tsx b/app/client/web/src/session/Session.tsx index 9b212560..e34dda11 100644 --- a/app/client/web/src/session/Session.tsx +++ b/app/client/web/src/session/Session.tsx @@ -14,7 +14,7 @@ import { Profile, ProfileParams } from '../profile/Profile' import { Details } from '../details/Details'; import { Content } from '../content/Content' import { Conversation } from '../conversation/Conversation' -import { Focus } from 'databag-client-sdk' +import { Focus, Card } from 'databag-client-sdk' import { useDisclosure } from '@mantine/hooks' import { IconAlertCircle } from '@tabler/icons-react' import { Calling } from '../calling/Calling'; @@ -29,7 +29,7 @@ export function Session() { const [details, { open: openDetails, close: closeDetails }] = useDisclosure(false) const [profile, { open: openProfile, close: closeProfile }] = useDisclosure(false) const [textCard, setTextCard] = useState({ cardId: null} as {cardId: null|string}); - const [callCard, setCallCard] = useState({ cardId: null} as {cardId: null|string}); + const [callCard, setCallCard] = useState({ card: null} as {card: null|Card}); const textContact = (cardId: string) => { console.log("MESSAGE: ", cardId); @@ -38,8 +38,8 @@ export function Session() { setTab('content'); } - const callContact = (cardId: string) => { - setCallCard({ cardId }); + const callContact = (card: Card) => { + setCallCard({ card }); } return (