Files
reticulum-meshchatX/src/audio_call_manager.py
2024-05-21 03:22:45 +12:00

209 lines
6.8 KiB
Python

import asyncio
from typing import List
import RNS
# todo optionally identity self over link
# todo allowlist/denylist for incoming calls
class AudioCall:
def __init__(self, link: RNS.Link, is_outbound: bool):
self.link = link
self.is_outbound = is_outbound
self.link.set_link_closed_callback(self.on_link_closed)
self.link.set_packet_callback(self.on_packet)
self.audio_packet_listeners = []
self.hangup_listeners = []
def register_audio_packet_listener(self, callback):
self.audio_packet_listeners.append(callback)
def unregister_audio_packet_listener(self, callback):
self.audio_packet_listeners.remove(callback)
def register_hangup_listener(self, callback):
self.hangup_listeners.append(callback)
# handle link being closed
def on_link_closed(self, link):
print("[AudioCall] on_link_closed")
# call all hangup listeners
for hangup_listener in self.hangup_listeners:
hangup_listener()
# handle packet received over link
def on_packet(self, message, packet):
# send audio received from call initiator to all audio packet listeners
for audio_packet_listener in self.audio_packet_listeners:
audio_packet_listener(message)
# send an audio packet over the link
def send_audio_packet(self, data):
# do nothing if link is not active
if self.is_active() is False:
return
# drop audio packet if it is too big to send
if len(data) > RNS.Link.MDU:
print("[AudioCall] dropping audio packet " + str(len(data)) + " bytes exceeds the link packet MDU of " + str(RNS.Link.MDU) + " bytes")
return
# send codec2 audio received from call receiver to call initiator over reticulum link
RNS.Packet(self.link, data).send()
# gets the identity of the other person, or returns None if they did not identify
def get_remote_identity(self):
return self.link.get_remote_identity()
# determine if this call is still active
def is_active(self):
return self.link.status == RNS.Link.ACTIVE
# handle hanging up the call
def hangup(self):
print("[AudioCall] hangup")
self.link.teardown()
pass
class AudioCallManager:
def __init__(self, identity: RNS.Identity):
self.identity = identity
self.on_incoming_call_callback = None
self.on_outgoing_call_callback = None
self.audio_call_receiver = AudioCallReceiver(manager=self)
# remember audio calls
self.audio_calls: List[AudioCall] = []
# announces the audio call destination
def announce(self, app_data=None):
self.audio_call_receiver.destination.announce(app_data)
print("[AudioCallManager] announced destination: " + RNS.prettyhexrep(self.audio_call_receiver.destination.hash))
# set the callback for incoming calls
def register_incoming_call_callback(self, callback):
self.on_incoming_call_callback = callback
# set the callback for outgoing calls
def register_outgoing_call_callback(self, callback):
self.on_outgoing_call_callback = callback
# handle incoming calls from audio call receiver
def handle_incoming_call(self, audio_call: AudioCall):
# remember it
self.audio_calls.append(audio_call)
# fire callback
if self.on_incoming_call_callback is not None:
self.on_incoming_call_callback(audio_call)
# handle outgoing calls
def handle_outgoing_call(self, audio_call: AudioCall):
# remember it
self.audio_calls.append(audio_call)
# fire callback
if self.on_outgoing_call_callback is not None:
self.on_outgoing_call_callback(audio_call)
# find an existing audio call from the provided link hash
def find_audio_call_by_link_hash(self, link_hash: bytes):
for audio_call in self.audio_calls:
if audio_call.link.hash == link_hash:
return audio_call
return None
# delete an existing audio call from the provided link hash
def delete_audio_call_by_link_hash(self, link_hash: bytes):
audio_call = self.find_audio_call_by_link_hash(link_hash)
if audio_call is not None:
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:
# wait until we have a path to the destination
# FIXME: implement timeout instead of looping forever
if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash)
while not RNS.Transport.has_path(destination_hash):
await asyncio.sleep(0.1)
# create outbound destination to initiate audio calls
server_identity = RNS.Identity.recall(destination_hash)
server_destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
"call",
"audio"
)
# create link
link = RNS.Link(server_destination)
# register link state callbacks
link.set_link_established_callback(self.on_link_established)
return link.hash
def on_link_established(self, link: RNS.Link):
# todo: this can be optional, it's only being sent by default for ui, can be removed
link.identify(self.identity)
# create audio call
audio_call = AudioCall(link, is_outbound=True)
# handle new outgoing call
self.handle_outgoing_call(audio_call)
class AudioCallReceiver:
def __init__(self, manager: AudioCallManager):
self.manager = manager
# create destination for receiver audio calls
self.destination = RNS.Destination(
self.manager.identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
"call",
"audio",
)
# register link state callbacks
self.destination.set_link_established_callback(self.client_connected)
# find an existing audio call from the provided link
def find_audio_call_by_link_hash(self, link_hash: bytes):
for audio_call in self.manager.audio_calls:
if audio_call.link.hash == link_hash:
return audio_call
return None
# client connected to us, set up an audio call instance
def client_connected(self, link: RNS.Link):
# todo: this can be optional, it's only being sent by default for ui, can be removed
link.identify(self.manager.identity)
# create audio call
audio_call = AudioCall(link, is_outbound=False)
# pass to manager
self.manager.handle_incoming_call(audio_call)