reworking ring context

This commit is contained in:
balzack 2025-02-03 11:47:19 -08:00
parent ccc658b5fb
commit ca145f4b40
2 changed files with 8224 additions and 5570 deletions

View File

@ -9,17 +9,17 @@ const CLOSE_POLL_MS = 100;
export function useRingContext() { export function useRingContext() {
const app = useContext(AppContext) as ContextType; const app = useContext(AppContext) as ContextType;
const display = useContext(DisplayContext) as ContextType; const display = useContext(DisplayContext) as ContextType;
const call = useRef(null as { policy: string, peer: RTCPeerConnection, link: Link, candidates: RTCIceCandidate[] } | null); const call = useRef(null as { peer: RTCPeerConnection, link: Link, candidates: RTCIceCandidate[] } | null);
const localStream = useRef(null as null|MediaStream); const localStream = useRef(null as null|MediaStream);
const localAudio = useRef(null as null|MediaStreamTrack); const localAudio = useRef(null as null|MediaStreamTrack);
const localVideo = useRef(null as null|MediaStreamTrack); const localVideo = useRef(null as null|MediaStreamTrack);
const remoteStream = useRef(null as null|MediaStream); const remoteStream = useRef(null as null|MediaStream);
const updatingPeer = useRef(false); const updatingPeer = useRef(false);
const peerUpdate = useRef([] as {type: string, data?: any}[]); const peerUpdate = useRef([] as {type: string, data?: any}[]);
const connecting = useRef(false);
const closing = useRef(false); const closing = useRef(false);
const [state, setState] = useState({ const [state, setState] = useState({
strings: display.state.strings,
ringing: [] as { cardId: string, callId: string }[], ringing: [] as { cardId: string, callId: string }[],
calls: [] as { callId: string, cardId: string}[], calls: [] as { callId: string, cardId: string}[],
cards: [] as Card[], cards: [] as Card[],
@ -74,93 +74,10 @@ export function useRingContext() {
if (call.current) { if (call.current) {
const { policy, peer, link } = call.current; const { policy, peer, link } = call.current;
if (status === 'connected') { if (status === 'connected') {
localVideo.current = null; await updatePeer('open');
localStream.current = null; await actions.enableAudio();
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') { } else if (status === 'closed') {
updatePeer('close'); await 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);
} }
} }
} }
@ -170,31 +87,94 @@ export function useRingContext() {
if (!updatingPeer.current) { if (!updatingPeer.current) {
updatingPeer.current = true; updatingPeer.current = true;
while (!closing.current && peerUpdate.current.length > 0) { while (!closing.current && call.current && peerUpdate.current.length > 0) {
const { peer, link, candidates } = call.current;
const { type, data } = peerUpdate.current.shift() || { type: '' }; const { type, data } = peerUpdate.current.shift() || { type: '' };
if (type === 'negotiate') { try {
await peerNegotiate(); switch (type) {
} else if (type === 'candidate') { case 'negotiate':
await peerCandidate(data); const description = await peer.createOffer();
} else if (type === 'message') { await peer.setLocalDescription(description);
await linkMessage(data); await link.sendMessage({ description });
} else if (type === 'remote_track') { break;
case 'candidate':
await link.sendMessage({ data });
break;
case 'message':
if (data.description) {
const offer = new RTCSessionDescription(data.description);
await peer.setRemoteDescription(offer);
if (data.description.type === 'offer') {
const description = await peer.createAnswer();
await peer.setLocalDescription(description);
link.sendMessage({ description });
}
for (const candidate of candidates) {
await peer.addIceCandidate(candidate);
};
call.current.candidates = [];
} else if (data.candidate) {
const candidate = new RTCIceCandidate(data.candidate);
if (peer.remoteDescription == null) {
candidates.push(candidate);
} else {
await peer.addIceCandidate(candidate);
}
}
break;
case 'remote_track':
if (remoteStream.current) { if (remoteStream.current) {
remoteStream.current.addTrack(data); remoteStream.current.addTrack(data);
if (data.kind === 'video') { if (data.kind === 'video') {
updateState({ remoteVideo: true }); updateState({ remoteVideo: true });
} }
} }
} else if (type === 'local_track') { break;
await peerTrack(data.track, data.stream); case 'local_track':
} else if (type === 'close' && call.current) { peer.addTrack(data.track, data.stream);
cleanup(); if (data.track.kind === 'audio') {
localAudio.current = data.track;
}
if (data.track.kind === 'video') {
localVideo.currrent = data.track;
localStream.current = data.stream;
updateState({ localVideo: true, localStream: localStream.current })
}
break;
case 'open':
updateState({ connected: true });
case 'close':
await cleanup();
break;
default:
console.log('unknown event');
break;
}
} catch (err) {
console.log(err);
updateState({ failed: true });
} }
} }
updatingPeer.current = false; updatingPeer.current = false;
} }
} }
const async setup = (link: Link) => {
localAudio.current = null;
localVideo.current = null;
localStream.current = null;
remoteStream.current = new MediaStream();
const ice = link.getIce();
const peer = transmit(ice);
const candidates = [] as RTCIceCandidate[];
call.current = { peer, link, candidates };
link.setStatusListener(linkStatus);
link.setMessageListener((msg: any) => updatePeer('message', msg));
updateState({ calling: card, failed: false, connected: false,
audioEnabled: false, videoEnabled: false, localVideo: false, remoteVideo: false,
localStream: localStream.current, remoteStream: remoteStream.current });
}
const async cleanup = () => { const async cleanup = () => {
closing.current = true; closing.current = true;
while (updatingPeer.current) { while (updatingPeer.current) {
@ -280,39 +260,39 @@ export function useRingContext() {
await cleanup(); await cleanup();
}, },
accept: async (callId: string, card: Card) => { accept: async (callId: string, card: Card) => {
if (closing.current || call.current) { if (connecting.current || closing.current || call.current) {
throw new Error('active call in progress'); throw new Error('not ready to accept calls');
} }
try {
connecting.current = true;
const { cardId, node } = card; const { cardId, node } = card;
const ring = app.state.session.getRing(); const ring = app.state.session.getRing();
const link = await ring.accept(cardId, callId, node); const link = await ring.accept(cardId, callId, node);
const ice = link.getIce(); await setup(link);
const peer = transmit(ice); connecting.current = false;
const policy = 'impolite'; } catch (err) {
const candidates = [] as RTCIceCandidate[]; connecting.current = false;
call.current = { policy, peer, link, candidates }; throw err;
link.setStatusListener(linkStatus); }
link.setMessageListener((msg: any) => updatePeer('message', msg));
updateState({ calling: card, failed: false, connected: false });
}, },
call: async (cardId: string) => { call: async (cardId: string) => {
if (closing.current || call.current) { if (connecting.current || closing.current || call.current) {
throw new Error('active call in proegress'); throw new Error('not ready make calls');
} }
try {
connecting.current = true;
const card = state.cards.find(contact => contact.cardId === cardId); const card = state.cards.find(contact => contact.cardId === cardId);
if (!card) { if (!card) {
throw new Error('calling contact not found'); throw new Error('calling contact not found');
} }
const contact = app.state.session.getContact(); const contact = app.state.session.getContact();
const link = await contact.callCard(cardId); const link = await contact.callCard(cardId);
const ice = link.getIce(); await setup(link);
const peer = transmit(ice); connecting.current = false;
const policy = 'polite'; } catch (err) {
const candidates = [] as RTCIceCandidate[]; connecting.current = false;
call.current = { policy, peer, link, candidates }; throw err;
link.setStatusListener(linkStatus); }
link.setMessageListener((msg: any) => updatePeer('message', msg));
updateState({ calling: card, failed: false, connected: false });
}, },
enableAudio: async () => { enableAudio: async () => {
if (!call.current) { if (!call.current) {
@ -324,14 +304,11 @@ export function useRingContext() {
if (!audioTrack) { if (!audioTrack) {
throw new Error('no available audio track'); throw new Error('no available audio track');
} }
localAudio.current = audioTrack;
localStream.current = audioStream;
updatePeer('local_track', { track: audioTrack, stream: audioStream }); updatePeer('local_track', { track: audioTrack, stream: audioStream });
updateState({ localAudio: true, localStream: audioStream, audioEnabled: true });
} else { } else {
localAudio.current.enabled = true; localAudio.current.enabled = true;
updateState({ audioEnabled: true });
} }
updateState({ audioEnabled: true });
}, },
disableAudio: async () => { disableAudio: async () => {
if (!call.current) { if (!call.current) {
@ -339,8 +316,8 @@ export function useRingContext() {
} }
if (localAudio.current) { if (localAudio.current) {
localAudio.current.enabled = false; localAudio.current.enabled = false;
updateState({ audioEnabled: false });
} }
updateState({ audioEnabled: false });
}, },
enableVideo: async () => { enableVideo: async () => {
if (!call.current) { if (!call.current) {
@ -350,16 +327,12 @@ export function useRingContext() {
const videoStream = await getVideoStream(null); const videoStream = await getVideoStream(null);
const videoTrack = videoStream.getTracks().find((track: MediaStreamTrack) => track.kind === 'video'); const videoTrack = videoStream.getTracks().find((track: MediaStreamTrack) => track.kind === 'video');
if (videoTrack) { if (videoTrack) {
localVideo.current = videoTrack;
localStream.current = videoStream;
updatePeer('local_track', { track: videoTrack, stream: videoStream }); updatePeer('local_track', { track: videoTrack, stream: videoStream });
updateState({ localVideo: true, localStream: videoStream });
} }
} } else {
if (localVideo.current) {
localVideo.current.enabled = true; localVideo.current.enabled = true;
updateState({ videoEnabled: true });
} }
updateState({ videoEnabled: true });
}, },
disableVideo: async () => { disableVideo: async () => {
if (!call.current) { if (!call.current) {
@ -367,8 +340,8 @@ export function useRingContext() {
} }
if (localVideo.current) { if (localVideo.current) {
localVideo.current.enabled = false; localVideo.current.enabled = false;
updateState({ videoEnabled: false });
} }
updateState({ videoEnabled: false });
}, },
} }

File diff suppressed because it is too large Load Diff