diff --git a/plugins/publish/publish_node.cpp b/plugins/publish/publish_node.cpp index d4d491b..9f7125f 100644 --- a/plugins/publish/publish_node.cpp +++ b/plugins/publish/publish_node.cpp @@ -727,11 +727,24 @@ private: } mk_media_init_complete(media_); + is_h265_ = (codec_id == 1); + splitter_ = mk_h264_splitter_create(&OnSplitFrame, this); + if (!splitter_) { + std::cerr << "[publish] zlm mk_h264_splitter_create failed" << "\n"; + mk_media_release(media_); + media_ = nullptr; + return false; + } + std::cout << "[publish] zlm rtsp server ready: rtsp://0.0.0.0:" << port_ << "/" << app_ << "/" << stream_ << "\n"; return true; } void Close() { + if (splitter_) { + mk_h264_splitter_release(splitter_); + splitter_ = nullptr; + } if (media_) { mk_media_release(media_); media_ = nullptr; @@ -741,23 +754,174 @@ private: void Write(const EncodedPacket& pkt, const std::vector& header, bool is_h265) { if (!media_ || pkt.data.empty()) return; + // Feed encoder header first to provide SPS/PPS(/VPS). if (!sent_header_ && !header.empty()) { - if (is_h265) { - mk_media_input_h265(media_, header.data(), static_cast(header.size()), pkt.pts_ms, pkt.pts_ms); - } else { - mk_media_input_h264(media_, header.data(), static_cast(header.size()), pkt.pts_ms, pkt.pts_ms); - } + FeedHeader(pkt.pts_ms, header, is_h265); sent_header_ = true; } - if (is_h265) { - mk_media_input_h265(media_, pkt.data.data(), static_cast(pkt.data.size()), pkt.pts_ms, pkt.pts_ms); - } else { - mk_media_input_h264(media_, pkt.data.data(), static_cast(pkt.data.size()), pkt.pts_ms, pkt.pts_ms); - } + FeedPacket(pkt.pts_ms, pkt.data, is_h265); + + // NOTE: We intentionally do not send data directly via mk_media_input_h264/h265. + // Using mk_h264_splitter + mk_media_input_frame is more robust for SPS/PPS handling. } private: + static uint16_t ReadBe16(const uint8_t* p) { + return static_cast(p[0] << 8) | static_cast(p[1]); + } + + static bool HasAnnexBStartCode(const uint8_t* d, size_t n) { + if (!d || n < 3) return false; + for (size_t i = 0; i + 3 < n; ++i) { + if (d[i] == 0 && d[i + 1] == 0 && d[i + 2] == 1) return true; + if (i + 4 < n && d[i] == 0 && d[i + 1] == 0 && d[i + 2] == 0 && d[i + 3] == 1) return true; + } + return false; + } + + static std::vector ConvertLengthPrefixedToAnnexB(const uint8_t* d, size_t n) { + // Best-effort conversion for AVCC-style length-prefixed NAL units (assume 4-byte length). + std::vector out; + if (!d || n < 8) return out; + size_t pos = 0; + while (pos + 4 <= n) { + uint32_t len = (static_cast(d[pos]) << 24) | + (static_cast(d[pos + 1]) << 16) | + (static_cast(d[pos + 2]) << 8) | + (static_cast(d[pos + 3])); + pos += 4; + if (len == 0 || pos + len > n) break; + out.insert(out.end(), {0, 0, 0, 1}); + out.insert(out.end(), d + pos, d + pos + len); + pos += len; + } + return out; + } + + static std::vector> ExtractConfigFromAvcc(const std::vector& avcc) { + std::vector> out; + if (avcc.size() < 7 || avcc[0] != 1) return out; + size_t pos = 5; + uint8_t num_sps = avcc[pos++] & 0x1F; + for (uint8_t i = 0; i < num_sps; ++i) { + if (pos + 2 > avcc.size()) return {}; + uint16_t len = ReadBe16(&avcc[pos]); + pos += 2; + if (pos + len > avcc.size()) return {}; + std::vector one{0, 0, 0, 1}; + one.insert(one.end(), avcc.begin() + static_cast(pos), avcc.begin() + static_cast(pos + len)); + out.push_back(std::move(one)); + pos += len; + } + if (pos + 1 > avcc.size()) return {}; + uint8_t num_pps = avcc[pos++]; + for (uint8_t i = 0; i < num_pps; ++i) { + if (pos + 2 > avcc.size()) return {}; + uint16_t len = ReadBe16(&avcc[pos]); + pos += 2; + if (pos + len > avcc.size()) return {}; + std::vector one{0, 0, 0, 1}; + one.insert(one.end(), avcc.begin() + static_cast(pos), avcc.begin() + static_cast(pos + len)); + out.push_back(std::move(one)); + pos += len; + } + return out; + } + + static std::vector> ExtractConfigFromHvcc(const std::vector& hvcc) { + std::vector> out; + if (hvcc.size() < 23 || hvcc[0] != 1) return out; + size_t pos = 22; + if (pos >= hvcc.size()) return out; + uint8_t num_arrays = hvcc[pos++]; + for (uint8_t i = 0; i < num_arrays; ++i) { + if (pos + 3 > hvcc.size()) return {}; + uint8_t nal_type = hvcc[pos++] & 0x3F; + uint16_t num_nalus = ReadBe16(&hvcc[pos]); + pos += 2; + for (uint16_t j = 0; j < num_nalus; ++j) { + if (pos + 2 > hvcc.size()) return {}; + uint16_t len = ReadBe16(&hvcc[pos]); + pos += 2; + if (pos + len > hvcc.size()) return {}; + if (nal_type == 32 || nal_type == 33 || nal_type == 34) { + std::vector one{0, 0, 0, 1}; + one.insert(one.end(), hvcc.begin() + static_cast(pos), hvcc.begin() + static_cast(pos + len)); + out.push_back(std::move(one)); + } + pos += len; + } + } + return out; + } + + void FeedHeader(int64_t ts_ms, const std::vector& header, bool is_h265) { + if (!splitter_ || header.empty()) return; + cur_ts_ms_ = ts_ms; + + if (HasAnnexBStartCode(header.data(), header.size())) { + mk_h264_splitter_input_data(splitter_, reinterpret_cast(header.data()), static_cast(header.size())); + return; + } + + // AVCC/HVCC extradata: extract config NALs and feed as AnnexB. + std::vector> cfg = is_h265 ? ExtractConfigFromHvcc(header) : ExtractConfigFromAvcc(header); + for (const auto& nal : cfg) { + if (nal.empty()) continue; + mk_h264_splitter_input_data(splitter_, reinterpret_cast(nal.data()), static_cast(nal.size())); + } + } + + void FeedPacket(int64_t ts_ms, const std::vector& data, bool /*is_h265*/) { + if (!splitter_ || data.empty()) return; + cur_ts_ms_ = ts_ms; + if (HasAnnexBStartCode(data.data(), data.size())) { + mk_h264_splitter_input_data(splitter_, reinterpret_cast(data.data()), static_cast(data.size())); + return; + } + auto annexb = ConvertLengthPrefixedToAnnexB(data.data(), data.size()); + if (!annexb.empty()) { + mk_h264_splitter_input_data(splitter_, reinterpret_cast(annexb.data()), static_cast(annexb.size())); + } + } + + static void API_CALL OnSplitFrame(void* user_data, mk_h264_splitter /*splitter*/, const char* frame, int size) { + auto* self = static_cast(user_data); + if (!self || !self->media_ || !frame || size <= 0) return; + + const auto* d = reinterpret_cast(frame); + bool has_start_code = false; + if (size >= 3 && d[0] == 0 && d[1] == 0 && d[2] == 1) { + has_start_code = true; + } else if (size >= 4 && d[0] == 0 && d[1] == 0 && d[2] == 0 && d[3] == 1) { + has_start_code = true; + } + + if (!has_start_code) { + thread_local std::vector prefixed; + prefixed.resize(static_cast(size) + 4); + prefixed[0] = 0; + prefixed[1] = 0; + prefixed[2] = 0; + prefixed[3] = 1; + std::memcpy(prefixed.data() + 4, frame, static_cast(size)); + + if (self->is_h265_) { + mk_media_input_h265(self->media_, prefixed.data(), static_cast(prefixed.size()), self->cur_ts_ms_, self->cur_ts_ms_); + } else { + mk_media_input_h264(self->media_, prefixed.data(), static_cast(prefixed.size()), self->cur_ts_ms_, self->cur_ts_ms_); + } + return; + } + + if (self->is_h265_) { + mk_media_input_h265(self->media_, frame, size, self->cur_ts_ms_, self->cur_ts_ms_); + } else { + mk_media_input_h264(self->media_, frame, size, self->cur_ts_ms_, self->cur_ts_ms_); + } + } + static void EnsureZlmEnv() { static std::once_flag once; std::call_once(once, [] { @@ -782,6 +946,9 @@ private: } mk_media media_ = nullptr; + mk_h264_splitter splitter_ = nullptr; + bool is_h265_ = false; + uint64_t cur_ts_ms_ = 0; std::string app_; std::string stream_; int port_ = 0;