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/gstrtpreceiver.cpp b/src/gstrtpreceiver.cpp index edbb5b05..d71866fa 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; // 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 + } + + 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,43 @@ 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; +} + static void initGstreamerOrThrow() { GError* error = nullptr; if (!gst_init_check(nullptr, nullptr, &error)) { @@ -872,6 +993,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 +1001,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 +1032,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 +1150,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 +1276,17 @@ VideoCodec GstRtpReceiver::switch_to_file_playback(const char * file_path) { void GstRtpReceiver::switch_to_stream() { stop_receiving(); - + + // 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) { + 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(); GError* error = nullptr; m_gst_pipeline = gst_parse_launch(pipeline.c_str(), &error); @@ -1220,9 +1361,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; // non-UNKNOWN: switch_to_stream keeps it as-is + 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..e3f1c1ef 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,17 @@ class GstRtpReceiver { void normal_playback(); void pause(); void resume(); + // 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. + 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 +82,14 @@ class GstRtpReceiver { GstElement * m_gst_pipeline=nullptr; NEW_FRAME_CALLBACK m_cb; VideoCodec m_video_codec; + // 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. + 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/icons/h264.png b/src/icons/h264.png new file mode 100644 index 00000000..9befc0df Binary files /dev/null and b/src/icons/h264.png differ diff --git a/src/icons/h264.svg b/src/icons/h264.svg new file mode 100644 index 00000000..ecf8e2d0 --- /dev/null +++ b/src/icons/h264.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/h265.png b/src/icons/h265.png new file mode 100644 index 00000000..084d7cc8 Binary files /dev/null and b/src/icons/h265.png differ 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 da627434..a2db685e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -973,12 +973,33 @@ void main_loop() { } uint64_t first_frame_ms=0; +// 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){ 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; + // 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); + }); 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 +1030,14 @@ void read_gstreamerpipe_stream(MppPacket *packet, int gst_udp_port, const char * } }; receiver->start_receiving(cb); + // 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"); @@ -1085,7 +1114,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 +1225,12 @@ int main(int argc, char **argv) __OnArgument("--codec") { char * codec_str = const_cast(__ArgValue); + if (!strcmp(codec_str, "auto")) { + // 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; + } codec = video_codec(codec_str); if (codec == VideoCodec::UNKNOWN ) { fprintf(stderr, "unsupported video codec");