From 19ac4131016287f2b7e405d2823a899be3a027fe Mon Sep 17 00:00:00 2001 From: OSSRS-AI Date: Mon, 27 Oct 2025 10:58:52 -0400 Subject: [PATCH] AI: Add workflow utest to verify rtc2rtmp audio async. --- trunk/configure | 8 +- trunk/src/app/srs_app_dash.cpp | 2 +- trunk/src/app/srs_app_hls.cpp | 2 +- trunk/src/app/srs_app_rtc_source.cpp | 38 ++- trunk/src/app/srs_app_rtc_source.hpp | 59 ++++- trunk/src/kernel/srs_kernel_packet.cpp | 44 ++++ trunk/src/kernel/srs_kernel_packet.hpp | 14 + trunk/src/utest/srs_utest_ai24.cpp | 135 +++++----- trunk/src/utest/srs_utest_ai24.hpp | 1 - trunk/src/utest/srs_utest_manual_mock.cpp | 27 +- trunk/src/utest/srs_utest_manual_mock.hpp | 7 + .../src/utest/srs_utest_workflow_rtc2rtmp.cpp | 240 ++++++++++++++++++ .../src/utest/srs_utest_workflow_rtc2rtmp.hpp | 32 +++ .../src/utest/srs_utest_workflow_rtc_conn.hpp | 6 +- .../srs_utest_workflow_rtc_playstream.cpp | 5 - .../srs_utest_workflow_rtc_publishstream.cpp | 5 - 16 files changed, 521 insertions(+), 104 deletions(-) create mode 100644 trunk/src/utest/srs_utest_workflow_rtc2rtmp.cpp create mode 100644 trunk/src/utest/srs_utest_workflow_rtc2rtmp.hpp diff --git a/trunk/configure b/trunk/configure index d3872e08e..7349d075c 100755 --- a/trunk/configure +++ b/trunk/configure @@ -381,12 +381,14 @@ if [[ $SRS_UTEST == YES ]]; then "srs_utest_manual_protocol" "srs_utest_manual_protocol2" "srs_utest_manual_kernel2" "srs_utest_manual_st" "srs_utest_manual_fmp4" "srs_utest_manual_source_lock" "srs_utest_manual_stream_token" "srs_utest_manual_rtc_recv_track" "srs_utest_manual_st2" "srs_utest_manual_hevc_structs" "srs_utest_manual_coworkers" "srs_utest_manual_pithy_print" "srs_utest_manual_protocol3" - "srs_utest_manual_app" "srs_utest_manual_mock" "srs_utest_workflow_rtc_playstream" "srs_utest_workflow_rtc_publishstream" - "srs_utest_workflow_rtc_conn" "srs_utest_workflow_rtmp_conn" "srs_utest_workflow_srt_conn" "srs_utest_workflow_http_conn") + "srs_utest_manual_app" "srs_utest_manual_mock") + MODULE_FILES+=("srs_utest_workflow_rtc_playstream" "srs_utest_workflow_rtc_publishstream" + "srs_utest_workflow_rtc_conn" "srs_utest_workflow_rtmp_conn" "srs_utest_workflow_srt_conn" "srs_utest_workflow_http_conn" + "srs_utest_workflow_forward" "srs_utest_workflow_rtc2rtmp") MODULE_FILES+=("srs_utest_ai01" "srs_utest_ai02" "srs_utest_ai03" "srs_utest_ai04" "srs_utest_ai05" "srs_utest_ai06" "srs_utest_ai07" "srs_utest_ai08" "srs_utest_ai09" "srs_utest_ai10" "srs_utest_ai11" "srs_utest_ai12" "srs_utest_ai13" "srs_utest_ai14" "srs_utest_ai15" "srs_utest_ai16" "srs_utest_ai17" - "srs_utest_ai18" "srs_utest_ai19" "srs_utest_ai20" "srs_utest_ai24" "srs_utest_workflow_forward") + "srs_utest_ai18" "srs_utest_ai19" "srs_utest_ai20" "srs_utest_ai24") if [[ $SRS_GB28181 == YES ]]; then MODULE_FILES+=("srs_utest_manual_gb28181" "srs_utest_ai23") fi diff --git a/trunk/src/app/srs_app_dash.cpp b/trunk/src/app/srs_app_dash.cpp index 0e04e314d..349dbef8c 100644 --- a/trunk/src/app/srs_app_dash.cpp +++ b/trunk/src/app/srs_app_dash.cpp @@ -878,7 +878,7 @@ SrsDash::~SrsDash() void SrsDash::dispose() { - // We disabled the reload, so DASH will not be enabled by reloading. + // We disabled the reload, so DASH will not be enabled by reloading. // As a result, if DASH is disabled, we don't need to dispose. if (!enabled_) { return; diff --git a/trunk/src/app/srs_app_hls.cpp b/trunk/src/app/srs_app_hls.cpp index 0c8fef9cc..5394256d8 100644 --- a/trunk/src/app/srs_app_hls.cpp +++ b/trunk/src/app/srs_app_hls.cpp @@ -2549,7 +2549,7 @@ srs_error_t SrsHls::do_reload(int *reloading, int *reloaded, int *refreshed) void SrsHls::dispose() { - // We disabled the reload, so HLS will not be enabled by reloading. + // We disabled the reload, so HLS will not be enabled by reloading. // As a result, if HLS is disabled, we don't need to dispose. if (!enabled_) { return; diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp index 1b5cdc7c2..cccd80a54 100644 --- a/trunk/src/app/srs_app_rtc_source.cpp +++ b/trunk/src/app/srs_app_rtc_source.cpp @@ -1377,6 +1377,14 @@ srs_error_t SrsRtcRtpBuilder::consume_packets(vector &pkts) return err; } +ISrsRtcFrameBuilderVideoPacketCache::ISrsRtcFrameBuilderVideoPacketCache() +{ +} + +ISrsRtcFrameBuilderVideoPacketCache::~ISrsRtcFrameBuilderVideoPacketCache() +{ +} + SrsRtcFrameBuilderVideoPacketCache::SrsRtcFrameBuilderVideoPacketCache() { memset(cache_pkts_, 0, sizeof(cache_pkts_)); @@ -1536,7 +1544,15 @@ bool SrsRtcFrameBuilderVideoPacketCache::check_frame_complete(const uint16_t sta return nn_fu_start == nn_fu_end; } -SrsRtcFrameBuilderVideoFrameDetector::SrsRtcFrameBuilderVideoFrameDetector(SrsRtcFrameBuilderVideoPacketCache *cache) +ISrsRtcFrameBuilderVideoFrameDetector::ISrsRtcFrameBuilderVideoFrameDetector() +{ +} + +ISrsRtcFrameBuilderVideoFrameDetector::~ISrsRtcFrameBuilderVideoFrameDetector() +{ +} + +SrsRtcFrameBuilderVideoFrameDetector::SrsRtcFrameBuilderVideoFrameDetector(ISrsRtcFrameBuilderVideoPacketCache *cache) { video_cache_ = cache; header_sn_ = 0; @@ -1640,6 +1656,14 @@ bool SrsRtcFrameBuilderVideoFrameDetector::is_lost_sn(uint16_t received) return lost_sn_ == received; } +ISrsRtcFrameBuilderAudioPacketCache::ISrsRtcFrameBuilderAudioPacketCache() +{ +} + +ISrsRtcFrameBuilderAudioPacketCache::~ISrsRtcFrameBuilderAudioPacketCache() +{ +} + SrsRtcFrameBuilderAudioPacketCache::SrsRtcFrameBuilderAudioPacketCache() { last_audio_seq_num_ = 0; @@ -3115,7 +3139,7 @@ SrsRtcRecvTrack::SrsRtcRecvTrack(ISrsRtcPacketReceiver *receiver, SrsRtcTrackDes if (track_desc_->media_) { rate_ = static_cast(track_desc_->media_->sample_) / 1000.0; srs_trace("RTC: Init %s track, ssrc=%u, rate from SDP=%.0f (RTP units per ms, will be updated after 2nd SR)", - track_desc_->type_.c_str(), track_desc_->ssrc_, rate_); + track_desc_->type_.c_str(), track_desc_->ssrc_, rate_); } last_sender_report_sys_time_ = 0; @@ -3177,11 +3201,11 @@ void SrsRtcRecvTrack::update_send_report_time(const SrsNtp &ntp, uint32_t rtp_ti if (rate > 0) { if (rate_ != rate) { srs_warn("RTC: SR update %s, ssrc=%u, ntp_ms=%u->%u (delta=%.0fms), rtp_time=%u->%u (delta=%.0f), rate %.0f->%.0f", - track_desc_->type_.c_str(), track_desc_->ssrc_, - last_sender_report_ntp1_.system_ms_, last_sender_report_ntp_.system_ms_, sys_time_elapsed, - (uint32_t)last_sender_report_rtp_time1_, (uint32_t)last_sender_report_rtp_time_, rtp_time_elpased, - rate_, rate); - } + track_desc_->type_.c_str(), track_desc_->ssrc_, + last_sender_report_ntp1_.system_ms_, last_sender_report_ntp_.system_ms_, sys_time_elapsed, + (uint32_t)last_sender_report_rtp_time1_, (uint32_t)last_sender_report_rtp_time_, rtp_time_elpased, + rate_, rate); + } rate_ = rate; } } diff --git a/trunk/src/app/srs_app_rtc_source.hpp b/trunk/src/app/srs_app_rtc_source.hpp index 3021562ee..9f5836d3a 100644 --- a/trunk/src/app/srs_app_rtc_source.hpp +++ b/trunk/src/app/srs_app_rtc_source.hpp @@ -419,9 +419,25 @@ SRS_DECLARE_PRIVATE: // clang-format on srs_error_t consume_packets(std::vector &pkts); }; +// Video packet cache interface +class ISrsRtcFrameBuilderVideoPacketCache +{ +public: + ISrsRtcFrameBuilderVideoPacketCache(); + virtual ~ISrsRtcFrameBuilderVideoPacketCache(); + +public: + virtual SrsRtpPacket *get_packet(uint16_t sequence_number) = 0; + virtual void store_packet(SrsRtpPacket *pkt) = 0; + virtual void clear_all() = 0; + virtual SrsRtpPacket *take_packet(uint16_t sequence_number) = 0; + virtual int32_t find_next_lost_sn(uint16_t current_sn, uint16_t header_sn, uint16_t &end_sn) = 0; + virtual bool check_frame_complete(const uint16_t start, const uint16_t end) = 0; +}; + // Video packet cache for RTP packet management // TODO: Maybe should use SrsRtpRingBuffer? -class SrsRtcFrameBuilderVideoPacketCache +class SrsRtcFrameBuilderVideoPacketCache : public ISrsRtcFrameBuilderVideoPacketCache { // clang-format off SRS_DECLARE_PRIVATE: // clang-format on @@ -462,18 +478,33 @@ SRS_DECLARE_PRIVATE: // clang-format on } }; +// Video frame detector interface +class ISrsRtcFrameBuilderVideoFrameDetector +{ +public: + ISrsRtcFrameBuilderVideoFrameDetector(); + virtual ~ISrsRtcFrameBuilderVideoFrameDetector(); + +public: + virtual void on_keyframe_start(SrsRtpPacket *pkt) = 0; + virtual srs_error_t detect_frame(uint16_t received, uint16_t &frame_start, uint16_t &frame_end, bool &frame_ready) = 0; + virtual srs_error_t detect_next_frame(uint16_t next_head, uint16_t &next_start, uint16_t &next_end, bool &next_ready) = 0; + virtual void on_keyframe_detached() = 0; + virtual bool is_lost_sn(uint16_t received) = 0; +}; + // Video frame detector for managing frame boundaries and packet loss detection -class SrsRtcFrameBuilderVideoFrameDetector +class SrsRtcFrameBuilderVideoFrameDetector : public ISrsRtcFrameBuilderVideoFrameDetector { // clang-format off SRS_DECLARE_PRIVATE: // clang-format on - SrsRtcFrameBuilderVideoPacketCache *video_cache_; + ISrsRtcFrameBuilderVideoPacketCache *video_cache_; uint16_t header_sn_; uint16_t lost_sn_; int64_t rtp_key_frame_ts_; public: - SrsRtcFrameBuilderVideoFrameDetector(SrsRtcFrameBuilderVideoPacketCache *cache); + SrsRtcFrameBuilderVideoFrameDetector(ISrsRtcFrameBuilderVideoPacketCache *cache); virtual ~SrsRtcFrameBuilderVideoFrameDetector(); public: @@ -484,8 +515,20 @@ public: bool is_lost_sn(uint16_t received); }; +// Audio packet cache interface +class ISrsRtcFrameBuilderAudioPacketCache +{ +public: + ISrsRtcFrameBuilderAudioPacketCache(); + virtual ~ISrsRtcFrameBuilderAudioPacketCache(); + +public: + virtual srs_error_t process_packet(SrsRtpPacket *src, std::vector &ready_packets) = 0; + virtual void clear_all() = 0; +}; + // Audio packet cache for RTP packet jitter buffer management -class SrsRtcFrameBuilderAudioPacketCache +class SrsRtcFrameBuilderAudioPacketCache : public ISrsRtcFrameBuilderAudioPacketCache { // clang-format off SRS_DECLARE_PRIVATE: // clang-format on @@ -530,9 +573,9 @@ SRS_DECLARE_PRIVATE: // clang-format on // clang-format off SRS_DECLARE_PRIVATE: // clang-format on - SrsRtcFrameBuilderAudioPacketCache *audio_cache_; - SrsRtcFrameBuilderVideoPacketCache *video_cache_; - SrsRtcFrameBuilderVideoFrameDetector *frame_detector_; + ISrsRtcFrameBuilderAudioPacketCache *audio_cache_; + ISrsRtcFrameBuilderVideoPacketCache *video_cache_; + ISrsRtcFrameBuilderVideoFrameDetector *frame_detector_; // clang-format off SRS_DECLARE_PRIVATE: // clang-format on diff --git a/trunk/src/kernel/srs_kernel_packet.cpp b/trunk/src/kernel/srs_kernel_packet.cpp index 790306e15..dfc8481b0 100644 --- a/trunk/src/kernel/srs_kernel_packet.cpp +++ b/trunk/src/kernel/srs_kernel_packet.cpp @@ -142,6 +142,25 @@ srs_error_t SrsParsedPacket::add_sample(char *bytes, int size) return err; } +SrsParsedPacket *SrsParsedPacket::copy() +{ + SrsParsedPacket *p = new SrsParsedPacket(); + do_copy(p); + return p; +} + +void SrsParsedPacket::do_copy(SrsParsedPacket *p) +{ + p->codec_ = codec_; + p->nb_samples_ = nb_samples_; + for (int i = 0; i < nb_samples_; i++) { + p->samples_[i].size_ = samples_[i].size_; + p->samples_[i].bytes_ = samples_[i].bytes_; + } + p->dts_ = dts_; + p->cts_ = cts_; +} + SrsParsedAudioPacket::SrsParsedAudioPacket() { aac_packet_type_ = SrsAudioAacFrameTraitForbidden; @@ -156,6 +175,16 @@ SrsAudioCodecConfig *SrsParsedAudioPacket::acodec() return (SrsAudioCodecConfig *)codec_; } +SrsParsedAudioPacket *SrsParsedAudioPacket::copy() +{ + SrsParsedAudioPacket *p = new SrsParsedAudioPacket(); + + do_copy(p); + p->aac_packet_type_ = aac_packet_type_; + + return p; +} + SrsParsedVideoPacket::SrsParsedVideoPacket() { frame_type_ = SrsVideoAvcFrameTypeForbidden; @@ -213,6 +242,21 @@ srs_error_t SrsParsedVideoPacket::add_sample(char *bytes, int size) return err; } +SrsParsedVideoPacket *SrsParsedVideoPacket::copy() +{ + SrsParsedVideoPacket *p = new SrsParsedVideoPacket(); + + do_copy(p); + p->frame_type_ = frame_type_; + p->avc_packet_type_ = avc_packet_type_; + p->has_idr_ = has_idr_; + p->has_aud_ = has_aud_; + p->has_sps_pps_ = has_sps_pps_; + p->first_nalu_type_ = first_nalu_type_; + + return p; +} + SrsVideoCodecConfig *SrsParsedVideoPacket::vcodec() { return (SrsVideoCodecConfig *)codec_; diff --git a/trunk/src/kernel/srs_kernel_packet.hpp b/trunk/src/kernel/srs_kernel_packet.hpp index 3ac7609cb..5f9e48b44 100644 --- a/trunk/src/kernel/srs_kernel_packet.hpp +++ b/trunk/src/kernel/srs_kernel_packet.hpp @@ -109,6 +109,12 @@ public: virtual srs_error_t initialize(SrsCodecConfig *c); // Add a sample to frame. virtual srs_error_t add_sample(char *bytes, int size); + +// clang-format off +SRS_DECLARE_PRIVATE: // clang-format on + // Copy the packet. + virtual SrsParsedPacket *copy(); + virtual void do_copy(SrsParsedPacket *p); }; // A parsed audio packet, besides a frame, contains the audio frame info, such as frame type. @@ -123,6 +129,10 @@ public: public: virtual SrsAudioCodecConfig *acodec(); + +// clang-format off +SRS_DECLARE_PRIVATE: // clang-format on + virtual SrsParsedAudioPacket *copy(); }; // A parsed video packet, besides a frame, contains the video frame info, such as frame type. @@ -151,6 +161,10 @@ public: // Add the sample without ANNEXB or IBMF header, or RAW AAC or MP3 data. virtual srs_error_t add_sample(char *bytes, int size); +// clang-format off +SRS_DECLARE_PRIVATE: // clang-format on + virtual SrsParsedVideoPacket *copy(); + public: virtual SrsVideoCodecConfig *vcodec(); diff --git a/trunk/src/utest/srs_utest_ai24.cpp b/trunk/src/utest/srs_utest_ai24.cpp index 2d8537b76..a0423e187 100644 --- a/trunk/src/utest/srs_utest_ai24.cpp +++ b/trunk/src/utest/srs_utest_ai24.cpp @@ -15,47 +15,51 @@ using namespace std; // Mock class to access protected members of SrsRtcRecvTrack class MockSrsRtcRecvTrackForAVSync : public SrsRtcRecvTrack { -SRS_DECLARE_PRIVATE: + SRS_DECLARE_PRIVATE: static SrsRtcTrackDescription* create_track_desc(const string& type, uint32_t ssrc, int sample_rate) { - SrsRtcTrackDescription* desc = new SrsRtcTrackDescription(); + SrsRtcTrackDescription *desc = new SrsRtcTrackDescription(); desc->type_ = type; desc->id_ = "test_track"; desc->ssrc_ = ssrc; desc->is_active_ = true; - + // Create media description with sample rate desc->media_ = new SrsAudioPayload(); desc->media_->sample_ = sample_rate; - + return desc; } public: - MockSrsRtcRecvTrackForAVSync(const string& type, uint32_t ssrc, int sample_rate, bool is_audio) + MockSrsRtcRecvTrackForAVSync(const string &type, uint32_t ssrc, int sample_rate, bool is_audio) : SrsRtcRecvTrack(NULL, create_track_desc(type, ssrc, sample_rate), is_audio) { } // Expose protected methods for testing double get_rate() const { return rate_; } - + void set_rate(double rate) { rate_ = rate; } - - int64_t test_cal_avsync_time(uint32_t rtp_time) { + + int64_t test_cal_avsync_time(uint32_t rtp_time) + { return cal_avsync_time(rtp_time); } - - void test_update_send_report_time(const SrsNtp& ntp, uint32_t rtp_time) { + + void test_update_send_report_time(const SrsNtp &ntp, uint32_t rtp_time) + { update_send_report_time(ntp, rtp_time); } // Implement pure virtual methods - virtual srs_error_t on_rtp(SrsSharedPtr& source, SrsRtpPacket* pkt) { + virtual srs_error_t on_rtp(SrsSharedPtr &source, SrsRtpPacket *pkt) + { return srs_success; } - - virtual srs_error_t check_send_nacks() { + + virtual srs_error_t check_send_nacks() + { return srs_success; } }; @@ -64,7 +68,7 @@ public: VOID TEST(RtcAVSyncTest, AudioRateInitFromSDP) { MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true); - + // Rate should be initialized to 48 (48000 Hz / 1000 = 48 RTP units per ms) EXPECT_DOUBLE_EQ(48.0, track.get_rate()); } @@ -73,7 +77,7 @@ VOID TEST(RtcAVSyncTest, AudioRateInitFromSDP) VOID TEST(RtcAVSyncTest, VideoRateInitFromSDP) { MockSrsRtcRecvTrackForAVSync track("video", 67890, 90000, false); - + // Rate should be initialized to 90 (90000 Hz / 1000 = 90 RTP units per ms) EXPECT_DOUBLE_EQ(90.0, track.get_rate()); } @@ -82,13 +86,13 @@ VOID TEST(RtcAVSyncTest, VideoRateInitFromSDP) VOID TEST(RtcAVSyncTest, CalAVSyncTimeWithSDPRate) { MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true); - + // Simulate first SR received SrsNtp ntp; - ntp.system_ms_ = 1000; // 1000 ms - uint32_t rtp_time = 48000; // 48000 RTP units + ntp.system_ms_ = 1000; // 1000 ms + uint32_t rtp_time = 48000; // 48000 RTP units track.test_update_send_report_time(ntp, rtp_time); - + // Calculate avsync time for a later RTP packet // RTP time: 48000 + 4800 = 52800 (100ms later at 48kHz) // Expected avsync_time: 1000 + (52800 - 48000) / 48 = 1000 + 100 = 1100 ms @@ -100,10 +104,10 @@ VOID TEST(RtcAVSyncTest, CalAVSyncTimeWithSDPRate) VOID TEST(RtcAVSyncTest, CalAVSyncTimeWithZeroRate) { MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true); - + // Manually set rate to 0 track.set_rate(0.0); - + // Should return -1 when rate is too small int64_t avsync_time = track.test_cal_avsync_time(1000); EXPECT_EQ(-1, avsync_time); @@ -113,25 +117,25 @@ VOID TEST(RtcAVSyncTest, CalAVSyncTimeWithZeroRate) VOID TEST(RtcAVSyncTest, AudioRateUpdateAfter2ndSR) { MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true); - + // Initial rate from SDP EXPECT_DOUBLE_EQ(48.0, track.get_rate()); - + // First SR SrsNtp ntp1; ntp1.system_ms_ = 1000; uint32_t rtp_time1 = 48000; track.test_update_send_report_time(ntp1, rtp_time1); - + // Rate should still be 48 (from SDP) EXPECT_DOUBLE_EQ(48.0, track.get_rate()); - + // Second SR (20ms later, RTP increased by 960) SrsNtp ntp2; - ntp2.system_ms_ = 1020; // 20ms later - uint32_t rtp_time2 = 48960; // 960 RTP units later (48 * 20) + ntp2.system_ms_ = 1020; // 20ms later + uint32_t rtp_time2 = 48960; // 960 RTP units later (48 * 20) track.test_update_send_report_time(ntp2, rtp_time2); - + // Rate should be updated to calculated value: 960 / 20 = 48 EXPECT_DOUBLE_EQ(48.0, track.get_rate()); } @@ -140,25 +144,25 @@ VOID TEST(RtcAVSyncTest, AudioRateUpdateAfter2ndSR) VOID TEST(RtcAVSyncTest, VideoRateUpdateAfter2ndSR) { MockSrsRtcRecvTrackForAVSync track("video", 67890, 90000, false); - + // Initial rate from SDP EXPECT_DOUBLE_EQ(90.0, track.get_rate()); - + // First SR SrsNtp ntp1; ntp1.system_ms_ = 2000; uint32_t rtp_time1 = 180000; track.test_update_send_report_time(ntp1, rtp_time1); - + // Rate should still be 90 (from SDP) EXPECT_DOUBLE_EQ(90.0, track.get_rate()); - + // Second SR (100ms later, RTP increased by 9000) SrsNtp ntp2; - ntp2.system_ms_ = 2100; // 100ms later - uint32_t rtp_time2 = 189000; // 9000 RTP units later (90 * 100) + ntp2.system_ms_ = 2100; // 100ms later + uint32_t rtp_time2 = 189000; // 9000 RTP units later (90 * 100) track.test_update_send_report_time(ntp2, rtp_time2); - + // Rate should be updated to calculated value: 9000 / 100 = 90 EXPECT_DOUBLE_EQ(90.0, track.get_rate()); } @@ -167,24 +171,24 @@ VOID TEST(RtcAVSyncTest, VideoRateUpdateAfter2ndSR) VOID TEST(RtcAVSyncTest, RateUpdateWithClockDrift) { MockSrsRtcRecvTrackForAVSync track("video", 67890, 90000, false); - + // Initial rate from SDP EXPECT_DOUBLE_EQ(90.0, track.get_rate()); - + // First SR SrsNtp ntp1; ntp1.system_ms_ = 1000; uint32_t rtp_time1 = 90000; track.test_update_send_report_time(ntp1, rtp_time1); - + // Second SR with slight clock drift // Expected: 100ms -> 9000 RTP units // Actual: 100ms -> 9010 RTP units (slight drift) SrsNtp ntp2; ntp2.system_ms_ = 1100; - uint32_t rtp_time2 = 99010; // Slightly more than expected + uint32_t rtp_time2 = 99010; // Slightly more than expected track.test_update_send_report_time(ntp2, rtp_time2); - + // Rate should be updated to: round(9010 / 100) = 90 EXPECT_DOUBLE_EQ(90.0, track.get_rate()); } @@ -193,19 +197,19 @@ VOID TEST(RtcAVSyncTest, RateUpdateWithClockDrift) VOID TEST(RtcAVSyncTest, RateUpdateWithLargeInterval) { MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true); - + // First SR SrsNtp ntp1; ntp1.system_ms_ = 5000; uint32_t rtp_time1 = 240000; track.test_update_send_report_time(ntp1, rtp_time1); - + // Second SR (1000ms later) SrsNtp ntp2; ntp2.system_ms_ = 6000; - uint32_t rtp_time2 = 288000; // 48000 RTP units later (48 * 1000) + uint32_t rtp_time2 = 288000; // 48000 RTP units later (48 * 1000) track.test_update_send_report_time(ntp2, rtp_time2); - + // Rate should be: 48000 / 1000 = 48 EXPECT_DOUBLE_EQ(48.0, track.get_rate()); } @@ -214,19 +218,19 @@ VOID TEST(RtcAVSyncTest, RateUpdateWithLargeInterval) VOID TEST(RtcAVSyncTest, CalAVSyncTimeAfter2ndSR) { MockSrsRtcRecvTrackForAVSync track("video", 67890, 90000, false); - + // First SR SrsNtp ntp1; ntp1.system_ms_ = 1000; uint32_t rtp_time1 = 90000; track.test_update_send_report_time(ntp1, rtp_time1); - + // Second SR SrsNtp ntp2; ntp2.system_ms_ = 1100; uint32_t rtp_time2 = 99000; track.test_update_send_report_time(ntp2, rtp_time2); - + // Now calculate avsync time for a packet // RTP time: 99000 + 4500 = 103500 (50ms later at 90kHz) // Expected: 1100 + (103500 - 99000) / 90 = 1100 + 50 = 1150 ms @@ -238,39 +242,39 @@ VOID TEST(RtcAVSyncTest, CalAVSyncTimeAfter2ndSR) VOID TEST(RtcAVSyncTest, ImmediateAVSyncAvailability) { MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true); - + // Before any SR, rate should be available from SDP EXPECT_DOUBLE_EQ(48.0, track.get_rate()); - + // First SR received SrsNtp ntp1; ntp1.system_ms_ = 1000; uint32_t rtp_time1 = 48000; track.test_update_send_report_time(ntp1, rtp_time1); - + // Should be able to calculate avsync_time immediately (not -1) - int64_t avsync_time = track.test_cal_avsync_time(48480); // 10ms later - EXPECT_GT(avsync_time, 0); // Should be > 0, not -1 - EXPECT_EQ(1010, avsync_time); // Should be 1000 + 10 = 1010 + int64_t avsync_time = track.test_cal_avsync_time(48480); // 10ms later + EXPECT_GT(avsync_time, 0); // Should be > 0, not -1 + EXPECT_EQ(1010, avsync_time); // Should be 1000 + 10 = 1010 } // Test: RTP timestamp wraparound handling VOID TEST(RtcAVSyncTest, RTPTimestampWraparound) { MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true); - + // First SR near wraparound SrsNtp ntp1; ntp1.system_ms_ = 1000; - uint32_t rtp_time1 = 0xFFFFF000; // Near max uint32_t + uint32_t rtp_time1 = 0xFFFFF000; // Near max uint32_t track.test_update_send_report_time(ntp1, rtp_time1); - + // Second SR after wraparound SrsNtp ntp2; - ntp2.system_ms_ = 1020; // 20ms later - uint32_t rtp_time2 = 0x000003C0; // Wrapped around, 960 units after wraparound + ntp2.system_ms_ = 1020; // 20ms later + uint32_t rtp_time2 = 0x000003C0; // Wrapped around, 960 units after wraparound track.test_update_send_report_time(ntp2, rtp_time2); - + // Note: Current implementation may not handle wraparound correctly // This test documents the current behavior // Rate calculation: (0x000003C0 - 0xFFFFF000) will underflow @@ -281,22 +285,21 @@ VOID TEST(RtcAVSyncTest, RTPTimestampWraparound) VOID TEST(RtcAVSyncTest, ZeroTimeElapsedBetweenSRs) { MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true); - + // First SR SrsNtp ntp1; ntp1.system_ms_ = 1000; uint32_t rtp_time1 = 48000; track.test_update_send_report_time(ntp1, rtp_time1); - + double rate_before = track.get_rate(); - + // Second SR with same timestamp (0ms elapsed) SrsNtp ntp2; - ntp2.system_ms_ = 1000; // Same time - uint32_t rtp_time2 = 48000; // Same RTP time + ntp2.system_ms_ = 1000; // Same time + uint32_t rtp_time2 = 48000; // Same RTP time track.test_update_send_report_time(ntp2, rtp_time2); - + // Rate should remain unchanged (SDP rate) EXPECT_DOUBLE_EQ(rate_before, track.get_rate()); } - diff --git a/trunk/src/utest/srs_utest_ai24.hpp b/trunk/src/utest/srs_utest_ai24.hpp index b1854ca3a..1a34c0082 100644 --- a/trunk/src/utest/srs_utest_ai24.hpp +++ b/trunk/src/utest/srs_utest_ai24.hpp @@ -9,4 +9,3 @@ #include #endif - diff --git a/trunk/src/utest/srs_utest_manual_mock.cpp b/trunk/src/utest/srs_utest_manual_mock.cpp index 858b21f88..a52ac05e1 100644 --- a/trunk/src/utest/srs_utest_manual_mock.cpp +++ b/trunk/src/utest/srs_utest_manual_mock.cpp @@ -180,6 +180,10 @@ MockRtcTrackDescriptionFactory::MockRtcTrackDescriptionFactory() audio_ssrc_ = 12345; video_ssrc_ = 67890; screen_ssrc_ = 98765; + + audio_pt_ = 111; + video_pt_ = 96; + screen_pt_ = 97; } MockRtcTrackDescriptionFactory::~MockRtcTrackDescriptionFactory() @@ -227,12 +231,17 @@ SrsRtcTrackDescription *MockRtcTrackDescriptionFactory::create_audio_track(uint3 audio_desc->is_active_ = true; audio_desc->direction_ = "sendrecv"; audio_desc->mid_ = mid; - audio_desc->media_ = new SrsAudioPayload(111, "opus", 48000, 2); + audio_desc->media_ = new SrsAudioPayload(audio_pt_, "opus", 48000, 2); return audio_desc; } SrsRtcTrackDescription *MockRtcTrackDescriptionFactory::create_video_track(uint32_t ssrc, std::string id, std::string mid) { + uint8_t pt = video_pt_; + if (ssrc == screen_ssrc_) { + pt = screen_pt_; + } + SrsRtcTrackDescription *video_desc = new SrsRtcTrackDescription(); video_desc->type_ = "video"; video_desc->ssrc_ = ssrc; @@ -240,7 +249,7 @@ SrsRtcTrackDescription *MockRtcTrackDescriptionFactory::create_video_track(uint3 video_desc->is_active_ = true; video_desc->direction_ = "sendrecv"; video_desc->mid_ = mid; - video_desc->media_ = new SrsVideoPayload(96, "H264", 90000); + video_desc->media_ = new SrsVideoPayload(pt, "H264", 90000); return video_desc; } @@ -1160,6 +1169,7 @@ MockLiveSource::MockLiveSource() on_audio_count_ = 0; on_video_count_ = 0; on_dump_packets_count_ = 0; + on_frame_count_ = 0; } MockLiveSource::~MockLiveSource() @@ -1196,16 +1206,25 @@ srs_error_t MockLiveSource::consumer_dumps(ISrsLiveConsumer *consumer, bool ds, srs_error_t MockLiveSource::on_audio(SrsRtmpCommonMessage *audio) { - on_audio_count_++; return SrsLiveSource::on_audio(audio); } srs_error_t MockLiveSource::on_video(SrsRtmpCommonMessage *video) { - on_video_count_++; return SrsLiveSource::on_video(video); } +srs_error_t MockLiveSource::on_frame(SrsMediaPacket *msg) +{ + on_frame_count_++; + if (msg->is_audio()) { + on_audio_count_++; + } else if (msg->is_video()) { + on_video_count_++; + } + return SrsLiveSource::on_frame(msg); +} + // Mock SRT source implementation MockSrtSource::MockSrtSource() { diff --git a/trunk/src/utest/srs_utest_manual_mock.hpp b/trunk/src/utest/srs_utest_manual_mock.hpp index 0fbd96d9a..d99e2b5c8 100644 --- a/trunk/src/utest/srs_utest_manual_mock.hpp +++ b/trunk/src/utest/srs_utest_manual_mock.hpp @@ -110,6 +110,11 @@ public: uint32_t video_ssrc_; uint32_t screen_ssrc_; +public: + uint8_t audio_pt_; + uint8_t video_pt_; + uint8_t screen_pt_; + public: // Create a map of track descriptions with audio and video tracks (for play stream) std::map create_audio_video_tracks(); @@ -634,6 +639,7 @@ public: int on_audio_count_; int on_video_count_; int on_dump_packets_count_; + int on_frame_count_; public: MockLiveSource(); @@ -647,6 +653,7 @@ public: public: virtual srs_error_t on_audio(SrsRtmpCommonMessage *audio); virtual srs_error_t on_video(SrsRtmpCommonMessage *video); + virtual srs_error_t on_frame(SrsMediaPacket *msg); }; // Mock SRT source for testing SrsRtcPublishStream diff --git a/trunk/src/utest/srs_utest_workflow_rtc2rtmp.cpp b/trunk/src/utest/srs_utest_workflow_rtc2rtmp.cpp new file mode 100644 index 000000000..6e34ed6e5 --- /dev/null +++ b/trunk/src/utest/srs_utest_workflow_rtc2rtmp.cpp @@ -0,0 +1,240 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2013-2025 Winlin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Create a mock audio cache ISrsRtcFrameBuilderAudioPacketCache +class MockAudioCache : public ISrsRtcFrameBuilderAudioPacketCache +{ +public: + int process_packet_count_; + +public: + MockAudioCache(); + virtual ~MockAudioCache(); + +public: + virtual srs_error_t process_packet(SrsRtpPacket *src, std::vector &ready_packets); + virtual void clear_all(); +}; + +MockAudioCache::MockAudioCache() +{ + process_packet_count_ = 0; +} + +MockAudioCache::~MockAudioCache() +{ +} + +srs_error_t MockAudioCache::process_packet(SrsRtpPacket *src, std::vector &ready_packets) +{ + process_packet_count_++; + + // Copy the packet. + SrsRtpPacket *copy = src->copy(); + ready_packets.push_back(copy); + + return srs_success; +} + +void MockAudioCache::clear_all() +{ +} + +// Mock the audio transcoder ISrsAudioTranscoder. +class MockAudioTranscoderForRtc2Rtmp : public ISrsAudioTranscoder +{ +public: + int transcode_count_; + std::vector output_packets_; + std::string aac_header_; + +public: + MockAudioTranscoderForRtc2Rtmp(); + virtual ~MockAudioTranscoderForRtc2Rtmp(); + +public: + virtual srs_error_t initialize(SrsAudioCodecId from, SrsAudioCodecId to, int channels, int sample_rate, int bit_rate); + virtual srs_error_t transcode(SrsParsedAudioPacket *in, std::vector &outs); + virtual void free_frames(std::vector &frames); + virtual void aac_codec_header(uint8_t **data, int *len); +}; + +MockAudioTranscoderForRtc2Rtmp::MockAudioTranscoderForRtc2Rtmp() +{ + transcode_count_ = 0; +} + +MockAudioTranscoderForRtc2Rtmp::~MockAudioTranscoderForRtc2Rtmp() +{ +} + +srs_error_t MockAudioTranscoderForRtc2Rtmp::initialize(SrsAudioCodecId from, SrsAudioCodecId to, int channels, int sample_rate, int bit_rate) +{ + return srs_success; +} + +srs_error_t MockAudioTranscoderForRtc2Rtmp::transcode(SrsParsedAudioPacket *in, std::vector &outs) +{ + transcode_count_++; + + SrsParsedAudioPacket *out = in->copy(); + output_packets_.push_back(out); + outs.push_back(out); + + return srs_success; +} + +void MockAudioTranscoderForRtc2Rtmp::free_frames(std::vector &frames) +{ +} + +void MockAudioTranscoderForRtc2Rtmp::aac_codec_header(uint8_t **data, int *len) +{ + int size = aac_header_.size(); + uint8_t *copy = new uint8_t[size]; + memcpy(copy, aac_header_.data(), size); + *data = copy; + *len = size; +} + +// This test is used to verify the basic workflow of the RTC connection. +// It's finished with the help of AI, but each step is manually designed +// and verified. So this is not dominated by AI, but by humanbeing. +VOID TEST(BasicWorkflowRtc2RtmpTest, ManuallyVerifyTypicalScenario) +{ + srs_error_t err; + + // Create mock objects for dependencies + SrsUniquePtr mock_config(new MockAppConfig()); + SrsUniquePtr mock_rtc_sources(new MockRtcSourceManager()); + SrsUniquePtr mock_stat(new MockAppStatistic()); + SrsUniquePtr mock_request(new MockRtcAsyncCallRequest("test.vhost", "live", "stream1")); + SrsUniquePtr mock_exec(new MockRtcAsyncTaskExecutor()); + SrsUniquePtr mock_expire(new MockExpire()); + SrsUniquePtr mock_receiver(new MockRtcPacketReceiver()); + SrsUniquePtr track_factory(new MockRtcTrackDescriptionFactory()); + SrsUniquePtr mock_sources(new MockLiveSourceManager()); + MockAudioCache *mock_audio_cache = new MockAudioCache(); + MockAudioTranscoderForRtc2Rtmp *mock_audio_transcoder = new MockAudioTranscoderForRtc2Rtmp(); + + mock_audio_transcoder->aac_header_ = std::string("\xAF\x00\x12\x10", 4); // AAC sequence header. + mock_config->rtc_to_rtmp_ = true; + + // Create RTC publish stream - use real pli_worker_ + SrsContextId cid; + cid.set_value("test-rtc2rtmp-workflow-typical-scenario"); + SrsUniquePtr publish_stream(new SrsRtcPublishStream(mock_exec.get(), mock_expire.get(), mock_receiver.get(), cid)); + + // Mock the publish stream object + if (true) { + // Inject mock dependencies + publish_stream->config_ = mock_config.get(); + publish_stream->rtc_sources_ = mock_rtc_sources.get(); + publish_stream->live_sources_ = mock_sources.get(); + publish_stream->stat_ = mock_stat.get(); + } + + // Initialize publish stream, rtc2rtmp bridge should be created + SrsRtcBridge *bridge = NULL; + SrsLiveSource *live_source = NULL; + SrsRtcFrameBuilder *frame_builder = NULL; + if (true) { + SrsUniquePtr stream_desc(track_factory->create_stream_description()); + + // Initialize the publish stream (it will take ownership of track descriptions) + HELPER_EXPECT_SUCCESS(publish_stream->initialize(mock_request.get(), stream_desc.get())); + + // Check the tracks, should be one audio track + EXPECT_EQ(publish_stream->audio_tracks_.size(), 1); + // Check the tracks, should be one video track + EXPECT_EQ(publish_stream->video_tracks_.size(), 1); + + // source bridge should be created + bridge = dynamic_cast(publish_stream->source_->rtc_bridge_); + EXPECT_TRUE(bridge != NULL); + + live_source = bridge->rtmp_target_.get(); + EXPECT_TRUE(live_source != NULL); + + frame_builder = bridge->frame_builder_; + EXPECT_TRUE(frame_builder != NULL); + } + + // Start the publish stream. + if (true) { + // Test: First call to start() should succeed + HELPER_EXPECT_SUCCESS(publish_stream->start()); + + // Verify is_sender_started_ flag is set + EXPECT_TRUE(publish_stream->is_sender_started_); + + // When starting the publish stream, the frame builder should be recreated + EXPECT_TRUE(frame_builder != bridge->frame_builder_); + frame_builder = bridge->frame_builder_; + EXPECT_TRUE(frame_builder != NULL); + + // Mock the frame builder object + srs_freep(frame_builder->audio_cache_); + frame_builder->audio_cache_ = mock_audio_cache; + srs_freep(frame_builder->audio_transcoder_); + frame_builder->audio_transcoder_ = mock_audio_transcoder; + } + + // Got a RTP audio packet. + MockLiveSource *mock_source = dynamic_cast(mock_sources->mock_source_.get()); + if (true) { + SrsRtpPacket pkt; + pkt.header_.set_ssrc(track_factory->audio_ssrc_); + pkt.header_.set_sequence(100); + pkt.header_.set_timestamp(1000); + pkt.header_.set_payload_type(track_factory->audio_pt_); + + SrsUniquePtr data(new char[1500]); + SrsBuffer buf(data.get(), 1500); + HELPER_EXPECT_SUCCESS(pkt.encode(&buf)); + + HELPER_EXPECT_SUCCESS(publish_stream->on_rtp_plaintext(data.get(), buf.pos())); + + // The live source should got 2 audio packets, one is sequence header, another is audio data. + EXPECT_EQ(mock_source->on_audio_count_, 2); + EXPECT_EQ(mock_source->on_frame_count_, 2); + EXPECT_EQ(mock_source->on_video_count_, 0); + } + + publish_stream->stop(); +} diff --git a/trunk/src/utest/srs_utest_workflow_rtc2rtmp.hpp b/trunk/src/utest/srs_utest_workflow_rtc2rtmp.hpp new file mode 100644 index 000000000..b2b6cf65a --- /dev/null +++ b/trunk/src/utest/srs_utest_workflow_rtc2rtmp.hpp @@ -0,0 +1,32 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2013-2025 Winlin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef SRS_UTEST_WORKFLOW_RTC2RTMP_HPP +#define SRS_UTEST_WORKFLOW_RTC2RTMP_HPP + +/* +#include +*/ +#include + +#endif diff --git a/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp b/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp index 1b9dc4d39..6ee035a6e 100644 --- a/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp +++ b/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp @@ -21,11 +21,11 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRS_UTEST_RTC_CONN_HPP -#define SRS_UTEST_RTC_CONN_HPP +#ifndef SRS_UTEST_WORKFLOW_RTC_CONN_HPP +#define SRS_UTEST_WORKFLOW_RTC_CONN_HPP /* -#include +#include */ #include diff --git a/trunk/src/utest/srs_utest_workflow_rtc_playstream.cpp b/trunk/src/utest/srs_utest_workflow_rtc_playstream.cpp index 4f756d241..cf582ab8d 100644 --- a/trunk/src/utest/srs_utest_workflow_rtc_playstream.cpp +++ b/trunk/src/utest/srs_utest_workflow_rtc_playstream.cpp @@ -221,9 +221,4 @@ VOID TEST(BasicWorkflowRtcPlayStreamTest, ManuallyVerify) // Stop the play stream play_stream->stop(); - - // Clean up - set to NULL to avoid double-free - play_stream->config_ = NULL; - play_stream->rtc_sources_ = NULL; - play_stream->stat_ = NULL; } diff --git a/trunk/src/utest/srs_utest_workflow_rtc_publishstream.cpp b/trunk/src/utest/srs_utest_workflow_rtc_publishstream.cpp index 5a8f48e57..1d8060a97 100644 --- a/trunk/src/utest/srs_utest_workflow_rtc_publishstream.cpp +++ b/trunk/src/utest/srs_utest_workflow_rtc_publishstream.cpp @@ -96,9 +96,4 @@ VOID TEST(BasicWorkflowRtcPublishStreamTest, ManuallyVerify) // Stop the publish stream publish_stream->stop(); - - // Clean up - set injected fields to NULL to avoid double-free - publish_stream->config_ = NULL; - publish_stream->rtc_sources_ = NULL; - publish_stream->stat_ = NULL; }