Skip to content

Commit

Permalink
simplestream updates to send variable-length JSON metadata along with…
Browse files Browse the repository at this point in the history
… audio (#891)

parent dfc4792
author aaknitt <andyknitt@gmail.com> 1703912162 -0600
committer aaknitt <andyknitt@gmail.com> 1703956056 -0600
  • Loading branch information
aaknitt authored Jan 6, 2024
1 parent dfc4792 commit a926a59
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 44 deletions.
35 changes: 22 additions & 13 deletions docs/CONFIGURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,13 +331,13 @@ This plugin makes it easy to connect Trunk Recorder with [Rdio Scanner](https://
**Name:** simplestream
**Library:** libsimplestream.so

This plugin streams uncompressed audio (16 bit Int, 8 kHz, mono) to UDP or TCP ports in real time as it is being recorded by trunk-recorder. It can be configured to stream audio from all talkgroups and systems being recorded or only specified talkgroups and systems. TGID information can be prepended to the audio data to allow the receiving program to take action based on the TGID. Audio from different Systems should be streamed to different UDP/TCP ports to prevent crosstalk and interleaved audio from talkgroups with the same TGID on different systems.
This plugin streams uncompressed audio (16 bit Int, 8 or 16 kHz, mono) to UDP or TCP ports in real time as it is being recorded by trunk-recorder. It can be configured to stream audio from all talkgroups and systems being recorded or only specified talkgroups and systems. TGID information can be prepended to the audio data to allow the receiving program to take action based on the TGID. Audio from different Systems should be streamed to different UDP/TCP ports to prevent crosstalk and interleaved audio from talkgroups with the same TGID on different systems.

This plugin does not, by itself, stream audio to any online services. Because it sends uncompressed PCM audio, it is not bandwidth efficient and is intended mostly to send audio to other programs running on the same computer as trunk-recorder or to other computers on the LAN. The programs receiving PCM audio from this plugin may play it on speakers, compress it and stream it to an online service, etc.

**NOTE 1: In order for this plugin to work, the audioStreaming option in the Global Configs section (see above) must be set to true.**

**NOTE 2: trunk-recorder passes analog audio to this plugin at 16 kHz sample rate and digital audio at 8 kHz sample rate. Since the audio data being streamed doesn't contain the sample rate, analog and digital audio should be configured to be sent to different ports to receivers that are matched to the same sample rate.**
**NOTE 2: trunk-recorder passes analog audio to this plugin at 16 kHz sample rate and digital audio at 8 kHz sample rate. JSON metadata (if enabled) will contain the sample rate of the audio being sent.**

| Key | Required | Default Value | Type | Description |
| ------- | :------: | ------------- | ------ | ------------------------------------------------------------ |
Expand All @@ -350,8 +350,17 @@ This plugin does not, by itself, stream audio to any online services. Because i
| address || | string | IP address to send this audio stream to. Use "127.0.0.1" to send to the same computer that trunk-recorder is running on. |
| port || | number | UDP or TCP port that this stream will send audio to. |
| TGID || | number | Audio from this Talkgroup ID will be sent on this stream. Set to 0 to stream all recorded talkgroups. |
| sendTGID | | false | **true** / **false** | When set to true, the TGID will be prepended in long integer format (4 bytes, little endian) to the audio data each time a packet is sent. |
| shortName | | | string | shortName of the System that audio should be streamed for. This should match the shortName of a system that is defined in the main section of the config file. When omitted, all Systems will be streamed to the address and port configured. If TGIDs from Systems overlap, each system must be sent to a different port to prevent interleaved audio for talkgroups from different Systems with the same TGID.
<<<<<<< HEAD
<<<<<<< HEAD
| sendJSON | | false | **true** / **false** | When set to true, JSON metadata will be prepended to the audio data each time a packet is sent. JSON fields are talkgroup, patched_talkgroups, src, freq, audoi_sample_rate, and short_name. The length of the JSON metadata is prepended to the metadata in long integer format (4 bytes, little endian). If this is set to **true**, the sendTGID field will be ignored.
=======
| sendJSON | | false | **true** / **false** | When set to true, JSON metadata will be prepended to the audio data each time a packet is sent. JSON fields are talkgroup, src, freq, audoi_sample_rate, and short_name. The length of the JSON metadata is prepended to the metadata in long integer format (4 bytes, little endian). If this is set to **true**, the sendTGID field will be ignored.
>>>>>>> 0e42072 (Update CONFIGURE.md)
=======
| sendJSON | | false | **true** / **false** | When set to true, JSON metadata will be prepended to the audio data each time a packet is sent. JSON fields are talkgroup, patched_talkgroups, src, freq, audoi_sample_rate, and short_name. The length of the JSON metadata is prepended to the metadata in long integer format (4 bytes, little endian). If this is set to **true**, the sendTGID field will be ignored.
>>>>>>> dcd66be (Update CONFIGURE.md)
| sendTGID | | false | **true** / **false** | Deprecated. Recommend using sendJSON for metadata instead. If sendJSON is set to true, this setting will be ignored. When set to true, the TGID will be prepended in long integer format (4 bytes, little endian) to the audio data each time a packet is sent. |
| shortName | | | string | shortName of the System that audio should be streamed for. This should match the shortName of a system that is defined in the main section of the config file. When omitted, all Systems will be streamed to the address and port configured. If TGIDs from Systems overlap, JSON metadata should be used to prevent interleaved audio for talkgroups from different Systems with the same TGID.
| useTCP | | false | **true** / **false** | When set to true, TCP will be used instead of UDP.

###### Plugin Object Example #1:
Expand All @@ -364,7 +373,7 @@ This example will stream audio from talkgroup 58914 on system "CountyTrunked" to
"TGID":58914,
"address":"127.0.0.1",
"port":9123,
"sendTGID":false,
"sendJSON":false,
"shortName":"CountyTrunked"}
}
```
Expand All @@ -379,19 +388,19 @@ This example will stream audio from talkgroup 58914 from System CountyTrunked to
"TGID":58914,
"address":"127.0.0.1",
"port":9123,
"sendTGID":false,
"sendJSON":false,
"shortName":"CountyTrunked"},
{"TGID":58916,
"address":"127.0.0.1",
"port":9124,
"sendTGID":false,
"sendJSON":false,
"shortName":"StateTrunked"}
]}
}
```

