Skip to content

Commit 5ee8d92

Browse files
committed
feat: use monitor names for go2rtc stream aliases
1 parent ee29134 commit 5ee8d92

9 files changed

Lines changed: 198 additions & 55 deletions

File tree

CMakeLists.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,17 @@ add_subdirectory(onvif)
893893

894894
if(BUILD_TEST_SUITE)
895895
message("Building unit tests: Yes")
896-
find_package(Catch2 REQUIRED)
896+
find_package(Catch2 QUIET)
897+
if(NOT Catch2_FOUND)
898+
message(STATUS "Catch2 not found via find_package; fetching it with FetchContent")
899+
include(FetchContent)
900+
FetchContent_Declare(
901+
Catch2
902+
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
903+
GIT_TAG v3.7.1
904+
)
905+
FetchContent_MakeAvailable(Catch2)
906+
endif()
897907

898908
include(CTest)
899909
add_subdirectory(tests)

src/zm_monitor_go2rtc.cpp

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -222,53 +222,59 @@ int Monitor::Go2RTCManager::add_to_Go2RTC() {
222222
// connection limits) and provides a single point of access.
223223
const std::string &primary_path = Use_RTSP_Restream ? rtsp_restream_path : rtsp_path;
224224
const std::string id_str = std::to_string(parent->Id());
225+
const std::string monitor_name = parent->Name();
226+
227+
const auto add_stream = [&](const std::string &stream_name,
228+
const std::string &display_name,
229+
const std::string &stream_path,
230+
const char *label) -> bool {
231+
Debug(1, "Go2RTC: Adding %s stream '%s' - path: %s", label, stream_name.c_str(), stream_path.c_str());
232+
std::string endpoint = Go2RTC_endpoint + "/streams?name=" + UriEncode(stream_name) + "&src=" + UriEncode(stream_path);
233+
std::string postData = "{\"name\" : \"" + escape_json_string(display_name) + "\", \"src\": \"" +
234+
escape_json_string(stream_path) + "\" }";
235+
Debug(2, "Go2RTC: PUT to %s with data: %s", endpoint.c_str(), postData.c_str());
236+
std::pair<CURLcode, std::string> response = CURL_PUT(endpoint, postData);
237+
if (response.first != CURLE_OK) {
238+
Warning("Go2RTC: Failed to add %s stream '%s'", label, stream_name.c_str());
239+
return false;
240+
}
241+
Debug(1, "Go2RTC: Successfully added %s stream '%s', response: %s",
242+
label, stream_name.c_str(), response.second.c_str());
243+
return true;
244+
};
225245

226246
// Add primary stream using just the monitor ID (for backward compatibility)
227-
Debug(1, "Go2RTC: Adding primary stream (monitor ID) - path: %s%s",
228-
primary_path.c_str(), Use_RTSP_Restream ? " (via RTSP restreamer)" : "");
229-
std::string endpoint = Go2RTC_endpoint + "/streams?name=" + id_str + "&src=" + UriEncode(primary_path);
230-
std::string postData = "{\"name\" : \"" + std::string(parent->Name()) + "\", \"src\": \"" + primary_path + "\" }";
231-
Debug(2, "Go2RTC: PUT to %s with data: %s", endpoint.c_str(), postData.c_str());
232-
std::pair<CURLcode, std::string> response = CURL_PUT(endpoint, postData);
233-
if (response.first != CURLE_OK) {
247+
if (!add_stream(id_str, monitor_name, primary_path, "primary (monitor ID)")) {
234248
curl_easy_cleanup(curl);
235-
Warning("Go2RTC: Failed to add primary stream (monitor ID)");
236249
return -1;
237250
}
238-
Debug(1, "Go2RTC: Successfully added primary stream (monitor ID), response: %s", response.second.c_str());
251+
if (monitor_name != id_str) {
252+
add_stream(monitor_name, monitor_name, primary_path, "primary (monitor name)");
253+
}
239254

240255
// Add ZoneMinder restream paths (when RTSP restreamer is enabled)
241256
if (Use_RTSP_Restream) {
242-
Debug(1, "Go2RTC: Adding ZoneMinderPrimary stream - path: %s", rtsp_restream_path.c_str());
243-
endpoint = Go2RTC_endpoint + "/streams?name=" + id_str + "_ZoneMinderPrimary&src=" + UriEncode(rtsp_restream_path);
244-
postData = "{\"name\" : \"" + std::string(parent->Name()) + " ZoneMinder Primary\", \"src\": \"" + rtsp_restream_path + "\" }";
245-
Debug(2, "Go2RTC: PUT to %s", endpoint.c_str());
246-
response = CURL_PUT(endpoint, postData);
247-
if (response.first == CURLE_OK) {
248-
Debug(1, "Go2RTC: Successfully added ZoneMinderPrimary, response: %s", response.second.c_str());
257+
if (add_stream(id_str + "_ZoneMinderPrimary", monitor_name + " ZoneMinder Primary",
258+
rtsp_restream_path, "ZoneMinderPrimary (monitor ID)") && monitor_name != id_str) {
259+
add_stream(monitor_name + "_ZoneMinderPrimary", monitor_name + " ZoneMinder Primary",
260+
rtsp_restream_path, "ZoneMinderPrimary (monitor name)");
249261
}
250262
}
251263

252264
// Add direct camera paths
253265
if (!rtsp_path.empty()) {
254-
Debug(1, "Go2RTC: Adding CameraDirectPrimary stream - path: %s", rtsp_path.c_str());
255-
endpoint = Go2RTC_endpoint + "/streams?name=" + id_str + "_CameraDirectPrimary&src=" + UriEncode(rtsp_path);
256-
postData = "{\"name\" : \"" + std::string(parent->Name()) + " Camera Direct Primary\", \"src\": \"" + rtsp_path + "\" }";
257-
Debug(2, "Go2RTC: PUT to %s", endpoint.c_str());
258-
response = CURL_PUT(endpoint, postData);
259-
if (response.first == CURLE_OK) {
260-
Debug(1, "Go2RTC: Successfully added CameraDirectPrimary, response: %s", response.second.c_str());
266+
if (add_stream(id_str + "_CameraDirectPrimary", monitor_name + " Camera Direct Primary",
267+
rtsp_path, "CameraDirectPrimary (monitor ID)") && monitor_name != id_str) {
268+
add_stream(monitor_name + "_CameraDirectPrimary", monitor_name + " Camera Direct Primary",
269+
rtsp_path, "CameraDirectPrimary (monitor name)");
261270
}
262271
}
263272

264273
if (!rtsp_second_path.empty()) {
265-
Debug(1, "Go2RTC: Adding CameraDirectSecondary stream - path: %s", rtsp_second_path.c_str());
266-
endpoint = Go2RTC_endpoint + "/streams?name=" + id_str + "_CameraDirectSecondary&src=" + UriEncode(rtsp_second_path);
267-
postData = "{\"name\" : \"" + std::string(parent->Name()) + " Camera Direct Secondary\", \"src\": \"" + rtsp_second_path + "\" }";
268-
Debug(2, "Go2RTC: PUT to %s", endpoint.c_str());
269-
response = CURL_PUT(endpoint, postData);
270-
if (response.first == CURLE_OK) {
271-
Debug(1, "Go2RTC: Successfully added CameraDirectSecondary, response: %s", response.second.c_str());
274+
if (add_stream(id_str + "_CameraDirectSecondary", monitor_name + " Camera Direct Secondary",
275+
rtsp_second_path, "CameraDirectSecondary (monitor ID)") && monitor_name != id_str) {
276+
add_stream(monitor_name + "_CameraDirectSecondary", monitor_name + " Camera Direct Secondary",
277+
rtsp_second_path, "CameraDirectSecondary (monitor name)");
272278
}
273279
} else {
274280
Debug(2, "Go2RTC: No secondary camera path configured");
@@ -404,4 +410,3 @@ std::pair<CURLcode, std::string> Monitor::Go2RTCManager::CURL_PUT(const std::str
404410
}
405411

406412
#endif // HAVE_LIBCURL
407-
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
const path = require('path');
5+
const ZM = require(path.join(__dirname, '../../web/js/go2rtc-stream-names.js'));
6+
7+
let passed = 0;
8+
let failed = 0;
9+
function test(name, fn) {
10+
try {
11+
fn();
12+
console.log(' ok ' + name);
13+
passed++;
14+
} catch (e) {
15+
console.error(' FAIL ' + name);
16+
console.error(' ' + e.message);
17+
failed++;
18+
}
19+
}
20+
21+
console.log('getGo2RTCStreamBase');
22+
test('prefers monitor name when present', () => {
23+
assert.strictEqual(ZM.getGo2RTCStreamBase(42, 'Front Door'), 'Front Door');
24+
});
25+
test('falls back to monitor id when name is empty', () => {
26+
assert.strictEqual(ZM.getGo2RTCStreamBase(42, ''), '42');
27+
});
28+
test('falls back to monitor id when name is null', () => {
29+
assert.strictEqual(ZM.getGo2RTCStreamBase(42, null), '42');
30+
});
31+
test('returns empty string when neither id nor name are available', () => {
32+
assert.strictEqual(ZM.getGo2RTCStreamBase(null, null), '');
33+
});
34+
35+
console.log('\ngetGo2RTCStreamName');
36+
test('builds the primary stream name from the monitor name', () => {
37+
assert.strictEqual(ZM.getGo2RTCStreamName(42, 'Front Door', ''), 'Front Door');
38+
});
39+
test('builds suffixed stream names from the monitor name', () => {
40+
assert.strictEqual(
41+
ZM.getGo2RTCStreamName(42, 'Front Door', '_CameraDirectPrimary'),
42+
'Front Door_CameraDirectPrimary');
43+
});
44+
test('builds suffixed stream names from the monitor id when needed', () => {
45+
assert.strictEqual(
46+
ZM.getGo2RTCStreamName(42, '', '_CameraDirectPrimary'),
47+
'42_CameraDirectPrimary');
48+
});
49+
50+
console.log('\n' + passed + ' passed, ' + failed + ' failed');
51+
process.exit(failed ? 1 : 0);

tests/zm_comms.cpp

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,42 @@
1919

2020
#include "zm_comms.h"
2121
#include <array>
22+
#include <cstdio>
23+
24+
namespace {
25+
26+
int reserveEphemeralPort(int socket_type) {
27+
const int sd = ::socket(AF_INET, socket_type, 0);
28+
REQUIRE(sd >= 0);
29+
30+
sockaddr_in addr = {};
31+
addr.sin_family = AF_INET;
32+
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
33+
addr.sin_port = 0;
34+
35+
const int reuse_addr = 1;
36+
REQUIRE(::setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &reuse_addr, sizeof(reuse_addr)) == 0);
37+
REQUIRE(::bind(sd, reinterpret_cast<const sockaddr *>(&addr), sizeof(addr)) == 0);
38+
39+
socklen_t addr_len = sizeof(addr);
40+
REQUIRE(::getsockname(sd, reinterpret_cast<sockaddr *>(&addr), &addr_len) == 0);
41+
42+
const int port = ntohs(addr.sin_port);
43+
REQUIRE(port > 0);
44+
REQUIRE(::close(sd) == 0);
45+
return port;
46+
}
47+
48+
std::string makeUnixSocketPath() {
49+
char path_template[] = "/tmp/zm.unittest.XXXXXX";
50+
const int fd = ::mkstemp(path_template);
51+
REQUIRE(fd >= 0);
52+
REQUIRE(::close(fd) == 0);
53+
REQUIRE(::unlink(path_template) == 0);
54+
return std::string(path_template) + ".sock";
55+
}
56+
57+
} // namespace
2258

2359
TEST_CASE("ZM::Pipe basics") {
2460
zm::Pipe pipe;
@@ -126,13 +162,15 @@ TEST_CASE("ZM::SockAddrUnix") {
126162

127163
TEST_CASE("ZM::UdpInetSocket basics") {
128164
zm::UdpInetSocket socket;
165+
const int port = reserveEphemeralPort(SOCK_DGRAM);
166+
const std::string port_str = std::to_string(port);
129167
REQUIRE(socket.isClosed() == true);
130168
REQUIRE(socket.isOpen() == false);
131169
REQUIRE(socket.isConnected() == false);
132170
REQUIRE(socket.isDisconnected() == false);
133171

134172
SECTION("bind with host and port") {
135-
REQUIRE(socket.bind(nullptr, "1234") == true);
173+
REQUIRE(socket.bind(nullptr, port_str.c_str()) == true);
136174
REQUIRE(socket.isOpen() == true);
137175
REQUIRE(socket.isDisconnected() == true);
138176
REQUIRE(socket.isClosed() == false);
@@ -148,21 +186,23 @@ TEST_CASE("ZM::UdpInetSocket basics") {
148186
}
149187

150188
SECTION("bind with port") {
151-
REQUIRE(socket.bind("1234") == true);
189+
REQUIRE(socket.bind(port_str.c_str()) == true);
152190
}
153191

154192
SECTION("bind with host and port number") {
155-
REQUIRE(socket.bind(nullptr, 1234) == true);
193+
REQUIRE(socket.bind(nullptr, port) == true);
156194
}
157195

158196
SECTION("bind with port number") {
159-
REQUIRE(socket.bind(1234) == true);
197+
REQUIRE(socket.bind(port) == true);
160198
}
161199
}
162200

163201
TEST_CASE("ZM::UdpInetSocket send/recv") {
164202
zm::UdpInetSocket srv_socket;
165203
zm::UdpInetSocket client_socket;
204+
const int port = reserveEphemeralPort(SOCK_DGRAM);
205+
const std::string port_str = std::to_string(port);
166206

167207
std::array<char, 3> msg = {'a', 'b', 'c'};
168208
std::array<char, msg.size()> rcv{};
@@ -173,10 +213,10 @@ TEST_CASE("ZM::UdpInetSocket send/recv") {
173213
}
174214

175215
SECTION("send/recv") {
176-
REQUIRE(srv_socket.bind("127.0.0.1", "1234") == true);
216+
REQUIRE(srv_socket.bind("127.0.0.1", port_str.c_str()) == true);
177217
REQUIRE(srv_socket.isOpen() == true);
178218

179-
REQUIRE(client_socket.connect("127.0.0.1", "1234") == true);
219+
REQUIRE(client_socket.connect("127.0.0.1", port_str.c_str()) == true);
180220
REQUIRE(client_socket.isConnected() == true);
181221

182222
REQUIRE(client_socket.send(msg.data(), msg.size()) == msg.size());
@@ -187,8 +227,7 @@ TEST_CASE("ZM::UdpInetSocket send/recv") {
187227
}
188228

189229
TEST_CASE("ZM::UdpUnixSocket basics") {
190-
std::string sock_path = "/tmp/zm.unittest.sock";
191-
unlink(sock_path.c_str()); // make sure the socket file does not exist
230+
const std::string sock_path = makeUnixSocketPath();
192231

193232
zm::UdpUnixSocket socket;
194233
REQUIRE(socket.isClosed() == true);
@@ -218,8 +257,7 @@ TEST_CASE("ZM::UdpUnixSocket basics") {
218257
}
219258

220259
TEST_CASE("ZM::UdpUnixSocket send/recv") {
221-
std::string sock_path = "/tmp/zm.unittest.sock";
222-
unlink(sock_path.c_str()); // make sure the socket file does not exist
260+
const std::string sock_path = makeUnixSocketPath();
223261

224262
zm::UdpUnixSocket srv_socket;
225263
zm::UdpUnixSocket client_socket;
@@ -281,12 +319,13 @@ TEST_CASE("ZM::TcpInetClient basics") {
281319

282320
TEST_CASE("ZM::TcpInetServer basics", "[notCI]") {
283321
zm::TcpInetServer server;
322+
const int port = reserveEphemeralPort(SOCK_STREAM);
284323
REQUIRE(server.isClosed() == true);
285324
REQUIRE(server.isOpen() == false);
286325
REQUIRE(server.isConnected() == false);
287326
REQUIRE(server.isDisconnected() == false);
288327

289-
REQUIRE(server.bind(1234) == true);
328+
REQUIRE(server.bind(port) == true);
290329
REQUIRE(server.isOpen() == true);
291330
REQUIRE(server.isClosed() == false);
292331
REQUIRE(server.isConnected() == false);
@@ -308,6 +347,7 @@ TEST_CASE("ZM::TcpInetServer basics", "[notCI]") {
308347
TEST_CASE("ZM::TcpInetClient/Server send/recv", "[notCI]") {
309348
zm::TcpInetServer server;
310349
zm::TcpInetClient client;
350+
const int port = reserveEphemeralPort(SOCK_STREAM);
311351

312352
std::array<char, 3> msg = {'a', 'b', 'c'};
313353
std::array<char, msg.size()> rcv{};
@@ -318,11 +358,11 @@ TEST_CASE("ZM::TcpInetClient/Server send/recv", "[notCI]") {
318358
}
319359

320360
SECTION("send/recv") {
321-
REQUIRE(server.bind(1234) == true);
361+
REQUIRE(server.bind(port) == true);
322362
REQUIRE(server.isOpen() == true);
323363
REQUIRE(server.listen() == true);
324364

325-
REQUIRE(client.connect("127.0.0.1", 1234) == true);
365+
REQUIRE(client.connect("127.0.0.1", port) == true);
326366
REQUIRE(client.isConnected() == true);
327367

328368
REQUIRE(server.accept() == true);

web/ajax/console.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,9 @@ function queryRequest() {
514514
if ($Monitor->Go2RTCEnabled() && defined('ZM_GO2RTC_PATH') && ZM_GO2RTC_PATH) {
515515
$liveStreamAttr = ' data-stream-type="go2rtc"'.
516516
' data-go2rtc-src="'.htmlspecialchars(ZM_GO2RTC_PATH).'"'.
517-
' data-monitor-id="'.$monitor['Id'].'"';
518-
$go2rtcAttr = ' go2rtc_src="'.htmlspecialchars(ZM_GO2RTC_PATH).'" go2rtc_mid="'.$monitor['Id'].'"';
517+
' data-monitor-id="'.$monitor['Id'].'"'.
518+
' data-monitor-name="'.htmlspecialchars($monitor['Name']).'"';
519+
$go2rtcAttr = ' go2rtc_src="'.htmlspecialchars(ZM_GO2RTC_PATH).'" go2rtc_mid="'.$monitor['Id'].'" go2rtc_name="'.htmlspecialchars($monitor['Name']).'"';
519520
$debugAttr .= ' data-debug-overlay="go2rtc"';
520521
} else if ($Monitor->RTSP2WebEnabled() && defined('ZM_RTSP2WEB_PATH') && ZM_RTSP2WEB_PATH) {
521522
$liveStreamAttr = ' data-stream-type="rtsp2web"'.

web/js/MonitorStream.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,10 +1690,11 @@ function MonitorStream(monitorData) {
16901690
const webrtcUrl = Go2RTCModUrl;
16911691
this.currentChannelStream = streamChannel;
16921692
const streamSuffix = this.getStreamSuffix(streamChannel);
1693-
console.log('go2rtc stream:', this.id + streamSuffix);
1693+
const streamName = ZMGo2RTCStreamNames.getGo2RTCStreamName(this.id, this.name, streamSuffix);
1694+
console.log('go2rtc stream:', streamName);
16941695
webrtcUrl.protocol = (url.protocol=='https:') ? 'wss:' : 'ws';
16951696
webrtcUrl.pathname += "/ws";
1696-
webrtcUrl.search = 'src=' + this.id + streamSuffix;
1697+
webrtcUrl.search = new URLSearchParams({src: streamName}).toString();
16971698
stream.src = webrtcUrl.href;
16981699

16991700
this.webrtc = stream; // track separately do to api differences between video tag and video-stream

web/js/go2rtc-stream-names.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"use strict";
2+
3+
(function(root, factory) {
4+
if (typeof module === 'object' && module.exports) {
5+
module.exports = factory();
6+
} else {
7+
root.ZMGo2RTCStreamNames = factory();
8+
}
9+
}(typeof globalThis !== 'undefined' ? globalThis : this, function() {
10+
function getGo2RTCStreamBase(monitorId, monitorName) {
11+
if (typeof monitorName === 'string' && monitorName.length) {
12+
return monitorName;
13+
}
14+
if (monitorId === undefined || monitorId === null) {
15+
return '';
16+
}
17+
return String(monitorId);
18+
}
19+
20+
function getGo2RTCStreamName(monitorId, monitorName, suffix) {
21+
return getGo2RTCStreamBase(monitorId, monitorName) + (suffix || '');
22+
}
23+
24+
return {
25+
getGo2RTCStreamBase,
26+
getGo2RTCStreamName,
27+
};
28+
}));

web/skins/classic/includes/functions.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,6 +1808,7 @@ function xhtmlFooter() {
18081808
?>
18091809
</script>
18101810
<script src="<?php echo cache_bust('js/logger.js')?>"></script>
1811+
<script src="<?php echo cache_bust('js/go2rtc-stream-names.js')?>"></script>
18111812
<?php
18121813
$viewJsFile = getSkinFile('views/js/'.$basename.'.js');
18131814
if ( $viewJsFile ) {

0 commit comments

Comments
 (0)