AI: Add workflow utest to verify rtc2rtmp audio async.

This commit is contained in:
OSSRS-AI 2025-10-27 10:58:52 -04:00 committed by winlin
parent 2943fcb10f
commit 19ac413101
16 changed files with 521 additions and 104 deletions

8
trunk/configure vendored
View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -1377,6 +1377,14 @@ srs_error_t SrsRtcRtpBuilder::consume_packets(vector<SrsRtpPacket *> &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<double>(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;
}
}

View File

@ -419,9 +419,25 @@ SRS_DECLARE_PRIVATE: // clang-format on
srs_error_t consume_packets(std::vector<SrsRtpPacket *> &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<SrsRtpPacket *> &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

View File

@ -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_;

View File

@ -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();

View File

@ -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<SrsRtcSource>& source, SrsRtpPacket* pkt) {
virtual srs_error_t on_rtp(SrsSharedPtr<SrsRtcSource> &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());
}

View File

@ -9,4 +9,3 @@
#include <srs_utest.hpp>
#endif

View File

@ -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()
{

View File

@ -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<uint32_t, SrsRtcTrackDescription *> 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

View File

@ -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 <srs_utest_workflow_rtc2rtmp.hpp>
#include <srs_app_factory.hpp>
#include <srs_app_rtc_codec.hpp>
#include <srs_app_rtc_conn.hpp>
#include <srs_app_rtc_server.hpp>
#include <srs_app_rtc_source.hpp>
#include <srs_app_stream_token.hpp>
#include <srs_kernel_error.hpp>
#include <srs_protocol_rtmp_stack.hpp>
#include <srs_utest_ai11.hpp>
#include <srs_utest_manual_mock.hpp>
#include <srs_utest_manual_service.hpp>
#include <srs_utest_workflow_rtc_conn.hpp>
// 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<SrsRtpPacket *> &ready_packets);
virtual void clear_all();
};
MockAudioCache::MockAudioCache()
{
process_packet_count_ = 0;
}
MockAudioCache::~MockAudioCache()
{
}
srs_error_t MockAudioCache::process_packet(SrsRtpPacket *src, std::vector<SrsRtpPacket *> &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<SrsParsedAudioPacket *> 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<SrsParsedAudioPacket *> &outs);
virtual void free_frames(std::vector<SrsParsedAudioPacket *> &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<SrsParsedAudioPacket *> &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<SrsParsedAudioPacket *> &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<MockAppConfig> mock_config(new MockAppConfig());
SrsUniquePtr<MockRtcSourceManager> mock_rtc_sources(new MockRtcSourceManager());
SrsUniquePtr<MockAppStatistic> mock_stat(new MockAppStatistic());
SrsUniquePtr<MockRtcAsyncCallRequest> mock_request(new MockRtcAsyncCallRequest("test.vhost", "live", "stream1"));
SrsUniquePtr<MockRtcAsyncTaskExecutor> mock_exec(new MockRtcAsyncTaskExecutor());
SrsUniquePtr<MockExpire> mock_expire(new MockExpire());
SrsUniquePtr<MockRtcPacketReceiver> mock_receiver(new MockRtcPacketReceiver());
SrsUniquePtr<MockRtcTrackDescriptionFactory> track_factory(new MockRtcTrackDescriptionFactory());
SrsUniquePtr<MockLiveSourceManager> 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<SrsRtcPublishStream> 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<SrsRtcSourceDescription> 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<SrsRtcBridge *>(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<MockLiveSource *>(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<char[]> 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();
}

View File

@ -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 <srs_utest_workflow_rtc2rtmp.hpp>
*/
#include <srs_utest.hpp>
#endif

View File

@ -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 <srs_utest_rtc_conn.hpp>
#include <srs_utest_workflow_rtc_conn.hpp>
*/
#include <srs_utest.hpp>

View File

@ -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;
}

View File

@ -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;
}