###### Plugin Object Example #3:
This example will stream audio from talkgroups 58914 and 58916 from all Systems to the local machine on the same UDP port 9123. It will prepend the TGID to the audio data in each UDP packet so that the receiving program can differentiate the two audio streams (the receiver may decide to only play one depending on priority, mix the two streams, play one left and one right, etc.)
This example will stream audio from talkgroups 58914 and 58916 from all Systems to the local machine on the same UDP port 9123. It will prepend the TGID and other JSON metadata to the audio data in each UDP packet so that the receiving program can differentiate the two audio streams (the receiver may decide to only play one depending on priority, mix the two streams, play one left and one right, etc.)
```yaml
{
"name":"simplestream",
Expand All @@ -400,16 +409,16 @@ This example will stream audio from talkgroups 58914 and 58916 from all Systems
"TGID":58914,
"address":"127.0.0.1",
"port":9123,
"sendTGID":true},
"sendJSON":true},
{"TGID":58916,
"address":"127.0.0.1",
"port":9123,
"sendTGID":true}
"sendJSON":true}
]}
}
```
###### Plugin Object Example #4:
This example will stream audio from all talkgroups being recorded on System CountyTrunked to the local machine on UDP port 9123. It will prepend the TGID to the audio data in each UDP packet so that the receiving program can decide which ones to play or otherwise handle)
This example will stream audio from all talkgroups being recorded on System CountyTrunked to the local machine on UDP port 9123. It will prepend the TGID and other JSON metadata to the audio data in each UDP packet so that the receiving program can decide which ones to play or otherwise handle)
```yaml
{
"name":"simplestream",
Expand All @@ -418,7 +427,7 @@ This example will stream audio from all talkgroups being recorded on System Coun
"TGID":0,
"address":"127.0.0.1",
"port":9123,
"sendTGID":true,
"sendJSON":true,
"shortName":"CountyTrunked"}
}
```
Expand All @@ -438,7 +447,7 @@ The matching simplestream config to send audio from talkgroup 58918 to TCP port
"TGID":58918,
"address":"127.0.0.1",
"port":9125,
"sendTGID":false,
"sendJSON":false,
"shortName":"CountyTrunked",
"useTCP":true}
}
Expand Down
57 changes: 44 additions & 13 deletions plugins/simplestream/example_audio_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
import struct
import pyaudio
import time
import json

TGID_in_stream = False #When set to True, we expect a 4 byte long int with the TGID prior to the audio in each packet
JSON_in_stream = False #When set to True, we expect a 4 byte long int with the length of the JSON metadata followed by the JSON metadata prior to the audio
TGID_to_play = 58917 #When TGID_in_stream is set to True, we'll only play audio if the received TGID matches this value
UDP_PORT = 9123 #UDP port to listen on
AUDIO_OUTPUT_DEVICE_INDEX = 2 #Audio device to play received audio on
AUDIO_OUTPUT_DEVICE_INDEX = 4 #Audio device to play received audio on
DEFAULT_AUDIO_SAMPLE_RATE = 8000

# Set up a UDP server
UDPSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
Expand All @@ -16,28 +19,56 @@
UDPSock.bind(listen_addr)

