diff --git a/docs/CONFIGURE.md b/docs/CONFIGURE.md index 30a047cc..c83774a7 100644 --- a/docs/CONFIGURE.md +++ b/docs/CONFIGURE.md @@ -674,15 +674,16 @@ This plugin does not, by itself, stream audio to any online services. Because i | Key | Required | Default Value | Type | Description | | --------- | :------: | ------------- | -------------------- | ------------------------------------------------------------ | -| 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. | +| 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. | +| address | | | string | **Deprecated.** Use `url` instead. IP address or hostname to send this audio stream to. | +| port | | | number | **Deprecated.** Use `url` instead. 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. | | 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. | | 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). | 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). | 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. +| 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. ###### Plugin Object Example #1: This example will stream audio from talkgroup 58914 on system "CountyTrunked" to the local machine on UDP port 9123. diff --git a/docs/Plugins.md b/docs/Plugins.md index 93286a77..9b3dd46c 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -115,12 +115,13 @@ This plugin does not, by itself, stream audio to any online services. Because i | Key | Required | Default Value | Type | Description | | --------- | :------: | ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 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. | +| 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. | +| address | | | string | **Deprecated.** Use `url` instead. IP address or hostname to send this audio stream to. | +| port | | | number | **Deprecated.** Use `url` instead. 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. | -| useTCP | | false | **true** / **false** | When set to true, TCP will be used instead of UDP. | +| 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. | ###### Plugin Object Example #1: This example will stream audio from talkgroup 58914 on system "CountyTrunked" to the local machine on UDP port 9123. diff --git a/plugins/simplestream/simplestream.cc b/plugins/simplestream/simplestream.cc index 16b9e995..3d810477 100644 --- a/plugins/simplestream/simplestream.cc +++ b/plugins/simplestream/simplestream.cc @@ -9,9 +9,6 @@ using namespace boost::asio; typedef struct plugin_t plugin_t; typedef struct stream_t stream_t; -std::vector streams; -io_service my_tcp_io_service; -long max_tcp_index = 0; struct plugin_t { Config* config; @@ -35,8 +32,11 @@ struct stream_t { class Simple_Stream : public Plugin_Api { typedef boost::asio::io_service io_service; io_service my_io_service; + io_service my_tcp_io_service; ip::udp::endpoint remote_endpoint; ip::udp::socket my_socket{my_io_service}; + std::vector streams; + long max_tcp_index = 0; public: Simple_Stream(){ @@ -47,16 +47,52 @@ class Simple_Stream : public Plugin_Api { for (json element : config_data["streams"]) { stream_t stream; stream.TGID = element["TGID"]; - stream.address = element["address"]; - stream.port = element["port"]; - stream.remote_endpoint = ip::udp::endpoint(ip::address::from_string(stream.address), stream.port); + + if (element.contains("url")) { + // Parse url field: udp://hostname:port or tcp://hostname:port + std::string url = element["url"]; + if (url.substr(0, 6) == "udp://") { + stream.tcp = false; + url = url.substr(6); + } else if (url.substr(0, 6) == "tcp://") { + stream.tcp = true; + url = url.substr(6); + } else { + BOOST_LOG_TRIVIAL(error) << "SimpleStream: invalid URL scheme in \"" << element["url"] << "\", expected udp:// or tcp://"; + continue; + } + size_t colon = url.rfind(':'); + if (colon == std::string::npos) { + BOOST_LOG_TRIVIAL(error) << "SimpleStream: missing port in URL \"" << element["url"].get() << "\""; + continue; + } + stream.address = url.substr(0, colon); + stream.port = static_cast(std::stoul(url.substr(colon + 1))); + } else { + BOOST_LOG_TRIVIAL(warning) << "SimpleStream: address/port/useTCP are deprecated, please use the url field instead (e.g. \"url\": \"udp://hostname:port\")"; + stream.address = element["address"]; + stream.port = element["port"]; + stream.tcp = element.value("useTCP", false); + } + + if (!stream.tcp) { + ip::udp::resolver udp_resolver(my_io_service); + ip::udp::resolver::query udp_query(stream.address, std::to_string(stream.port)); + boost::system::error_code ec; + ip::udp::resolver::iterator iter = udp_resolver.resolve(udp_query, ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << "SimpleStream: failed to resolve UDP address " << stream.address << ": " << ec.message(); + continue; + } + stream.remote_endpoint = iter->endpoint(); + } + stream.sendTGID = element.value("sendTGID",false); stream.sendJSON = element.value("sendJSON",false); stream.sendCallStart = element.value("sendCallStart",false); stream.sendCallEnd = element.value("sendCallEnd",false); - stream.tcp = element.value("useTCP",false); stream.short_name = element.value("shortName", ""); - BOOST_LOG_TRIVIAL(info) << "simplestreamer will stream audio from TGID " <send(send_buffer); + try { + stream.tcp_socket->send(send_buffer); + } catch (const boost::system::system_error &e) { + BOOST_LOG_TRIVIAL(error) << "SimpleStream TCP send failed: " << e.what(); + } } else{ my_socket.send_to(send_buffer, stream.remote_endpoint, 0, error); + if (error) { + BOOST_LOG_TRIVIAL(warning) << "SimpleStream UDP send failed: " << error.message(); + } } } } @@ -165,7 +208,7 @@ class Simple_Stream : public Plugin_Api { } } - BOOST_FOREACH (auto stream, streams){ + BOOST_FOREACH (const auto &stream, streams){ if (stream.sendJSON == true && stream.sendCallStart == true){ if (0==stream.short_name.compare(call_short_name) || (0==stream.short_name.compare(""))){ //Check if shortName matches or is not specified if (patched_talkgroups.size() == 0){ @@ -201,10 +244,17 @@ class Simple_Stream : public Plugin_Api { send_buffer.push_back(buffer(json_string)); //prepend json data } if(stream.tcp == true){ - stream.tcp_socket->send(send_buffer); + try { + stream.tcp_socket->send(send_buffer); + } catch (const boost::system::system_error &e) { + BOOST_LOG_TRIVIAL(error) << "SimpleStream TCP send failed: " << e.what(); + } } else{ my_socket.send_to(send_buffer, stream.remote_endpoint, 0, error); + if (error) { + BOOST_LOG_TRIVIAL(warning) << "SimpleStream UDP send failed: " << error.message(); + } } } } @@ -216,7 +266,7 @@ class Simple_Stream : public Plugin_Api { int call_end(Call_Data_t call_info) { boost::system::error_code error; - BOOST_FOREACH (auto stream, streams){ + BOOST_FOREACH (const auto &stream, streams){ if (stream.sendJSON == true && stream.sendCallEnd == true){ if (0==stream.short_name.compare(call_info.short_name) || (0==stream.short_name.compare(""))){ //Check if shortName matches or is not specified std::vector patched_talkgroups; @@ -249,10 +299,17 @@ class Simple_Stream : public Plugin_Api { send_buffer.push_back(buffer(json_string)); //prepend json data } if(stream.tcp == true){ - stream.tcp_socket->send(send_buffer); + try { + stream.tcp_socket->send(send_buffer); + } catch (const boost::system::system_error &e) { + BOOST_LOG_TRIVIAL(error) << "SimpleStream TCP send failed: " << e.what(); + } } else{ my_socket.send_to(send_buffer, stream.remote_endpoint, 0, error); + if (error) { + BOOST_LOG_TRIVIAL(warning) << "SimpleStream UDP send failed: " << error.message(); + } } } } @@ -267,7 +324,18 @@ class Simple_Stream : public Plugin_Api { if (stream.tcp == true){ ip::tcp::socket *my_tcp_socket = new ip::tcp::socket{my_tcp_io_service}; stream.tcp_socket = my_tcp_socket; - stream.tcp_socket->connect(ip::tcp::endpoint( boost::asio::ip::address::from_string(stream.address), stream.port )); + ip::tcp::resolver tcp_resolver(my_tcp_io_service); + ip::tcp::resolver::query tcp_query(stream.address, std::to_string(stream.port)); + boost::system::error_code ec; + ip::tcp::resolver::iterator iter = tcp_resolver.resolve(tcp_query, ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << "SimpleStream: failed to resolve TCP address " << stream.address << ": " << ec.message(); + continue; + } + stream.tcp_socket->connect(iter->endpoint(), ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << "SimpleStream: TCP connect failed to " << stream.address << ":" << stream.port << ": " << ec.message(); + } } } my_socket.open(ip::udp::v4());