#include "cvmmap_streamer/protocol/rtmp_publisher.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace cvmmap_streamer::protocol { namespace { constexpr std::uint8_t kRtmpVersion = 3; constexpr std::size_t kRtmpHandshakePartLength = 1536; constexpr std::uint32_t kDefaultChunkSize = 128; constexpr std::uint32_t kReconnectBackoffInitialMs = 250; constexpr std::uint32_t kReconnectBackoffMaxMs = 8'000; constexpr std::uint8_t kMsgAmf0Command = 20; constexpr std::uint8_t kMsgVideo = 9; constexpr std::uint32_t kChunkStreamCommand = 3; constexpr std::uint32_t kChunkStreamVideo = 6; constexpr std::uint64_t kErrorLogFirstPackets = 8; constexpr std::uint64_t kErrorLogEveryNPackets = 120; struct ParsedRtmpUrl { std::string host{}; std::uint16_t port{1935}; std::string app{}; std::string stream{}; std::string tc_url{}; }; [[nodiscard]] std::expected send_all(int fd, std::span data) { std::size_t written = 0; while (written < data.size()) { const auto n = send( fd, reinterpret_cast(data.data() + written), data.size() - written, MSG_NOSIGNAL); if (n < 0) { if (errno == EINTR) { continue; } return std::unexpected(std::format("send failed: {}", std::strerror(errno))); } if (n == 0) { return std::unexpected("send returned 0 (peer closed)"); } written += static_cast(n); } return {}; } [[nodiscard]] std::expected recv_exact(int fd, std::span data) { std::size_t read = 0; while (read < data.size()) { const auto n = recv( fd, reinterpret_cast(data.data() + read), data.size() - read, 0); if (n < 0) { if (errno == EINTR) { continue; } return std::unexpected(std::format("recv failed: {}", std::strerror(errno))); } if (n == 0) { return std::unexpected("peer closed while receiving"); } read += static_cast(n); } return {}; } void append_be24(std::vector &out, std::uint32_t v) { out.push_back(static_cast((v >> 16) & 0xffu)); out.push_back(static_cast((v >> 8) & 0xffu)); out.push_back(static_cast(v & 0xffu)); } void append_be32(std::vector &out, std::uint32_t v) { out.push_back(static_cast((v >> 24) & 0xffu)); out.push_back(static_cast((v >> 16) & 0xffu)); out.push_back(static_cast((v >> 8) & 0xffu)); out.push_back(static_cast(v & 0xffu)); } void append_le32(std::vector &out, std::uint32_t v) { out.push_back(static_cast(v & 0xffu)); out.push_back(static_cast((v >> 8) & 0xffu)); out.push_back(static_cast((v >> 16) & 0xffu)); out.push_back(static_cast((v >> 24) & 0xffu)); } void append_basic_header(std::vector &out, std::uint8_t fmt, std::uint32_t csid) { if (csid <= 63) { out.push_back(static_cast((fmt << 6) | csid)); return; } if (csid <= 319) { out.push_back(static_cast((fmt << 6) | 0)); out.push_back(static_cast(csid - 64)); return; } out.push_back(static_cast((fmt << 6) | 1)); const std::uint32_t normalized = csid - 64; out.push_back(static_cast(normalized & 0xff)); out.push_back(static_cast((normalized >> 8) & 0xff)); } [[nodiscard]] std::expected send_rtmp_message( int fd, std::uint32_t chunk_stream_id, std::uint32_t timestamp, std::uint8_t type_id, std::uint32_t message_stream_id, std::span payload, std::uint32_t chunk_size) { if (chunk_size == 0) { return std::unexpected("invalid RTMP chunk size 0"); } std::vector packet; packet.reserve(payload.size() + 64); const std::uint32_t timestamp_field = std::min(timestamp, 0x00ffffffu); append_basic_header(packet, 0, chunk_stream_id); append_be24(packet, timestamp_field); append_be24(packet, static_cast(payload.size())); packet.push_back(type_id); append_le32(packet, message_stream_id); if (timestamp >= 0x00ffffffu) { append_be32(packet, timestamp); } std::size_t offset = 0; const std::size_t first_chunk = std::min(chunk_size, payload.size()); packet.insert(packet.end(), payload.begin(), payload.begin() + static_cast(first_chunk)); offset += first_chunk; while (offset < payload.size()) { append_basic_header(packet, 3, chunk_stream_id); if (timestamp >= 0x00ffffffu) { append_be32(packet, timestamp); } const std::size_t part = std::min(chunk_size, payload.size() - offset); packet.insert( packet.end(), payload.begin() + static_cast(offset), payload.begin() + static_cast(offset + part)); offset += part; } auto send_result = send_all(fd, packet); if (!send_result) { return std::unexpected(send_result.error()); } return packet.size(); } void append_amf0_string(std::vector &out, std::string_view value) { out.push_back(0x02); out.push_back(static_cast((value.size() >> 8) & 0xff)); out.push_back(static_cast(value.size() & 0xff)); out.insert(out.end(), value.begin(), value.end()); } void append_amf0_number(std::vector &out, double value) { out.push_back(0x00); std::uint64_t bits{0}; std::memcpy(&bits, &value, sizeof(bits)); out.push_back(static_cast((bits >> 56) & 0xff)); out.push_back(static_cast((bits >> 48) & 0xff)); out.push_back(static_cast((bits >> 40) & 0xff)); out.push_back(static_cast((bits >> 32) & 0xff)); out.push_back(static_cast((bits >> 24) & 0xff)); out.push_back(static_cast((bits >> 16) & 0xff)); out.push_back(static_cast((bits >> 8) & 0xff)); out.push_back(static_cast(bits & 0xff)); } void append_amf0_null(std::vector &out) { out.push_back(0x05); } void append_amf0_object_start(std::vector &out) { out.push_back(0x03); } void append_amf0_object_end(std::vector &out) { out.push_back(0x00); out.push_back(0x00); out.push_back(0x09); } void append_amf0_object_key(std::vector &out, std::string_view key) { out.push_back(static_cast((key.size() >> 8) & 0xff)); out.push_back(static_cast(key.size() & 0xff)); out.insert(out.end(), key.begin(), key.end()); } void append_amf0_object_string_property( std::vector &out, std::string_view key, std::string_view value) { append_amf0_object_key(out, key); append_amf0_string(out, value); } void append_amf0_object_number_property(std::vector &out, std::string_view key, double value) { append_amf0_object_key(out, key); append_amf0_number(out, value); } [[nodiscard]] std::expected parse_rtmp_url(std::string_view url) { constexpr std::string_view kPrefix{"rtmp://"}; if (!url.starts_with(kPrefix)) { return std::unexpected(std::format( "invalid RTMP url '{}': must start with rtmp://", url)); } const auto raw = url.substr(kPrefix.size()); const auto slash = raw.find('/'); if (slash == std::string_view::npos || slash == 0 || slash + 1 >= raw.size()) { return std::unexpected(std::format( "invalid RTMP url '{}': expected rtmp://[:port]//", url)); } ParsedRtmpUrl parsed{}; const auto host_port = raw.substr(0, slash); const auto path = raw.substr(slash + 1); const auto colon = host_port.rfind(':'); if (colon != std::string_view::npos && colon + 1 < host_port.size()) { parsed.host = std::string(host_port.substr(0, colon)); std::uint16_t port{0}; const auto port_raw = host_port.substr(colon + 1); auto [ptr, ec] = std::from_chars( port_raw.data(), port_raw.data() + port_raw.size(), port, 10); if (ec != std::errc{} || ptr != port_raw.data() + port_raw.size() || port == 0) { return std::unexpected(std::format( "invalid RTMP url '{}': invalid port '{}'", url, port_raw)); } parsed.port = port; } else { parsed.host = std::string(host_port); } if (parsed.host.empty()) { return std::unexpected(std::format("invalid RTMP url '{}': host must not be empty", url)); } const auto app_sep = path.find('/'); if (app_sep == std::string_view::npos || app_sep == 0 || app_sep + 1 >= path.size()) { return std::unexpected(std::format( "invalid RTMP url '{}': expected //", url)); } parsed.app = std::string(path.substr(0, app_sep)); parsed.stream = std::string(path.substr(app_sep + 1)); if (parsed.app.empty() || parsed.stream.empty()) { return std::unexpected(std::format("invalid RTMP url '{}': app/stream must not be empty", url)); } parsed.tc_url = std::format( "rtmp://{}:{}/{}", parsed.host, parsed.port, parsed.app); return parsed; } [[nodiscard]] std::expected open_tcp(std::string_view host, std::uint16_t port) { addrinfo hints{}; hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; addrinfo *result = nullptr; const auto port_text = std::to_string(port); const int gai = getaddrinfo( std::string(host).c_str(), port_text.c_str(), &hints, &result); if (gai != 0) { return std::unexpected(std::format( "getaddrinfo failed for '{}:{}': {}", host, port, gai_strerror(gai))); } int fd{-1}; for (auto *it = result; it != nullptr; it = it->ai_next) { fd = socket(it->ai_family, it->ai_socktype, it->ai_protocol); if (fd < 0) { continue; } if (connect(fd, it->ai_addr, it->ai_addrlen) == 0) { break; } close(fd); fd = -1; } freeaddrinfo(result); if (fd < 0) { return std::unexpected(std::format( "connect failed for '{}:{}': {}", host, port, std::strerror(errno))); } return fd; } [[nodiscard]] std::expected send_connect_message(int fd, std::uint32_t chunk_size, const ParsedRtmpUrl &url) { std::vector payload; payload.reserve(256); append_amf0_string(payload, "connect"); append_amf0_number(payload, 1.0); append_amf0_object_start(payload); append_amf0_object_string_property(payload, "app", url.app); append_amf0_object_string_property(payload, "tcUrl", url.tc_url); append_amf0_object_number_property(payload, "objectEncoding", 0.0); append_amf0_object_end(payload); auto sent = send_rtmp_message( fd, kChunkStreamCommand, 0, kMsgAmf0Command, 0, payload, chunk_size); if (!sent) { return std::unexpected(sent.error()); } return {}; } [[nodiscard]] std::expected send_create_stream_message(int fd, std::uint32_t chunk_size) { std::vector payload; payload.reserve(64); append_amf0_string(payload, "createStream"); append_amf0_number(payload, 2.0); append_amf0_null(payload); auto sent = send_rtmp_message( fd, kChunkStreamCommand, 0, kMsgAmf0Command, 0, payload, chunk_size); if (!sent) { return std::unexpected(sent.error()); } return {}; } [[nodiscard]] std::expected send_publish_message( int fd, std::uint32_t chunk_size, std::uint32_t stream_id, std::string_view stream_name) { std::vector payload; payload.reserve(128); append_amf0_string(payload, "publish"); append_amf0_number(payload, 3.0); append_amf0_null(payload); append_amf0_string(payload, stream_name); append_amf0_string(payload, "live"); auto sent = send_rtmp_message( fd, kChunkStreamCommand, 0, kMsgAmf0Command, stream_id, payload, chunk_size); if (!sent) { return std::unexpected(sent.error()); } return {}; } [[nodiscard]] std::expected run_handshake(int fd) { std::array c0c1{}; c0c1[0] = kRtmpVersion; for (std::size_t i = 0; i < kRtmpHandshakePartLength; ++i) { c0c1[1 + i] = static_cast((i * 17) & 0xff); } auto c0c1_send = send_all(fd, c0c1); if (!c0c1_send) { return std::unexpected(std::format("write C0+C1 failed: {}", c0c1_send.error())); } std::array s0s1s2{}; auto s0s1s2_read = recv_exact(fd, s0s1s2); if (!s0s1s2_read) { return std::unexpected(std::format("read S0+S1+S2 failed: {}", s0s1s2_read.error())); } if (s0s1s2[0] != kRtmpVersion) { return std::unexpected(std::format( "unexpected S0 RTMP version {} (expected {})", static_cast(s0s1s2[0]), static_cast(kRtmpVersion))); } std::array c2{}; std::copy_n(s0s1s2.begin() + 1, kRtmpHandshakePartLength, c2.begin()); auto c2_send = send_all(fd, c2); if (!c2_send) { return std::unexpected(std::format("write C2 failed: {}", c2_send.error())); } return {}; } [[nodiscard]] bool h264_idr_access_unit(std::span access_unit) { if (access_unit.empty()) { return false; } for (std::size_t i = 0; i < access_unit.size(); ++i) { if (i + 4 <= access_unit.size() && access_unit[i] == 0x00 && access_unit[i + 1] == 0x00 && access_unit[i + 2] == 0x00 && access_unit[i + 3] == 0x01) { const std::size_t nal = i + 4; if (nal < access_unit.size()) { return (access_unit[nal] & 0x1fu) == 5u; } } if (i + 3 <= access_unit.size() && access_unit[i] == 0x00 && access_unit[i + 1] == 0x00 && access_unit[i + 2] == 0x01) { const std::size_t nal = i + 3; if (nal < access_unit.size()) { return (access_unit[nal] & 0x1fu) == 5u; } } } return (access_unit[0] & 0x1fu) == 5u; } [[nodiscard]] std::vector make_h264_sequence_header() { return { 0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x64, 0x00, 0x1f, 0xff, 0xe1, 0x00, 0x04, 0x67, 0x64, 0x00, 0x1f, }; } [[nodiscard]] std::vector make_h264_video_payload(std::span access_unit) { const bool is_idr = h264_idr_access_unit(access_unit); std::vector payload; payload.reserve(5 + access_unit.size()); payload.push_back(is_idr ? 0x17 : 0x27); payload.push_back(0x01); payload.push_back(0x00); payload.push_back(0x00); payload.push_back(0x00); payload.insert(payload.end(), access_unit.begin(), access_unit.end()); return payload; } [[nodiscard]] std::vector make_h265_enhanced_sequence_header() { return { 0x90, 'h', 'v', 'c', '1', 0x01, 0x01, 0x60, }; } [[nodiscard]] std::vector make_h265_domestic_sequence_header() { return { 0x1c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x60, }; } [[nodiscard]] std::vector make_h265_enhanced_video_payload(std::span access_unit) { std::vector payload; payload.reserve(9 + access_unit.size()); payload.push_back(0x91); payload.push_back('h'); payload.push_back('v'); payload.push_back('c'); payload.push_back('1'); payload.push_back(0x00); payload.push_back(0x00); payload.push_back(0x00); payload.push_back(0x00); payload.insert(payload.end(), access_unit.begin(), access_unit.end()); return payload; } [[nodiscard]] bool h265_idr_access_unit(std::span access_unit) { if (access_unit.size() < 2) { return false; } for (std::size_t i = 0; i < access_unit.size(); ++i) { std::size_t nal = access_unit.size(); if (i + 4 <= access_unit.size() && access_unit[i] == 0x00 && access_unit[i + 1] == 0x00 && access_unit[i + 2] == 0x00 && access_unit[i + 3] == 0x01) { nal = i + 4; } else if (i + 3 <= access_unit.size() && access_unit[i] == 0x00 && access_unit[i + 1] == 0x00 && access_unit[i + 2] == 0x01) { nal = i + 3; } if (nal + 1 >= access_unit.size()) { continue; } const std::uint8_t nal_type = static_cast((access_unit[nal] >> 1) & 0x3fu); if (nal_type == 19u || nal_type == 20u || nal_type == 21u) { return true; } } const std::uint8_t nal_type = static_cast((access_unit[0] >> 1) & 0x3fu); return nal_type == 19u || nal_type == 20u || nal_type == 21u; } [[nodiscard]] std::vector make_h265_domestic_video_payload(std::span access_unit) { const bool is_idr = h265_idr_access_unit(access_unit); std::vector payload; payload.reserve(5 + access_unit.size()); payload.push_back(is_idr ? 0x1c : 0x2c); payload.push_back(0x01); payload.push_back(0x00); payload.push_back(0x00); payload.push_back(0x00); payload.insert(payload.end(), access_unit.begin(), access_unit.end()); return payload; } [[nodiscard]] std::uint32_t to_rtmp_timestamp_ms(std::uint64_t pts_ns) { return static_cast((pts_ns / 1'000'000ull) & 0xffffffffu); } } RtmpPublisher::~RtmpPublisher() { for (auto &session : sessions_) { if (session.socket_fd >= 0) { close(session.socket_fd); session.socket_fd = -1; } } } RtmpPublisher::RtmpPublisher(RtmpPublisher &&other) noexcept { codec_ = std::exchange(other.codec_, CodecType::H264); mode_ = std::exchange(other.mode_, RtmpMode::Enhanced); sessions_ = std::move(other.sessions_); stats_ = other.stats_; } RtmpPublisher &RtmpPublisher::operator=(RtmpPublisher &&other) noexcept { if (this == &other) { return *this; } for (auto &session : sessions_) { if (session.socket_fd >= 0) { close(session.socket_fd); session.socket_fd = -1; } } codec_ = std::exchange(other.codec_, CodecType::H264); mode_ = std::exchange(other.mode_, RtmpMode::Enhanced); sessions_ = std::move(other.sessions_); stats_ = other.stats_; return *this; } void RtmpPublisher::close_session(Session &session) { if (session.socket_fd >= 0) { close(session.socket_fd); session.socket_fd = -1; } } std::expected RtmpPublisher::connect_session(Session &session) { auto fd = open_tcp(session.host, session.port); if (!fd) { return std::unexpected(std::format("RTMP connect failed: {}", fd.error())); } auto handshake = run_handshake(*fd); if (!handshake) { close(*fd); return std::unexpected(std::format("RTMP handshake failed: {}", handshake.error())); } ParsedRtmpUrl parsed{}; parsed.host = session.host; parsed.port = session.port; parsed.app = session.app; parsed.stream = session.stream; parsed.tc_url = session.tc_url; auto connect_message = send_connect_message(*fd, kDefaultChunkSize, parsed); if (!connect_message) { close(*fd); return std::unexpected(std::format("RTMP connect-command failed: {}", connect_message.error())); } auto create_stream = send_create_stream_message(*fd, kDefaultChunkSize); if (!create_stream) { close(*fd); return std::unexpected(std::format("RTMP createStream failed: {}", create_stream.error())); } auto publish_message = send_publish_message(*fd, kDefaultChunkSize, 1, session.stream); if (!publish_message) { close(*fd); return std::unexpected(std::format("RTMP publish-command failed: {}", publish_message.error())); } close_session(session); session.socket_fd = *fd; session.out_chunk_size = kDefaultChunkSize; session.stream_id = 1; session.sequence_header_sent = false; session.consecutive_reconnect_failures = 0; session.reconnect_backoff_ms = kReconnectBackoffInitialMs; session.in_cooldown = false; return {}; } void RtmpPublisher::schedule_reconnect(Session &session, std::string_view reason, bool startup_path) { close_session(session); session.sequence_header_sent = false; const auto now = std::chrono::steady_clock::now(); if (startup_path) { session.reconnect_due_at = now; session.in_cooldown = false; } else { session.reconnect_due_at = now + std::chrono::milliseconds(session.reconnect_backoff_ms); session.in_cooldown = true; } spdlog::warn( "RTMP_SESSION_RECONNECT_SCHEDULED codec={} mode={} url={} reason='{}' cooldown_ms={} failures={}", to_string(codec_), to_string(mode_), session.original_url, reason, startup_path ? 0u : session.reconnect_backoff_ms, session.consecutive_reconnect_failures); } std::expected RtmpPublisher::create(const RuntimeConfig &config) { if (!config.outputs.rtmp.enabled) { return std::unexpected("invalid RTMP publisher init: RTMP output disabled"); } if (config.outputs.rtmp.urls.empty()) { return std::unexpected("invalid RTMP publisher init: no RTMP URL configured"); } if (config.outputs.rtmp.mode == RtmpMode::Domestic && config.codec != CodecType::H265) { return std::unexpected( "invalid mode matrix: --rtmp-mode domestic requires --codec h265 (h264+domestic is unsupported)"); } spdlog::info( "RTMP_MODE_SELECTED codec={} mode={} urls={}", to_string(config.codec), to_string(config.outputs.rtmp.mode), config.outputs.rtmp.urls.size()); RtmpPublisher publisher{}; publisher.codec_ = config.codec; publisher.mode_ = config.outputs.rtmp.mode; publisher.sessions_.reserve(config.outputs.rtmp.urls.size()); for (const auto &url : config.outputs.rtmp.urls) { auto parsed = parse_rtmp_url(url); if (!parsed) { return std::unexpected(parsed.error()); } Session session{}; session.original_url = url; session.host = parsed->host; session.port = parsed->port; session.app = parsed->app; session.stream = parsed->stream; session.tc_url = parsed->tc_url; session.reconnect_backoff_ms = kReconnectBackoffInitialMs; session.reconnect_due_at = std::chrono::steady_clock::now(); auto connect_result = publisher.connect_session(session); if (!connect_result) { return std::unexpected(std::format( "RTMP initial session setup failed for '{}': {}", url, connect_result.error())); } publisher.sessions_.push_back(std::move(session)); spdlog::info( "RTMP_SESSION_READY codec={} mode={} url={} app={} stream={}", to_string(publisher.codec_), to_string(publisher.mode_), url, parsed->app, parsed->stream); } return publisher; } std::expected RtmpPublisher::publish_access_unit(std::span access_unit, std::uint64_t pts_ns) { stats_.access_units += 1; stats_.access_unit_bytes += access_unit.size(); const auto now = std::chrono::steady_clock::now(); std::size_t connected_sessions{0}; for (auto &session : sessions_) { if (session.socket_fd >= 0) { connected_sessions += 1; continue; } if (session.in_cooldown && now < session.reconnect_due_at) { continue; } stats_.reconnect_attempts += 1; spdlog::info( "RTMP_SESSION_RECONNECT_ATTEMPT codec={} mode={} url={} attempt={} failures={} cooldown_elapsed_ms={}", to_string(codec_), to_string(mode_), session.original_url, stats_.reconnect_attempts, session.consecutive_reconnect_failures, session.in_cooldown ? session.reconnect_backoff_ms : 0u); auto reconnect = connect_session(session); if (!reconnect) { stats_.reconnect_failures += 1; session.consecutive_reconnect_failures += 1; session.reconnect_backoff_ms = std::min( kReconnectBackoffMaxMs, std::max(kReconnectBackoffInitialMs, session.reconnect_backoff_ms * 2)); schedule_reconnect(session, reconnect.error(), false); continue; } stats_.reconnect_successes += 1; connected_sessions += 1; spdlog::info( "RTMP_SESSION_RECONNECTED codec={} mode={} url={} successes={} failures={}", to_string(codec_), to_string(mode_), session.original_url, stats_.reconnect_successes, session.consecutive_reconnect_failures); } if (access_unit.empty()) { return {}; } if (connected_sessions == 0) { if (!warned_all_sessions_closed_) { spdlog::warn( "RTMP_PUBLISH_STOPPED codec={} mode={} reason='no active RTMP sessions' reconnect_attempts={} reconnect_successes={} reconnect_failures={}", to_string(codec_), to_string(mode_), stats_.reconnect_attempts, stats_.reconnect_successes, stats_.reconnect_failures); warned_all_sessions_closed_ = true; } return {}; } const std::uint32_t timestamp_ms = to_rtmp_timestamp_ms(pts_ns); std::size_t index{0}; while (index < sessions_.size()) { auto &session = sessions_[index]; if (session.socket_fd < 0) { ++index; continue; } if (!session.sequence_header_sent) { std::vector sequence_header{}; if (codec_ == CodecType::H264) { sequence_header = make_h264_sequence_header(); } else { if (mode_ == RtmpMode::Enhanced) { sequence_header = make_h265_enhanced_sequence_header(); } else if (mode_ == RtmpMode::Domestic) { sequence_header = make_h265_domestic_sequence_header(); } else { return std::unexpected(std::format( "unsupported RTMP mode '{}' for codec '{}'", to_string(mode_), to_string(codec_))); } } auto config_send = send_rtmp_message( session.socket_fd, kChunkStreamVideo, timestamp_ms, kMsgVideo, session.stream_id, sequence_header, session.out_chunk_size); if (!config_send) { stats_.send_errors += 1; stats_.publish_failures += 1; session.consecutive_reconnect_failures += 1; session.reconnect_backoff_ms = std::min( kReconnectBackoffMaxMs, std::max(kReconnectBackoffInitialMs, session.reconnect_backoff_ms * 2)); if (stats_.send_errors <= kErrorLogFirstPackets || (stats_.send_errors % kErrorLogEveryNPackets) == 0) { spdlog::warn( "RTMP_SEND_ERROR codec={} mode={} url={} stage=sequence-header detail='{}' send_errors={}", to_string(codec_), to_string(mode_), session.original_url, config_send.error(), stats_.send_errors); } schedule_reconnect(session, config_send.error(), false); continue; } stats_.video_messages += 1; stats_.bytes_sent += *config_send; session.sequence_header_sent = true; } std::vector frame_payload{}; if (codec_ == CodecType::H264) { frame_payload = make_h264_video_payload(access_unit); } else { if (mode_ == RtmpMode::Enhanced) { frame_payload = make_h265_enhanced_video_payload(access_unit); } else if (mode_ == RtmpMode::Domestic) { frame_payload = make_h265_domestic_video_payload(access_unit); } else { return std::unexpected(std::format( "unsupported RTMP mode '{}' for codec '{}'", to_string(mode_), to_string(codec_))); } } auto frame_send = send_rtmp_message( session.socket_fd, kChunkStreamVideo, timestamp_ms, kMsgVideo, session.stream_id, frame_payload, session.out_chunk_size); if (!frame_send) { stats_.send_errors += 1; stats_.publish_failures += 1; session.consecutive_reconnect_failures += 1; session.reconnect_backoff_ms = std::min( kReconnectBackoffMaxMs, std::max(kReconnectBackoffInitialMs, session.reconnect_backoff_ms * 2)); if (stats_.send_errors <= kErrorLogFirstPackets || (stats_.send_errors % kErrorLogEveryNPackets) == 0) { spdlog::warn( "RTMP_SEND_ERROR codec={} mode={} url={} stage=video-frame detail='{}' send_errors={}", to_string(codec_), to_string(mode_), session.original_url, frame_send.error(), stats_.send_errors); } schedule_reconnect(session, frame_send.error(), false); continue; } stats_.video_messages += 1; stats_.bytes_sent += *frame_send; had_successful_video_message_ = true; session.consecutive_reconnect_failures = 0; session.reconnect_backoff_ms = kReconnectBackoffInitialMs; session.in_cooldown = false; ++index; } const bool any_connected = std::any_of(sessions_.begin(), sessions_.end(), [](const Session &session) { return session.socket_fd >= 0; }); if (!any_connected && !had_successful_video_message_ && !warned_all_sessions_closed_) { spdlog::warn( "RTMP_EARLY_DISCONNECT codec={} mode={} reason='all sessions disconnected before first video delivery; keeping publisher alive for reconnect/backoff'", to_string(codec_), to_string(mode_)); } if (any_connected) { warned_all_sessions_closed_ = false; } return {}; } const RtmpPublisherStats &RtmpPublisher::stats() const { return stats_; } void RtmpPublisher::on_stream_reset() { spdlog::info("RTMP_STREAM_RESET codec={} mode={} sessions={}", to_string(codec_), to_string(mode_), sessions_.size()); for (auto &session : sessions_) { session.sequence_header_sent = false; if (session.socket_fd >= 0) { spdlog::info("RTMP_STREAM_RESET_REBASE url={} action=force_sequence_header", session.original_url); } } } void RtmpPublisher::log_metrics() const { spdlog::info( "RTMP_METRICS codec={} mode={} sessions={} access_units={} access_unit_bytes={} video_messages={} bytes_sent={} send_errors={} publish_failures={} reconnect_attempts={} reconnect_successes={} reconnect_failures={}", to_string(codec_), to_string(mode_), sessions_.size(), stats_.access_units, stats_.access_unit_bytes, stats_.video_messages, stats_.bytes_sent, stats_.send_errors, stats_.publish_failures, stats_.reconnect_attempts, stats_.reconnect_successes, stats_.reconnect_failures); } }