p = pyaudio.PyAudio()
chunk = int(3456/2)
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 8000
stream = p.open(format = FORMAT,
channels = CHANNELS,
rate = RATE,
print(p.get_host_api_info_by_index(0))

def create_audio_stream(rate=DEFAULT_AUDIO_SAMPLE_RATE):
stream = p.open(format = pyaudio.paInt16,
channels = 1,
rate = rate,
input = False,
output = True,
frames_per_buffer = chunk,
frames_per_buffer = int(3456/2),
output_device_index = AUDIO_OUTPUT_DEVICE_INDEX,)
return stream

streams = {}
if TGID_to_play != 0:
streams[TGID_to_play] = create_audio_stream()

while True:
try:
data,addr = UDPSock.recvfrom(2048*2)
if TGID_in_stream:
audio_start_byte = 0
playit = False
this_audio_sample_rate = DEFAULT_AUDIO_SAMPLE_RATE
if JSON_in_stream:
json_length = int.from_bytes(data[0:4],"little")
audio_start_byte = json_length + 4
json_bytes = data[4:json_length+4]
json_string = json_bytes.decode('utf8')
# Load the JSON to a Python list & dump it back out as formatted JSON
json_data = json.loads(json_string)
tgid = json_data['talkgroup']
this_audio_sample_rate = json_data['audio_sample_rate']
if tgid == TGID_to_play or TGID_to_play == 0:
playit = True
#print(json.dumps(json_data, indent=4, sort_keys=True))
elif TGID_in_stream:
tgid = int.from_bytes(data[0:4],"little")
print(tgid," ",len(data))
if (tgid == TGID_to_play or TGID_to_play == 0):
stream.write(data[4:])
playit = True
audio_start_byte = 4
else:
stream.write(data)
print(data)
tgid = TGID_to_play
playit = True
if playit == True:
#stream.write(data[4:])
if tgid not in streams:
streams[tgid] = create_audio_stream(rate=this_audio_sample_rate)
streams[tgid].write(data[audio_start_byte:])
print(json.dumps(json_data, indent=4, sort_keys=True))
#print(data)
print("received",len(data),"bytes and writing",len(data)-audio_start_byte,"audio bytes")
except socket.timeout:
pass
46 changes: 28 additions & 18 deletions plugins/simplestream/simplestream.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct stream_t {
ip::udp::endpoint remote_endpoint;
ip::tcp::socket *tcp_socket;
bool sendTGID = false;
bool sendJSON = false;
bool tcp = false;
};

Expand All @@ -49,6 +50,7 @@ class Simple_Stream : public Plugin_Api {
stream.port = element["port"];
stream.remote_endpoint = ip::udp::endpoint(ip::address::from_string(stream.address), stream.port);
stream.sendTGID = element.value("sendTGID",false);
stream.sendJSON = element.value("sendJSON",false);
stream.tcp = element.value("useTCP",false);
stream.short_name = element.value("shortName", "");
BOOST_LOG_TRIVIAL(info) << "simplestreamer will stream audio from TGID " <<stream.TGID << " on System " <<stream.short_name << " to " << stream.address <<" on port " << stream.port << " tcp is "<<stream.tcp;
Expand All @@ -69,27 +71,35 @@ class Simple_Stream : public Plugin_Api {
BOOST_FOREACH (auto& TGID, patched_talkgroups){
if ((TGID==stream.TGID || stream.TGID==0)){ //setting TGID to 0 in the config file will stream everything
BOOST_LOG_TRIVIAL(debug) << "got " <<sampleCount <<" samples - " <<sampleCount*2<<" bytes from recorder "<<recorder_id<<" for TGID "<<TGID;
if (stream.sendTGID==true){
//prepend 4 byte long tgid to the audio data
boost::array<mutable_buffer, 2> buf1 = {
buffer(&TGID,4),
buffer(samples, sampleCount*2)
json json_object;
std::string json_string;
std::vector<boost::asio::const_buffer> send_buffer;
if (stream.sendJSON==true){
//create JSON metadata
json_object = {
{"src", call->get_current_source_id()},
{"talkgroup", TGID},
{"patched_talkgroups",patched_talkgroups},
{"freq", call->get_freq()},
{"short_name", call->get_short_name()},
{"audio_sample_rate",recorder->get_wav_hz()},
};
if(stream.tcp==true){
stream.tcp_socket->send(buf1);
}
else{
my_socket.send_to(buf1, stream.remote_endpoint, 0, error);
}
json_string = json_object.dump();
uint32_t json_length = json_string.length(); //determine length in bytes
//BOOST_LOG_TRIVIAL(debug) << "json_length is " <<json_length <<" bytes";
send_buffer.push_back(buffer(&json_length,4)); //prepend length of the json data
send_buffer.push_back(buffer(json_string)); //prepend json data
//BOOST_LOG_TRIVIAL(debug) << "json_string is " <<json_string;
}
else if (stream.sendTGID==true){
send_buffer.push_back(buffer(&TGID,4)); //prepend 4 byte long tgid to the audio data
}
send_buffer.push_back(buffer(samples, sampleCount*2));
if(stream.tcp == true){
stream.tcp_socket->send(send_buffer);
}
else{
//just send the audio data
if(stream.tcp == true){
stream.tcp_socket->send(buffer(samples, sampleCount*2));
}
else{
my_socket.send_to(buffer(samples, sampleCount*2), stream.remote_endpoint, 0, error);
}
my_socket.send_to(send_buffer, stream.remote_endpoint, 0, error);
}
}
}
Expand Down

0 comments on commit a926a59

Please sign in to comment.