Files
zoneminder/src/zm_stream_socket.cpp
T
Steve GilvarryandClaude Fable 5.1 0ac5a2cc83 fix: refuse over-long stream socket paths and media for unknown stream ids
zm::UnixSocket copies the path with a truncating strncpy, so a
PATH_SOCKS long enough to overflow sun_path made the server bind one
file while chmod, chown and unlink acted on another, and the client
connect to a truncated, different path. Both now refuse such a path with
an error. Start() also closes the listener on its own failure paths.

SendMedia indexed the two-entry sequence array with the stream id; a
caller passing StreamId::Monitor would have written past it. Ignore
anything that is not video or audio.

Tests: server and client refuse a 200 character path; a Monitor stream
id is ignored and does not disturb the video sequence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 15:07:15 +10:00

695 lines
24 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_stream_socket.h"
#include "zm_config.h"
#include "zm_logger.h"
#include "zm_utils.h"
#include <algorithm>
#include <cerrno>
#include <cinttypes>
#include <cstring>
#include <limits>
#include <grp.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <sys/un.h>
#include <unistd.h>
using namespace zm::stream_socket;
StreamSocket::StreamSocket(unsigned int monitor_id, std::string path) :
StreamSocket(monitor_id, std::move(path), Config()) {
}
StreamSocket::StreamSocket(unsigned int monitor_id, std::string path, Config config) :
monitor_id_(monitor_id),
path_(std::move(path)),
config_(std::move(config)) {
}
StreamSocket::~StreamSocket() {
Stop();
}
bool StreamSocket::Start() {
if (thread_.joinable())
return true;
// zm::UnixSocket copies the path with a truncating strncpy, so an over-long
// path would bind one file while the chmod, chown and unlink below act on
// another. Refuse it instead.
if (path_.size() >= sizeof(sockaddr_un::sun_path)) {
Error("StreamSocket: socket path %s is longer than the %zu bytes a unix socket"
" address allows", path_.c_str(), sizeof(sockaddr_un::sun_path) - 1);
return false;
}
::unlink(path_.c_str());
if (!listener_.bind(path_.c_str())) {
Error("StreamSocket: failed to bind %s", path_.c_str());
listener_.close();
return false;
}
ApplyPermissions();
if (!listener_.listen()) {
Error("StreamSocket: failed to listen on %s", path_.c_str());
listener_.close();
::unlink(path_.c_str());
return false;
}
listener_.setBlocking(false);
if (!wake_.open()) {
Error("StreamSocket: failed to open wake pipe");
listener_.close();
::unlink(path_.c_str());
return false;
}
wake_.setBlocking(false);
terminate_ = false;
thread_ = std::thread(&StreamSocket::Run, this);
Debug(1, "StreamSocket: monitor %u listening on %s", monitor_id_, path_.c_str());
return true;
}
void StreamSocket::Stop() {
if (thread_.joinable()) {
terminate_ = true;
Wake();
thread_.join();
}
if (listener_.isOpen()) {
listener_.close();
::unlink(path_.c_str());
}
wake_.close();
}
void StreamSocket::ApplyPermissions() {
if (chmod(path_.c_str(), 0660) < 0)
Warning("StreamSocket: chmod 0660 %s failed: %s", path_.c_str(), strerror(errno));
if (config_.group.empty())
return;
long size = sysconf(_SC_GETGR_R_SIZE_MAX);
std::vector<char> buffer(size > 0 ? size : 1024);
group grp = {};
group *result = nullptr;
if (getgrnam_r(config_.group.c_str(), &grp, buffer.data(), buffer.size(), &result) != 0
or !result) {
Warning("StreamSocket: group %s not found, leaving default group on %s",
config_.group.c_str(), path_.c_str());
return;
}
if (chown(path_.c_str(), static_cast<uid_t>(-1), grp.gr_gid) < 0)
Warning("StreamSocket: chown group %s on %s failed: %s",
config_.group.c_str(), path_.c_str(), strerror(errno));
}
StreamSocket::Config StreamSocket::ConfigFromStatic() {
Config config;
config.group = staticConfig.STREAM_SOCKET_GROUP;
config.allowed_uids = ParseAllowedUids(staticConfig.STREAM_SOCKET_ALLOWED_UIDS);
if (staticConfig.STREAM_SOCKET_MAX_CLIENTS > 0)
config.max_clients = staticConfig.STREAM_SOCKET_MAX_CLIENTS;
if (staticConfig.STREAM_SOCKET_QUEUE_BYTES > 0)
config.queue_max_bytes = staticConfig.STREAM_SOCKET_QUEUE_BYTES;
if (staticConfig.STREAM_SOCKET_QUEUE_MSGS > 0)
config.queue_max_msgs = staticConfig.STREAM_SOCKET_QUEUE_MSGS;
if (staticConfig.STREAM_SOCKET_STALL_SECS > 0)
config.stall_timeout = std::chrono::seconds(staticConfig.STREAM_SOCKET_STALL_SECS);
return config;
}
std::vector<uid_t> StreamSocket::ParseAllowedUids(const std::string &value) {
std::vector<uid_t> uids;
for (const std::string &token : Split(value, ',')) {
std::string trimmed = Trim(token, " \t");
if (trimmed.empty())
continue;
errno = 0;
char *end = nullptr;
unsigned long uid = strtoul(trimmed.c_str(), &end, 10);
// strtoul wraps a leading '-' and saturates on overflow; reject both, and
// any value that does not round-trip through uid_t, so a typo cannot
// silently allow a different uid (e.g. 2^32 truncating to uid 0).
bool out_of_range = errno == ERANGE
or uid != static_cast<unsigned long>(static_cast<uid_t>(uid));
if (trimmed[0] != '-' and end != trimmed.c_str() and *end == '\0' and !out_of_range) {
uids.push_back(static_cast<uid_t>(uid));
} else {
Warning("StreamSocket: ignoring malformed uid '%s' in allowed uids", trimmed.c_str());
}
}
return uids;
}
bool StreamSocket::CheckPeer(int fd, uid_t &uid, pid_t &pid) const {
// Kernel-verified peer credentials: SO_PEERCRED on Linux, getpeereid on the
// BSDs and macOS (which report no pid). Without either the uid allow-list
// cannot be enforced and only the filesystem permissions apply.
#if defined(SO_PEERCRED)
ucred cred = {};
socklen_t len = sizeof(cred);
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
Warning("StreamSocket: SO_PEERCRED failed on %s: %s", path_.c_str(), strerror(errno));
return config_.allowed_uids.empty();
}
uid = cred.uid;
pid = cred.pid;
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
gid_t gid = 0;
if (getpeereid(fd, &uid, &gid) != 0) {
Warning("StreamSocket: getpeereid failed on %s: %s", path_.c_str(), strerror(errno));
return config_.allowed_uids.empty();
}
pid = 0;
#else
if (!config_.allowed_uids.empty()) {
Warning("StreamSocket: peer credentials unavailable on this platform,"
" ZM_STREAM_SOCKET_ALLOWED_UIDS is not enforced on %s", path_.c_str());
}
return true;
#endif
if (config_.allowed_uids.empty() or uid == geteuid())
return true;
return std::find(config_.allowed_uids.begin(), config_.allowed_uids.end(), uid)
!= config_.allowed_uids.end();
}
namespace {
// HELLO body for a stream, or empty when the source has no such stream.
std::vector<uint8_t> HelloPayload(const AVCodecParameters *par, AVRational frame_rate) {
if (!par or par->codec_id == AV_CODEC_ID_NONE)
return {};
return BuildHello(par, frame_rate);
}
} // namespace
void StreamSocket::SetStreams(const AVCodecParameters *video, AVRational frame_rate,
const AVCodecParameters *audio) {
std::vector<uint8_t> video_payload = HelloPayload(video, frame_rate);
std::vector<uint8_t> audio_payload = HelloPayload(audio, {0, 0});
bool changed;
{
std::lock_guard<std::mutex> lock(mutex_);
changed = ApplyStreamsLocked(std::move(video_payload), std::move(audio_payload));
}
if (changed)
Wake();
}
void StreamSocket::SetVideoParams(const AVCodecParameters *par, AVRational frame_rate) {
std::vector<uint8_t> payload = HelloPayload(par, frame_rate);
bool changed;
{
std::lock_guard<std::mutex> lock(mutex_);
changed = ApplyStreamsLocked(std::move(payload), std::vector<uint8_t>(hello_audio_payload_));
}
if (changed)
Wake();
}
void StreamSocket::SetAudioParams(const AVCodecParameters *par) {
std::vector<uint8_t> payload = HelloPayload(par, {0, 0});
bool changed;
{
std::lock_guard<std::mutex> lock(mutex_);
changed = ApplyStreamsLocked(std::vector<uint8_t>(hello_video_payload_), std::move(payload));
}
if (changed)
Wake();
}
void StreamSocket::ClearVideoParams() {
SetVideoParams(nullptr, {0, 0});
}
void StreamSocket::ClearAudioParams() {
SetAudioParams(nullptr);
}
bool StreamSocket::ApplyStreamsLocked(std::vector<uint8_t> video, std::vector<uint8_t> audio) {
bool video_changed = video != hello_video_payload_;
bool audio_changed = audio != hello_audio_payload_;
if (!video_changed and !audio_changed)
return false;
// A generation is complete once its video HELLO has gone out; from then on
// any change - new parameters, a stream appearing, a stream disappearing -
// starts a new one. Before that (the initial audio-then-video announcement)
// only a change to an already announced stream does.
bool bump = !hello_video_payload_.empty()
or (audio_changed and !hello_audio_payload_.empty());
if (bump) {
++generation_;
sequence_[0] = sequence_[1] = 0;
keyframe_.reset(); // its header carries the old generation
Info("StreamSocket: monitor %u stream parameters changed (video %s, audio %s),"
" generation now %u", monitor_id_,
video.empty() ? "none" : (video_changed ? "changed" : "same"),
audio.empty() ? "none" : (audio_changed ? "changed" : "same"),
generation_);
}
hello_video_payload_ = std::move(video);
hello_audio_payload_ = std::move(audio);
// On a bump every remaining stream is re-announced under the new generation;
// otherwise only what is new. Audio first: the video HELLO is always the
// last HELLO of a generation.
if (hello_audio_payload_.empty()) {
hello_audio_.reset();
} else if (bump or audio_changed) {
hello_audio_ = MakeMessage(MessageType::Hello, StreamId::Audio, 0, 0, 0,
std::vector<uint8_t>(hello_audio_payload_), true);
BroadcastLocked(hello_audio_);
}
if (hello_video_payload_.empty()) {
hello_video_.reset();
} else if (bump or video_changed) {
hello_video_ = MakeMessage(MessageType::Hello, StreamId::Video, 0, 0, 0,
std::vector<uint8_t>(hello_video_payload_), true);
BroadcastLocked(hello_video_);
}
return true;
}
void StreamSocket::SendMedia(const AVPacket *packet, StreamId stream,
bool keyframe, int64_t pts_us) {
if (!packet or packet->size <= 0)
return;
// Only the two media streams have a sequence counter and a HELLO
if (stream != StreamId::Video and stream != StreamId::Audio)
return;
bool video_keyframe = keyframe and stream == StreamId::Video;
std::unique_lock<std::mutex> lock(mutex_);
// A stream is on the wire only once its parameters are announced. Without a
// HELLO a consumer cannot decode the payload (no codec id or extradata), so
// dropping here keeps the protocol's "HELLO precedes MEDIA" guarantee for
// e.g. audio packets a monitor forwards before record_audio announces them.
const std::vector<uint8_t> &hello =
stream == StreamId::Audio ? hello_audio_payload_ : hello_video_payload_;
if (hello.empty())
return;
uint32_t sequence = sequence_[static_cast<size_t>(stream)]++;
bool have_clients = !clients_.empty();
// With no consumers the message only counts against the sequence; skip the
// packet reference and fan-out entirely. Keyframes are still cached so the
// first consumer to connect gets an immediate first frame.
if (!have_clients and !video_keyframe)
return;
Header header = {};
header.length = kHeaderLengthBytes + packet->size;
header.version = kProtocolVersion;
header.type = static_cast<uint8_t>(MessageType::Media);
header.stream = static_cast<uint8_t>(stream);
header.flags = video_keyframe ? kFlagKeyframe : 0;
header.sequence = sequence;
header.generation = generation_;
header.pts_us = pts_us; // Header.pts_us is signed
if (video_keyframe) {
// Cache the keyframe for fast-start of late joiners; the cache references
// the packet's buffer, no copy is made.
av_packet_ptr cache_clone{av_packet_clone(packet)};
if (cache_clone) {
auto cache = std::make_shared<Message>();
Header cache_header = header;
cache_header.type = static_cast<uint8_t>(MessageType::Keyframe);
SerializeHeader(cache_header, cache->header.data());
cache->av_payload = std::move(cache_clone);
keyframe_ = std::move(cache);
}
}
if (!have_clients)
return;
// Reference the payload buffer; no copy of the media data is made.
av_packet_ptr clone{av_packet_clone(packet)};
if (!clone) {
Error("StreamSocket: av_packet_clone failed for monitor %u", monitor_id_);
return;
}
auto message = std::make_shared<Message>();
SerializeHeader(header, message->header.data());
message->av_payload = std::move(clone);
BroadcastLocked(message);
lock.unlock();
Wake();
}
void StreamSocket::SendMonitorEvent(std::vector<uint8_t> payload) {
std::unique_lock<std::mutex> lock(mutex_);
uint32_t sequence = event_sequence_++;
if (clients_.empty())
return; // sequence still advanced, so a late joiner sees the gap
MessagePtr message = MakeMessage(MessageType::Event, StreamId::Monitor, 0,
sequence, 0, std::move(payload), true);
BroadcastLocked(message);
lock.unlock();
Wake();
}
void StreamSocket::SetSnapshotEvent(std::vector<uint8_t> payload) {
std::lock_guard<std::mutex> lock(mutex_);
// The snapshot is the consumer's authoritative current status on connect. It
// is not broadcast, and only the body is kept: AcceptClient frames it, so
// the header carries the generation and the event-sequence baseline that are
// current when the consumer connects rather than when the status last moved.
snapshot_payload_ = std::move(payload);
}
void StreamSocket::InvalidateKeyframe() {
std::lock_guard<std::mutex> lock(mutex_);
keyframe_.reset();
}
StreamSocket::MessagePtr StreamSocket::MakeMessage(
MessageType type, StreamId stream, uint8_t flags, uint32_t sequence,
int64_t pts_us, std::vector<uint8_t> payload, bool control) const {
auto message = std::make_shared<Message>();
Header header = {};
header.length = kHeaderLengthBytes + payload.size();
header.version = kProtocolVersion;
header.type = static_cast<uint8_t>(type);
header.stream = static_cast<uint8_t>(stream);
header.flags = flags;
header.sequence = sequence;
header.generation = generation_;
header.pts_us = pts_us; // Header.pts_us is signed
SerializeHeader(header, message->header.data());
message->blob_payload = std::move(payload);
message->control = control;
return message;
}
void StreamSocket::BroadcastLocked(const MessagePtr &message) {
for (std::unique_ptr<Client> &client : clients_)
EnqueueLocked(*client, message);
}
void StreamSocket::EnqueueLocked(Client &client, const MessagePtr &message) {
while (!client.queue.empty()
and (client.queued_bytes + message->size() > config_.queue_max_bytes
or client.queue.size() + 1 > config_.queue_max_msgs)) {
// Drop the oldest non-control message. The front message is unsafe to
// drop once partially written (removing it would desync the framing) or
// while the listener is writing it with the lock released.
bool front_pinned = client.front_offset > 0 or client.front_in_flight;
auto begin = client.queue.begin() + (front_pinned ? 1 : 0);
auto victim = std::find_if(begin, client.queue.end(),
[](const MessagePtr &m) { return !m->control; });
if (victim == client.queue.end())
break;
client.queued_bytes -= (*victim)->size();
client.queue.erase(victim);
++client.dropped;
}
if (client.queued_bytes + message->size() > config_.queue_max_bytes
or client.queue.size() + 1 > config_.queue_max_msgs) {
if (!message->control) {
// Nothing droppable left; drop the incoming message instead.
++client.dropped;
return;
}
// Control messages (HELLO/BYE) always go through, even above the limit.
}
client.queue.push_back(message);
client.queued_bytes += message->size();
}
bool StreamSocket::DrainClient(Client &client) {
while (true) {
// Peek the front message under the lock and pin it: EnqueueLocked may
// drop queued messages to make room while we are writing, but never the
// one in flight, so the bookkeeping below always refers to it.
MessagePtr front;
size_t offset = 0;
{
std::lock_guard<std::mutex> lock(mutex_);
if (client.queue.empty())
return true;
front = client.queue.front();
offset = client.front_offset;
client.front_in_flight = true;
}
const Message &message = *front;
iovec iov[2];
int iovcnt = 0;
if (offset < kHeaderSize) {
iov[iovcnt].iov_base = const_cast<uint8_t *>(message.header.data()) + offset;
iov[iovcnt].iov_len = kHeaderSize - offset;
++iovcnt;
if (message.payload_size() > 0) {
iov[iovcnt].iov_base = const_cast<uint8_t *>(message.payload_data());
iov[iovcnt].iov_len = message.payload_size();
++iovcnt;
}
} else {
size_t payload_offset = offset - kHeaderSize;
iov[iovcnt].iov_base = const_cast<uint8_t *>(message.payload_data()) + payload_offset;
iov[iovcnt].iov_len = message.payload_size() - payload_offset;
++iovcnt;
}
msghdr msg = {};
msg.msg_iov = iov;
msg.msg_iovlen = iovcnt;
// MSG_NOSIGNAL: a disconnected client must produce EPIPE, not SIGPIPE.
// This is the kernel copy of the payload; it runs unlocked.
ssize_t written = ::sendmsg(client.sock->getDesc(), &msg, MSG_NOSIGNAL);
int error = written < 0 ? errno : 0;
std::lock_guard<std::mutex> lock(mutex_);
client.front_in_flight = false;
if (written < 0) {
if (error == EAGAIN or error == EWOULDBLOCK)
return true;
if (error == EINTR)
continue;
Debug(1, "StreamSocket: sendmsg to client pid %d failed: %s",
client.pid, strerror(error));
return false;
}
client.front_offset += written;
client.last_progress = std::chrono::steady_clock::now();
if (client.front_offset >= message.size()) {
client.queued_bytes -= message.size();
client.queue.pop_front();
client.front_offset = 0;
++client.sent;
}
}
}
void StreamSocket::SendStatsLocked(Client &client, TimePoint now) {
if (now - client.last_stats < config_.stats_interval)
return;
client.last_stats = now;
MessagePtr stats = MakeMessage(MessageType::Stats, StreamId::Video, 0, 0, 0,
BuildStats(client.sent, client.dropped), false);
EnqueueLocked(client, stats);
}
void StreamSocket::AcceptClient() {
while (true) {
int fd = ::accept(listener_.getDesc(), nullptr, nullptr);
if (fd < 0) {
if (errno != EAGAIN and errno != EWOULDBLOCK and errno != EINTR)
Warning("StreamSocket: accept on %s failed: %s", path_.c_str(), strerror(errno));
return;
}
auto sock = std::make_unique<zm::TcpUnixSocket>(listener_, fd);
uid_t uid = 0;
pid_t pid = 0;
if (!CheckPeer(fd, uid, pid)) {
Warning("StreamSocket: rejecting client uid %u pid %d on %s (not in allowed uids)",
uid, pid, path_.c_str());
continue; // sock destructor closes the connection
}
std::lock_guard<std::mutex> lock(mutex_);
if (clients_.size() >= config_.max_clients) {
Warning("StreamSocket: rejecting client uid %u pid %d on %s (%zu clients connected)",
uid, pid, path_.c_str(), clients_.size());
continue;
}
sock->setBlocking(false);
auto client = std::make_unique<Client>();
client->sock = std::move(sock);
client->uid = uid;
client->pid = pid;
client->last_progress = std::chrono::steady_clock::now();
client->last_stats = client->last_progress;
// New consumers get the stream parameters first (audio HELLO before video
// HELLO, the video HELLO always completes a generation's set), then the
// current-status snapshot, then the cached keyframe so they can render
// immediately instead of waiting for the next GOP.
if (hello_audio_)
EnqueueLocked(*client, hello_audio_);
if (hello_video_)
EnqueueLocked(*client, hello_video_);
if (!snapshot_payload_.empty()) {
EnqueueLocked(*client, MakeMessage(MessageType::Event, StreamId::Monitor, 0,
event_sequence_, 0,
std::vector<uint8_t>(snapshot_payload_), true));
}
if (keyframe_)
EnqueueLocked(*client, keyframe_);
Info("StreamSocket: monitor %u client connected uid %u pid %d (%zu clients)",
monitor_id_, uid, pid, clients_.size() + 1);
clients_.push_back(std::move(client));
}
}
void StreamSocket::Wake() {
if (wake_.isOpen()) {
uint8_t byte = 0;
if (::write(wake_.getWriteDesc(), &byte, 1) < 0 and errno != EAGAIN) {
Debug(1, "StreamSocket: wake write failed: %s", strerror(errno));
}
}
}
void StreamSocket::Run() {
std::vector<pollfd> fds;
while (!terminate_) {
fds.clear();
fds.push_back({listener_.getDesc(), POLLIN, 0});
fds.push_back({wake_.getReadDesc(), POLLIN, 0});
{
std::lock_guard<std::mutex> lock(mutex_);
for (std::unique_ptr<Client> &client : clients_) {
short events = POLLIN;
if (!client->queue.empty())
events |= POLLOUT;
fds.push_back({client->sock->getDesc(), events, 0});
}
}
// With no clients there is nothing to drain or report; sleep until a
// connection or a wake. Idle monitors cost no periodic wakeups.
int timeout = fds.size() > 2 ? 100 : -1;
int ready = ::poll(fds.data(), fds.size(), timeout);
if (ready < 0) {
if (errno == EINTR)
continue;
Error("StreamSocket: poll failed on %s: %s", path_.c_str(), strerror(errno));
break;
}
if (terminate_)
break;
if (fds[1].revents & POLLIN) {
uint8_t buffer[64];
while (::read(wake_.getReadDesc(), buffer, sizeof(buffer)) > 0) {}
}
if (fds[0].revents & POLLIN)
AcceptClient();
// clients_ and each Client's socket are owned by this thread; only the
// queues and counters are shared with producers, so the lock is taken
// just around those, never around the socket writes.
TimePoint now = std::chrono::steady_clock::now();
size_t fd_index = 2;
for (auto it = clients_.begin(); it != clients_.end();) {
Client &client = **it;
short revents = 0;
if (fd_index < fds.size() and fds[fd_index].fd == client.sock->getDesc()) {
revents = fds[fd_index].revents;
++fd_index;
}
bool remove = false;
if (revents & (POLLERR | POLLHUP | POLLNVAL))
remove = true;
if (!remove and (revents & POLLIN)) {
// Protocol v1 has no client->server messages; discard inbound bytes,
// a zero read means the peer closed.
uint8_t buffer[256];
ssize_t bytes = ::recv(client.sock->getDesc(), buffer, sizeof(buffer), 0);
if (bytes == 0)
remove = true;
}
if (!remove)
remove = !DrainClient(client);
if (!remove) {
std::lock_guard<std::mutex> lock(mutex_);
SendStatsLocked(client, now);
if (client.queued_bytes > 0 and now - client.last_progress > config_.stall_timeout) {
Warning("StreamSocket: monitor %u disconnecting stalled client uid %u pid %d"
" (%zu bytes queued, %" PRIu64 " dropped)",
monitor_id_, client.uid, client.pid, client.queued_bytes, client.dropped);
remove = true;
}
}
if (remove) {
std::lock_guard<std::mutex> lock(mutex_);
Info("StreamSocket: monitor %u client uid %u pid %d disconnected"
" (sent %" PRIu64 ", dropped %" PRIu64 ")",
monitor_id_, client.uid, client.pid, client.sent, client.dropped);
it = clients_.erase(it);
} else {
++it;
}
}
}
// Best-effort BYE so consumers can tell shutdown from failure
{
std::lock_guard<std::mutex> lock(mutex_);
MessagePtr bye = MakeMessage(MessageType::Bye, StreamId::Video, 0, 0, 0, {}, true);
for (std::unique_ptr<Client> &client : clients_)
EnqueueLocked(*client, bye);
}
for (std::unique_ptr<Client> &client : clients_)
DrainClient(*client);
std::lock_guard<std::mutex> lock(mutex_);
clients_.clear();
}