From da3696aa8b22f5ed5bd3f31c4294ad821dc4a3c0 Mon Sep 17 00:00:00 2001 From: Henk Wiedig Date: Fri, 26 Jun 2026 15:56:14 +0200 Subject: [PATCH 1/5] poc --- src/gstrtpreceiver.cpp | 241 ++++++++++++++++++++++++++++++++++++++++- src/gstrtpreceiver.h | 17 +++ src/main.cpp | 31 +++++- 3 files changed, 285 insertions(+), 4 deletions(-) diff --git a/src/gstrtpreceiver.cpp b/src/gstrtpreceiver.cpp index edbb5b05..ff9a6750 100644 --- a/src/gstrtpreceiver.cpp +++ b/src/gstrtpreceiver.cpp @@ -113,6 +113,9 @@ static VideoCodec detect_mp4_codec(const char* file_path) { return result; } +// Defined below; classifies a single RTP packet as H264/H265/UNKNOWN. +static VideoCodec classify_rtp_packet(const uint8_t* pkt, size_t len); + namespace { static constexpr int kIdrUdpPort = 11223; static constexpr int kIdrBurstCount = 3; @@ -130,6 +133,22 @@ namespace { static constexpr uint64_t kDecodeStallPktWindowMs = 500; static constexpr uint64_t kRtpSeqResetMs = 1000; + // Number of consecutive RTP packets that must classify as the *other* codec + // before a mid-stream switch is accepted. Frequent active-codec packets reset + // the run, so a stray misclassified packet cannot trigger a rebuild. + static constexpr int kCodecSwitchConfirm = 12; + + // Mid-stream codec-switch detection. g_active_codec is the codec the running + // pipeline was built for; g_codec_switch_cb performs the rebuild and is set + // by the receiver. The cb is invoked at most once until the rebuild clears + // g_codec_switch_pending. + static std::atomic g_codec_auto{false}; + static std::atomic g_active_codec{static_cast(VideoCodec::UNKNOWN)}; + static std::atomic g_codec_switch_run{0}; + static std::atomic g_codec_switch_pending{false}; + static std::mutex g_codec_switch_mutex; + static std::function g_codec_switch_cb; + static std::mutex g_idr_sock_mutex; static int g_idr_sock = -1; static std::atomic g_idr_sock_ready{false}; @@ -502,6 +521,66 @@ namespace { g_last_rtp_seq_ms.store(now, std::memory_order_relaxed); } + static void note_pipeline_codec(VideoCodec codec) { + g_active_codec.store(static_cast(codec), std::memory_order_relaxed); + g_codec_switch_run.store(0, std::memory_order_relaxed); + g_codec_switch_pending.store(false, std::memory_order_relaxed); + } + + static void set_codec_switch_callback(std::function cb) { + std::lock_guard lock(g_codec_switch_mutex); + g_codec_switch_cb = std::move(cb); + } + + // Inspect a raw RTP packet and, if the stream has switched to the other + // codec for a sustained run, hand off a rebuild to the registered callback. + // Called from RTP-ingress threads; must not block or rebuild inline. + static void maybe_detect_codec_switch(const uint8_t* rtp, size_t len) { + if (!g_codec_auto.load(std::memory_order_relaxed)) { + return; // codec pinned by the user; never override it + } + const int active = g_active_codec.load(std::memory_order_relaxed); + if (active == static_cast(VideoCodec::UNKNOWN)) { + return; // codec not resolved yet (startup sniff handles it) + } + if (g_codec_switch_pending.load(std::memory_order_relaxed)) { + return; // a switch is already being applied + } + + const VideoCodec c = classify_rtp_packet(rtp, len); + if (c == VideoCodec::UNKNOWN) { + return; // ambiguous packet: neither confirm nor reset + } + if (static_cast(c) == active) { + g_codec_switch_run.store(0, std::memory_order_relaxed); + return; + } + + if (g_codec_switch_run.fetch_add(1, std::memory_order_relaxed) + 1 < kCodecSwitchConfirm) { + return; + } + + // Confirmed switch; ensure we only fire once until the rebuild completes. + if (g_codec_switch_pending.exchange(true, std::memory_order_relaxed)) { + return; + } + g_codec_switch_run.store(0, std::memory_order_relaxed); + spdlog::info("[CODEC] Mid-stream switch detected: {} -> {}", + active == static_cast(VideoCodec::H265) ? "H.265" : "H.264", + c == VideoCodec::H265 ? "H.265" : "H.264"); + + std::function cb; + { + std::lock_guard lock(g_codec_switch_mutex); + cb = g_codec_switch_cb; + } + if (cb) { + cb(c); + } else { + g_codec_switch_pending.store(false, std::memory_order_relaxed); + } + } + static void for_each_nal(const uint8_t* data, size_t size, const std::function& cb) { auto find_start = [&](size_t from, size_t& start_len) -> size_t { @@ -791,6 +870,11 @@ namespace { if (buf) { on_incoming_stream_buffer(buf, "udpsrc"); maybe_track_rtp_sequence(buf); + GstMapInfo map; + if (gst_buffer_map(buf, &map, GST_MAP_READ)) { + maybe_detect_codec_switch(map.data, map.size); + gst_buffer_unmap(buf, &map); + } } } return GST_PAD_PROBE_OK; @@ -860,6 +944,100 @@ namespace { } } +// --- RTP codec autodetection --------------------------------------------- +// Classify a single RTP/AVP packet as H264 or H265 by inspecting the NAL +// header that follows the RTP header. Returns UNKNOWN when the packet is not a +// usable video packet or is ambiguous. +static VideoCodec classify_rtp_packet(const uint8_t* pkt, size_t len) { + if (len < RTP_HEADER_LEN + 1) return VideoCodec::UNKNOWN; + if (((pkt[0] >> 6) & 0x3) != 2) return VideoCodec::UNKNOWN; // RTP version 2 + size_t off = RTP_HEADER_LEN + (pkt[0] & 0x0F) * 4; // skip CSRCs + if (pkt[0] & 0x10) { // skip extension + if (len < off + 4) return VideoCodec::UNKNOWN; + off += 4 + ((pkt[off + 2] << 8 | pkt[off + 3]) * 4); + } + if (off >= len) return VideoCodec::UNKNOWN; + + const uint8_t nb = pkt[off]; + if (nb & 0x80) return VideoCodec::UNKNOWN; // forbidden_zero_bit + + // Fragmentation units carry the bulk of a video stream and use disjoint + // header bytes between the two codecs, so they are the most reliable signal. + if (nb == 0x1C || nb == 0x3C || nb == 0x5C || nb == 0x7C) // H264 FU-A (type 28) + return VideoCodec::H264; + if (nb == 0x62 || nb == 0x63) // H265 FU (type 49) + return VideoCodec::H265; + + // Otherwise (parameter sets, SEI, single-NAL slices) prefer the codec whose + // NAL-type interpretation is valid while the other's is not, e.g. VPS(32) + // -> 0x40 is invalid as H264 type 0, H264 SPS(7) -> 0x67 is invalid as + // H265 type 51. + const uint8_t t264 = nb & 0x1F; + const uint8_t t265 = (nb >> 1) & 0x3F; + const bool h264_valid = (t264 >= 1 && t264 <= 23); + const bool h265_valid = (t265 <= 40); + if (h265_valid && !h264_valid) return VideoCodec::H265; + if (h264_valid && !h265_valid) return VideoCodec::H264; + return VideoCodec::UNKNOWN; +} + +// Read RTP packets from an already-bound datagram fd and vote on the codec. +// Blocks up to timeout_ms; returns UNKNOWN if no video packets were seen. +static VideoCodec sniff_rtp_codec_fd(int fd, int timeout_ms) { + uint8_t buf[MAX_PACKET_SIZE]; + int votes264 = 0, votes265 = 0; + const auto start = std::chrono::steady_clock::now(); + for (;;) { + const auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + const long remaining = timeout_ms - elapsed; + if (remaining <= 0) break; + + fd_set read_fds; + FD_ZERO(&read_fds); + FD_SET(fd, &read_fds); + struct timeval tv = { remaining / 1000, (remaining % 1000) * 1000 }; + if (select(fd + 1, &read_fds, nullptr, nullptr, &tv) <= 0) continue; + + const ssize_t n = recv(fd, buf, sizeof(buf), 0); + if (n <= 0) continue; + + const VideoCodec c = classify_rtp_packet(buf, static_cast(n)); + if (c == VideoCodec::H264) votes264++; + else if (c == VideoCodec::H265) votes265++; + + // Stop early once one codec has a clear lead. + if (votes264 >= 3 && votes264 > votes265 * 2) return VideoCodec::H264; + if (votes265 >= 3 && votes265 > votes264 * 2) return VideoCodec::H265; + } + if (votes264 == 0 && votes265 == 0) return VideoCodec::UNKNOWN; + return votes264 >= votes265 ? VideoCodec::H264 : VideoCodec::H265; +} + +// Sniff the codec on a UDP port without disturbing the GStreamer udpsrc: bind a +// temporary socket, sample packets, then release the port for the pipeline. +static VideoCodec sniff_rtp_codec_udp(int port, int timeout_ms) { + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) { + spdlog::warn("[CODEC] sniff socket() failed: {}", strerror(errno)); + return VideoCodec::UNKNOWN; + } + int reuse = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(static_cast(port)); + if (bind(fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + spdlog::warn("[CODEC] sniff bind(port={}) failed: {}", port, strerror(errno)); + close(fd); + return VideoCodec::UNKNOWN; + } + const VideoCodec result = sniff_rtp_codec_fd(fd, timeout_ms); + close(fd); + return result; +} + static void initGstreamerOrThrow() { GError* error = nullptr; if (!gst_init_check(nullptr, nullptr, &error)) { @@ -872,6 +1050,7 @@ GstRtpReceiver::GstRtpReceiver(int udp_port, const VideoCodec& codec) { m_port=udp_port; m_video_codec=codec; + m_auto_codec = (codec == VideoCodec::UNKNOWN); initGstreamerOrThrow(); } @@ -879,6 +1058,7 @@ GstRtpReceiver::GstRtpReceiver(int udp_port, const VideoCodec& codec) GstRtpReceiver::GstRtpReceiver(const char *s, const VideoCodec& codec) { unix_socket = strdup(s); m_video_codec = codec; + m_auto_codec = (codec == VideoCodec::UNKNOWN); initGstreamerOrThrow(); spdlog::debug("Creating receiver socket on {}", unix_socket); @@ -909,6 +1089,11 @@ GstRtpReceiver::GstRtpReceiver(const char *s, const VideoCodec& codec) { } GstRtpReceiver::~GstRtpReceiver(){ + // Drop the detection callback so the ingress threads can never call back + // into a destroyed receiver. + set_codec_switch_callback(nullptr); + g_codec_auto.store(false, std::memory_order_relaxed); + note_pipeline_codec(VideoCodec::UNKNOWN); if (sock >= 0) { close(sock); } @@ -1022,8 +1207,11 @@ static void loop_read_socket(bool& keep_looping, int sock_fd, GstAppSrc* appsrc) // Read data directly into buffer ssize_t n = recv(sock_fd, map.data, map.size, 0); + if (n > 0) { + maybe_detect_codec_switch(map.data, static_cast(n)); + } gst_buffer_unmap(buffer, &map); - + if (n <= RTP_HEADER_LEN) { spdlog::warn("Invalid RTP packet size: {}", n); gst_buffer_unref(buffer); @@ -1145,7 +1333,23 @@ VideoCodec GstRtpReceiver::switch_to_file_playback(const char * file_path) { void GstRtpReceiver::switch_to_stream() { stop_receiving(); - + + // Resolve "auto" codec by sniffing the live RTP stream. The whole pipeline + // (depay/parse/caps) is codec-specific, so this must happen before it is built. + if (m_video_codec == VideoCodec::UNKNOWN) { + VideoCodec detected = unix_socket + ? sniff_rtp_codec_fd(sock, 2000) + : sniff_rtp_codec_udp(m_port, 2000); + if (detected == VideoCodec::UNKNOWN) { + detected = VideoCodec::H265; + spdlog::warn("[CODEC] Autodetect saw no RTP video; defaulting to H.265"); + } else { + spdlog::info("[CODEC] Autodetected {} stream", + detected == VideoCodec::H265 ? "H.265" : "H.264"); + } + m_video_codec = detected; + } + const auto pipeline = construct_gstreamer_pipeline(); GError* error = nullptr; m_gst_pipeline = gst_parse_launch(pipeline.c_str(), &error); @@ -1220,9 +1424,40 @@ void GstRtpReceiver::switch_to_stream() { assert(m_app_sink_element); gst_element_set_state(m_gst_pipeline, GST_STATE_PLAYING); - + m_pull_samples_run = true; m_pull_samples_thread = std::make_unique(&GstRtpReceiver::loop_pull_samples, this); + + // Arm mid-stream codec-switch detection for the codec we just built for, + // but only in auto mode (a pinned codec is never overridden). + g_codec_auto.store(m_auto_codec, std::memory_order_relaxed); + set_codec_switch_callback([this](VideoCodec new_codec) { + request_codec_switch(new_codec); + }); + note_pipeline_codec(m_video_codec); +} + +void GstRtpReceiver::request_codec_switch(VideoCodec new_codec) { + // Rebuild on a detached thread: switch_to_stream() tears down the pipeline + // (set_state NULL) and joins the pull/socket threads, neither of which is + // safe to do from a GStreamer streaming thread or the socket reader itself. + std::thread([this, new_codec]() { + m_video_codec = new_codec; // skips re-sniffing in switch_to_stream + switch_to_stream(); // also clears the pending flag + std::function cb; + { + std::lock_guard lock(m_codec_changed_mutex); + cb = m_on_codec_changed; + } + if (cb) { + cb(new_codec); // let the host realign its decoder + } + }).detach(); +} + +void GstRtpReceiver::set_codec_changed_callback(std::function cb) { + std::lock_guard lock(m_codec_changed_mutex); + m_on_codec_changed = std::move(cb); } void GstRtpReceiver::set_playback_rate(double rate) { diff --git a/src/gstrtpreceiver.h b/src/gstrtpreceiver.h index f405e2f6..40c5372e 100644 --- a/src/gstrtpreceiver.h +++ b/src/gstrtpreceiver.h @@ -16,6 +16,7 @@ #include #include #include +#include #define MAX_PACKET_SIZE 4096 #define RTP_HEADER_LEN 12 @@ -62,7 +63,16 @@ class GstRtpReceiver { void normal_playback(); void pause(); void resume(); + // The codec actually used by the live pipeline. When constructed with + // VideoCodec::UNKNOWN this is resolved by sniffing the RTP stream in + // switch_to_stream(); valid only after start_receiving()/switch_to_stream(). + VideoCodec get_active_codec() const { return m_video_codec; } + // Invoked (on an internal thread) after a mid-stream codec switch has been + // detected and the pipeline rebuilt, so the host can realign its decoder. + void set_codec_changed_callback(std::function cb); private: + // Rebuild the pipeline for new_codec after a mid-stream switch is detected. + void request_codec_switch(VideoCodec new_codec); std::string construct_gstreamer_pipeline(); std::string construct_file_playback_pipeline(const char * file_path); void loop_pull_samples(); @@ -71,7 +81,14 @@ class GstRtpReceiver { GstElement * m_gst_pipeline=nullptr; NEW_FRAME_CALLBACK m_cb; VideoCodec m_video_codec; + // True when constructed with VideoCodec::UNKNOWN ("auto"): enables startup + // sniffing and mid-stream codec-switch detection. False when the user pinned + // a codec, in which case we never override their choice. + bool m_auto_codec = false; VideoCodec m_playback_codec = VideoCodec::UNKNOWN; + // Notified after a detected mid-stream codec switch + pipeline rebuild. + std::function m_on_codec_changed; + std::mutex m_codec_changed_mutex; int m_port; // appsink GstElement *m_app_sink_element = nullptr; diff --git a/src/main.cpp b/src/main.cpp index da627434..c30d03cf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -973,12 +973,26 @@ void main_loop() { } uint64_t first_frame_ms=0; +// Publish the active video codec as a string OSD fact ("h264"/"h265"). +static void publish_codec_fact(VideoCodec c) { + osd_publish_str_fact("video.codec", NULL, 0, + (c == VideoCodec::H265) ? "h265" : "h264"); +} + void read_gstreamerpipe_stream(MppPacket *packet, int gst_udp_port, const char *sock ,const VideoCodec& codec){ if (sock) { receiver = std::make_unique(sock, codec); } else { receiver = std::make_unique(gst_udp_port, codec); } + // Realign the MPP decoder whenever the receiver detects a mid-stream codec + // switch and rebuilds its pipeline. + receiver->set_codec_changed_callback([](VideoCodec c) { + MppCodingType t = (c == VideoCodec::H265) ? MPP_VIDEO_CodingHEVC : MPP_VIDEO_CodingAVC; + stream_mpp_type = t; + reinit_mpp_decoder(t); + publish_codec_fact(c); + }); long long bytes_received = 0; uint64_t period_start=0; auto cb=[&packet,/*&decoder_stalled_count,*/ &bytes_received, &period_start](std::shared_ptr> frame){ @@ -1009,6 +1023,16 @@ void read_gstreamerpipe_stream(MppPacket *packet, int gst_udp_port, const char * } }; receiver->start_receiving(cb); + // When the codec was autodetected, align the MPP decoder with what the + // pipeline actually resolved (no-op if it already matches). + { + VideoCodec active = receiver->get_active_codec(); + MppCodingType active_type = (active == VideoCodec::H265) + ? MPP_VIDEO_CodingHEVC : MPP_VIDEO_CodingAVC; + stream_mpp_type = active_type; + reinit_mpp_decoder(active_type); + publish_codec_fact(active); + } main_loop(); receiver->stop_receiving(); spdlog::info("Feeding eos"); @@ -1085,7 +1109,7 @@ void printHelp() { "\n" " --mavlink-dvr-on-arm - Start recording when armed\n" "\n" - " --codec - Video codec, should be the same as on VTX (Default: h265 )\n" + " --codec - Video codec, should be the same as on VTX (Default: h265 )\n" "\n" " --log-level - Log verbosity level, debug|info|warn|error (Default: info)\n" "\n" @@ -1196,6 +1220,11 @@ int main(int argc, char **argv) __OnArgument("--codec") { char * codec_str = const_cast(__ArgValue); + if (!strcmp(codec_str, "auto")) { + // Sentinel: codec is sniffed from the RTP stream at connect time. + codec = VideoCodec::UNKNOWN; + continue; + } codec = video_codec(codec_str); if (codec == VideoCodec::UNKNOWN ) { fprintf(stderr, "unsupported video codec"); From 35ed750f5989a5f254a601f40dce086872b3fc0d Mon Sep 17 00:00:00 2001 From: Henk Wiedig Date: Fri, 26 Jun 2026 16:08:37 +0200 Subject: [PATCH 2/5] fix recordings on mid stream codec switch --- src/dvr.cpp | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- src/dvr.h | 7 +++++++ src/main.cpp | 5 +++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/dvr.cpp b/src/dvr.cpp index 89afb843..686e978a 100644 --- a/src/dvr.cpp +++ b/src/dvr.cpp @@ -158,9 +158,32 @@ void Dvr::loop() { { SPDLOG_DEBUG("got rpc SET_PARAMS"); if (write_ctx.f == NULL) { + break; // no open file; params are kept for the next start() + } + if (!writer_inited) { + // First valid params for this file: initialize the writer. + if (video_frm_width > 0 && video_frm_height > 0 && + codec != VideoCodec::UNKNOWN) { + init(); + } break; } - init(); + // Writer already initialized. A codec or resolution change + // can't be applied to an open mp4 track, so roll to a fresh + // file. Unchanged params are a no-op (re-initializing the open + // mux would corrupt the recording). + if (codec != init_codec || + video_frm_width != init_w || video_frm_height != init_h) { + spdlog::info("DVR params changed (codec/resolution); rolling to new file"); + stop(); + if (start() == 0) { + idr_request_record_start(); + if (video_frm_width > 0 && video_frm_height > 0 && + codec != VideoCodec::UNKNOWN) { + init(); + } + } + } break; } case dvr_rpc::RPC_START: @@ -171,7 +194,7 @@ void Dvr::loop() { } if (start() == 0) { idr_request_record_start(); - if (video_frm_width > 0 && video_frm_height > 0) { + if (video_frm_width > 0 && video_frm_height > 0 && codec != VideoCodec::UNKNOWN) { init(); } } @@ -192,7 +215,7 @@ void Dvr::loop() { if (write_ctx.f == NULL) { if (start() == 0) { idr_request_record_start(); - if (video_frm_width > 0 && video_frm_height > 0) { + if (video_frm_width > 0 && video_frm_height > 0 && codec != VideoCodec::UNKNOWN) { init(); } } @@ -332,6 +355,13 @@ int Dvr::start() { } write_ctx.file_size = 0; mux = MP4E_open(0 /*sequential_mode*/, mp4_fragmentation_mode, &write_ctx, write_callback); + // Fresh file: writer not yet initialized and parameter sets must be + // recached for whatever codec this file ends up carrying. + writer_inited = false; + cached_vps.clear(); + cached_sps.clear(); + cached_pps.clear(); + params_complete = false; if (max_file_size > 0) spdlog::info("DVR file splitting enabled at {} MB", max_file_size / (1024*1024)); return 0; @@ -348,6 +378,12 @@ void Dvr::init() { spdlog::error("mp4_h26x_write_init failed"); mux = NULL; write_ctx.f = NULL; + writer_inited = false; + } else { + writer_inited = true; + init_codec = codec; + init_w = video_frm_width; + init_h = video_frm_height; } _ready_to_write = 1; if (on_start_cb) on_start_cb(); @@ -364,6 +400,7 @@ void Dvr::stop() { write_ctx.f = NULL; write_ctx.file_size = 0; _ready_to_write = 0; + writer_inited = false; } // Check if buffer contains an IDR/IRAP slice. @@ -448,6 +485,12 @@ void Dvr::split() { video_frm_width, video_frm_height, codec == VideoCodec::H265)) { spdlog::error("mp4_h26x_write_init failed on split file"); _ready_to_write = 0; + writer_inited = false; + } else { + writer_inited = true; + init_codec = codec; + init_w = video_frm_width; + init_h = video_frm_height; } } diff --git a/src/dvr.h b/src/dvr.h index 4b7b565a..08606a00 100644 --- a/src/dvr.h +++ b/src/dvr.h @@ -97,6 +97,13 @@ class Dvr { uint32_t video_frm_width; uint32_t video_frm_height; VideoCodec codec; + // What the currently-open mp4 writer was initialized with. A mid-recording + // codec/resolution change cannot be applied to an open mp4 track, so we roll + // to a fresh file instead of re-initializing (which corrupts the mux). + VideoCodec init_codec = VideoCodec::UNKNOWN; + uint32_t init_w = 0; + uint32_t init_h = 0; + bool writer_inited = false; int _ready_to_write = 0; bool split_pending = false; int split_part = 0; diff --git a/src/main.cpp b/src/main.cpp index c30d03cf..35ffc032 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -990,6 +990,9 @@ void read_gstreamerpipe_stream(MppPacket *packet, int gst_udp_port, const char * receiver->set_codec_changed_callback([](VideoCodec c) { MppCodingType t = (c == VideoCodec::H265) ? MPP_VIDEO_CodingHEVC : MPP_VIDEO_CodingAVC; stream_mpp_type = t; + // Keep the global codec in sync so the decoder info-change handler + // (init_buffer) hands the raw DVR the right codec and rolls its file. + ::codec = c; reinit_mpp_decoder(t); publish_codec_fact(c); }); @@ -1030,6 +1033,8 @@ void read_gstreamerpipe_stream(MppPacket *packet, int gst_udp_port, const char * MppCodingType active_type = (active == VideoCodec::H265) ? MPP_VIDEO_CodingHEVC : MPP_VIDEO_CodingAVC; stream_mpp_type = active_type; + // Reflect the resolved codec globally so DVR setup (init_buffer) uses it. + ::codec = active; reinit_mpp_decoder(active_type); publish_codec_fact(active); } From f19783aacd4b09975b3035df7b54c72dd1440f56 Mon Sep 17 00:00:00 2001 From: Henk Wiedig Date: Fri, 26 Jun 2026 16:37:56 +0200 Subject: [PATCH 3/5] drop startup sniff --- src/gstrtpreceiver.cpp | 77 ++++-------------------------------------- 1 file changed, 7 insertions(+), 70 deletions(-) diff --git a/src/gstrtpreceiver.cpp b/src/gstrtpreceiver.cpp index ff9a6750..dd7b9da0 100644 --- a/src/gstrtpreceiver.cpp +++ b/src/gstrtpreceiver.cpp @@ -981,63 +981,6 @@ static VideoCodec classify_rtp_packet(const uint8_t* pkt, size_t len) { return VideoCodec::UNKNOWN; } -// Read RTP packets from an already-bound datagram fd and vote on the codec. -// Blocks up to timeout_ms; returns UNKNOWN if no video packets were seen. -static VideoCodec sniff_rtp_codec_fd(int fd, int timeout_ms) { - uint8_t buf[MAX_PACKET_SIZE]; - int votes264 = 0, votes265 = 0; - const auto start = std::chrono::steady_clock::now(); - for (;;) { - const auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start).count(); - const long remaining = timeout_ms - elapsed; - if (remaining <= 0) break; - - fd_set read_fds; - FD_ZERO(&read_fds); - FD_SET(fd, &read_fds); - struct timeval tv = { remaining / 1000, (remaining % 1000) * 1000 }; - if (select(fd + 1, &read_fds, nullptr, nullptr, &tv) <= 0) continue; - - const ssize_t n = recv(fd, buf, sizeof(buf), 0); - if (n <= 0) continue; - - const VideoCodec c = classify_rtp_packet(buf, static_cast(n)); - if (c == VideoCodec::H264) votes264++; - else if (c == VideoCodec::H265) votes265++; - - // Stop early once one codec has a clear lead. - if (votes264 >= 3 && votes264 > votes265 * 2) return VideoCodec::H264; - if (votes265 >= 3 && votes265 > votes264 * 2) return VideoCodec::H265; - } - if (votes264 == 0 && votes265 == 0) return VideoCodec::UNKNOWN; - return votes264 >= votes265 ? VideoCodec::H264 : VideoCodec::H265; -} - -// Sniff the codec on a UDP port without disturbing the GStreamer udpsrc: bind a -// temporary socket, sample packets, then release the port for the pipeline. -static VideoCodec sniff_rtp_codec_udp(int port, int timeout_ms) { - int fd = socket(AF_INET, SOCK_DGRAM, 0); - if (fd < 0) { - spdlog::warn("[CODEC] sniff socket() failed: {}", strerror(errno)); - return VideoCodec::UNKNOWN; - } - int reuse = 1; - setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_ANY); - addr.sin_port = htons(static_cast(port)); - if (bind(fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { - spdlog::warn("[CODEC] sniff bind(port={}) failed: {}", port, strerror(errno)); - close(fd); - return VideoCodec::UNKNOWN; - } - const VideoCodec result = sniff_rtp_codec_fd(fd, timeout_ms); - close(fd); - return result; -} - static void initGstreamerOrThrow() { GError* error = nullptr; if (!gst_init_check(nullptr, nullptr, &error)) { @@ -1334,20 +1277,14 @@ VideoCodec GstRtpReceiver::switch_to_file_playback(const char * file_path) { void GstRtpReceiver::switch_to_stream() { stop_receiving(); - // Resolve "auto" codec by sniffing the live RTP stream. The whole pipeline - // (depay/parse/caps) is codec-specific, so this must happen before it is built. + // Auto mode: build for H.265 up front and let mid-stream detection flip to + // H.264 from the RTP ingress if the stream turns out to be H.264. The + // ingress classifier sees raw RTP before depay/parse, so it works even + // though the initial pipeline guesses wrong, and this avoids blocking + // startup to sniff (which would stall when no stream is live yet). if (m_video_codec == VideoCodec::UNKNOWN) { - VideoCodec detected = unix_socket - ? sniff_rtp_codec_fd(sock, 2000) - : sniff_rtp_codec_udp(m_port, 2000); - if (detected == VideoCodec::UNKNOWN) { - detected = VideoCodec::H265; - spdlog::warn("[CODEC] Autodetect saw no RTP video; defaulting to H.265"); - } else { - spdlog::info("[CODEC] Autodetected {} stream", - detected == VideoCodec::H265 ? "H.265" : "H.264"); - } - m_video_codec = detected; + m_video_codec = VideoCodec::H265; + spdlog::info("[CODEC] Auto mode: defaulting to H.265; mid-stream detection will correct if needed"); } const auto pipeline = construct_gstreamer_pipeline(); From 335442be323af7275ebade1d1eb8b014cae4187f Mon Sep 17 00:00:00 2001 From: Henk Wiedig Date: Fri, 26 Jun 2026 16:58:05 +0200 Subject: [PATCH 4/5] cleanup --- src/gstrtpreceiver.cpp | 4 ++-- src/gstrtpreceiver.h | 13 +++++++------ src/main.cpp | 23 ++++++++++------------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/gstrtpreceiver.cpp b/src/gstrtpreceiver.cpp index dd7b9da0..d71866fa 100644 --- a/src/gstrtpreceiver.cpp +++ b/src/gstrtpreceiver.cpp @@ -541,7 +541,7 @@ namespace { } const int active = g_active_codec.load(std::memory_order_relaxed); if (active == static_cast(VideoCodec::UNKNOWN)) { - return; // codec not resolved yet (startup sniff handles it) + return; // no pipeline built yet, nothing to compare against } if (g_codec_switch_pending.load(std::memory_order_relaxed)) { return; // a switch is already being applied @@ -1379,7 +1379,7 @@ void GstRtpReceiver::request_codec_switch(VideoCodec new_codec) { // (set_state NULL) and joins the pull/socket threads, neither of which is // safe to do from a GStreamer streaming thread or the socket reader itself. std::thread([this, new_codec]() { - m_video_codec = new_codec; // skips re-sniffing in switch_to_stream + m_video_codec = new_codec; // non-UNKNOWN: switch_to_stream keeps it as-is switch_to_stream(); // also clears the pending flag std::function cb; { diff --git a/src/gstrtpreceiver.h b/src/gstrtpreceiver.h index 40c5372e..e3f1c1ef 100644 --- a/src/gstrtpreceiver.h +++ b/src/gstrtpreceiver.h @@ -63,9 +63,10 @@ class GstRtpReceiver { void normal_playback(); void pause(); void resume(); - // The codec actually used by the live pipeline. When constructed with - // VideoCodec::UNKNOWN this is resolved by sniffing the RTP stream in - // switch_to_stream(); valid only after start_receiving()/switch_to_stream(). + // The codec the live pipeline is currently built for. When constructed with + // VideoCodec::UNKNOWN ("auto") this defaults to H.265 in switch_to_stream() + // and is updated if mid-stream detection flips it; valid only after + // start_receiving()/switch_to_stream(). VideoCodec get_active_codec() const { return m_video_codec; } // Invoked (on an internal thread) after a mid-stream codec switch has been // detected and the pipeline rebuilt, so the host can realign its decoder. @@ -81,9 +82,9 @@ class GstRtpReceiver { GstElement * m_gst_pipeline=nullptr; NEW_FRAME_CALLBACK m_cb; VideoCodec m_video_codec; - // True when constructed with VideoCodec::UNKNOWN ("auto"): enables startup - // sniffing and mid-stream codec-switch detection. False when the user pinned - // a codec, in which case we never override their choice. + // True when constructed with VideoCodec::UNKNOWN ("auto"): the pipeline + // builds for H.265 and mid-stream codec-switch detection is enabled. False + // when the user pinned a codec, in which case we never override their choice. bool m_auto_codec = false; VideoCodec m_playback_codec = VideoCodec::UNKNOWN; // Notified after a detected mid-stream codec switch + pipeline rebuild. diff --git a/src/main.cpp b/src/main.cpp index 35ffc032..67a9bfa8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1026,18 +1026,14 @@ void read_gstreamerpipe_stream(MppPacket *packet, int gst_udp_port, const char * } }; receiver->start_receiving(cb); - // When the codec was autodetected, align the MPP decoder with what the - // pipeline actually resolved (no-op if it already matches). - { - VideoCodec active = receiver->get_active_codec(); - MppCodingType active_type = (active == VideoCodec::H265) - ? MPP_VIDEO_CodingHEVC : MPP_VIDEO_CodingAVC; - stream_mpp_type = active_type; - // Reflect the resolved codec globally so DVR setup (init_buffer) uses it. - ::codec = active; - reinit_mpp_decoder(active_type); - publish_codec_fact(active); - } + // In auto mode the global codec stays UNKNOWN through receiver construction + // (that sentinel is how the receiver knows to auto-detect). Now that the + // pipeline has resolved a codec, reflect it globally so DVR setup + // (init_buffer) and the OSD use it. The MPP decoder was already initialized + // for this codec in main(); a genuine mid-stream change is handled by the + // codec-changed callback above. + ::codec = receiver->get_active_codec(); + publish_codec_fact(::codec); main_loop(); receiver->stop_receiving(); spdlog::info("Feeding eos"); @@ -1226,7 +1222,8 @@ int main(int argc, char **argv) __OnArgument("--codec") { char * codec_str = const_cast(__ArgValue); if (!strcmp(codec_str, "auto")) { - // Sentinel: codec is sniffed from the RTP stream at connect time. + // Sentinel: build for H.265 and let the receiver flip codec from the + // RTP ingress if the stream turns out to be H.264. codec = VideoCodec::UNKNOWN; continue; } From 8c25cf524d3d44abffd296b5dcf2d08d3e5288ce Mon Sep 17 00:00:00 2001 From: Henk Wiedig Date: Fri, 26 Jun 2026 17:26:02 +0200 Subject: [PATCH 5/5] add icons --- src/icons/h264.png | Bin 0 -> 786 bytes src/icons/h264.svg | 1 + src/icons/h265.png | Bin 0 -> 563 bytes src/icons/h265.svg | 1 + src/main.cpp | 6 +++++- 5 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/icons/h264.png create mode 100644 src/icons/h264.svg create mode 100644 src/icons/h265.png create mode 100644 src/icons/h265.svg diff --git a/src/icons/h264.png b/src/icons/h264.png new file mode 100644 index 0000000000000000000000000000000000000000..9befc0dffd7c9291c4371d5debcb34837bca7390 GIT binary patch literal 786 zcmV+t1MU2YP){6Hy)4OVzD^P=krb);DmM0481Dd9Jpw6@{Se`3`01k)4E9!7v zx9hrYI}HJ>s~m&D;48hG!JsdeWxY#72+CK1z@bzs36lcCwV5RYOu8;-KZ3|h zN01v*Jg+R0>cpO!*h zbLyCFjP27#XbZr1BmBNA7BH8~?QkR@4y)CwNuk|=4EQe5LJuIeU(kAb=-FGI-SupT z=b+F&gjg=}uuidn>m~*L$d_-u_65(3+vCVPgPK2n;AlX1*+(*a&h(E`+?E!uNBdo|Dh^z7}YTkT47q$KD9^eSzHj%&5r`v|}9iUq`Tv=5nxp5#N$LVLw$7={7=YlvyX#6^E`tg6*&&9pCM zgQ-b}pRlojCq|~!$N-ZD{%k}LZ=L^HLj5}a_T5ZF$gS5j;@}V_iD126|E?Vv)P}lg z2vBY1D3{AWSQkK6(D|5l69ks0%Habd*S%zp!dIn+Z>yb#u>Z8k02Y65=>sZpqtO`g z+nR \ No newline at end of file diff --git a/src/icons/h265.png b/src/icons/h265.png new file mode 100644 index 0000000000000000000000000000000000000000..084d7cc8a86b9f19b5a6d2d0e2e50bde7b3a663c GIT binary patch literal 563 zcmV-30?hr1P)Y5WTw~0Sm!4$%Tmi1q%^tTMJPTE%k5c|FBd+#m3UY#zL%Z(?}9>e?SaKl5^g< z@3OZp$Av8v*>I0HA3Kxb?c7DdAOHCQT`$Nh=oNHTz_s4*pjXgU0oQuJgI+;b1zhW` zJJ|2{Q-IB31B1a}7csHV1=$&$Rc@qWC-4E~r^DfJQTG`KOYa-xOjrjr57D1YCZV#) z%AHXdhS$nFRGd?z(fH`~dOLYQGtxBO)7;#gYrwo}W<11k91<#gsdx+A?RJ;IL=;5} zw2eB33tT#&NPh|xDvHi3@&rh(gsjh+M0o}*?vLjH z5xGJt3%4>#2b6t!1K6!rtEI{&kB}!+E~g|()+M)zfYe@qWsgCXQ97Vp*9yRH(wr{` z`3ZS~=Y(Gc!FW98^#;vm^BFN26jT|d16J;09F*(K)Dk{2QdU=XIQwMTlCA@8vpJZ5 zFr7}XV9WI`gLO#klRjzy`Zk_xK)D|79@*A2=yW=Xk$9mmSA9=gXHolB_i0-PG+%rA zvTJuJ6#rO|fB)k6`&)<{VdOU$2OGJpPrKdTN6v(GK=aUV`hGxH^}T{#L01J_>-`RT z1zig literal 0 HcmV?d00001 diff --git a/src/icons/h265.svg b/src/icons/h265.svg new file mode 100644 index 00000000..8fd9f7ea --- /dev/null +++ b/src/icons/h265.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 67a9bfa8..a2db685e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -973,10 +973,14 @@ void main_loop() { } uint64_t first_frame_ms=0; -// Publish the active video codec as a string OSD fact ("h264"/"h265"). +// Publish the active video codec as OSD facts: a human-readable string +// ("video.codec" = "h264"/"h265") for text widgets, and a numeric id +// ("video.codec_id" = 0/1) for IconSelectorWidget, which selects by number. static void publish_codec_fact(VideoCodec c) { osd_publish_str_fact("video.codec", NULL, 0, (c == VideoCodec::H265) ? "h265" : "h264"); + osd_publish_uint_fact("video.codec_id", NULL, 0, + (c == VideoCodec::H265) ? 1u : 0u); } void read_gstreamerpipe_stream(MppPacket *packet, int gst_udp_port, const char *sock ,const VideoCodec& codec){