codec2 audio stream working over reticulum link
This commit is contained in:
3780
public/assets/js/codec2-emscripten/c2dec.js
Executable file
3780
public/assets/js/codec2-emscripten/c2dec.js
Executable file
File diff suppressed because it is too large
Load Diff
BIN
public/assets/js/codec2-emscripten/c2dec.wasm
Executable file
BIN
public/assets/js/codec2-emscripten/c2dec.wasm
Executable file
Binary file not shown.
3780
public/assets/js/codec2-emscripten/c2enc.js
Executable file
3780
public/assets/js/codec2-emscripten/c2enc.js
Executable file
File diff suppressed because it is too large
Load Diff
BIN
public/assets/js/codec2-emscripten/c2enc.wasm
Executable file
BIN
public/assets/js/codec2-emscripten/c2enc.wasm
Executable file
Binary file not shown.
127
public/assets/js/codec2-emscripten/codec2-lib.js
Normal file
127
public/assets/js/codec2-emscripten/codec2-lib.js
Normal file
@@ -0,0 +1,127 @@
|
||||
class Codec2Lib {
|
||||
|
||||
static arrayBufferToBase64(buffer) {
|
||||
let binary = "";
|
||||
let bytes = new Uint8Array(buffer);
|
||||
for (let byte of bytes) {
|
||||
binary += String.fromCharCode(byte);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
static base64ToArrayBuffer(base64) {
|
||||
let binary = window.atob(base64);
|
||||
let bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
static readFileAsArrayBuffer(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
static runDecode(mode, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const module = {
|
||||
arguments: [mode, "input.bit", "output.raw"],
|
||||
preRun: () => {
|
||||
module.FS.writeFile("input.bit", new Uint8Array(data));
|
||||
},
|
||||
postRun: () => {
|
||||
let buffer = module.FS.readFile("output.raw", {
|
||||
encoding: "binary",
|
||||
});
|
||||
resolve(buffer);
|
||||
},
|
||||
};
|
||||
createC2Dec(module);
|
||||
});
|
||||
}
|
||||
|
||||
static runEncode(mode, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const module = {
|
||||
arguments: [mode, "input.raw", "output.bit"],
|
||||
preRun: () => {
|
||||
module.FS.writeFile("input.raw", new Uint8Array(data));
|
||||
},
|
||||
postRun: () => {
|
||||
let buffer = module.FS.readFile("output.bit", {
|
||||
encoding: "binary",
|
||||
});
|
||||
resolve(buffer);
|
||||
},
|
||||
};
|
||||
createC2Enc(module);
|
||||
});
|
||||
}
|
||||
|
||||
static rawToWav(buffer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const module = {
|
||||
arguments: [
|
||||
"-r",
|
||||
"8000",
|
||||
"-L",
|
||||
"-e",
|
||||
"signed-integer",
|
||||
"-b",
|
||||
"16",
|
||||
"-c",
|
||||
"1",
|
||||
"input.raw",
|
||||
"output.wav",
|
||||
],
|
||||
preRun: () => {
|
||||
module.FS.writeFile("input.raw", new Uint8Array(buffer));
|
||||
},
|
||||
postRun: () => {
|
||||
let output = module.FS.readFile("output.wav", {
|
||||
encoding: "binary",
|
||||
});
|
||||
resolve(output);
|
||||
},
|
||||
};
|
||||
SOXModule(module);
|
||||
});
|
||||
}
|
||||
|
||||
static audioFileToRaw(buffer, filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const module = {
|
||||
arguments: [
|
||||
filename,
|
||||
"-r",
|
||||
"8000",
|
||||
"-L",
|
||||
"-e",
|
||||
"signed-integer",
|
||||
"-b",
|
||||
"16",
|
||||
"-c",
|
||||
"1",
|
||||
"output.raw",
|
||||
],
|
||||
preRun: () => {
|
||||
module.FS.writeFile(filename, new Uint8Array(buffer));
|
||||
},
|
||||
postRun: () => {
|
||||
let output = module.FS.readFile("output.raw", {
|
||||
encoding: "binary",
|
||||
});
|
||||
resolve(output);
|
||||
},
|
||||
};
|
||||
SOXModule(module);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
127
public/assets/js/codec2-emscripten/index.html
Normal file
127
public/assets/js/codec2-emscripten/index.html
Normal file
@@ -0,0 +1,127 @@
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<div>Select a *.wav audio file.</div>
|
||||
<input id="file-input" type="file" accept="audio/wav"/>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<span>Select Codec2 Mode:</span>
|
||||
<select id="codec-mode">
|
||||
<option value="3200">3200</option>
|
||||
<option value="2400">2400</option>
|
||||
<option value="1600">1600</option>
|
||||
<option value="1400">1400</option>
|
||||
<option value="1300">1300</option>
|
||||
<option value="1200">1200</option>
|
||||
<option value="700C" selected>700C</option>
|
||||
<option value="450">450</option>
|
||||
<option value="450PWB">450PWB</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<div>Click to encode audio file as Codec2</div>
|
||||
<button type="submit" onclick="encode()">Encode</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<div>Codec2 audio represented as Base64</div>
|
||||
<textarea id="encoded-output" style="width:500px" rows="8"></textarea>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<div>Click to decode Codec2 audio back to WAVE audio</div>
|
||||
<button type="submit" onclick="decode()">Decode</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<div>Decoded audio available to listen to</div>
|
||||
<audio id="decoded-audio" controls></audio>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem;">
|
||||
<div>Input File Size: <span id="input-size">0 Bytes</span></div>
|
||||
<div>Encoded Data Size: <span id="encoded-size">0 Bytes</span></div>
|
||||
<div>Decoded Data Size: <span id="decoded-size">0 Bytes</span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="c2enc.js"></script>
|
||||
<script src="c2dec.js"></script>
|
||||
<script src="sox.js"></script>
|
||||
<script src="codec2-lib.js"></script>
|
||||
<script>
|
||||
|
||||
// find elements
|
||||
const codecModeElement = document.getElementById("codec-mode");
|
||||
const encodedOutputElement = document.getElementById("encoded-output");
|
||||
const fileInputElement = document.getElementById("file-input");
|
||||
const decodedAudioElement = document.getElementById("decoded-audio");
|
||||
const inputSizeElement = document.getElementById("input-size");
|
||||
const encodedSizeElement = document.getElementById("encoded-size");
|
||||
const decodedSizeElement = document.getElementById("decoded-size");
|
||||
|
||||
// update file size stats on change
|
||||
fileInputElement.onchange = function() {
|
||||
if(fileInputElement.files.length > 0){
|
||||
const file = fileInputElement.files[0];
|
||||
inputSizeElement.innerText = formatBytes(file.size);
|
||||
}
|
||||
}
|
||||
|
||||
async function encode() {
|
||||
|
||||
const file = fileInputElement.files[0];
|
||||
if(!file){
|
||||
alert("select a file first");
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = codecModeElement.value;
|
||||
|
||||
const buffer = await Codec2Lib.readFileAsArrayBuffer(file);
|
||||
const rawBuffer = await Codec2Lib.audioFileToRaw(buffer, file.name || "input.wav");
|
||||
const encoded = await Codec2Lib.runEncode(mode, rawBuffer);
|
||||
|
||||
encodedOutputElement.value = Codec2Lib.arrayBufferToBase64(encoded);
|
||||
inputSizeElement.innerText = formatBytes(file.size);
|
||||
encodedSizeElement.innerText = formatBytes(encoded.length);
|
||||
|
||||
}
|
||||
|
||||
async function decode() {
|
||||
|
||||
const mode = codecModeElement.value;
|
||||
const input = encodedOutputElement.value;
|
||||
|
||||
const encoded = Codec2Lib.base64ToArrayBuffer(input);
|
||||
const decodedRaw = await Codec2Lib.runDecode(mode, encoded);
|
||||
const decodedWav = await Codec2Lib.rawToWav(decodedRaw);
|
||||
|
||||
decodedAudioElement.src = URL.createObjectURL(new Blob([decodedWav], { type: "audio/wav" }));
|
||||
decodedSizeElement.innerText = formatBytes(decodedWav.length);
|
||||
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
|
||||
if(bytes === 0){
|
||||
return '0 Bytes';
|
||||
}
|
||||
|
||||
const k = 1024;
|
||||
const decimals = 0;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
55
public/assets/js/codec2-emscripten/processor.js
Normal file
55
public/assets/js/codec2-emscripten/processor.js
Normal file
@@ -0,0 +1,55 @@
|
||||
class AudioProcessor extends AudioWorkletProcessor {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.bufferSize = 4096; // Adjust the buffer size as needed
|
||||
this.sampleRate = 8000; // Target sample rate
|
||||
this.inputBuffer = new Float32Array(this.bufferSize);
|
||||
this.bufferIndex = 0;
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
const input = inputs[0];
|
||||
if (input.length > 0) {
|
||||
const inputData = input[0];
|
||||
for (let i = 0; i < inputData.length; i++) {
|
||||
if (this.bufferIndex < this.bufferSize) {
|
||||
this.inputBuffer[this.bufferIndex++] = inputData[i];
|
||||
}
|
||||
if (this.bufferIndex === this.bufferSize) {
|
||||
// Downsample the buffer and send to the main thread
|
||||
const downsampledBuffer = this.downsampleBuffer(this.inputBuffer, this.sampleRate);
|
||||
this.port.postMessage(downsampledBuffer);
|
||||
this.bufferIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
downsampleBuffer(buffer, targetSampleRate) {
|
||||
if (targetSampleRate === this.sampleRate) {
|
||||
return buffer;
|
||||
}
|
||||
const sampleRateRatio = this.sampleRate / targetSampleRate;
|
||||
const newLength = Math.round(buffer.length / sampleRateRatio);
|
||||
const result = new Float32Array(newLength);
|
||||
let offsetResult = 0;
|
||||
let offsetBuffer = 0;
|
||||
while (offsetResult < result.length) {
|
||||
const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
|
||||
let accum = 0;
|
||||
let count = 0;
|
||||
for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
|
||||
accum += buffer[i];
|
||||
count++;
|
||||
}
|
||||
result[offsetResult] = accum / count;
|
||||
offsetResult++;
|
||||
offsetBuffer = nextOffsetBuffer;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('audio-processor', AudioProcessor);
|
||||
5128
public/assets/js/codec2-emscripten/sox.js
Executable file
5128
public/assets/js/codec2-emscripten/sox.js
Executable file
File diff suppressed because it is too large
Load Diff
BIN
public/assets/js/codec2-emscripten/sox.wasm
Executable file
BIN
public/assets/js/codec2-emscripten/sox.wasm
Executable file
Binary file not shown.
231
public/call.html
Normal file
231
public/call.html
Normal file
@@ -0,0 +1,231 @@
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
|
||||
<div>
|
||||
<button onclick="startStreaming()">Start Streaming</button>
|
||||
<button onclick="stopStreaming()">Stop Streaming</button>
|
||||
<button onclick="startListening()">Start Listening</button>
|
||||
<button onclick="stopListening()">Stop Listening</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<select id="codec-mode">
|
||||
<option value="3200">3200</option>
|
||||
<option value="2400">2400</option>
|
||||
<option value="1600">1600</option>
|
||||
<option value="1400">1400</option>
|
||||
<option value="1300">1300</option>
|
||||
<option value="1200" selected>1200</option>
|
||||
<option value="700C">700C</option>
|
||||
<option value="450">450</option>
|
||||
<option value="450PWB">450PWB</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>Encoded Bytes Sent: <span id="encoded-bytes-sent"></span></div>
|
||||
|
||||
</div>
|
||||
<script src="assets/js/codec2-emscripten/c2enc.js"></script>
|
||||
<script src="assets/js/codec2-emscripten/c2dec.js"></script>
|
||||
<script src="assets/js/codec2-emscripten/sox.js"></script>
|
||||
<script src="assets/js/codec2-emscripten/codec2-lib.js"></script>
|
||||
<script>
|
||||
|
||||
// find elements
|
||||
const codecModeElement = document.getElementById("codec-mode");
|
||||
const encodedBytesSentElement = document.getElementById("encoded-bytes-sent");
|
||||
|
||||
var encodedBytesSent = 0;
|
||||
|
||||
let audioContext;
|
||||
let mediaStreamSource;
|
||||
let audioWorkletNode;
|
||||
let microphoneMediaStream;
|
||||
|
||||
var callWebsocket = null;
|
||||
var listenWebsocket = null;
|
||||
|
||||
async function startStreaming() {
|
||||
try {
|
||||
|
||||
// reset stats
|
||||
encodedBytesSent = 0;
|
||||
|
||||
// ask who to call
|
||||
const destinationHash = prompt("Enter destination hash to call");
|
||||
if(!destinationHash){
|
||||
return;
|
||||
}
|
||||
|
||||
// connect to websocket
|
||||
callWebsocket = new WebSocket(location.origin.replace(/^http/, 'ws') + "/call/initiate/" + destinationHash);
|
||||
callWebsocket.onopen = () => {
|
||||
console.log("connected to websocket");
|
||||
};
|
||||
|
||||
// load audio worklet module
|
||||
audioContext = new AudioContext({ sampleRate: 8000 });
|
||||
await audioContext.audioWorklet.addModule('assets/js/codec2-emscripten/processor.js');
|
||||
audioWorkletNode = new AudioWorkletNode(audioContext, 'audio-processor');
|
||||
|
||||
// handle audio received from audio worklet
|
||||
audioWorkletNode.port.onmessage = async (event) => {
|
||||
|
||||
// convert audio received from worklet processor to wav
|
||||
const buffer = encodeWAV(event.data, 8000);
|
||||
|
||||
// convert wav audio to codec2
|
||||
const rawBuffer = await Codec2Lib.audioFileToRaw(buffer, "audio.wav");
|
||||
const encoded = await Codec2Lib.runEncode(codecModeElement.value, rawBuffer);
|
||||
|
||||
// update stats
|
||||
encodedBytesSent += encoded.length;
|
||||
encodedBytesSentElement.innerText = formatBytes(encodedBytesSent);
|
||||
|
||||
// send encoded audio to websocket
|
||||
if(callWebsocket.readyState === WebSocket.OPEN){
|
||||
callWebsocket.send(encoded);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// request access to the microphone
|
||||
microphoneMediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
|
||||
// send mic audio to audio worklet
|
||||
mediaStreamSource = audioContext.createMediaStreamSource(microphoneMediaStream);
|
||||
mediaStreamSource.connect(audioWorkletNode);
|
||||
|
||||
} catch(error) {
|
||||
alert(error);
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function stopStreaming() {
|
||||
|
||||
// disconnect websocket
|
||||
if(callWebsocket){
|
||||
callWebsocket.close()
|
||||
}
|
||||
|
||||
// disconnect media stream source
|
||||
if(mediaStreamSource){
|
||||
mediaStreamSource.disconnect();
|
||||
}
|
||||
|
||||
// stop using microphone
|
||||
if(microphoneMediaStream){
|
||||
microphoneMediaStream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
// disconnect the audio worklet node
|
||||
if(audioWorkletNode){
|
||||
audioWorkletNode.disconnect();
|
||||
}
|
||||
|
||||
// close audio context
|
||||
if(audioContext){
|
||||
audioContext.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function startListening() {
|
||||
|
||||
// connect to websocket to get codec2 packets
|
||||
listenWebsocket = new WebSocket(location.origin.replace(/^http/, 'ws') + "/call/listen");
|
||||
listenWebsocket.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);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function stopListening() {
|
||||
if(listenWebsocket){
|
||||
listenWebsocket.close();
|
||||
listenWebsocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
function encodeWAV(samples, sampleRate = 8000, numChannels = 1) {
|
||||
|
||||
const buffer = new ArrayBuffer(44 + samples.length * 2);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
// RIFF chunk descriptor
|
||||
writeString(view, 0, 'RIFF');
|
||||
view.setUint32(4, 36 + samples.length * 2, true); // file length
|
||||
writeString(view, 8, 'WAVE');
|
||||
|
||||
// fmt sub-chunk
|
||||
writeString(view, 12, 'fmt ');
|
||||
view.setUint32(16, 16, true); // sub-chunk size
|
||||
view.setUint16(20, 1, true); // audio format (1 = PCM)
|
||||
view.setUint16(22, numChannels, true); // number of channels
|
||||
view.setUint32(24, sampleRate, true); // sample rate
|
||||
view.setUint32(28, sampleRate * numChannels * 2, true); // byte rate
|
||||
view.setUint16(32, numChannels * 2, true); // block align
|
||||
view.setUint16(34, 16, true); // bits per sample
|
||||
|
||||
// data sub-chunk
|
||||
writeString(view, 36, 'data');
|
||||
view.setUint32(40, samples.length * 2, true); // data chunk length
|
||||
|
||||
// write the PCM samples
|
||||
floatTo16BitPCM(view, 44, samples);
|
||||
|
||||
return buffer;
|
||||
|
||||
}
|
||||
|
||||
function writeString(view, offset, string) {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
function floatTo16BitPCM(output, offset, input) {
|
||||
for (let i = 0; i < input.length; i++, offset += 2) {
|
||||
const s = Math.max(-1, Math.min(1, input[i]));
|
||||
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
|
||||
if(bytes === 0){
|
||||
return '0 Bytes';
|
||||
}
|
||||
|
||||
const k = 1024;
|
||||
const decimals = 0;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
135
web.py
135
web.py
@@ -146,6 +146,141 @@ class ReticulumWebChat:
|
||||
|
||||
return websocket_response
|
||||
|
||||
# handle websocket clients for initiating a call
|
||||
@routes.get("/call/initiate/{destination_hash}")
|
||||
async def ws(request):
|
||||
|
||||
# get path params
|
||||
destination_hash = request.match_info.get("destination_hash", "")
|
||||
|
||||
# convert destination hash to bytes
|
||||
destination_hash = bytes.fromhex(destination_hash)
|
||||
|
||||
# prepare websocket response
|
||||
websocket_response = web.WebSocketResponse()
|
||||
await websocket_response.prepare(request)
|
||||
|
||||
# wait until we have a path to the destination
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
print("waiting for path to server")
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
while not RNS.Transport.has_path(destination_hash):
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# connect to server
|
||||
print("establishing link with server")
|
||||
server_identity = RNS.Identity.recall(destination_hash)
|
||||
server_destination = RNS.Destination(
|
||||
server_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
"call",
|
||||
"audio"
|
||||
)
|
||||
|
||||
# todo implement
|
||||
def link_established(link):
|
||||
print("link established")
|
||||
|
||||
# todo implement
|
||||
def link_closed(link):
|
||||
if link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
print("The link timed out, exiting now")
|
||||
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
|
||||
print("The link was closed by the server, exiting now")
|
||||
else:
|
||||
print("Link closed")
|
||||
|
||||
# todo implement
|
||||
def client_packet_received(message, packet):
|
||||
# todo, we don't send anything from the call initiator from the call receiver yet...
|
||||
pass
|
||||
|
||||
# create link
|
||||
link = RNS.Link(server_destination)
|
||||
|
||||
# register link state callbacks
|
||||
link.set_packet_callback(client_packet_received)
|
||||
link.set_link_established_callback(link_established)
|
||||
link.set_link_closed_callback(link_closed)
|
||||
|
||||
# handle websocket messages until disconnected
|
||||
async for msg in websocket_response:
|
||||
msg: WSMessage = msg
|
||||
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 initiator on websocket, to call receiver over reticulum link
|
||||
print("sending bytes to call receiver: {}".format(len(msg.data)))
|
||||
RNS.Packet(link, msg.data).send()
|
||||
|
||||
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())
|
||||
|
||||
return websocket_response
|
||||
|
||||
# handle websocket clients for listening for calls
|
||||
@routes.get("/call/listen")
|
||||
async def ws(request):
|
||||
|
||||
# prepare websocket response
|
||||
websocket_response = web.WebSocketResponse()
|
||||
await websocket_response.prepare(request)
|
||||
|
||||
# create destination to allow incoming audio calls
|
||||
server_identity = self.identity
|
||||
server_destination = RNS.Destination(
|
||||
server_identity,
|
||||
RNS.Destination.IN,
|
||||
RNS.Destination.SINGLE,
|
||||
"call",
|
||||
"audio",
|
||||
)
|
||||
|
||||
# client connected to us
|
||||
def client_connected(link):
|
||||
print("client connected")
|
||||
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")
|
||||
|
||||
# client sent us a packet
|
||||
def server_packet_received(message, packet):
|
||||
|
||||
# send audio received from call initiator to call receiver websocket
|
||||
asyncio.run(websocket_response.send_bytes(message))
|
||||
|
||||
# todo send our audio back to call initiator
|
||||
|
||||
# register link state callbacks
|
||||
server_destination.set_link_established_callback(client_connected)
|
||||
|
||||
# announce our call.audio destination
|
||||
print("call.audio announced and waiting for connection: "+ RNS.prettyhexrep(server_destination.hash))
|
||||
server_destination.announce()
|
||||
|
||||
# handle websocket messages until disconnected
|
||||
async for msg in websocket_response:
|
||||
msg: WSMessage = msg
|
||||
if msg.type == WSMsgType.ERROR:
|
||||
# ignore errors while handling message
|
||||
print('ws connection error %s' % websocket_response.exception())
|
||||
|
||||
return websocket_response
|
||||
|
||||
# serve announces
|
||||
@routes.get("/api/v1/announces")
|
||||
async def index(request):
|
||||
|
||||
Reference in New Issue
Block a user