diff --git a/public/call.html b/public/call.html
index 41054d2..126b641 100644
--- a/public/call.html
+++ b/public/call.html
@@ -344,7 +344,11 @@
try {
// initiate call
- const response = await axios.get(`/api/v1/calls/initiate/${destinationHash}`);
+ const response = await axios.get(`/api/v1/calls/initiate/${destinationHash}`, {
+ params: {
+ timeout: 15, // how long to attempt to initiate call
+ },
+ });
// get call hash from response
const hash = response.data.hash;
@@ -372,8 +376,13 @@
alert("timed out attempting to join call");
} catch(e) {
- alert("failed to initiate call");
+
console.log(e);
+
+ // show error message from response, or fallback to default
+ const message = e.response?.data?.message ?? "failed to initiate call";
+ alert(message);
+
} finally {
// hide loading
this.isInitiatingCall = false;
diff --git a/src/audio_call_manager.py b/src/audio_call_manager.py
index 2be9e95..e0b1718 100644
--- a/src/audio_call_manager.py
+++ b/src/audio_call_manager.py
@@ -1,4 +1,5 @@
import asyncio
+import time
from typing import List
import RNS
@@ -130,16 +131,23 @@ class AudioCallManager:
self.audio_calls.remove(audio_call)
# attempts to initiate a call to the provided destination and returns the link hash on success
- # FIXME: implement timeout. at the moment, it loops forever if no path is found
- async def initiate(self, destination_hash: bytes) -> bytes:
+ async def initiate(self, destination_hash: bytes, timeout_seconds: int = 15) -> bytes | None:
- # wait until we have a path to the destination
- # FIXME: implement timeout instead of looping forever
+ # check if we have a path to the destination
if not RNS.Transport.has_path(destination_hash):
+
+ # we don't have a path, so we need to request it
RNS.Transport.request_path(destination_hash)
- while not RNS.Transport.has_path(destination_hash):
+
+ # wait until we have a path, or give up after the configured timeout
+ timeout_after_seconds = time.time() + timeout_seconds
+ while not RNS.Transport.has_path(destination_hash) and time.time() < timeout_after_seconds:
await asyncio.sleep(0.1)
+ # if we still don't have a path, we can't establish a link, so bail out
+ if not RNS.Transport.has_path(destination_hash):
+ return None
+
# create outbound destination to initiate audio calls
server_identity = RNS.Identity.recall(destination_hash)
server_destination = RNS.Destination(
diff --git a/web.py b/web.py
index e6c8001..2c75827 100644
--- a/web.py
+++ b/web.py
@@ -304,12 +304,19 @@ class ReticulumWebChat:
# get path params
destination_hash = request.match_info.get("destination_hash", "")
+ timeout_seconds = int(request.query.get("timeout", 15))
+
+ print(timeout_seconds)
# convert destination hash to bytes
destination_hash = bytes.fromhex(destination_hash)
# initiate audio call
- link_hash = await self.audio_call_manager.initiate(destination_hash)
+ link_hash = await self.audio_call_manager.initiate(destination_hash, timeout_seconds)
+ if link_hash is None:
+ return web.json_response({
+ "message": "timed out initiating call",
+ }, status=503)
return web.json_response({
"hash": link_hash.hex(),