mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-02 07:25:02 -04:00
The consumer class dispatched HELLO, MEDIA, KEYFRAME, STATS and BYE but had no case for the EVENT type added for the monitor lifecycle channel, so every event fell into the unknown-type branch and was dropped at debug level 2. Add an on_event callback that receives the parsed MonitorEvent together with the header (event sequence and media generation), and reject malformed payloads with a warning like HELLO. Tests: a client connected to a StreamSocket receives the cached snapshot on connect and a broadcast state_changed, with the codes, state names, health code, message and wall clock intact. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
325 lines
11 KiB
C++
325 lines
11 KiB
C++
/*
|
|
* This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation; either version 2 of the License, or (at your
|
|
* option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "zm_catch2.h"
|
|
|
|
#include "zm_stream_socket.h"
|
|
#include "zm_stream_socket_client.h"
|
|
#include "zm_stream_socket_protocol.h"
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <sys/socket.h>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
using namespace zm::stream_socket;
|
|
|
|
namespace {
|
|
|
|
constexpr char kSockPath[] = "/tmp/zm.stream_socket_client.unittest.sock";
|
|
|
|
struct AVCodecParametersDeleter {
|
|
void operator()(AVCodecParameters *par) const { avcodec_parameters_free(&par); }
|
|
};
|
|
using codec_parameters_ptr = std::unique_ptr<AVCodecParameters, AVCodecParametersDeleter>;
|
|
|
|
// Collects callback invocations with waiting helpers
|
|
struct Collector {
|
|
std::mutex mutex;
|
|
std::condition_variable cv;
|
|
std::vector<std::pair<StreamId, HelloInfo>> hellos;
|
|
std::vector<std::pair<Header, std::vector<uint8_t>>> media;
|
|
std::vector<std::pair<Header, MonitorEvent>> events;
|
|
int byes = 0;
|
|
int disconnects = 0;
|
|
|
|
StreamSocketClient::Callbacks MakeCallbacks() {
|
|
StreamSocketClient::Callbacks callbacks;
|
|
callbacks.on_hello = [this](StreamId stream, const HelloInfo &info, uint32_t) {
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
hellos.emplace_back(stream, info);
|
|
cv.notify_all();
|
|
};
|
|
callbacks.on_event = [this](const Header &header, const MonitorEvent &event) {
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
events.emplace_back(header, event);
|
|
cv.notify_all();
|
|
};
|
|
callbacks.on_media = [this](const Header &header, const uint8_t *data, size_t size) {
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
media.emplace_back(header, std::vector<uint8_t>(data, data + size));
|
|
cv.notify_all();
|
|
};
|
|
callbacks.on_bye = [this]() {
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
++byes;
|
|
cv.notify_all();
|
|
};
|
|
callbacks.on_disconnect = [this]() {
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
++disconnects;
|
|
cv.notify_all();
|
|
};
|
|
return callbacks;
|
|
}
|
|
|
|
template <typename Pred>
|
|
bool WaitFor(Pred pred, std::chrono::milliseconds timeout = std::chrono::seconds(5)) {
|
|
std::unique_lock<std::mutex> lock(mutex);
|
|
return cv.wait_for(lock, timeout, pred);
|
|
}
|
|
};
|
|
|
|
av_packet_ptr make_packet(size_t size, uint8_t fill) {
|
|
av_packet_ptr packet{av_packet_alloc()};
|
|
REQUIRE(av_new_packet(packet.get(), size) == 0);
|
|
memset(packet->data, fill, size);
|
|
return packet;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("StreamSocketClient receives HELLO and media end to end", "[stream_socket_client]") {
|
|
StreamSocket server(7, kSockPath);
|
|
REQUIRE(server.Start());
|
|
|
|
codec_parameters_ptr par{avcodec_parameters_alloc()};
|
|
par->codec_type = AVMEDIA_TYPE_VIDEO;
|
|
par->codec_id = AV_CODEC_ID_HEVC;
|
|
par->width = 2560;
|
|
par->height = 1440;
|
|
server.SetVideoParams(par.get(), {20, 1});
|
|
|
|
Collector collector;
|
|
StreamSocketClient client(std::string(kSockPath), collector.MakeCallbacks());
|
|
|
|
REQUIRE(collector.WaitFor([&] { return !collector.hellos.empty(); }));
|
|
{
|
|
std::lock_guard<std::mutex> lock(collector.mutex);
|
|
REQUIRE(collector.hellos[0].first == StreamId::Video);
|
|
REQUIRE(collector.hellos[0].second.codec_id == AV_CODEC_ID_HEVC);
|
|
REQUIRE(collector.hellos[0].second.width == 2560);
|
|
REQUIRE(collector.hellos[0].second.fps_num == 20);
|
|
}
|
|
|
|
// wait until the server has the client registered, then send media
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
av_packet_ptr packet = make_packet(4096, 0xC3);
|
|
server.SendMedia(packet.get(), StreamId::Video, true, 777000);
|
|
|
|
REQUIRE(collector.WaitFor([&] { return !collector.media.empty(); }));
|
|
{
|
|
std::lock_guard<std::mutex> lock(collector.mutex);
|
|
const Header &header = collector.media[0].first;
|
|
REQUIRE(header.type == static_cast<uint8_t>(MessageType::Media));
|
|
REQUIRE(header.flags == kFlagKeyframe);
|
|
REQUIRE(header.pts_us == 777000);
|
|
REQUIRE(collector.media[0].second.size() == 4096);
|
|
REQUIRE(collector.media[0].second[100] == 0xC3);
|
|
}
|
|
|
|
client.Stop();
|
|
server.Stop();
|
|
}
|
|
|
|
TEST_CASE("StreamSocketClient gets BYE on server stop and reconnects", "[stream_socket_client]") {
|
|
auto server = std::make_unique<StreamSocket>(7, kSockPath);
|
|
REQUIRE(server->Start());
|
|
|
|
codec_parameters_ptr par{avcodec_parameters_alloc()};
|
|
par->codec_type = AVMEDIA_TYPE_VIDEO;
|
|
par->codec_id = AV_CODEC_ID_H264;
|
|
server->SetVideoParams(par.get(), {0, 0});
|
|
|
|
Collector collector;
|
|
StreamSocketClient client(std::string(kSockPath), collector.MakeCallbacks());
|
|
|
|
REQUIRE(collector.WaitFor([&] { return collector.hellos.size() >= 1; }));
|
|
|
|
server->Stop();
|
|
REQUIRE(collector.WaitFor([&] { return collector.byes >= 1 && collector.disconnects >= 1; }));
|
|
|
|
// Producer comes back (zmc restart): client reconnects and gets a new HELLO
|
|
server = std::make_unique<StreamSocket>(7, kSockPath);
|
|
REQUIRE(server->Start());
|
|
server->SetVideoParams(par.get(), {0, 0});
|
|
|
|
REQUIRE(collector.WaitFor([&] { return collector.hellos.size() >= 2; },
|
|
std::chrono::seconds(10)));
|
|
|
|
client.Stop();
|
|
server->Stop();
|
|
}
|
|
|
|
TEST_CASE("StreamSocketClient handles fragmented delivery", "[stream_socket_client]") {
|
|
int fds[2];
|
|
REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
|
|
|
|
Collector collector;
|
|
StreamSocketClient client(fds[0], collector.MakeCallbacks());
|
|
|
|
// Build one MEDIA message and dribble it through in small fragments
|
|
std::vector<uint8_t> payload(300, 0x5e);
|
|
Header header = {};
|
|
header.length = kHeaderLengthBytes + payload.size();
|
|
header.version = kProtocolVersion;
|
|
header.type = static_cast<uint8_t>(MessageType::Media);
|
|
header.stream = static_cast<uint8_t>(StreamId::Audio);
|
|
header.sequence = 9;
|
|
header.pts_us = 42;
|
|
uint8_t wire[kHeaderSize];
|
|
SerializeHeader(header, wire);
|
|
|
|
std::vector<uint8_t> message(wire, wire + kHeaderSize);
|
|
message.insert(message.end(), payload.begin(), payload.end());
|
|
for (size_t pos = 0; pos < message.size(); pos += 7) {
|
|
size_t chunk = std::min<size_t>(7, message.size() - pos);
|
|
REQUIRE(::send(fds[1], message.data() + pos, chunk, 0) == static_cast<ssize_t>(chunk));
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
|
|
REQUIRE(collector.WaitFor([&] { return !collector.media.empty(); }));
|
|
{
|
|
std::lock_guard<std::mutex> lock(collector.mutex);
|
|
REQUIRE(collector.media.size() == 1);
|
|
REQUIRE(collector.media[0].first.sequence == 9);
|
|
REQUIRE(collector.media[0].second == payload);
|
|
}
|
|
|
|
::close(fds[1]);
|
|
REQUIRE(collector.WaitFor([&] { return collector.disconnects >= 1; }));
|
|
client.Stop();
|
|
}
|
|
|
|
TEST_CASE("StreamSocketClient disconnects on malformed input", "[stream_socket_client]") {
|
|
int fds[2];
|
|
REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
|
|
|
|
Collector collector;
|
|
StreamSocketClient client(fds[0], collector.MakeCallbacks());
|
|
|
|
// Garbage: invalid version byte in an otherwise plausible header
|
|
std::vector<uint8_t> garbage(kHeaderSize, 0xFF);
|
|
REQUIRE(::send(fds[1], garbage.data(), garbage.size(), 0)
|
|
== static_cast<ssize_t>(garbage.size()));
|
|
|
|
REQUIRE(collector.WaitFor([&] { return collector.disconnects >= 1; }));
|
|
{
|
|
std::lock_guard<std::mutex> lock(collector.mutex);
|
|
REQUIRE(collector.media.empty());
|
|
REQUIRE(collector.hellos.empty());
|
|
}
|
|
::close(fds[1]);
|
|
client.Stop();
|
|
}
|
|
|
|
TEST_CASE("StreamSocketClient skips unknown message types", "[stream_socket_client]") {
|
|
int fds[2];
|
|
REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
|
|
|
|
Collector collector;
|
|
StreamSocketClient client(fds[0], collector.MakeCallbacks());
|
|
|
|
// Unknown type 0x7f with a small payload, then a valid MEDIA
|
|
Header header = {};
|
|
header.length = kHeaderLengthBytes + 4;
|
|
header.version = kProtocolVersion;
|
|
header.type = 0x7f;
|
|
uint8_t wire[kHeaderSize];
|
|
SerializeHeader(header, wire);
|
|
std::vector<uint8_t> message(wire, wire + kHeaderSize);
|
|
message.insert(message.end(), {1, 2, 3, 4});
|
|
|
|
header.type = static_cast<uint8_t>(MessageType::Media);
|
|
header.length = kHeaderLengthBytes + 2;
|
|
SerializeHeader(header, wire);
|
|
message.insert(message.end(), wire, wire + kHeaderSize);
|
|
message.insert(message.end(), {0xAA, 0xBB});
|
|
|
|
REQUIRE(::send(fds[1], message.data(), message.size(), 0)
|
|
== static_cast<ssize_t>(message.size()));
|
|
|
|
REQUIRE(collector.WaitFor([&] { return !collector.media.empty(); }));
|
|
{
|
|
std::lock_guard<std::mutex> lock(collector.mutex);
|
|
REQUIRE(collector.media.size() == 1);
|
|
REQUIRE(collector.media[0].second == std::vector<uint8_t>({0xAA, 0xBB}));
|
|
}
|
|
::close(fds[1]);
|
|
client.Stop();
|
|
}
|
|
|
|
TEST_CASE("StreamSocketClient delivers EVENT frames", "[stream_socket_client]") {
|
|
StreamSocket server(7, kSockPath);
|
|
REQUIRE(server.Start());
|
|
|
|
// A cached snapshot is replayed on connect, then a broadcast state change
|
|
MonitorEvent snapshot;
|
|
snapshot.code = kEventSnapshot;
|
|
snapshot.state_id = 0; snapshot.has_state_id = true;
|
|
snapshot.state_name = "IDLE";
|
|
snapshot.health_code = kEventCaptureFailed; snapshot.has_health_code = true;
|
|
snapshot.message = "Capture failed";
|
|
server.SetSnapshotEvent(BuildEvent(snapshot));
|
|
|
|
Collector collector;
|
|
StreamSocketClient client(std::string(kSockPath), collector.MakeCallbacks());
|
|
|
|
REQUIRE(collector.WaitFor([&] { return collector.events.size() >= 1; }));
|
|
{
|
|
std::lock_guard<std::mutex> lock(collector.mutex);
|
|
const Header &header = collector.events[0].first;
|
|
const MonitorEvent &event = collector.events[0].second;
|
|
REQUIRE(header.stream == static_cast<uint8_t>(StreamId::Monitor));
|
|
REQUIRE(event.code == kEventSnapshot);
|
|
REQUIRE(event.state_name == "IDLE");
|
|
REQUIRE(event.has_health_code);
|
|
REQUIRE(event.health_code == kEventCaptureFailed);
|
|
REQUIRE(event.message == "Capture failed");
|
|
}
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
MonitorEvent change;
|
|
change.code = kEventStateChanged;
|
|
change.state_id = 2; change.has_state_id = true;
|
|
change.prev_state_id = 0; change.has_prev_state_id = true;
|
|
change.state_name = "ALARM";
|
|
change.wall_clock_us = 1718355103501000ULL; change.has_wall_clock = true;
|
|
server.SendMonitorEvent(BuildEvent(change));
|
|
|
|
REQUIRE(collector.WaitFor([&] { return collector.events.size() >= 2; }));
|
|
{
|
|
std::lock_guard<std::mutex> lock(collector.mutex);
|
|
const Header &header = collector.events[1].first;
|
|
const MonitorEvent &event = collector.events[1].second;
|
|
REQUIRE(header.sequence == 0); // first event produced by this monitor
|
|
REQUIRE(event.code == kEventStateChanged);
|
|
REQUIRE(event.state_id == 2);
|
|
REQUIRE(event.prev_state_id == 0);
|
|
REQUIRE(event.state_name == "ALARM");
|
|
REQUIRE(event.wall_clock_us == 1718355103501000ULL);
|
|
}
|
|
|
|
client.Stop();
|
|
server.Stop();
|
|
}
|