diff --git a/trunk/configure b/trunk/configure
index cbe49c8f3f0..fdc909631fc 100755
--- a/trunk/configure
+++ b/trunk/configure
@@ -471,7 +471,7 @@ if [[ $SRS_UTEST == YES ]]; then
"srs_utest_config" "srs_utest_rtmp" "srs_utest_http" "srs_utest_avc" "srs_utest_reload"
"srs_utest_mp4" "srs_utest_service" "srs_utest_app" "srs_utest_rtc" "srs_utest_config2"
"srs_utest_protocol" "srs_utest_protocol2" "srs_utest_kernel2" "srs_utest_protocol3"
- "srs_utest_st" "srs_utest_rtc2")
+ "srs_utest_st" "srs_utest_rtc2" "srs_utest_rtc3")
if [[ $SRS_SRT == YES ]]; then
MODULE_FILES+=("srs_utest_srt")
fi
diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md
index 2a2ba9626da..9aa3649bffb 100644
--- a/trunk/doc/CHANGELOG.md
+++ b/trunk/doc/CHANGELOG.md
@@ -7,6 +7,7 @@ The changelog for SRS.
## SRS 7.0 Changelog
+* v7.0, 2025-07-16, Merge [#4295](https://github.com/ossrs/srs/pull/4295): RTC: audio packet jitter buffer. v7.0.48 (#4295)
* v7.0, 2025-07-11, Merge [#4333](https://github.com/ossrs/srs/pull/4333): NEW PROTOCOL: Support viewing stream over RTSP. v7.0.47 (#4333)
* v7.0, 2025-07-10, Merge [#4414](https://github.com/ossrs/srs/pull/4414): Fix H.264 B-frame detection logic to comply with specification. v7.0.46 (#4414)
* v7.0, 2025-07-04, Merge [#4412](https://github.com/ossrs/srs/pull/4412): Refine code and add tests for #4289. v7.0.45 (#4412)
diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp
index 99ba4c51231..74f9277206a 100644
--- a/trunk/src/app/srs_app_rtc_source.cpp
+++ b/trunk/src/app/srs_app_rtc_source.cpp
@@ -1006,7 +1006,7 @@ srs_error_t SrsRtcRtpBuilder::on_audio(SrsSharedPtrMessage* msg)
return err;
}
- // ts support audio codec: aac/mp3
+ // support audio codec: aac/mp3
SrsAudioCodecId acodec = format->acodec->id;
if (acodec != SrsAudioCodecIdAAC && acodec != SrsAudioCodecIdMP3) {
return err;
@@ -1205,7 +1205,7 @@ srs_error_t SrsRtcRtpBuilder::on_video(SrsSharedPtrMessage* msg)
// If merge Nalus, we pcakges all NALUs(samples) as one NALU, in a RTP or FUA packet.
vector pkts;
- // auto free when exit
+ // TODO: FIXME: Should rename to pkts_disposer.
SrsUniquePtr> pkts_ptr(&pkts, free_packets);
if (merge_nalus && nn_samples > 1) {
@@ -1593,12 +1593,122 @@ bool SrsRtcFrameBuilderVideoFrameDetector::is_lost_sn(uint16_t received)
return lost_sn_ == received;
}
+SrsRtcFrameBuilderAudioPacketCache::SrsRtcFrameBuilderAudioPacketCache()
+{
+ last_audio_seq_num_ = 0;
+ last_audio_process_time_ = 0;
+ initialized_ = false;
+ timeout_ = MAX_AUDIO_WAIT_MS * SRS_UTIME_MILLISECONDS; // Default timeout in microseconds
+}
+
+SrsRtcFrameBuilderAudioPacketCache::~SrsRtcFrameBuilderAudioPacketCache()
+{
+ clear_all();
+}
+
+void SrsRtcFrameBuilderAudioPacketCache::set_timeout(srs_utime_t timeout)
+{
+ timeout_ = timeout;
+}
+
+srs_error_t SrsRtcFrameBuilderAudioPacketCache::process_packet(SrsRtpPacket* src, std::vector& ready_packets)
+{
+ srs_error_t err = srs_success;
+
+ uint16_t seq = src->header.get_sequence();
+ srs_utime_t now = srs_update_system_time();
+
+ if (!initialized_) {
+ last_audio_seq_num_ = seq - 1;
+ last_audio_process_time_ = now;
+ initialized_ = true;
+ }
+
+ // Check if packet is too old (already processed)
+ if (srs_rtp_seq_distance(last_audio_seq_num_, seq) < 0) {
+ srs_warn("Discard late audio packet, seq=%u, last_seq=%u", seq, last_audio_seq_num_);
+ return err;
+ }
+
+ // Store packet in jitter buffer
+ if (true) {
+ std::map::iterator it = audio_buffer_.find(seq);
+ if (it != audio_buffer_.end()) {
+ SrsRtpPacket* pkt = it->second;
+ srs_freep(pkt);
+ }
+ audio_buffer_[seq] = src->copy();
+ }
+
+ // Try to process packets in the sliding window
+ bool force_process = audio_buffer_.size() >= AUDIO_JITTER_BUFFER_SIZE ||
+ (now - last_audio_process_time_) > timeout_;
+ uint16_t window_end = last_audio_seq_num_ + SLIDING_WINDOW_SIZE;
+
+ while (!audio_buffer_.empty()) {
+ std::map::iterator it = audio_buffer_.begin();
+ uint16_t next_seq = it->first;
+
+ // Check if the packet is within our sliding window
+ if (!force_process) {
+ // If packet is before window start (shouldn't happen normally)
+ if (srs_rtp_seq_distance(last_audio_seq_num_, next_seq) < 0) {
+ // Process it anyway as it's already late
+ srs_warn("Late audio packet, seq=%u, expected>=%u", next_seq, last_audio_seq_num_);
+ } else if (srs_rtp_seq_distance(next_seq, window_end) < 0) {
+ // If packet is beyond window end, stop processing
+ srs_warn("Audio packet beyond window end, seq=%u, window_end=%u", next_seq, window_end);
+ break;
+ } else if (srs_rtp_seq_distance(last_audio_seq_num_, next_seq) > 1) {
+ // If there's a gap and we haven't exceeded wait time, wait for missing packets
+ if ((now - last_audio_process_time_) <= timeout_) {
+ break;
+ }
+ srs_warn("Audio packet loss, expected=%u, got=%u", last_audio_seq_num_ + 1, next_seq);
+ }
+ }
+
+ // Take the packet from buffer
+ SrsRtpPacket* pkt = it->second;
+ audio_buffer_.erase(it);
+
+ // Update last sequence number
+ last_audio_seq_num_ = next_seq;
+ last_audio_process_time_ = now;
+
+ // Add to ready packets for processing
+ ready_packets.push_back(pkt);
+
+ // Update window end for next iteration
+ window_end = last_audio_seq_num_ + SLIDING_WINDOW_SIZE;
+ }
+
+ // If buffer is getting too full, force process oldest packets
+ if (audio_buffer_.size() >= AUDIO_JITTER_BUFFER_SIZE * 0.8) {
+ srs_warn("Audio jitter buffer nearly full, size=%zu", audio_buffer_.size());
+ }
+
+ return err;
+}
+
+void SrsRtcFrameBuilderAudioPacketCache::clear_all()
+{
+ std::map::iterator it;
+ for (it = audio_buffer_.begin(); it != audio_buffer_.end(); ++it) {
+ SrsRtpPacket* pkt = it->second;
+ srs_freep(pkt);
+ }
+
+ audio_buffer_.clear();
+}
+
SrsRtcFrameBuilder::SrsRtcFrameBuilder(ISrsStreamBridge* bridge)
{
bridge_ = bridge;
is_first_audio_ = true;
audio_transcoder_ = NULL;
video_codec_ = SrsVideoCodecIdAVC;
+ audio_cache_ = new SrsRtcFrameBuilderAudioPacketCache();
video_cache_ = new SrsRtcFrameBuilderVideoPacketCache();
frame_detector_ = new SrsRtcFrameBuilderVideoFrameDetector(video_cache_);
sync_state_ = -1;
@@ -1608,6 +1718,7 @@ SrsRtcFrameBuilder::SrsRtcFrameBuilder(ISrsStreamBridge* bridge)
SrsRtcFrameBuilder::~SrsRtcFrameBuilder()
{
srs_freep(audio_transcoder_);
+ srs_freep(audio_cache_);
srs_freep(video_cache_);
srs_freep(frame_detector_);
srs_freep(obs_whip_vps_);
@@ -1648,6 +1759,7 @@ srs_error_t SrsRtcFrameBuilder::on_publish()
void SrsRtcFrameBuilder::on_unpublish()
{
+ audio_cache_->clear_all();
}
srs_error_t SrsRtcFrameBuilder::on_rtp(SrsRtpPacket *pkt)
@@ -1675,7 +1787,7 @@ srs_error_t SrsRtcFrameBuilder::on_rtp(SrsRtpPacket *pkt)
}
if (pkt->is_audio()) {
- err = transcode_audio(pkt);
+ err = packet_audio(pkt);
} else {
err = packet_video(pkt);
}
@@ -1683,6 +1795,30 @@ srs_error_t SrsRtcFrameBuilder::on_rtp(SrsRtpPacket *pkt)
return err;
}
+srs_error_t SrsRtcFrameBuilder::packet_audio(SrsRtpPacket* src)
+{
+ srs_error_t err = srs_success;
+
+ std::vector ready_packets;
+ SrsUniquePtr> pkts_disposer(&ready_packets, free_packets);
+
+ // Use audio cache to process packet through jitter buffer
+ if ((err = audio_cache_->process_packet(src, ready_packets)) != srs_success) {
+ return srs_error_wrap(err, "audio cache process");
+ }
+
+ // Process all ready packets in order
+ for (size_t i = 0; i < ready_packets.size(); ++i) {
+ SrsRtpPacket* pkt = ready_packets[i];
+
+ if ((err = transcode_audio(pkt)) != srs_success) {
+ return srs_error_wrap(err, "transcode audio");
+ }
+ }
+
+ return err;
+}
+
srs_error_t SrsRtcFrameBuilder::transcode_audio(SrsRtpPacket *pkt)
{
srs_error_t err = srs_success;
@@ -1709,6 +1845,7 @@ srs_error_t SrsRtcFrameBuilder::transcode_audio(SrsRtpPacket *pkt)
is_first_audio_ = false;
}
+ // TODO: FIXME: Should use SrsUniquePtr to dispose it automatically.
std::vector out_pkts;
SrsRtpRawPayload *payload = dynamic_cast(pkt->payload());
diff --git a/trunk/src/app/srs_app_rtc_source.hpp b/trunk/src/app/srs_app_rtc_source.hpp
index 32056c4526c..35486abe6dd 100644
--- a/trunk/src/app/srs_app_rtc_source.hpp
+++ b/trunk/src/app/srs_app_rtc_source.hpp
@@ -52,6 +52,13 @@ const int kVideoPayloadType = 102;
// Chrome HEVC defaults as 49.
const int KVideoPayloadTypeHevc = 49;
+// Audio jitter buffer size (in packets)
+const int AUDIO_JITTER_BUFFER_SIZE = 100;
+// Sliding window size for continuous processing
+const int SLIDING_WINDOW_SIZE = 10;
+// Maximum waiting time for out-of-order packets (in ms)
+const int MAX_AUDIO_WAIT_MS = 100;
+
class SrsNtp
{
public:
@@ -378,6 +385,33 @@ class SrsRtcFrameBuilderVideoFrameDetector
bool is_lost_sn(uint16_t received);
};
+// Audio packet cache for RTP packet jitter buffer management
+class SrsRtcFrameBuilderAudioPacketCache
+{
+private:
+ // Audio jitter buffer, map sequence number to packet
+ std::map audio_buffer_;
+ // Last processed sequence number
+ uint16_t last_audio_seq_num_;
+ // Last time we processed the jitter buffer
+ srs_utime_t last_audio_process_time_;
+ // Whether the cache has been initialized
+ bool initialized_;
+ // Timeout for waiting out-of-order packets (in microseconds)
+ srs_utime_t timeout_;
+public:
+ SrsRtcFrameBuilderAudioPacketCache();
+ virtual ~SrsRtcFrameBuilderAudioPacketCache();
+public:
+ // Set timeout for waiting out-of-order packets (in microseconds)
+ void set_timeout(srs_utime_t timeout);
+ // Process audio packet through jitter buffer
+ // Returns packets ready for transcoding in order
+ srs_error_t process_packet(SrsRtpPacket* src, std::vector& ready_packets);
+ // Clear all cached packets
+ void clear_all();
+};
+
// Collect and build WebRTC RTP packets to AV frames.
class SrsRtcFrameBuilder
{
@@ -386,9 +420,9 @@ class SrsRtcFrameBuilder
private:
bool is_first_audio_;
SrsAudioTranscoder *audio_transcoder_;
-
SrsVideoCodecId video_codec_;
private:
+ SrsRtcFrameBuilderAudioPacketCache* audio_cache_;
SrsRtcFrameBuilderVideoPacketCache* video_cache_;
SrsRtcFrameBuilderVideoFrameDetector* frame_detector_;
private:
@@ -408,6 +442,7 @@ class SrsRtcFrameBuilder
virtual void on_unpublish();
virtual srs_error_t on_rtp(SrsRtpPacket *pkt);
private:
+ srs_error_t packet_audio(SrsRtpPacket* pkt);
srs_error_t transcode_audio(SrsRtpPacket *pkt);
void packet_aac(SrsCommonMessage* audio, char* data, int len, uint32_t pts, bool is_header);
private:
diff --git a/trunk/src/core/srs_core_time.hpp b/trunk/src/core/srs_core_time.hpp
index d840f2beea4..a05021a9218 100644
--- a/trunk/src/core/srs_core_time.hpp
+++ b/trunk/src/core/srs_core_time.hpp
@@ -42,5 +42,11 @@ srs_utime_t srs_duration(srs_utime_t start, srs_utime_t end);
// Never timeout.
#define SRS_UTIME_NO_TIMEOUT ((srs_utime_t) -1LL)
+// Get current system time in srs_utime_t, use cache to avoid performance problem
+extern srs_utime_t srs_get_system_time();
+extern srs_utime_t srs_get_system_startup_time();
+// A daemon st-thread updates it.
+extern srs_utime_t srs_update_system_time();
+
#endif
diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp
index deb6c501df8..8934e32a92b 100644
--- a/trunk/src/core/srs_core_version7.hpp
+++ b/trunk/src/core/srs_core_version7.hpp
@@ -9,6 +9,6 @@
#define VERSION_MAJOR 7
#define VERSION_MINOR 0
-#define VERSION_REVISION 47
+#define VERSION_REVISION 48
#endif
\ No newline at end of file
diff --git a/trunk/src/utest/srs_utest_rtc3.cpp b/trunk/src/utest/srs_utest_rtc3.cpp
new file mode 100644
index 00000000000..644b8d2b890
--- /dev/null
+++ b/trunk/src/utest/srs_utest_rtc3.cpp
@@ -0,0 +1,329 @@
+//
+// Copyright (c) 2013-2025 The SRS Authors
+//
+// SPDX-License-Identifier: MIT
+//
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+// Helper function to create a mock RTP packet for testing
+SrsRtpPacket* mock_create_audio_rtp_packet(uint16_t sequence, uint32_t timestamp, const char* payload_data = NULL, int payload_size = 10)
+{
+ SrsRtpPacket* pkt = new SrsRtpPacket();
+
+ // Set RTP header
+ pkt->header.set_padding(false);
+ pkt->header.set_marker(false);
+ pkt->header.set_payload_type(111); // Audio payload type
+ pkt->header.set_sequence(sequence);
+ pkt->header.set_timestamp(timestamp);
+ pkt->header.set_ssrc(0x12345678);
+
+ // For audio cache testing, we don't need actual payload data or avsync time
+ // The cache is only concerned with sequence numbers and system time for jitter buffer logic
+ // We're not testing the downstream transcoding, just the cache reordering behavior
+
+ // Set frame type for audio
+ pkt->frame_type = SrsFrameTypeAudio;
+
+ return pkt;
+}
+
+// Helper function to free a vector of RTP packets
+void free_audio_packets(std::vector& packets)
+{
+ for (size_t i = 0; i < packets.size(); ++i) {
+ srs_freep(packets[i]);
+ }
+ packets.clear();
+}
+
+VOID TEST(RTC3AudioCacheTest, BasicPacketProcessing)
+{
+ srs_error_t err;
+
+ // Test basic packet processing in order
+ if (true) {
+ SrsRtcFrameBuilderAudioPacketCache cache;
+ std::vector ready_packets;
+
+ // Process first packet
+ SrsUniquePtr pkt1(mock_create_audio_rtp_packet(100, 1000));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt1.get(), ready_packets));
+
+ // First packet should be processed immediately
+ EXPECT_EQ(1, (int)ready_packets.size());
+ EXPECT_EQ(100, ready_packets[0]->header.get_sequence());
+
+ free_audio_packets(ready_packets);
+
+ // Process second packet in sequence
+ SrsUniquePtr pkt2(mock_create_audio_rtp_packet(101, 1020));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt2.get(), ready_packets));
+
+ // Second packet should be processed immediately
+ EXPECT_EQ(1, (int)ready_packets.size());
+ EXPECT_EQ(101, ready_packets[0]->header.get_sequence());
+
+ free_audio_packets(ready_packets);
+ }
+}
+
+VOID TEST(RTC3AudioCacheTest, OutOfOrderPackets)
+{
+ srs_error_t err;
+
+ // Test out-of-order packet handling
+ if (true) {
+ SrsRtcFrameBuilderAudioPacketCache cache;
+ std::vector ready_packets;
+
+ // Process first packet 100 to initialize
+ SrsUniquePtr pkt1(mock_create_audio_rtp_packet(100, 1000));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt1.get(), ready_packets));
+ EXPECT_EQ(1, (int)ready_packets.size());
+ free_audio_packets(ready_packets);
+
+ // Process packet 103 (out of order - missing 101, 102)
+ SrsUniquePtr pkt3(mock_create_audio_rtp_packet(103, 1060));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt3.get(), ready_packets));
+
+ // Should not process yet, waiting for missing packets
+ EXPECT_EQ(0, (int)ready_packets.size());
+
+ // Process packet 101 (fills gap)
+ SrsUniquePtr pkt2(mock_create_audio_rtp_packet(101, 1020));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt2.get(), ready_packets));
+
+ // Should process packet 101 now
+ EXPECT_EQ(1, (int)ready_packets.size());
+ EXPECT_EQ(101, ready_packets[0]->header.get_sequence());
+ free_audio_packets(ready_packets);
+
+ // Process packet 102 (completes sequence)
+ SrsUniquePtr pkt4(mock_create_audio_rtp_packet(102, 1040));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt4.get(), ready_packets));
+
+ // Should process both 102 and 103
+ EXPECT_EQ(2, (int)ready_packets.size());
+ EXPECT_EQ(102, ready_packets[0]->header.get_sequence());
+ EXPECT_EQ(103, ready_packets[1]->header.get_sequence());
+
+ free_audio_packets(ready_packets);
+ }
+}
+
+VOID TEST(RTC3AudioCacheTest, LatePacketHandling)
+{
+ srs_error_t err;
+
+ // Test late packet detection and discard
+ if (true) {
+ SrsRtcFrameBuilderAudioPacketCache cache;
+ std::vector ready_packets;
+
+ // Process packets 100, 101, 102 in order
+ for (uint16_t seq = 100; seq <= 102; seq++) {
+ SrsUniquePtr pkt(mock_create_audio_rtp_packet(seq, 1000 + (seq - 100) * 20));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt.get(), ready_packets));
+ EXPECT_EQ(1, (int)ready_packets.size());
+ EXPECT_EQ(seq, ready_packets[0]->header.get_sequence());
+ free_audio_packets(ready_packets);
+ }
+
+ // Try to process packet 99 (late packet - already processed)
+ SrsUniquePtr late_pkt(mock_create_audio_rtp_packet(99, 980));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(late_pkt.get(), ready_packets));
+
+ // Late packet should be discarded, no packets ready
+ EXPECT_EQ(0, (int)ready_packets.size());
+ }
+}
+
+VOID TEST(RTC3AudioCacheTest, SequenceNumberWrapAround)
+{
+ srs_error_t err;
+
+ // Test sequence number wrap-around (16-bit overflow)
+ if (true) {
+ SrsRtcFrameBuilderAudioPacketCache cache;
+ std::vector ready_packets;
+
+ // Process packets near the 16-bit boundary
+ uint16_t seq_near_max = 65534;
+ SrsUniquePtr pkt1(mock_create_audio_rtp_packet(seq_near_max, 1000));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt1.get(), ready_packets));
+ EXPECT_EQ(1, (int)ready_packets.size());
+ EXPECT_EQ(seq_near_max, ready_packets[0]->header.get_sequence());
+ free_audio_packets(ready_packets);
+
+ // Process packet 65535
+ SrsUniquePtr pkt2(mock_create_audio_rtp_packet(65535, 1020));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt2.get(), ready_packets));
+ EXPECT_EQ(1, (int)ready_packets.size());
+ EXPECT_EQ(65535, ready_packets[0]->header.get_sequence());
+ free_audio_packets(ready_packets);
+
+ // Process packet 0 (after wrap-around)
+ SrsUniquePtr pkt3(mock_create_audio_rtp_packet(0, 1040));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt3.get(), ready_packets));
+ EXPECT_EQ(1, (int)ready_packets.size());
+ EXPECT_EQ(0, ready_packets[0]->header.get_sequence());
+ free_audio_packets(ready_packets);
+
+ // Process packet 1
+ SrsUniquePtr pkt4(mock_create_audio_rtp_packet(1, 1060));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt4.get(), ready_packets));
+ EXPECT_EQ(1, (int)ready_packets.size());
+ EXPECT_EQ(1, ready_packets[0]->header.get_sequence());
+ free_audio_packets(ready_packets);
+ }
+}
+
+VOID TEST(RTC3AudioCacheTest, PacketLossWithTimeout)
+{
+ srs_error_t err;
+
+ // Test packet loss handling with timeout
+ if (true) {
+ SrsRtcFrameBuilderAudioPacketCache cache;
+ std::vector ready_packets;
+
+ // Set a very short timeout for testing (1ms)
+ cache.set_timeout(1 * SRS_UTIME_MILLISECONDS);
+
+ // Process first packet to initialize
+ SrsUniquePtr pkt1(mock_create_audio_rtp_packet(100, 1000));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt1.get(), ready_packets));
+ EXPECT_EQ(1, (int)ready_packets.size());
+ free_audio_packets(ready_packets);
+
+ // Process packet 103 (missing 101, 102)
+ SrsUniquePtr pkt3(mock_create_audio_rtp_packet(103, 1060));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt3.get(), ready_packets));
+
+ // Should not process yet, waiting for missing packets
+ EXPECT_EQ(0, (int)ready_packets.size());
+
+ // Sleep for 10ms to exceed the 1ms timeout
+ srs_usleep(10 * SRS_UTIME_MILLISECONDS);
+
+ // Process another packet to trigger timeout check
+ SrsUniquePtr pkt4(mock_create_audio_rtp_packet(104, 1080));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt4.get(), ready_packets));
+
+ // Should process packet 103 despite missing 101, 102 due to timeout
+ bool found_103 = false;
+ for (size_t i = 0; i < ready_packets.size(); i++) {
+ if (ready_packets[i]->header.get_sequence() == 103) {
+ found_103 = true;
+ break;
+ }
+ }
+ EXPECT_TRUE(found_103);
+ free_audio_packets(ready_packets);
+ }
+}
+
+VOID TEST(RTC3AudioCacheTest, BufferOverflowProtection)
+{
+ srs_error_t err;
+
+ // Test buffer overflow protection
+ if (true) {
+ SrsRtcFrameBuilderAudioPacketCache cache;
+ std::vector ready_packets;
+
+ // Process first packet to initialize
+ SrsUniquePtr pkt1(mock_create_audio_rtp_packet(100, 1000));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt1.get(), ready_packets));
+ EXPECT_EQ(1, (int)ready_packets.size());
+ free_audio_packets(ready_packets);
+
+ // Fill buffer with out-of-order packets to trigger overflow protection
+ // Add packets with large gaps to fill the buffer
+ for (uint16_t i = 0; i < AUDIO_JITTER_BUFFER_SIZE + 10; i++) {
+ uint16_t seq = 200 + i * 2; // Create gaps to avoid immediate processing
+ SrsUniquePtr pkt(mock_create_audio_rtp_packet(seq, 2000 + i * 20));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt.get(), ready_packets));
+ }
+
+ // Buffer overflow protection should have kicked in and processed some packets
+ EXPECT_GT((int)ready_packets.size(), 0);
+ free_audio_packets(ready_packets);
+ }
+}
+
+VOID TEST(RTC3AudioCacheTest, ClearAllFunctionality)
+{
+ srs_error_t err;
+
+ // Test clear_all functionality
+ if (true) {
+ SrsRtcFrameBuilderAudioPacketCache cache;
+ std::vector ready_packets;
+
+ // Process first packet to initialize
+ SrsUniquePtr pkt1(mock_create_audio_rtp_packet(100, 1000));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt1.get(), ready_packets));
+ free_audio_packets(ready_packets);
+
+ // Add some out-of-order packets to buffer
+ SrsUniquePtr pkt3(mock_create_audio_rtp_packet(103, 1060));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt3.get(), ready_packets));
+
+ SrsUniquePtr pkt5(mock_create_audio_rtp_packet(105, 1100));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt5.get(), ready_packets));
+
+ // Should have some packets waiting in buffer
+ EXPECT_EQ(0, (int)ready_packets.size());
+
+ // Clear all cached packets
+ cache.clear_all();
+
+ EXPECT_EQ(0, (int)cache.audio_buffer_.size());
+ }
+}
+
+VOID TEST(RTC3AudioCacheTest, DuplicatePacketHandling)
+{
+ srs_error_t err;
+
+ // Test duplicate packet handling
+ if (true) {
+ SrsRtcFrameBuilderAudioPacketCache cache;
+ std::vector ready_packets;
+
+ // Process first packet
+ SrsUniquePtr pkt1(mock_create_audio_rtp_packet(100, 1000));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt1.get(), ready_packets));
+ EXPECT_EQ(1, (int)ready_packets.size());
+ free_audio_packets(ready_packets);
+
+ // Process packet 102 (out of order)
+ SrsUniquePtr pkt3(mock_create_audio_rtp_packet(102, 1040));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt3.get(), ready_packets));
+ EXPECT_EQ(0, (int)ready_packets.size()); // Waiting for 101
+
+ // Process duplicate packet 102
+ SrsUniquePtr pkt3_dup(mock_create_audio_rtp_packet(102, 1040));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt3_dup.get(), ready_packets));
+ EXPECT_EQ(0, (int)ready_packets.size()); // Still waiting for 101
+
+ // Process packet 101 to complete sequence
+ SrsUniquePtr pkt2(mock_create_audio_rtp_packet(101, 1020));
+ HELPER_EXPECT_SUCCESS(cache.process_packet(pkt2.get(), ready_packets));
+
+ // Should process 101 and one instance of 102 (duplicate should be handled)
+ EXPECT_GE((int)ready_packets.size(), 2);
+ EXPECT_EQ(101, ready_packets[0]->header.get_sequence());
+ EXPECT_EQ(102, ready_packets[1]->header.get_sequence());
+ free_audio_packets(ready_packets);
+ }
+}
diff --git a/trunk/src/utest/srs_utest_rtc3.hpp b/trunk/src/utest/srs_utest_rtc3.hpp
new file mode 100644
index 00000000000..9fd1375c7a7
--- /dev/null
+++ b/trunk/src/utest/srs_utest_rtc3.hpp
@@ -0,0 +1,15 @@
+//
+// Copyright (c) 2013-2025 The SRS Authors
+//
+// SPDX-License-Identifier: MIT
+//
+
+#ifndef SRS_UTEST_RTC3_HPP
+#define SRS_UTEST_RTC3_HPP
+
+/*
+#include
+*/
+#include
+
+#endif