High performance Asterisk AudioSocket server implementation in asynchronous Python.
| Rev. | Time | Author | Message |
|---|---|---|---|
| cbeb3946c013 | 2023-03-10 03:12:14 | Ssor | tip Initial commit. |
| Name | Rev. | Time | Author |
|---|---|---|---|
| tip | cbeb3946c013 | 2023-03-10 03:12:14 | Ssor |
| Name | Rev. | Time | Author | Message |
|---|---|---|---|---|
| default | cbeb3946c013 | 2023-03-10 03:12:14 | Ssor | Initial commit. |
A high performance Asterisk AudioSocket server written in asynchronous Python.
AudioSocket is a relatively new addition to the Asterisk IP-PBX project that allows external applications to easily access the raw audio (both read/write) of active calls, as opposed to audio only being available via RTP.
Such external applications act as a TCP server that Asterisk connects to after encountering an appropriate line of code in the dialplan. After, Asterisk will begin sending audio to the specified address/port (the encoding of this audio varies, as discussed in the tips section). This module is an implementation of such a TCP server, in Python.
Note: Formal documentation is provided below and in the module file itself, for
real-world examples, see the the examples directory in the repository.
This Python module provides a clean and high performance interface for dealing with AudioSocket connections. To take advantage of the conventions established by the Python standard library, and to offer the best performance, this module is modeled around Python's concept of Protocols.
Underneath, this means that all connections are handled within a single thread,
and I/O notification primitives provided by the underlying OS are used to
signal when there is activity on any given connection, preventing the need to
have an full OS thread per-connection. As for users of this module, this means
all interaction with it occurs via two callbacks, and requires asyncio to
be imported as well.
The entire module is implemented within a single file, audiosocket.py. To
start using it, simply place the file somewhere your project can import it
from. After importing it (import audiosocket) and asyncio, the server can
be setup with two function calls - just like asyncio's own start_server -
by invoking:
server = await audiosocket.start_server(<on_audio_callback>, <on_exception_callback>, <address>, <port>)`
await server.serve_forever()
start_server() behaves just like asyncio.start_server(), meaning the
host, port and keyword arguments accepted by that function have the same
behavior with this one.
The function callbacks should accept the following arguments:
on_audio_callback, called any time audio data from the connected peer is
received. It is expected to be a function that accepts the following
arguments:
- uuid: The universally unique ID that identifies this specific
call's audio, provided as a hexdecimal string.
- `peer_name`: A tuple consisting of the IP address and port number of the
remote host the audio is being sent from.
- `audio`: A `bytearray` instance containing the received audio data.
An empty `bytearray` instance (`len(audio) == 0`)
indicates the call hung up and no more audio will
be received. Audio is either encoded in 8KHz, 16-bit
mono PCM (when using the standalone dialplan applcation), or
whatever audio codec was decided upon during call setup
(when using Dial() application). If this argument is empty
(has a length of 0), the call has been hung up and will not
generate any more audio.
- Any extra keyword arguments are passed along to
`asyncio.loop.create_server()`.
To send audio back to Asterisk, a bytes-like object must be returned by this callback. Audio must be sent back in chunks of 65,536 bytes or less (the size must be able to fit into a 16-bit unsigned integer). This audio must always be encoded as 8KHz, 16-bit mono PCM, regardless of the codec in use for the call.
Returning audiosocket.HANGUP_CALL_MESSAGE (or an empty bytearray instance)
will request that the call represented by the value of the uuid parameter
be hungup.
on_exception_callback, called any time an exception relating to the
connected peer is raised. It is expected to be a function that accepts the
following arguments:
- uuid: The universally unique ID that identifies the specific
call which caused the exception.
- `peer_name`: A tuple consisting of the IP address and port number of the
remote host the exception-causing call came from.
- `error`: An instance of the exception that occurred.
There are two ways to begin an AudioSocket connection within Asterisk, via a
standalone dialplan application (AudioSocket(<uuid>,<address:port>)) or as a
channel driver to Dial() (Dial(AudioSocket/<address:port>/<uuid>)). Using
the standalone application will cause Asterisk to send the server 8KHz, 16-bit
mono PCM (see Issues section). Using the Dial() application will cause
Asterisk to send audio encoded in whatever codec was agreed upon during call
setup (most commonly ULAW).
Since the programming model with this module is callback-based (which is not all that common), as opposed to making state that must persist between function calls global, a recommended approach is to make the callback functions methods in a class. That way instance variables can be used to reduce the scope of such variables, while still allowing the callback paradigm to work as intended.
If the standalone dialplan application is used to initiate an AudioSocket connection, delivering PCM audio to th server, the CPU core/thread the connection within Asterisk is running on will reach 100% utilization (likely a runaway loop, which I will try to identify and fix/report to them).
If there is a wireless medium at any point in the network link between Asterisk and the AudioSocket server, the Asterisk module encounters strange read/write errors. This is a problem with the Asterisk module/Asterisk itself.