diff --git a/public/call.html b/public/call.html index d730a52..98fac1c 100644 --- a/public/call.html +++ b/public/call.html @@ -101,8 +101,25 @@ // connect to websocket callWebsocket = new WebSocket(location.origin.replace(/^http/, 'ws') + "/call/initiate/" + destinationHash); - callWebsocket.onopen = () => { - console.log("connected to websocket"); + callWebsocket.onmessage = async function(event) { + + // get encoded codec2 bytes from websocket message + const encoded = await event.data.arrayBuffer(); + + // decode codec2 audio + const decoded = await Codec2Lib.runDecode(codecModeElement.value, encoded); + + // convert decoded codec2 to wav audio + const wavAudio = await Codec2Lib.rawToWav(decoded); + + // play wav audio buffer + let audioCtx = new AudioContext() + const audioBuffer = await audioCtx.decodeAudioData(wavAudio.buffer); + const sampleSource = audioCtx.createBufferSource(); + sampleSource.buffer = audioBuffer; + sampleSource.connect(audioCtx.destination) + sampleSource.start(0); + }; // record mic to send to websocket @@ -187,6 +204,28 @@ }; + // record mic to send to websocket + await startRecordingMicrophone((encoded) => { + + // do nothing if websocket closed + if(listenWebsocket.readyState !== WebSocket.OPEN){ + return; + } + + // do nothing when audio muted + if(checkboxMuteMicElement.checked){ + return; + } + + // send encoded audio to websocket + listenWebsocket.send(encoded); + + // update stats + encodedBytesSent += encoded.length; + encodedBytesSentElement.innerText = formatBytes(encodedBytesSent); + + }); + } function stopListening() { diff --git a/web.py b/web.py index 05f6f2a..912edda 100644 --- a/web.py +++ b/web.py @@ -84,6 +84,8 @@ class ReticulumWebChat: # remember websocket clients self.websocket_clients: List[web.WebSocketResponse] = [] + self.link_call_audio = None + # web server has shutdown, likely ctrl+c, but if we don't do the following, the script never exits async def shutdown(self, app): @@ -193,8 +195,9 @@ class ReticulumWebChat: # todo implement def client_packet_received(message, packet): - # todo, we don't send anything from the call initiator from the call receiver yet... - pass + + # send audio received from call receiver to call initiator websocket + asyncio.run(websocket_response.send_bytes(message)) # create link link = RNS.Link(server_destination) @@ -250,12 +253,14 @@ class ReticulumWebChat: # client connected to us def client_connected(link): print("client connected") + self.link_call_audio = link link.set_link_closed_callback(client_disconnected) link.set_packet_callback(server_packet_received) # client disconnected from us def client_disconnected(link): print("client disconnected") + self.link_call_audio = None # client sent us a packet def server_packet_received(message, packet): @@ -275,7 +280,26 @@ class ReticulumWebChat: # handle websocket messages until disconnected async for msg in websocket_response: msg: WSMessage = msg - if msg.type == WSMsgType.ERROR: + if msg.type == WSMsgType.BINARY: + try: + + # drop audio packet if it is too big to send + if len(msg.data) > RNS.Link.MDU: + print("dropping packet " + str(len(msg.data)) + " bytes exceeds the link packet MDU of " + str(RNS.Link.MDU) + " bytes") + continue + + # send codec2 audio received from call receiver on websocket, to call initiator over reticulum link + if self.link_call_audio is not None: + print("sending bytes to call initiator: {}".format(len(msg.data))) + RNS.Packet(self.link_call_audio, msg.data).send() + else: + print("link to call initiator not available") + + except Exception as e: + # ignore errors while handling message + print("failed to process client message") + print(e) + elif msg.type == WSMsgType.ERROR: # ignore errors while handling message print('ws connection error %s' % websocket_response.exception())