Skip to content

Commit 697a54e

Browse files
Pr 1123 merge (#1144)
* Move globals into respective class File-scope globals were shared across every instance of the class. If more than one SimpleStream instance were ever loaded, they would silently share the same stream list and TCP I/O service, likely causing data corruption and race conditions. * Use const auto & in BOOST_FOREACH stream loops Copying a struct with a raw pointer wont manage the socket lifetime, and two copies of the same pointer could interact badly. It also needlessly copies two std::string members and a UDP endpoint on every audio packet, which is a hot path. The const auto & form avoids all copies entirely. The start() and stop() loops already used auto& correctly, so this change just introduces consistency * Wrap all TCP calls with try/catch and add error logging Dropped TCP connections throws boots::system::system_error, which would terminate the TR process. It should now log errors and continue. For UDP, there was no logging for errors to know when packets are dropped * Use resolver for hostname support and add error logging from_string() only accepts literal IP addresses, entering a hostname would throw an error and crash TR with no explanation. * Implement URL-based configuration Adds support for a url field in the simplestream config. Can now be defined as udp://hostname:port or tcp://hostname:port instead of separate address, port, and useTCP fields. Added deprecation warning log and updated documentation to reflect changes and deprecation intent * Merge master into PR #1123 (SimpleStream improvements) Resolves conflicts in plugins/simplestream/simplestream.cc between the PR's class-encapsulation/resolver work and master's ASIO modernization (#1129). PR intent preserved: - streams/io_context/max_tcp_index moved from file-scope globals into Simple_Stream class members - BOOST_FOREACH loops use const auto& to avoid copying stream_t - TCP send wrapped in try/catch; UDP send_to errors logged - Hostname resolution support via resolver (now using the modern resolve(host, service) overload instead of the deprecated resolver::query / resolver::iterator API, since master moved to io_context) - url-based config (udp://host:port / tcp://host:port) with deprecation warning for address/port/useTCP Master changes preserved: - io_service -> io_context rename (#1129) applied to both my_io_context and the moved my_tcp_io_context - json_length scope fix (#1107) already absorbed via the PR base Docs auto-merged: docs/CONFIGURE.md keeps both master's outputRawAudio additions and the PR's url field documentation. --------- Co-authored-by: jameson-dev <178156579+jameson-dev@users.noreply.github.com>
1 parent c8ebef7 commit 697a54e

3 files changed

Lines changed: 89 additions & 21 deletions

File tree

docs/CONFIGURE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -708,15 +708,16 @@ This plugin does not, by itself, stream audio to any online services. Because i
708708

709709
| Key | Required | Default Value | Type | Description |
710710
| --------- | :------: | ------------- | -------------------- | ------------------------------------------------------------ |
711-
| 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. |
712-
| port || | number | UDP or TCP port that this stream will send audio to. |
711+
| url | | | string | URL specifying the protocol, host, and port for this stream, e.g. `udp://hostname.tld:8600` or `tcp://192.168.1.10:9000`. When set, `address` and `port` are not required, and `useTCP` is ignored — the protocol is determined by the URL scheme. Supports both hostnames and literal IP addresses. |
712+
| address | | | string | **Deprecated.** Use `url` instead. IP address or hostname to send this audio stream to. |
713+
| port | | | number | **Deprecated.** Use `url` instead. UDP or TCP port that this stream will send audio to. |
713714
| TGID || | number | Audio from this Talkgroup ID will be sent on this stream. Set to 0 to stream all recorded talkgroups. |
714715
| 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, src_tag, freq, audio_sample_rate, short_name, event (set to "audio"). 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. |
715716
| sendCallStart | | false | **true** / **false** | Only used if sendJSON is set to **true**. When set to true, a JSON message will be sent at the start of each call that includes the following JSON fields: talkgroup, talkgroup_tag, patched_talkgroups, patched_talkgroup_tags, src, src_tag, freq, short_name, event (set to "call_start"). The length of the JSON metadata is prepended to the metadata in long integer format (4 bytes, little endian).
716717
| sendCallEnd | | false | **true** / **false** | Only used if sendJSON is set to **true**. When set to true, a JSON message will be sent at the end of each call that includes the following JSON fields: talkgroup, patched_talkgroups, freq, short_name, event (set to "call_end"). The length of the JSON metadata is prepended to the metadata in long integer format (4 bytes, little endian).
717718
| 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. |
718719
| 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.
719-
| useTCP | | false | **true** / **false** | When set to true, TCP will be used instead of UDP.
720+
| useTCP | | false | **true** / **false** | **Deprecated.** Use `url` instead. When set to true, TCP will be used instead of UDP. Ignored if `url` is set — the protocol is determined by the URL scheme instead.
720721

721722
###### Plugin Object Example #1:
722723
This example will stream audio from talkgroup 58914 on system "CountyTrunked" to the local machine on UDP port 9123.

docs/Plugins.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,13 @@ This plugin does not, by itself, stream audio to any online services. Because i
115115

116116
| Key | Required | Default Value | Type | Description |
117117
| --------- | :------: | ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
118-
| 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. |
119-
| port || | number | UDP or TCP port that this stream will send audio to. |
118+
| url | | | string | URL specifying the protocol, host, and port for this stream, e.g. `udp://hostname.tld:8600` or `tcp://192.168.1.10:9000`. When set, `address` and `port` are not required, and `useTCP` is ignored — the protocol is determined by the URL scheme. Supports both hostnames and literal IP addresses. |
119+
| address | | | string | **Deprecated.** Use `url` instead. IP address or hostname to send this audio stream to. |
120+
| port | | | number | **Deprecated.** Use `url` instead. UDP or TCP port that this stream will send audio to. |
120121
| TGID || | number | Audio from this Talkgroup ID will be sent on this stream. Set to 0 to stream all recorded talkgroups. |
121122
| 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. |
122123
| 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. |
123-
| useTCP | | false | **true** / **false** | When set to true, TCP will be used instead of UDP. |
124+
| useTCP | | false | **true** / **false** | **Deprecated.** Use `url` instead. When set to true, TCP will be used instead of UDP. Ignored if `url` is set — the protocol is determined by the URL scheme instead. |
124125

125126
###### Plugin Object Example #1:
126127
This example will stream audio from talkgroup 58914 on system "CountyTrunked" to the local machine on UDP port 9123.

plugins/simplestream/simplestream.cc

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ using namespace boost::asio;
99

1010
typedef struct plugin_t plugin_t;
1111
typedef struct stream_t stream_t;
12-
std::vector<stream_t> streams;
13-
boost::asio::io_context my_tcp_io_context;
14-
long max_tcp_index = 0;
1512

1613
struct plugin_t {
1714
Config* config;
@@ -34,8 +31,11 @@ struct stream_t {
3431

3532
class Simple_Stream : public Plugin_Api {
3633
boost::asio::io_context my_io_context;
34+
boost::asio::io_context my_tcp_io_context;
3735
ip::udp::endpoint remote_endpoint;
3836
ip::udp::socket my_socket{my_io_context};
37+
std::vector<stream_t> streams;
38+
long max_tcp_index = 0;
3939
public:
4040

4141
Simple_Stream(){
@@ -46,16 +46,51 @@ class Simple_Stream : public Plugin_Api {
4646
for (json element : config_data["streams"]) {
4747
stream_t stream;
4848
stream.TGID = element["TGID"];
49-
stream.address = element["address"];
50-
stream.port = element["port"];
51-
stream.remote_endpoint = ip::udp::endpoint(ip::make_address(stream.address), stream.port);
49+
50+
if (element.contains("url")) {
51+
// Parse url field: udp://hostname:port or tcp://hostname:port
52+
std::string url = element["url"];
53+
if (url.substr(0, 6) == "udp://") {
54+
stream.tcp = false;
55+
url = url.substr(6);
56+
} else if (url.substr(0, 6) == "tcp://") {
57+
stream.tcp = true;
58+
url = url.substr(6);
59+
} else {
60+
BOOST_LOG_TRIVIAL(error) << "SimpleStream: invalid URL scheme in \"" << element["url"] << "\", expected udp:// or tcp://";
61+
continue;
62+
}
63+
size_t colon = url.rfind(':');
64+
if (colon == std::string::npos) {
65+
BOOST_LOG_TRIVIAL(error) << "SimpleStream: missing port in URL \"" << element["url"].get<std::string>() << "\"";
66+
continue;
67+
}
68+
stream.address = url.substr(0, colon);
69+
stream.port = static_cast<uint32_t>(std::stoul(url.substr(colon + 1)));
70+
} else {
71+
BOOST_LOG_TRIVIAL(warning) << "SimpleStream: address/port/useTCP are deprecated, please use the url field instead (e.g. \"url\": \"udp://hostname:port\")";
72+
stream.address = element["address"];
73+
stream.port = element["port"];
74+
stream.tcp = element.value("useTCP", false);
75+
}
76+
77+
if (!stream.tcp) {
78+
ip::udp::resolver udp_resolver(my_io_context);
79+
boost::system::error_code ec;
80+
auto results = udp_resolver.resolve(stream.address, std::to_string(stream.port), ec);
81+
if (ec || results.empty()) {
82+
BOOST_LOG_TRIVIAL(error) << "SimpleStream: failed to resolve UDP address " << stream.address << ": " << ec.message();
83+
continue;
84+
}
85+
stream.remote_endpoint = results.begin()->endpoint();
86+
}
87+
5288
stream.sendTGID = element.value("sendTGID",false);
5389
stream.sendJSON = element.value("sendJSON",false);
5490
stream.sendCallStart = element.value("sendCallStart",false);
5591
stream.sendCallEnd = element.value("sendCallEnd",false);
56-
stream.tcp = element.value("useTCP",false);
5792
stream.short_name = element.value("shortName", "");
58-
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;
93+
BOOST_LOG_TRIVIAL(info) << "SimpleStream will stream audio from TGID " <<stream.TGID << " on System " <<stream.short_name << " to " << stream.address <<" on port " << stream.port << " tcp is "<<stream.tcp;
5994
streams.push_back(stream);
6095
}
6196
return 0;
@@ -92,7 +127,7 @@ class Simple_Stream : public Plugin_Api {
92127
int recorder_id = local_recorder.get_num();
93128
long wav_hz = local_recorder.get_wav_hz();
94129
boost::system::error_code error;
95-
BOOST_FOREACH (auto stream, streams){
130+
BOOST_FOREACH (const auto &stream, streams){
96131
if (0==stream.short_name.compare(call_short_name) || (0==stream.short_name.compare(""))){ //Check if shortName matches or is not specified
97132
if (patched_talkgroups.size() == 0){
98133
patched_talkgroups.push_back(call_tgid); //call_info.talkgroup may be negative - we cast stream.TGID to signed for comparison
@@ -128,10 +163,17 @@ class Simple_Stream : public Plugin_Api {
128163
}
129164
send_buffer.push_back(buffer(samples, sampleCount*2));
130165
if(stream.tcp == true){
131-
stream.tcp_socket->send(send_buffer);
166+
try {
167+
stream.tcp_socket->send(send_buffer);
168+
} catch (const boost::system::system_error &e) {
169+
BOOST_LOG_TRIVIAL(error) << "SimpleStream TCP send failed: " << e.what();
170+
}
132171
}
133172
else{
134173
my_socket.send_to(send_buffer, stream.remote_endpoint, 0, error);
174+
if (error) {
175+
BOOST_LOG_TRIVIAL(warning) << "SimpleStream UDP send failed: " << error.message();
176+
}
135177
}
136178
}
137179
}
@@ -164,7 +206,7 @@ class Simple_Stream : public Plugin_Api {
164206
}
165207
}
166208

167-
BOOST_FOREACH (auto stream, streams){
209+
BOOST_FOREACH (const auto &stream, streams){
168210
if (stream.sendJSON == true && stream.sendCallStart == true){
169211
if (0==stream.short_name.compare(call_short_name) || (0==stream.short_name.compare(""))){ //Check if shortName matches or is not specified
170212
if (patched_talkgroups.size() == 0){
@@ -200,10 +242,17 @@ class Simple_Stream : public Plugin_Api {
200242
send_buffer.push_back(buffer(json_string)); //prepend json data
201243
}
202244
if(stream.tcp == true){
203-
stream.tcp_socket->send(send_buffer);
245+
try {
246+
stream.tcp_socket->send(send_buffer);
247+
} catch (const boost::system::system_error &e) {
248+
BOOST_LOG_TRIVIAL(error) << "SimpleStream TCP send failed: " << e.what();
249+
}
204250
}
205251
else{
206252
my_socket.send_to(send_buffer, stream.remote_endpoint, 0, error);
253+
if (error) {
254+
BOOST_LOG_TRIVIAL(warning) << "SimpleStream UDP send failed: " << error.message();
255+
}
207256
}
208257
}
209258
}
@@ -215,7 +264,7 @@ class Simple_Stream : public Plugin_Api {
215264

216265
int call_end(Call_Data_t call_info) {
217266
boost::system::error_code error;
218-
BOOST_FOREACH (auto stream, streams){
267+
BOOST_FOREACH (const auto &stream, streams){
219268
if (stream.sendJSON == true && stream.sendCallEnd == true){
220269
if (0==stream.short_name.compare(call_info.short_name) || (0==stream.short_name.compare(""))){ //Check if shortName matches or is not specified
221270
std::vector<long> patched_talkgroups;
@@ -248,10 +297,17 @@ class Simple_Stream : public Plugin_Api {
248297
send_buffer.push_back(buffer(json_string)); //prepend json data
249298
}
250299
if(stream.tcp == true){
251-
stream.tcp_socket->send(send_buffer);
300+
try {
301+
stream.tcp_socket->send(send_buffer);
302+
} catch (const boost::system::system_error &e) {
303+
BOOST_LOG_TRIVIAL(error) << "SimpleStream TCP send failed: " << e.what();
304+
}
252305
}
253306
else{
254307
my_socket.send_to(send_buffer, stream.remote_endpoint, 0, error);
308+
if (error) {
309+
BOOST_LOG_TRIVIAL(warning) << "SimpleStream UDP send failed: " << error.message();
310+
}
255311
}
256312
}
257313
}
@@ -266,7 +322,17 @@ class Simple_Stream : public Plugin_Api {
266322
if (stream.tcp == true){
267323
ip::tcp::socket *my_tcp_socket = new ip::tcp::socket{my_tcp_io_context};
268324
stream.tcp_socket = my_tcp_socket;
269-
stream.tcp_socket->connect(ip::tcp::endpoint( ip::make_address(stream.address), stream.port ));
325+
ip::tcp::resolver tcp_resolver(my_tcp_io_context);
326+
boost::system::error_code ec;
327+
auto results = tcp_resolver.resolve(stream.address, std::to_string(stream.port), ec);
328+
if (ec || results.empty()) {
329+
BOOST_LOG_TRIVIAL(error) << "SimpleStream: failed to resolve TCP address " << stream.address << ": " << ec.message();
330+
continue;
331+
}
332+
stream.tcp_socket->connect(results.begin()->endpoint(), ec);
333+
if (ec) {
334+
BOOST_LOG_TRIVIAL(error) << "SimpleStream: TCP connect failed to " << stream.address << ":" << stream.port << ": " << ec.message();
335+
}
270336
}
271337
}
272338
my_socket.open(ip::udp::v4());

0 commit comments

Comments
 (0)