const allRPCs = {} // ensure no data gets lost when sent through the wire function K(obj) { return JSON.parse(JSON.stringify(obj)) } // https://stackoverflow.com/a/2117523 function uuidv4() { return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ) } function rpcStatusCheck() { console.table(Object.values(allRPCs).map(({target, kind, rpc}) => ({target, kind, status: rpc.iceConnectionState}) )) } function rpcCleanUp(kind, target) { for([uid, info] of Object.entries(allRPCs)) { const sameKind = info.kind === kind const sameTarget = info.target === target const shouldClose = (false || !State.online.includes(info.target) || (sameKind && kind === 'screen') || (sameKind && kind === 'video' && sameTarget) ) if(shouldClose) { info.rpc.close() delete allRPCs[uid] console.log(`${info.target} ${info.kind} closed`) } } } addEventListener('rpc-needed', ({detail}) => { const {kind, target} = detail.value const isInitiatedLocally = !detail.source const uid = detail.value.uid || uuidv4() const rpc = new RTCPeerConnection(rpcConfig) rpc.onicecandidate = ({candidate}) => { if(candidate && candidate.candidate) { wire({kind: 'ice-candidate', value: {...K(candidate), uid}, target}) } } rpc.ontrack = ({streams: [stream]}) => { stream.getTracks().forEach(track => { allRPCs[uid].receiver.addTrack(track, stream) }) } rpc.oniceconnectionstatechange = (e) => { console.log(`${target} ${kind} ${rpc.iceConnectionState}`) } rpc.onnegotiationneeded = (e) => { // if(isInitiatedLocally) { // signal({kind: 'rpc-initiate', value: {uid}}) // } } rpcCleanUp(kind, target) allRPCs[uid] = {kind, target, rpc, isInitiatedLocally} signal({kind: 'rpc-new', value: {kind, target, uid}}) if(isInitiatedLocally) { wire({kind: 'rpc-needed', value: {kind, target: State.username, uid}, target}) } }) addEventListener('rpc-setup', async ({detail}) => { const {uid, sender, receiver} = detail.value const {rpc, isInitiatedLocally} = allRPCs[uid] allRPCs[uid].sender = sender allRPCs[uid].receiver = receiver if(sender) { sender.getTracks().forEach(tr => rpc.addTrack(tr, sender)) } allRPCs[uid].loadedMedia = true if(isInitiatedLocally) { signal({kind: 'rpc-initiate', value: {uid}}) } else { signal({kind: 'rpc-respond', value: {uid}}) } }) addEventListener('rpc-initiate', async({detail}) => { const {uid} = detail.value const {rpc, target} = allRPCs[uid] const localOffer = await rpc.createOffer() await rpc.setLocalDescription(localOffer) wire({kind: 'rpc-offer', value: {...K(localOffer), uid}, target}) }) addEventListener('rpc-offer', async ({detail}) => { const {uid} = detail.value const {rpc} = allRPCs[uid] await rpc.setRemoteDescription(detail.value) allRPCs[uid].hasOffer = true signal({kind: 'rpc-respond', value: {uid}}) }) addEventListener('rpc-respond', async({detail}) => { const {uid} = detail.value const {rpc, target, loadedMedia, hasOffer} = allRPCs[uid] if(loadedMedia && hasOffer) { const localAnswer = await rpc.createAnswer() await rpc.setLocalDescription(localAnswer) wire({kind: 'rpc-answer', value: {...K(localAnswer), uid}, target}) } }) addEventListener('rpc-answer', async ({detail}) => { const {uid} = detail.value await allRPCs[uid].rpc.setRemoteDescription(detail.value) }) addEventListener('ice-candidate', async ({detail}) => { const {uid} = detail.value await allRPCs[uid].rpc.addIceCandidate(detail.value) }) addEventListener('leave', () => { rpcCleanUp() }) addEventListener('screen-stop', () => { rpcCleanUp('screen') }) addEventListener('load', () => { doNotLog.add('rpc-needed') doNotLog.add('rpc-offer') doNotLog.add('rpc-answer') doNotLog.add('ice-candidate') })