/*
* 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 .
*/
#include "zm_catch2.h"
#include "zm_comms.h"
#include "zm_stream_socket.h"
#include "zm_stream_socket_protocol.h"
#include
#include
#include
#include
#include
#include
#include
#include
using namespace zm::stream_socket;
namespace {
// Per-process path so concurrent test runs do not share a socket file
const std::string kSockPathStr = "/tmp/zm.stream_socket.unittest." + std::to_string(getpid()) + ".sock";
const char *kSockPath = kSockPathStr.c_str();
struct AVCodecParametersDeleter {
void operator()(AVCodecParameters *par) const { avcodec_parameters_free(&par); }
};
using codec_parameters_ptr = std::unique_ptr;
struct ReceivedMessage {
Header header;
std::vector payload;
};
// Blocking exact-size reads against the non-blocking-server socket
class TestClient {
public:
bool Connect() {
return sock_.connect(kSockPath);
}
// Returns false on timeout/close instead of blocking forever
bool ReadMessage(ReceivedMessage &message,
std::chrono::milliseconds timeout = std::chrono::seconds(5)) {
uint8_t header_bytes[kHeaderSize];
if (!ReadExact(header_bytes, kHeaderSize, timeout))
return false;
if (!ParseHeader(header_bytes, message.header))
return false;
message.payload.resize(message.header.payload_size());
if (message.payload.empty())
return true;
return ReadExact(message.payload.data(), message.payload.size(), timeout);
}
zm::TcpUnixClient sock_;
private:
bool ReadExact(uint8_t *out, size_t len, std::chrono::milliseconds timeout) {
timeval tv = {};
tv.tv_sec = timeout.count() / 1000;
tv.tv_usec = (timeout.count() % 1000) * 1000;
setsockopt(sock_.getDesc(), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
size_t done = 0;
while (done < len) {
ssize_t bytes = ::recv(sock_.getDesc(), out + done, len - done, 0);
if (bytes <= 0)
return false;
done += bytes;
}
return true;
}
};
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;
}
codec_parameters_ptr make_h264_parameters() {
codec_parameters_ptr par{avcodec_parameters_alloc()};
par->codec_type = AVMEDIA_TYPE_VIDEO;
par->codec_id = AV_CODEC_ID_H264;
par->width = 640;
par->height = 480;
return par;
}
codec_parameters_ptr make_aac_parameters() {
codec_parameters_ptr par{avcodec_parameters_alloc()};
par->codec_type = AVMEDIA_TYPE_AUDIO;
par->codec_id = AV_CODEC_ID_AAC;
par->sample_rate = 48000;
return par;
}
} // namespace
TEST_CASE("StreamSocket lifecycle", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE_FALSE(server.IsRunning());
REQUIRE(server.Start());
REQUIRE(server.IsRunning());
SECTION("socket file exists with 0660 permissions") {
struct stat st = {};
REQUIRE(stat(kSockPath, &st) == 0);
REQUIRE(S_ISSOCK(st.st_mode));
REQUIRE((st.st_mode & 0777) == 0660);
}
SECTION("client can connect") {
TestClient client;
REQUIRE(client.Connect());
}
server.Stop();
REQUIRE_FALSE(server.IsRunning());
// socket file removed on Stop
struct stat st = {};
REQUIRE(stat(kSockPath, &st) != 0);
}
TEST_CASE("StreamSocket sends HELLO first, then media", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
codec_parameters_ptr par = make_h264_parameters();
server.SetVideoParams(par.get(), {30, 1});
TestClient client;
REQUIRE(client.Connect());
ReceivedMessage hello;
REQUIRE(client.ReadMessage(hello));
REQUIRE(hello.header.type == static_cast(MessageType::Hello));
REQUIRE(hello.header.stream == static_cast(StreamId::Video));
HelloInfo info;
REQUIRE(ParseHello(hello.payload.data(), hello.payload.size(), info));
REQUIRE(info.codec_id == AV_CODEC_ID_H264);
REQUIRE(info.width == 640);
REQUIRE(info.height == 480);
REQUIRE(info.fps_num == 30);
// wait for the server to register the client before sending
std::this_thread::sleep_for(std::chrono::milliseconds(50));
av_packet_ptr packet = make_packet(1000, 0xAB);
server.SendMedia(packet.get(), StreamId::Video, true, 123456);
ReceivedMessage media;
REQUIRE(client.ReadMessage(media));
REQUIRE(media.header.type == static_cast(MessageType::Media));
REQUIRE(media.header.flags == kFlagKeyframe);
REQUIRE(media.header.pts_us == 123456);
REQUIRE(media.payload.size() == 1000);
REQUIRE(media.payload[0] == 0xAB);
REQUIRE(media.payload[999] == 0xAB);
server.Stop();
}
TEST_CASE("StreamSocket late joiner receives cached keyframe", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
codec_parameters_ptr par = make_h264_parameters();
server.SetVideoParams(par.get(), {0, 0});
av_packet_ptr keyframe = make_packet(2000, 0x5A);
server.SendMedia(keyframe.get(), StreamId::Video, true, 1000);
av_packet_ptr delta = make_packet(100, 0x11);
server.SendMedia(delta.get(), StreamId::Video, false, 2000);
TestClient client;
REQUIRE(client.Connect());
ReceivedMessage hello;
REQUIRE(client.ReadMessage(hello));
REQUIRE(hello.header.type == static_cast(MessageType::Hello));
ReceivedMessage cached;
REQUIRE(client.ReadMessage(cached));
REQUIRE(cached.header.type == static_cast(MessageType::Keyframe));
REQUIRE(cached.header.pts_us == 1000);
REQUIRE(cached.payload.size() == 2000);
REQUIRE(cached.payload[0] == 0x5A);
server.Stop();
}
TEST_CASE("StreamSocket queue overflow drops media but never HELLO", "[stream_socket]") {
StreamSocket::Config config;
config.queue_max_bytes = 64 * 1024;
config.queue_max_msgs = 16;
config.stats_interval = std::chrono::milliseconds(200);
config.stall_timeout = std::chrono::seconds(60); // not under test here
StreamSocket server(1, kSockPath, config);
REQUIRE(server.Start());
codec_parameters_ptr par = make_h264_parameters();
server.SetVideoParams(par.get(), {0, 0});
TestClient client;
REQUIRE(client.Connect());
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// Push much more than the queue + kernel buffers can hold while the client
// is not reading.
constexpr size_t kPacketSize = 16 * 1024;
constexpr size_t kPackets = 200;
av_packet_ptr packet = make_packet(kPacketSize, 0x42);
for (size_t i = 0; i < kPackets; ++i) {
server.SendMedia(packet.get(), StreamId::Video, false, i);
}
// Now drain everything; track sequence gaps and stats
uint64_t received_media = 0;
uint64_t reported_dropped = 0;
bool got_stats_with_drops = false;
uint32_t last_sequence = 0;
bool first_media = true;
uint64_t gap_total = 0;
// The server emits STATS continuously, so bound the drain by a streak of
// non-media messages rather than a read timeout.
int non_media_streak = 0;
ReceivedMessage message;
while (non_media_streak < 5
and client.ReadMessage(message, std::chrono::milliseconds(1000))) {
++non_media_streak;
if (message.header.type == static_cast(MessageType::Hello))
continue;
if (message.header.type == static_cast(MessageType::Stats)) {
uint64_t sent = 0;
REQUIRE(ParseStats(message.payload.data(), message.payload.size(), sent, reported_dropped));
if (reported_dropped > 0)
got_stats_with_drops = true;
continue;
}
if (message.header.type == static_cast(MessageType::Media)) {
non_media_streak = 0;
if (!first_media) {
REQUIRE(message.header.sequence > last_sequence); // monotonic
gap_total += message.header.sequence - last_sequence - 1;
} else {
gap_total += message.header.sequence; // drops before the first received
first_media = false;
}
last_sequence = message.header.sequence;
++received_media;
}
}
// The client never saw some packets, and the loss is observable both ways
REQUIRE(received_media < kPackets);
REQUIRE(gap_total > 0);
REQUIRE(got_stats_with_drops);
REQUIRE(gap_total + received_media == kPackets);
server.Stop();
}
TEST_CASE("StreamSocket stalled client does not affect a live one", "[stream_socket]") {
StreamSocket::Config config;
config.queue_max_bytes = 32 * 1024;
config.queue_max_msgs = 8;
StreamSocket server(1, kSockPath, config);
REQUIRE(server.Start());
codec_parameters_ptr par = make_h264_parameters();
server.SetVideoParams(par.get(), {0, 0});
TestClient stalled;
REQUIRE(stalled.Connect());
TestClient live;
REQUIRE(live.Connect());
std::this_thread::sleep_for(std::chrono::milliseconds(100));
ReceivedMessage message;
REQUIRE(live.ReadMessage(message)); // HELLO
REQUIRE(message.header.type == static_cast(MessageType::Hello));
constexpr size_t kPackets = 50;
av_packet_ptr packet = make_packet(8 * 1024, 0x99);
uint64_t live_received = 0;
bool live_gap = false;
uint32_t expected_sequence = 0;
for (size_t i = 0; i < kPackets; ++i) {
server.SendMedia(packet.get(), StreamId::Video, false, i);
// live client reads continuously; stalled never reads
if (live.ReadMessage(message)) {
if (message.header.type == static_cast(MessageType::Media)) {
if (message.header.sequence != expected_sequence)
live_gap = true;
expected_sequence = message.header.sequence + 1;
++live_received;
}
}
}
REQUIRE(live_received == kPackets);
REQUIRE_FALSE(live_gap);
server.Stop();
}
TEST_CASE("StreamSocket parameter change bumps generation and resends HELLO", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
codec_parameters_ptr par = make_h264_parameters();
server.SetVideoParams(par.get(), {0, 0});
TestClient client;
REQUIRE(client.Connect());
ReceivedMessage hello;
REQUIRE(client.ReadMessage(hello));
REQUIRE(hello.header.generation == 0);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// Same parameters: no new HELLO, no generation bump
server.SetVideoParams(par.get(), {0, 0});
// Changed parameters: generation bump + new HELLO
par->width = 1920;
par->height = 1080;
server.SetVideoParams(par.get(), {0, 0});
ReceivedMessage hello2;
REQUIRE(client.ReadMessage(hello2));
REQUIRE(hello2.header.type == static_cast(MessageType::Hello));
REQUIRE(hello2.header.generation == 1);
HelloInfo info;
REQUIRE(ParseHello(hello2.payload.data(), hello2.payload.size(), info));
REQUIRE(info.width == 1920);
// Media now carries the new generation, sequence restarted
std::this_thread::sleep_for(std::chrono::milliseconds(50));
av_packet_ptr packet = make_packet(100, 0x77);
server.SendMedia(packet.get(), StreamId::Video, false, 5000);
ReceivedMessage media;
REQUIRE(client.ReadMessage(media));
REQUIRE(media.header.type == static_cast(MessageType::Media));
REQUIRE(media.header.generation == 1);
REQUIRE(media.header.sequence == 0);
server.Stop();
}
TEST_CASE("StreamSocket sends BYE on stop", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
TestClient client;
REQUIRE(client.Connect());
std::this_thread::sleep_for(std::chrono::milliseconds(100));
server.Stop();
ReceivedMessage message;
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Bye));
}
namespace {
std::vector make_state_event(uint16_t code, uint32_t state_id,
uint32_t prev_state_id, const char *name) {
MonitorEvent ev;
ev.code = code;
ev.state_id = state_id; ev.has_state_id = true;
ev.prev_state_id = prev_state_id; ev.has_prev_state_id = true;
ev.state_name = name;
ev.wall_clock_us = 1718355103501000ULL;
ev.has_wall_clock = true;
return BuildEvent(ev);
}
} // namespace
TEST_CASE("StreamSocket broadcasts a monitor EVENT", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
TestClient client;
REQUIRE(client.Connect());
std::this_thread::sleep_for(std::chrono::milliseconds(50));
server.SendMonitorEvent(make_state_event(kEventStateChanged, 2, 0, "Alarm"));
ReceivedMessage message;
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Event));
REQUIRE(message.header.stream == static_cast(StreamId::Monitor));
REQUIRE(message.header.sequence == 0);
MonitorEvent ev;
REQUIRE(ParseEvent(message.payload.data(), message.payload.size(), ev));
REQUIRE(ev.code == kEventStateChanged);
REQUIRE(ev.state_id == 2);
REQUIRE(ev.prev_state_id == 0);
REQUIRE(ev.state_name == "Alarm");
REQUIRE(ev.has_wall_clock);
server.Stop();
}
TEST_CASE("StreamSocket event sequence advances without clients", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
// Two events produced before any consumer connects; the sequence still
// advances so the gap is observable to a later joiner.
server.SendMonitorEvent(make_state_event(kEventConnectionFailed, 0, 0, ""));
server.SendMonitorEvent(make_state_event(kEventConnectionRestored, 0, 0, ""));
TestClient client;
REQUIRE(client.Connect());
std::this_thread::sleep_for(std::chrono::milliseconds(50));
server.SendMonitorEvent(make_state_event(kEventStateChanged, 1, 0, "PreAlarm"));
ReceivedMessage message;
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Event));
REQUIRE(message.header.sequence == 2); // 0 and 1 dropped before connect
server.Stop();
}
TEST_CASE("StreamSocket replays snapshot on connect", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
codec_parameters_ptr par = make_h264_parameters();
server.SetVideoParams(par.get(), {30, 1});
server.SetSnapshotEvent(make_state_event(kEventSnapshot, 0, 0, "Idle"));
TestClient client;
REQUIRE(client.Connect());
// HELLO first
ReceivedMessage hello;
REQUIRE(client.ReadMessage(hello));
REQUIRE(hello.header.type == static_cast(MessageType::Hello));
// then the snapshot EVENT
ReceivedMessage snap;
REQUIRE(client.ReadMessage(snap));
REQUIRE(snap.header.type == static_cast(MessageType::Event));
REQUIRE(snap.header.stream == static_cast(StreamId::Monitor));
MonitorEvent ev;
REQUIRE(ParseEvent(snap.payload.data(), snap.payload.size(), ev));
REQUIRE(ev.code == kEventSnapshot);
REQUIRE(ev.state_id == 0);
REQUIRE(ev.state_name == "Idle");
server.Stop();
}
TEST_CASE("StreamSocket::ParseAllowedUids", "[stream_socket]") {
REQUIRE(StreamSocket::ParseAllowedUids("").empty());
REQUIRE(StreamSocket::ParseAllowedUids("33") == std::vector{33});
REQUIRE(StreamSocket::ParseAllowedUids("33,1000") == std::vector{33, 1000});
REQUIRE(StreamSocket::ParseAllowedUids(" 33 , 1000 ") == std::vector{33, 1000});
REQUIRE(StreamSocket::ParseAllowedUids("33,,1000") == std::vector{33, 1000});
REQUIRE(StreamSocket::ParseAllowedUids("33,bogus,1000") == std::vector{33, 1000});
// Values that cannot be a uid are rejected, never wrapped or truncated
REQUIRE(StreamSocket::ParseAllowedUids("-1").empty());
REQUIRE(StreamSocket::ParseAllowedUids("99999999999999999999").empty()); // > ULONG_MAX
if (sizeof(uid_t) < sizeof(unsigned long)) {
REQUIRE(StreamSocket::ParseAllowedUids("18446744073709551615").empty()); // ULONG_MAX
REQUIRE(StreamSocket::ParseAllowedUids("4294967296").empty()); // 2^32 would truncate to 0
}
REQUIRE(StreamSocket::ParseAllowedUids("33,-1,1000") == std::vector{33, 1000});
}
TEST_CASE("StreamSocket drops media for a stream with no HELLO", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
// Only video is announced; audio never gets a HELLO.
codec_parameters_ptr par = make_h264_parameters();
server.SetVideoParams(par.get(), {0, 0});
TestClient client;
REQUIRE(client.Connect());
ReceivedMessage hello;
REQUIRE(client.ReadMessage(hello));
REQUIRE(hello.header.type == static_cast(MessageType::Hello));
REQUIRE(hello.header.stream == static_cast(StreamId::Video));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// An audio packet arrives before audio is announced: it must not reach the
// wire (a consumer could not decode it without a HELLO).
av_packet_ptr audio = make_packet(200, 0x33);
server.SendMedia(audio.get(), StreamId::Audio, false, 111);
// A following video packet does go out; if the audio had leaked we would
// read it first.
av_packet_ptr video = make_packet(200, 0x44);
server.SendMedia(video.get(), StreamId::Video, false, 222);
ReceivedMessage message;
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Media));
REQUIRE(message.header.stream == static_cast(StreamId::Video));
REQUIRE(message.header.pts_us == 222);
server.Stop();
}
TEST_CASE("StreamSocket::ClearAudioParams stops announcing audio", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
// Both streams announced together, as a monitor prime does: generation 0
codec_parameters_ptr vpar = make_h264_parameters();
codec_parameters_ptr apar = make_aac_parameters();
server.SetStreams(vpar.get(), {0, 0}, apar.get());
// A keyframe is cached for late joiners under generation 0
av_packet_ptr keyframe = make_packet(300, 0x5A);
server.SendMedia(keyframe.get(), StreamId::Video, true, 500);
// First consumer sees both HELLOs, audio first (the video HELLO completes
// a generation's parameter set), then the cached keyframe.
{
TestClient client;
REQUIRE(client.Connect());
ReceivedMessage m;
REQUIRE(client.ReadMessage(m));
REQUIRE(m.header.type == static_cast(MessageType::Hello));
REQUIRE(m.header.stream == static_cast(StreamId::Audio));
REQUIRE(client.ReadMessage(m));
REQUIRE(m.header.type == static_cast(MessageType::Hello));
REQUIRE(m.header.stream == static_cast(StreamId::Video));
REQUIRE(client.ReadMessage(m));
REQUIRE(m.header.type == static_cast(MessageType::Keyframe));
REQUIRE(m.header.generation == 0);
}
// Audio goes away on a re-prime: a full generation bump, so the video HELLO
// is re-issued under it and the generation-0 keyframe is dropped.
server.ClearAudioParams();
// A fresh consumer is told about video only, at the new generation.
TestClient client;
REQUIRE(client.Connect());
ReceivedMessage video_hello;
REQUIRE(client.ReadMessage(video_hello));
REQUIRE(video_hello.header.type == static_cast(MessageType::Hello));
REQUIRE(video_hello.header.stream == static_cast(StreamId::Video));
REQUIRE(video_hello.header.generation == 1);
// No audio HELLO and no stale keyframe follow; the next thing on the wire
// is fresh video media, with its sequence restarted for the generation.
std::this_thread::sleep_for(std::chrono::milliseconds(50));
av_packet_ptr video = make_packet(100, 0x66);
server.SendMedia(video.get(), StreamId::Video, false, 900);
ReceivedMessage next;
REQUIRE(client.ReadMessage(next));
REQUIRE(next.header.type == static_cast(MessageType::Media));
REQUIRE(next.header.stream == static_cast(StreamId::Video));
REQUIRE(next.header.generation == 1);
REQUIRE(next.header.sequence == 0);
server.Stop();
}
TEST_CASE("StreamSocket re-issues HELLOs audio first on a video reconfigure", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
codec_parameters_ptr apar = make_aac_parameters();
server.SetAudioParams(apar.get());
codec_parameters_ptr vpar = make_h264_parameters();
server.SetVideoParams(vpar.get(), {0, 0});
TestClient client;
REQUIRE(client.Connect());
ReceivedMessage m;
REQUIRE(client.ReadMessage(m)); // audio HELLO
REQUIRE(client.ReadMessage(m)); // video HELLO
REQUIRE(m.header.stream == static_cast(StreamId::Video));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// Video parameters change: both HELLOs are re-issued under generation 1,
// the unchanged audio one first so the video HELLO completes the set.
vpar->width = 1920;
vpar->height = 1080;
server.SetVideoParams(vpar.get(), {0, 0});
REQUIRE(client.ReadMessage(m));
REQUIRE(m.header.type == static_cast(MessageType::Hello));
REQUIRE(m.header.stream == static_cast(StreamId::Audio));
REQUIRE(m.header.generation == 1);
REQUIRE(client.ReadMessage(m));
REQUIRE(m.header.type == static_cast(MessageType::Hello));
REQUIRE(m.header.stream == static_cast(StreamId::Video));
REQUIRE(m.header.generation == 1);
HelloInfo info;
REQUIRE(ParseHello(m.payload.data(), m.payload.size(), info));
REQUIRE(info.width == 1920);
server.Stop();
}
TEST_CASE("StreamSocket::InvalidateKeyframe stops replaying a stale keyframe", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
codec_parameters_ptr par = make_h264_parameters();
server.SetVideoParams(par.get(), {0, 0});
av_packet_ptr keyframe = make_packet(500, 0x5A);
server.SendMedia(keyframe.get(), StreamId::Video, true, 1000);
// The capture source closes: the cached keyframe belongs to the old session
server.InvalidateKeyframe();
TestClient client;
REQUIRE(client.Connect());
std::this_thread::sleep_for(std::chrono::milliseconds(50));
ReceivedMessage hello;
REQUIRE(client.ReadMessage(hello));
REQUIRE(hello.header.type == static_cast(MessageType::Hello));
// Nothing else is queued for the new consumer until fresh media arrives
av_packet_ptr fresh = make_packet(100, 0x11);
server.SendMedia(fresh.get(), StreamId::Video, false, 2000);
ReceivedMessage next;
REQUIRE(client.ReadMessage(next));
REQUIRE(next.header.type == static_cast(MessageType::Media));
REQUIRE(next.header.pts_us == 2000);
server.Stop();
}
TEST_CASE("StreamSocket bumps the generation when audio joins an announced video stream", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
// A complete video-only generation 0
codec_parameters_ptr video = make_h264_parameters();
server.SetVideoParams(video.get(), {0, 0});
TestClient client;
REQUIRE(client.Connect());
ReceivedMessage message;
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Hello));
REQUIRE(message.header.stream == static_cast(StreamId::Video));
REQUIRE(message.header.generation == 0);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// A re-prime finds audio for the first time. The video HELLO already
// completed generation 0, so this is a new generation: audio HELLO first,
// then the video HELLO re-issued to complete the new set.
codec_parameters_ptr audio = make_aac_parameters();
server.SetAudioParams(audio.get());
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Hello));
REQUIRE(message.header.stream == static_cast(StreamId::Audio));
REQUIRE(message.header.generation == 1);
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Hello));
REQUIRE(message.header.stream == static_cast(StreamId::Video));
REQUIRE(message.header.generation == 1);
server.Stop();
}
TEST_CASE("StreamSocket::SetStreams keeps the initial announcement in generation 0", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
codec_parameters_ptr video = make_h264_parameters();
codec_parameters_ptr audio = make_aac_parameters();
server.SetStreams(video.get(), {25, 1}, audio.get());
// Re-applying the same set (a camera reconnect with nothing changed) is a no-op
server.SetStreams(video.get(), {25, 1}, audio.get());
TestClient client;
REQUIRE(client.Connect());
ReceivedMessage message;
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.stream == static_cast(StreamId::Audio));
REQUIRE(message.header.generation == 0);
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.stream == static_cast(StreamId::Video));
REQUIRE(message.header.generation == 0);
server.Stop();
}
TEST_CASE("StreamSocket::SetStreams changes both streams in one generation", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
codec_parameters_ptr video = make_h264_parameters();
codec_parameters_ptr audio = make_aac_parameters();
server.SetStreams(video.get(), {0, 0}, audio.get());
TestClient client;
REQUIRE(client.Connect());
ReceivedMessage message;
REQUIRE(client.ReadMessage(message)); // audio HELLO, generation 0
REQUIRE(client.ReadMessage(message)); // video HELLO, generation 0
REQUIRE(message.header.generation == 0);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// The camera comes back with a different resolution and sample rate
video->width = 1920;
video->height = 1080;
audio->sample_rate = 8000;
server.SetStreams(video.get(), {0, 0}, audio.get());
// Exactly one new generation, audio first, and the pairing is consistent:
// no intermediate generation that mixes new audio with old video.
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.stream == static_cast(StreamId::Audio));
REQUIRE(message.header.generation == 1);
HelloInfo info;
REQUIRE(ParseHello(message.payload.data(), message.payload.size(), info));
REQUIRE(info.sample_rate == 8000);
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.stream == static_cast(StreamId::Video));
REQUIRE(message.header.generation == 1);
REQUIRE(ParseHello(message.payload.data(), message.payload.size(), info));
REQUIRE(info.width == 1920);
// The next thing on the wire is media in generation 1, not another HELLO
av_packet_ptr packet = make_packet(100, 0x33);
server.SendMedia(packet.get(), StreamId::Video, false, 1);
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Media));
REQUIRE(message.header.generation == 1);
REQUIRE(message.header.sequence == 0);
server.Stop();
}
TEST_CASE("StreamSocket::SetStreams drops a video stream the source no longer has", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
codec_parameters_ptr video = make_h264_parameters();
codec_parameters_ptr audio = make_aac_parameters();
server.SetStreams(video.get(), {0, 0}, audio.get());
av_packet_ptr keyframe = make_packet(500, 0x5A);
server.SendMedia(keyframe.get(), StreamId::Video, true, 1000);
// Re-prime without video (null, or a stream with no codec id)
codec_parameters_ptr none{avcodec_parameters_alloc()};
none->codec_type = AVMEDIA_TYPE_VIDEO;
none->codec_id = AV_CODEC_ID_NONE;
server.SetStreams(none.get(), {0, 0}, audio.get());
// A new consumer is told about audio only: no stale video HELLO, no keyframe
TestClient client;
REQUIRE(client.Connect());
std::this_thread::sleep_for(std::chrono::milliseconds(50));
ReceivedMessage message;
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Hello));
REQUIRE(message.header.stream == static_cast(StreamId::Audio));
REQUIRE(message.header.generation == 1);
// Video packets are no longer forwarded; audio still is
av_packet_ptr packet = make_packet(100, 0x44);
server.SendMedia(packet.get(), StreamId::Video, true, 2000);
server.SendMedia(packet.get(), StreamId::Audio, false, 3000);
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Media));
REQUIRE(message.header.stream == static_cast(StreamId::Audio));
REQUIRE(message.header.pts_us == 3000);
server.Stop();
}
TEST_CASE("StreamSocket frames the snapshot with the generation and event sequence at connect", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
// Snapshot cached while generation 0 and no events produced yet
server.SetSnapshotEvent(make_state_event(kEventSnapshot, 0, 0, "IDLE"));
// Then the stream is reconfigured and two events go by with nobody listening
codec_parameters_ptr video = make_h264_parameters();
server.SetVideoParams(video.get(), {0, 0});
video->width = 1280;
server.SetVideoParams(video.get(), {0, 0});
server.SendMonitorEvent(make_state_event(kEventConnectionFailed, 0, 0, ""));
server.SendMonitorEvent(make_state_event(kEventConnectionRestored, 0, 0, ""));
TestClient client;
REQUIRE(client.Connect());
ReceivedMessage message;
REQUIRE(client.ReadMessage(message)); // video HELLO
REQUIRE(message.header.generation == 1);
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Event));
// Header reflects the moment of connect, not the moment the status last moved
REQUIRE(message.header.generation == 1);
REQUIRE(message.header.sequence == 2);
server.Stop();
}
TEST_CASE("StreamSocket refuses a path too long for a unix socket address", "[stream_socket]") {
std::string long_path = "/tmp/" + std::string(200, 'x') + ".sock";
StreamSocket server(1, long_path);
REQUIRE_FALSE(server.Start());
REQUIRE_FALSE(server.IsRunning());
}
TEST_CASE("StreamSocket ignores media for a stream id that is not audio or video", "[stream_socket]") {
StreamSocket server(1, kSockPath);
REQUIRE(server.Start());
codec_parameters_ptr video = make_h264_parameters();
server.SetVideoParams(video.get(), {0, 0});
av_packet_ptr packet = make_packet(100, 0x55);
server.SendMedia(packet.get(), StreamId::Monitor, false, 1); // must not index past the sequence counters
server.SendMedia(packet.get(), StreamId::Video, false, 2);
TestClient client;
REQUIRE(client.Connect());
std::this_thread::sleep_for(std::chrono::milliseconds(50));
ReceivedMessage message;
REQUIRE(client.ReadMessage(message)); // HELLO
server.SendMedia(packet.get(), StreamId::Video, false, 3);
REQUIRE(client.ReadMessage(message));
REQUIRE(message.header.type == static_cast(MessageType::Media));
REQUIRE(message.header.sequence == 1); // only the two video packets counted
server.Stop();
}