diff --git a/.gitignore b/.gitignore
index 0319e6d68..33dfbd971 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,4 +42,5 @@ cmake-build-debug
/trunk/ide/srs_clion/srs
/trunk/ide/srs_clion/Testing/
/trunk/ide/vscode-build
+/build/
diff --git a/trunk/.gitignore b/trunk/.gitignore
index a2561531a..93cf6db46 100644
--- a/trunk/.gitignore
+++ b/trunk/.gitignore
@@ -52,3 +52,4 @@ bug
*.svg
/3rdparty/st-srs/srs
/3rdparty/st-srs/.circleci
+/test_detail.xml
diff --git a/trunk/3rdparty/srs-bench/README.md b/trunk/3rdparty/srs-bench/README.md
index 3ce475f91..c4cadf59a 100644
--- a/trunk/3rdparty/srs-bench/README.md
+++ b/trunk/3rdparty/srs-bench/README.md
@@ -30,10 +30,10 @@ cd srs-bench && make
```bash
git clone https://github.com/ossrs/srs.git &&
cd srs/trunk && ./configure --h265=on --gb28181=on && make &&
-./objs/srs -c conf/console.conf
+./objs/srs -c conf/regression-test-for-clion.conf
```
-> Note: Use valgrind to check memory leak, please use `valgrind --leak-check=full ./objs/srs -c conf/console.conf >/dev/null` to start SRS.
+> Note: Use valgrind to check memory leak, please use `valgrind --leak-check=full ./objs/srs -c conf/regression-test-for-clion.conf >/dev/null` to start SRS.
具体场景,请按下面的操作启动测试。
@@ -160,7 +160,7 @@ done
回归测试需要先启动[SRS](https://github.com/ossrs/srs/issues/307),支持WebRTC推拉流:
```bash
-./objs/srs -c conf/rtc.conf
+./objs/srs -c conf/regression-test-for-clion.conf
```
然后运行回归测试用例,如果只跑一次,可以直接运行:
diff --git a/trunk/3rdparty/srs-bench/srs/ingester.go b/trunk/3rdparty/srs-bench/srs/ingester.go
index d8a0288e7..455d523df 100644
--- a/trunk/3rdparty/srs-bench/srs/ingester.go
+++ b/trunk/3rdparty/srs-bench/srs/ingester.go
@@ -40,6 +40,7 @@ import (
type videoIngester struct {
sourceVideo string
+ codecType string // "h264", "hevc", or "vp8"
fps int
markerInterceptor *rtpInterceptor
sVideoTrack *webrtc.TrackLocalStaticSample
@@ -48,9 +49,26 @@ type videoIngester struct {
readyCancel context.CancelFunc
}
-func newVideoIngester(sourceVideo string) *videoIngester {
- v := &videoIngester{markerInterceptor: &rtpInterceptor{}, sourceVideo: sourceVideo}
+// WithCodec sets the codec type for the video ingester
+func WithCodec(codecType string) func(*videoIngester) {
+ return func(v *videoIngester) {
+ v.codecType = codecType
+ }
+}
+
+func newVideoIngester(sourceVideo string, opts ...func(*videoIngester)) *videoIngester {
+ v := &videoIngester{
+ markerInterceptor: &rtpInterceptor{},
+ sourceVideo: sourceVideo,
+ codecType: "h264", // default codec
+ }
v.ready, v.readyCancel = context.WithCancel(context.Background())
+
+ // Apply options
+ for _, opt := range opts {
+ opt(v)
+ }
+
return v
}
@@ -66,8 +84,20 @@ func (v *videoIngester) AddTrack(pc *webrtc.PeerConnection, fps int) error {
v.fps = fps
mimeType, trackID := "video/H264", "video"
- if strings.HasSuffix(v.sourceVideo, ".ivf") {
+
+ // Determine MIME type based on codec type or file extension
+ switch v.codecType {
+ case "hevc", "h265":
+ mimeType = "video/H265"
+ case "vp8":
mimeType = "video/VP8"
+ case "h264":
+ mimeType = "video/H264"
+ default:
+ // Fallback to file extension detection
+ if strings.HasSuffix(v.sourceVideo, ".ivf") {
+ mimeType = "video/VP8"
+ }
}
var err error
diff --git a/trunk/3rdparty/srs-bench/srs/rtc_test.go b/trunk/3rdparty/srs-bench/srs/rtc_test.go
index 36fc56946..c0a742806 100644
--- a/trunk/3rdparty/srs-bench/srs/rtc_test.go
+++ b/trunk/3rdparty/srs-bench/srs/rtc_test.go
@@ -2432,3 +2432,142 @@ func TestRtcPublish_HttpFlvPlay(t *testing.T) {
}()
}()
}
+
+// Test WebRTC-to-RTMP conversion with HEVC codec (PR #4349)
+func TestRtcPublish_HttpFlvPlay_HEVC(t *testing.T) {
+ ctx := logger.WithContext(context.Background())
+ ctx, cancel := context.WithTimeout(ctx, time.Duration(*srsTimeout)*time.Millisecond)
+
+ var r0, r1, r2, r3 error
+ defer func(ctx context.Context) {
+ if err := filterTestError(ctx.Err(), r0, r1, r2, r3); err != nil {
+ t.Errorf("Fail for err %+v", err)
+ } else {
+ logger.Tf(ctx, "test done with err %+v", err)
+ }
+ }(ctx)
+
+ var resources []io.Closer
+ defer func() {
+ for _, resource := range resources {
+ _ = resource.Close()
+ }
+ }()
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+
+ // The event notify.
+ var thePublisher *testPublisher
+
+ mainReady, mainReadyCancel := context.WithCancel(context.Background())
+ publishReady, publishReadyCancel := context.WithCancel(context.Background())
+
+ streamSuffix := fmt.Sprintf("hevc-publish-flvplay-%v-%v", os.Getpid(), rand.Int())
+ // Objects init.
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ defer cancel()
+
+ doInit := func() (err error) {
+ // Initialize publisher with HEVC codec support.
+ if thePublisher, err = newTestPublisher(registerHEVCCodecs, func(pub *testPublisher) error {
+ pub.streamSuffix = streamSuffix
+ pub.streamQuery = "codec=hevc" // Add codec=hevc parameter for HEVC support
+ pub.videoCodec = "hevc" // Set video codec to HEVC
+ pub.iceReadyCancel = publishReadyCancel
+ resources = append(resources, pub)
+
+ return pub.Setup(*srsVnetClientIP)
+ }); err != nil {
+ return err
+ }
+
+ // Init done.
+ mainReadyCancel()
+
+ <-ctx.Done()
+ return nil
+ }
+
+ if err := doInit(); err != nil {
+ r1 = err
+ }
+ }()
+
+ // Run publisher.
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ defer cancel()
+
+ select {
+ case <-ctx.Done():
+ case <-mainReady.Done():
+ r2 = thePublisher.Run(logger.WithContext(ctx), cancel)
+ logger.Tf(ctx, "pub done")
+ }
+ }()
+
+ // Run player.
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ defer cancel()
+
+ select {
+ case <-ctx.Done():
+ return
+ case <-publishReady.Done():
+ }
+
+ player := NewFLVPlayer()
+ defer player.Close()
+
+ r3 = func() error {
+ flvUrl := fmt.Sprintf("http://%v%v-%v.flv", *srsHttpServer, *srsStream, streamSuffix)
+ if err := player.Play(ctx, flvUrl); err != nil {
+ return err
+ }
+
+ var nnVideo, nnAudio int
+ var hasVideo, hasAudio bool
+ var hevcDetected bool
+ player.onRecvHeader = func(ha, hv bool) error {
+ hasAudio, hasVideo = ha, hv
+ return nil
+ }
+ player.onRecvTag = func(tagType flv.TagType, size, timestamp uint32, tag []byte) error {
+ if tagType == flv.TagTypeAudio {
+ nnAudio++
+ } else if tagType == flv.TagTypeVideo {
+ nnVideo++
+ // Check for HEVC Enhanced RTMP format
+ if len(tag) >= 5 {
+ // Check for Enhanced RTMP header (0x80 | frame_type << 4 | packet_type)
+ // and HEVC fourCC 'hvc1'
+ if (tag[0]&0x80) != 0 && len(tag) >= 5 {
+ if tag[1] == 'h' && tag[2] == 'v' && tag[3] == 'c' && tag[4] == '1' {
+ hevcDetected = true
+ logger.Tf(ctx, "HEVC Enhanced RTMP format detected in video tag")
+ }
+ }
+ }
+ }
+ logger.Tf(ctx, "got %v tag, %v %vms %vB, hevc=%v", nnVideo+nnAudio, tagType, timestamp, len(tag), hevcDetected)
+
+ if audioPacketsOK, videoPacketsOK := hasAudio && nnAudio >= 10, hasVideo && nnVideo >= 10; audioPacketsOK && videoPacketsOK && hevcDetected {
+ logger.Tf(ctx, "HEVC Flv recv %v/%v audio, %v/%v video, hevc detected=%v", hasAudio, nnAudio, hasVideo, nnVideo, hevcDetected)
+ cancel()
+ }
+ return nil
+ }
+ if err := player.Consume(ctx); err != nil {
+ return err
+ }
+
+ return nil
+ }()
+ }()
+}
diff --git a/trunk/3rdparty/srs-bench/srs/util.go b/trunk/3rdparty/srs-bench/srs/util.go
index 2bb497c51..7836bf701 100644
--- a/trunk/3rdparty/srs-bench/srs/util.go
+++ b/trunk/3rdparty/srs-bench/srs/util.go
@@ -674,6 +674,34 @@ func registerMiniCodecsWithoutNack(api *testWebRTCAPI) error {
return nil
}
+// Implements interface testWebRTCAPIInitFunc to init testWebRTCAPI with HEVC support
+func registerHEVCCodecs(api *testWebRTCAPI) error {
+ v := api
+
+ // Register Opus audio codec
+ if err := v.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
+ RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
+ PayloadType: 111,
+ }, webrtc.RTPCodecTypeAudio); err != nil {
+ return err
+ }
+
+ // Register HEVC/H.265 video codec
+ videoRTCPFeedback := []webrtc.RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}}
+ if err := v.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
+ RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH265, 90000, 0, "profile-id=1", videoRTCPFeedback},
+ PayloadType: 49, // Use payload type 49 for HEVC as mentioned in PR description
+ }, webrtc.RTPCodecTypeVideo); err != nil {
+ return err
+ }
+
+ if err := webrtc.RegisterDefaultInterceptors(v.mediaEngine, v.registry); err != nil {
+ return err
+ }
+
+ return nil
+}
+
func newTestWebRTCAPI(inits ...testWebRTCAPIInitFunc) (*testWebRTCAPI, error) {
v := &testWebRTCAPI{}
@@ -1052,6 +1080,10 @@ type testPublisher struct {
api *testWebRTCAPI
// Optional suffix for stream url.
streamSuffix string
+ // Optional query parameters for stream url.
+ streamQuery string
+ // Video codec type: "h264", "hevc", "vp8"
+ videoCodec string
// To cancel the publisher, pass by Run.
cancel context.CancelFunc
// The config for peer connection.
@@ -1064,7 +1096,8 @@ func newTestPublisher(init testWebRTCAPIInitFunc, options ...testPublisherOption
sourceVideo, sourceAudio := *srsPublishVideo, *srsPublishAudio
v := &testPublisher{
- pcc: &webrtc.Configuration{},
+ pcc: &webrtc.Configuration{},
+ videoCodec: "h264", // Default to H.264
}
api, err := newTestWebRTCAPI(init)
@@ -1084,7 +1117,7 @@ func newTestPublisher(init testWebRTCAPIInitFunc, options ...testPublisherOption
v.aIngester = newAudioIngester(sourceAudio)
}
if sourceVideo != "" {
- v.vIngester = newVideoIngester(sourceVideo)
+ v.vIngester = newVideoIngester(sourceVideo, WithCodec(v.videoCodec))
}
// Setup the interceptors for packets.
@@ -1148,6 +1181,9 @@ func (v *testPublisher) Run(ctx context.Context, cancel context.CancelFunc) erro
if v.streamSuffix != "" {
r = fmt.Sprintf("%v-%v", r, v.streamSuffix)
}
+ if v.streamQuery != "" {
+ r = fmt.Sprintf("%v?%v", r, v.streamQuery)
+ }
sourceVideo, sourceAudio, fps := *srsPublishVideo, *srsPublishAudio, *srsPublishVideoFps
logger.Tf(ctx, "Run publish url=%v, audio=%v, video=%v, fps=%v",
diff --git a/trunk/configure b/trunk/configure
index 49a645e86..0a2e5cc26 100755
--- a/trunk/configure
+++ b/trunk/configure
@@ -464,8 +464,8 @@ if [[ $SRS_UTEST == YES ]]; then
MODULE_FILES=("srs_utest" "srs_utest_amf0" "srs_utest_kernel" "srs_utest_core"
"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_protocol" "srs_utest_protocol2" "srs_utest_kernel2" "srs_utest_protocol3"
+ "srs_utest_st" "srs_utest_rtc2")
if [[ $SRS_SRT == YES ]]; then
MODULE_FILES+=("srs_utest_srt")
fi
diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md
index f0485522b..1b046a4a7 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-03, Merge [#4349](https://github.com/ossrs/srs/pull/4349): rtc2rtmp: Support WebRTC-to-RTMP conversion with HEVC. v7.0.43 (#4349)
* v7.0, 2025-06-04, Merge [#4310](https://github.com/ossrs/srs/pull/4310): Player: Get codec by webrtc api: pc.getStats. v7.0.42 (#4310)
* v7.0, 2025-06-04, Merge [#4325](https://github.com/ossrs/srs/pull/4325): fix bug: loop transcoding #3516. v7.0.41 (#4325)
* v7.0, 2025-06-04, Merge [#4341](https://github.com/ossrs/srs/pull/4341): Update the release in the README for consistent. v7.0.40 (#4341)
diff --git a/trunk/src/app/srs_app_gb28181.cpp b/trunk/src/app/srs_app_gb28181.cpp
index e918fc3c5..e9ae2390e 100644
--- a/trunk/src/app/srs_app_gb28181.cpp
+++ b/trunk/src/app/srs_app_gb28181.cpp
@@ -1961,7 +1961,7 @@ srs_error_t SrsGbMuxer::write_h265_ipb_frame(char* frame, int frame_size, uint32
// F.3.29 intra random access point (IRAP) picture
// ITU-T-H.265-2021.pdf, page 462.
SrsVideoAvcFrameType frame_type = SrsVideoAvcFrameTypeInterFrame;
- if (nt >= SrsHevcNaluType_CODED_SLICE_BLA && nt <= SrsHevcNaluType_RESERVED_23) {
+ if (SrsIsIRAP(nt)) {
frame_type = SrsVideoAvcFrameTypeKeyFrame;
}
diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp
index 410db4185..cfa461049 100644
--- a/trunk/src/app/srs_app_rtc_conn.cpp
+++ b/trunk/src/app/srs_app_rtc_conn.cpp
@@ -1500,7 +1500,7 @@ srs_error_t SrsRtcPublishStream::check_send_nacks()
return err;
}
-void SrsRtcPublishStream::on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtspPacketPayloadType* ppt)
+void SrsRtcPublishStream::on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtpPacketPayloadType* ppt)
{
// No payload, ignore.
if (buf->empty()) {
@@ -2725,7 +2725,7 @@ srs_error_t SrsRtcConnection::negotiate_publish_capability(SrsRtcUserConfig* ruc
// Only choose one match opus codec.
break;
}
- } else if (remote_media_desc.is_video() && ruc->codec_ == "av1") {
+ } else if (remote_media_desc.is_video() && srs_video_codec_str2id(ruc->codec_) == SrsVideoCodecIdAV1) {
std::vector payloads = remote_media_desc.find_media_with_encoding_name("AV1");
if (payloads.empty()) {
// Be compatible with the Chrome M96, still check the AV1X encoding name
@@ -2762,7 +2762,7 @@ srs_error_t SrsRtcConnection::negotiate_publish_capability(SrsRtcUserConfig* ruc
track_desc->set_codec_payload((SrsCodecPayload*)video_payload);
break;
}
- } else if (remote_media_desc.is_video() && ruc->codec_ == "hevc") {
+ } else if (remote_media_desc.is_video() && srs_video_codec_str2id(ruc->codec_) == SrsVideoCodecIdHEVC) {
std::vector payloads = remote_media_desc.find_media_with_encoding_name("H265");
if (payloads.empty()) {
return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no found valid H.265 payload type");
@@ -2819,7 +2819,7 @@ srs_error_t SrsRtcConnection::negotiate_publish_capability(SrsRtcUserConfig* ruc
bool profile_matched = (!has_42e01f || h264_param.profile_level_id == "42e01f");
// Try to pick the "best match" H.264 payload type.
- if (profile_matched && h264_param.packetization_mode == "1" && h264_param.level_asymmerty_allow == "1") {
+ if (profile_matched && h264_param.packetization_mode == "1" && h264_param.level_asymmetry_allow == "1") {
// if the playload is opus, and the encoding_param_ is channel
SrsVideoPayload* video_payload = new SrsVideoPayload(payload.payload_type_, payload.encoding_name_, payload.clock_rate_);
video_payload->set_h264_param_desc(payload.format_specific_param_);
@@ -3127,24 +3127,19 @@ srs_error_t SrsRtcConnection::negotiate_play_capability(SrsRtcUserConfig* ruc, s
remote_payload = payloads.at(0);
track_descs = source->get_track_desc("audio", "opus");
} else if (remote_media_desc.is_video()) {
- std::string prefer_codec = ruc->codec_;
- if (prefer_codec.empty()) {
+ SrsVideoCodecId prefer_codec = srs_video_codec_str2id(ruc->codec_);
+ if (prefer_codec == SrsVideoCodecIdReserved) {
// Get the source codec if not specified.
std::vector track_descs = source->get_track_desc("video", "");
if (!track_descs.empty()) {
- std::string codec_name = track_descs.at(0)->media_->name_;
- std::transform(codec_name.begin(), codec_name.end(), codec_name.begin(), ::tolower);
- if (codec_name == "h265") {
- prefer_codec = "hevc";
- } else {
- prefer_codec = codec_name;
- }
+ SrsRtcTrackDescription* first_track = track_descs.at(0);
+ prefer_codec = srs_video_codec_str2id(first_track->media_->name_);
} else {
return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no video track in source");
}
}
- if (prefer_codec == "av1") {
+ if (prefer_codec == SrsVideoCodecIdAV1) {
std::vector payloads = remote_media_desc.find_media_with_encoding_name("AV1");
if (payloads.empty()) {
// Be compatible with the Chrome M96, still check the AV1X encoding name
@@ -3162,7 +3157,7 @@ srs_error_t SrsRtcConnection::negotiate_play_capability(SrsRtcUserConfig* ruc, s
// @see https://bugs.chromium.org/p/webrtc/issues/detail?id=13166
track_descs = source->get_track_desc("video", "AV1X");
}
- } else if (prefer_codec == "hevc") {
+ } else if (prefer_codec == SrsVideoCodecIdHEVC) {
std::vector payloads = remote_media_desc.find_media_with_encoding_name("H265");
if (payloads.empty()) {
return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no valid found h265 payload type");
@@ -3312,7 +3307,7 @@ void video_track_generate_play_offer(SrsRtcTrackDescription* track, string mid,
SrsVideoPayload* payload = (SrsVideoPayload*)track->media_;
- if (payload->name_ == "H265") {
+ if (srs_video_codec_str2id(payload->name_) == SrsVideoCodecIdHEVC) {
local_media_desc.payload_types_.push_back(payload->generate_media_payload_type_h265());
} else {
local_media_desc.payload_types_.push_back(payload->generate_media_payload_type());
diff --git a/trunk/src/app/srs_app_rtc_conn.hpp b/trunk/src/app/srs_app_rtc_conn.hpp
index 5a2df2700..89fb465b3 100644
--- a/trunk/src/app/srs_app_rtc_conn.hpp
+++ b/trunk/src/app/srs_app_rtc_conn.hpp
@@ -319,7 +319,7 @@ public:
};
// A RTC publish stream, client push and publish stream to SRS.
-class SrsRtcPublishStream : public ISrsRtspPacketDecodeHandler
+class SrsRtcPublishStream : public ISrsRtpPacketDecodeHandler
, public ISrsRtcPublishStream, public ISrsRtcPLIWorkerHandler
{
private:
@@ -378,7 +378,7 @@ private:
public:
srs_error_t check_send_nacks();
public:
- virtual void on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtspPacketPayloadType* ppt);
+ virtual void on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtpPacketPayloadType* ppt);
private:
srs_error_t send_periodic_twcc();
public:
diff --git a/trunk/src/app/srs_app_rtc_sdp.cpp b/trunk/src/app/srs_app_rtc_sdp.cpp
index d87c5e097..4d8028a81 100644
--- a/trunk/src/app/srs_app_rtc_sdp.cpp
+++ b/trunk/src/app/srs_app_rtc_sdp.cpp
@@ -75,7 +75,7 @@ srs_error_t srs_parse_h264_fmtp(const std::string& fmtp, H264SpecificParam& h264
// @see https://tools.ietf.org/html/rfc6184#section-6.3
h264_param.packetization_mode = kv[1];
} else if (kv[0] == "level-asymmetry-allowed") {
- h264_param.level_asymmerty_allow = kv[1];
+ h264_param.level_asymmetry_allow = kv[1];
}
}
@@ -85,7 +85,7 @@ srs_error_t srs_parse_h264_fmtp(const std::string& fmtp, H264SpecificParam& h264
if (h264_param.packetization_mode.empty()) {
return srs_error_new(ERROR_RTC_SDP_DECODE, "no h264 param: packetization-mode");
}
- if (h264_param.level_asymmerty_allow.empty()) {
+ if (h264_param.level_asymmetry_allow.empty()) {
return srs_error_new(ERROR_RTC_SDP_DECODE, "no h264 param: level-asymmetry-allowed");
}
diff --git a/trunk/src/app/srs_app_rtc_sdp.hpp b/trunk/src/app/srs_app_rtc_sdp.hpp
index 39dbb8f0d..261555124 100644
--- a/trunk/src/app/srs_app_rtc_sdp.hpp
+++ b/trunk/src/app/srs_app_rtc_sdp.hpp
@@ -94,7 +94,7 @@ struct H264SpecificParam
{
std::string profile_level_id;
std::string packetization_mode;
- std::string level_asymmerty_allow;
+ std::string level_asymmetry_allow;
};
struct H265SpecificParam
diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp
index f1bf7c098..7325760e7 100644
--- a/trunk/src/app/srs_app_rtc_source.cpp
+++ b/trunk/src/app/srs_app_rtc_source.cpp
@@ -626,7 +626,18 @@ srs_error_t SrsRtcSource::on_publish()
// If bridge to other source, handle event and start timer to request PLI.
if (bridge_) {
#ifdef SRS_FFMPEG_FIT
- if ((err = frame_builder_->initialize(req)) != srs_success) {
+ SrsAudioCodecId audio_codec = SrsAudioCodecIdOpus;
+ if (stream_desc_->audio_track_desc_ && stream_desc_->audio_track_desc_->media_) {
+ audio_codec = SrsAudioCodecId(stream_desc_->audio_track_desc_->media_->codec(false));
+ }
+
+ SrsVideoCodecId video_codec = SrsVideoCodecIdAVC;
+ if (stream_desc_->video_track_descs_.size() > 0) {
+ SrsRtcTrackDescription* track_desc = stream_desc_->video_track_descs_.at(0);
+ video_codec = SrsVideoCodecId(track_desc->media_->codec(true));
+ }
+
+ if ((err = frame_builder_->initialize(req, audio_codec, video_codec)) != srs_success) {
return srs_error_wrap(err, "frame builder initialize");
}
@@ -775,9 +786,8 @@ std::vector SrsRtcSource::get_track_desc(std::string ty
return track_descs;
}
- string name = stream_desc_->audio_track_desc_->media_->name_;
- std::transform(name.begin(), name.end(), name.begin(), static_cast(std::tolower));
- if (name == media_name) {
+ SrsAudioCodecId codec = SrsAudioCodecId(stream_desc_->audio_track_desc_->media_->codec(false));
+ if (codec == srs_audio_codec_str2id(media_name)) {
track_descs.push_back(stream_desc_->audio_track_desc_);
}
}
@@ -788,9 +798,8 @@ std::vector SrsRtcSource::get_track_desc(std::string ty
if (media_name.empty()) {
track_descs.push_back(*it);
} else {
- string name = (*it)->media_->name_;
- std::transform(name.begin(), name.end(), name.begin(), static_cast(std::toupper));
- if (name == media_name) {
+ SrsVideoCodecId codec = SrsVideoCodecId((*it)->media_->codec(true));
+ if (codec == srs_video_codec_str2id(media_name)) {
track_descs.push_back(*it);
}
}
@@ -1044,7 +1053,7 @@ srs_error_t SrsRtcRtpBuilder::package_opus(SrsAudioFrame* audio, SrsRtpPacket* p
pkt->header.set_timestamp(audio->dts * 48);
SrsRtpRawPayload* raw = new SrsRtpRawPayload();
- pkt->set_payload(raw, SrsRtspPacketPayloadTypeRaw);
+ pkt->set_payload(raw, SrsRtpPacketPayloadTypeRaw);
srs_assert(audio->nb_samples == 1);
raw->payload = pkt->wrap(audio->samples[0].bytes, audio->samples[0].size);
@@ -1088,11 +1097,9 @@ srs_error_t SrsRtcRtpBuilder::on_video(SrsSharedPtrMessage* msg)
return err;
}
-#ifdef SRS_H265
if ((err = bridge_->update_codec(vcodec)) != srs_success) {
return srs_error_wrap(err, "update codec");
}
-#endif
bool has_idr = false;
vector samples;
@@ -1215,6 +1222,7 @@ srs_error_t SrsRtcRtpBuilder::package_stap_a(SrsSharedPtrMessage* msg, SrsRtpPac
ISrsRtpPayloader* stap = NULL;
vector*> params;
int size = 0;
+
if (format->vcodec->id == SrsVideoCodecIdHEVC) {
for (size_t i = 0; i < format->vcodec->hevc_dec_conf_record_.nalu_vec.size(); i++) {
if (format->vcodec->hevc_dec_conf_record_.nalu_vec[i].nal_unit_type == SrsHevcNaluType_VPS
@@ -1227,7 +1235,7 @@ srs_error_t SrsRtcRtpBuilder::package_stap_a(SrsSharedPtrMessage* msg, SrsRtpPac
}
stap = new SrsRtpSTAPPayloadHevc();
- pkt->set_payload(stap, SrsRtspPacketPayloadTypeSTAPHevc);
+ pkt->set_payload(stap, SrsRtpPacketPayloadTypeSTAPHevc);
pkt->nalu_type = kStapHevc;
} else if (format->vcodec->id == SrsVideoCodecIdAVC) {
params.push_back(&format->vcodec->sequenceParameterSetNALUnit);
@@ -1235,7 +1243,7 @@ srs_error_t SrsRtcRtpBuilder::package_stap_a(SrsSharedPtrMessage* msg, SrsRtpPac
size = format->vcodec->sequenceParameterSetNALUnit.size() + format->vcodec->pictureParameterSetNALUnit.size();
stap = new SrsRtpSTAPPayload();
- pkt->set_payload(stap, SrsRtspPacketPayloadTypeSTAP);
+ pkt->set_payload(stap, SrsRtpPacketPayloadTypeSTAP);
pkt->nalu_type = kStapA;
}
@@ -1307,7 +1315,7 @@ srs_error_t SrsRtcRtpBuilder::package_nalus(SrsSharedPtrMessage* msg, const vect
pkt->nalu_type = first_nalu_type;
pkt->header.set_sequence(video_sequence++);
pkt->header.set_timestamp(msg->timestamp * 90);
- pkt->set_payload(raw_raw, SrsRtspPacketPayloadTypeNALU);
+ pkt->set_payload(raw_raw, SrsRtpPacketPayloadTypeNALU);
pkt->wrap(msg);
} else {
// We must free it, should never use RTP packets to free it,
@@ -1348,7 +1356,7 @@ srs_error_t SrsRtcRtpBuilder::package_nalus(SrsSharedPtrMessage* msg, const vect
fua->start = bool(i == 0);
fua->end = bool(i == num_of_packet - 1);
- pkt->set_payload(fua, SrsRtspPacketPayloadTypeFUAHevc);
+ pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUAHevc);
} else {
SrsRtpFUAPayload* fua = new SrsRtpFUAPayload();
if ((err = raw->read_samples(fua->nalus, packet_size)) != srs_success) {
@@ -1359,7 +1367,7 @@ srs_error_t SrsRtcRtpBuilder::package_nalus(SrsSharedPtrMessage* msg, const vect
fua->start = bool(i == 0);
fua->end = bool(i == num_of_packet - 1);
- pkt->set_payload(fua, SrsRtspPacketPayloadTypeFUA);
+ pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUA);
}
pkt->wrap(msg);
@@ -1386,7 +1394,7 @@ srs_error_t SrsRtcRtpBuilder::package_single_nalu(SrsSharedPtrMessage* msg, SrsS
pkt->header.set_timestamp(msg->timestamp * 90);
SrsRtpRawPayload* raw = new SrsRtpRawPayload();
- pkt->set_payload(raw, SrsRtspPacketPayloadTypeRaw);
+ pkt->set_payload(raw, SrsRtpPacketPayloadTypeRaw);
raw->payload = sample->bytes;
raw->nn_payload = sample->size;
@@ -1430,7 +1438,7 @@ srs_error_t SrsRtcRtpBuilder::package_fu_a(SrsSharedPtrMessage* msg, SrsSample*
if (is_hevc) {
// H265 FU-A header
SrsRtpFUAPayloadHevc2* fua = new SrsRtpFUAPayloadHevc2();
- pkt->set_payload(fua, SrsRtspPacketPayloadTypeFUAHevc2);
+ pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUAHevc2);
fua->nalu_type = SrsHevcNaluTypeParse(header);
fua->start = bool(i == 0);
@@ -1441,7 +1449,7 @@ srs_error_t SrsRtcRtpBuilder::package_fu_a(SrsSharedPtrMessage* msg, SrsSample*
} else {
// H264 FU-A header
SrsRtpFUAPayload2* fua = new SrsRtpFUAPayload2();
- pkt->set_payload(fua, SrsRtspPacketPayloadTypeFUA2);
+ pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUA2);
fua->nri = (SrsAvcNaluType)header;
fua->nalu_type = SrsAvcNaluTypeParse(header);
@@ -1477,42 +1485,299 @@ srs_error_t SrsRtcRtpBuilder::consume_packets(vector& pkts)
return err;
}
+SrsRtcFrameBuilderVideoPacketCache::SrsRtcFrameBuilderVideoPacketCache()
+{
+ memset(cache_pkts_, 0, sizeof(cache_pkts_));
+}
+
+SrsRtcFrameBuilderVideoPacketCache::~SrsRtcFrameBuilderVideoPacketCache()
+{
+ clear_all();
+}
+
+SrsRtpPacket* SrsRtcFrameBuilderVideoPacketCache::get_packet(uint16_t sequence_number)
+{
+ uint16_t index = cache_index(sequence_number);
+ const RtcPacketCache& cache = cache_pkts_[index];
+
+ // Since cache uses modulo indexing, different sequence numbers can map to the
+ // same cache slot, so we must verify the stored sn matches the requested one.
+ if (!cache.in_use || cache.sn != sequence_number) {
+ return NULL;
+ }
+
+ return cache.pkt;
+}
+
+void SrsRtcFrameBuilderVideoPacketCache::store_packet(SrsRtpPacket* pkt)
+{
+ if (!pkt) {
+ return; // Ignore null packets
+ }
+
+ uint16_t index = cache_index(pkt->header.get_sequence());
+ RtcPacketCache& cache = cache_pkts_[index];
+
+ cache.in_use = true;
+ srs_freep(cache.pkt);
+ cache.pkt = pkt;
+ cache.sn = pkt->header.get_sequence();
+ cache.ts = pkt->get_avsync_time();
+ cache.rtp_ts = pkt->header.get_timestamp();
+}
+
+bool SrsRtcFrameBuilderVideoPacketCache::is_slot_in_use(uint16_t sequence_number)
+{
+ uint16_t index = cache_index(sequence_number);
+ const RtcPacketCache& cache = cache_pkts_[index];
+ return cache.in_use;
+}
+
+uint32_t SrsRtcFrameBuilderVideoPacketCache::get_rtp_timestamp(uint16_t sequence_number)
+{
+ uint16_t index = cache_index(sequence_number);
+ const RtcPacketCache& cache = cache_pkts_[index];
+ return cache.rtp_ts;
+}
+
+void SrsRtcFrameBuilderVideoPacketCache::clear_all()
+{
+ for (size_t i = 0; i < cache_size_; i++) {
+ RtcPacketCache& cache = cache_pkts_[i];
+ if (cache.in_use) {
+ srs_freep(cache.pkt);
+ cache.sn = 0;
+ cache.ts = 0;
+ cache.rtp_ts = 0;
+ cache.in_use = false;
+ }
+ }
+}
+
+SrsRtpPacket* SrsRtcFrameBuilderVideoPacketCache::take_packet(uint16_t sequence_number)
+{
+ uint16_t index = cache_index(sequence_number);
+ RtcPacketCache& cache = cache_pkts_[index];
+
+ // Since cache uses modulo indexing, different sequence numbers can map to the
+ // same cache slot, so we must verify the stored sn matches the requested one.
+ if (!cache.in_use || cache.sn != sequence_number) {
+ return NULL;
+ }
+
+ SrsRtpPacket* pkt = cache.pkt;
+
+ // Clear the slot after taking the packet
+ cache.in_use = false;
+ // Note: No memory leak here - the packet ownership is transferred to caller
+ cache.pkt = NULL;
+ cache.ts = 0;
+ cache.rtp_ts = 0;
+ cache.sn = 0;
+
+ return pkt;
+}
+
+int32_t SrsRtcFrameBuilderVideoPacketCache::find_next_lost_sn(uint16_t current_sn, uint16_t header_sn, uint16_t& end_sn)
+{
+ uint32_t last_rtp_ts = get_rtp_timestamp(header_sn);
+ for (int i = 0; i < cache_size_; ++i) {
+ uint16_t lost_sn = current_sn + i;
+
+ if (!is_slot_in_use(lost_sn)) {
+ return lost_sn;
+ }
+
+ //check time first, avoid two small frame mixed case decode fail
+ if (last_rtp_ts != get_rtp_timestamp(lost_sn)) {
+ end_sn = lost_sn - 1;
+ return -1;
+ }
+
+ SrsRtpPacket* pkt = get_packet(lost_sn);
+ if (pkt && pkt->header.get_marker()) {
+ end_sn = lost_sn;
+ return -1;
+ }
+ }
+
+ srs_error("cache overflow. the packet count of video frame is more than %u", cache_size_);
+ return -2;
+}
+
+bool SrsRtcFrameBuilderVideoPacketCache::check_frame_complete(const uint16_t start, const uint16_t end)
+{
+ int16_t cnt = srs_rtp_seq_distance(start, end) + 1;
+ srs_assert(cnt >= 1);
+
+ uint16_t nn_fu_start = 0;
+ uint16_t nn_fu_end = 0;
+ for (uint16_t i = 0; i < (uint16_t)cnt; ++i) {
+ uint16_t sequence_number = start + i;
+ SrsRtpPacket* pkt = get_packet(sequence_number);
+
+ // fix crash when pkt->payload() if pkt is nullptr;
+ if (!pkt) continue;
+
+ SrsRtpFUAPayload2* fua_payload = dynamic_cast(pkt->payload());
+ if (!fua_payload) continue;
+
+ if (fua_payload->start) {
+ ++nn_fu_start;
+ }
+
+ if (fua_payload->end) {
+ ++nn_fu_end;
+ }
+ }
+
+ return nn_fu_start == nn_fu_end;
+}
+
+SrsRtcFrameBuilderVideoFrameDetector::SrsRtcFrameBuilderVideoFrameDetector(SrsRtcFrameBuilderVideoPacketCache* cache)
+{
+ video_cache_ = cache;
+ header_sn_ = 0;
+ lost_sn_ = 0;
+ rtp_key_frame_ts_ = -1;
+}
+
+SrsRtcFrameBuilderVideoFrameDetector::~SrsRtcFrameBuilderVideoFrameDetector()
+{
+}
+
+void SrsRtcFrameBuilderVideoFrameDetector::on_keyframe_start(SrsRtpPacket* pkt)
+{
+ if (-1 == rtp_key_frame_ts_) {
+ rtp_key_frame_ts_ = pkt->header.get_timestamp();
+ header_sn_ = pkt->header.get_sequence();
+ lost_sn_ = header_sn_ + 1;
+ // Received key frame and clean cache of old p frame pkts
+ video_cache_->clear_all();
+ srs_trace("RTC2RTMP: keyframe set ts=%u, header=%hu, lost=%hu", (uint32_t)rtp_key_frame_ts_, header_sn_, lost_sn_);
+ } else if (rtp_key_frame_ts_ != pkt->header.get_timestamp()) {
+ //new key frame, clean cache
+ int64_t old_ts = rtp_key_frame_ts_;
+ uint16_t old_header_sn = header_sn_;
+ uint16_t old_lost_sn = lost_sn_;
+ rtp_key_frame_ts_ = pkt->header.get_timestamp();
+ header_sn_ = pkt->header.get_sequence();
+ lost_sn_ = header_sn_ + 1;
+ video_cache_->clear_all();
+ srs_warn("RTC2RTMP: keyframe drop old ts=%u, header=%hu, lost=%hu, set new ts=%u, header=%hu, lost=%hu",
+ (uint32_t)old_ts, old_header_sn, old_lost_sn, (uint32_t)rtp_key_frame_ts_, header_sn_, lost_sn_);
+ }
+}
+
+srs_error_t SrsRtcFrameBuilderVideoFrameDetector::detect_frame(uint16_t received, uint16_t& frame_start, uint16_t& frame_end, bool& frame_ready)
+{
+ srs_error_t err = srs_success;
+ frame_ready = false;
+
+ int32_t sn;
+ uint16_t tail_sn = 0;
+ if (srs_rtp_seq_distance(header_sn_, received) < 0){
+ // When receive previous pkt in the same frame, update header sn;
+ header_sn_ = received;
+ sn = video_cache_->find_next_lost_sn(received, header_sn_, tail_sn);
+ } else if (lost_sn_ == received) {
+ sn = video_cache_->find_next_lost_sn(received, header_sn_, tail_sn);
+ } else {
+ sn = lost_sn_;
+ }
+
+ if (-1 == sn) {
+ if (video_cache_->check_frame_complete(header_sn_, tail_sn)) {
+ frame_start = header_sn_;
+ frame_end = tail_sn;
+ frame_ready = true;
+ }
+ } else if (-2 == sn) {
+ return srs_error_new(ERROR_RTC_RTP_MUXER, "video cache is overflow");
+ } else {
+ lost_sn_ = (uint16_t)sn;
+ }
+
+ return err;
+}
+
+srs_error_t SrsRtcFrameBuilderVideoFrameDetector::detect_next_frame(uint16_t next_head, uint16_t& frame_start, uint16_t& frame_end, bool& frame_ready)
+{
+ srs_error_t err = srs_success;
+ frame_ready = false;
+
+ header_sn_ = next_head;
+ uint16_t tail_sn = 0;
+ int32_t sn = video_cache_->find_next_lost_sn(header_sn_, header_sn_, tail_sn);
+
+ if (-1 == sn) {
+ if (video_cache_->check_frame_complete(header_sn_, tail_sn)) {
+ frame_start = header_sn_;
+ frame_end = tail_sn;
+ frame_ready = true;
+ }
+ } else if (-2 == sn) {
+ return srs_error_new(ERROR_RTC_RTP_MUXER, "video cache is overflow");
+ } else {
+ lost_sn_ = (uint16_t)sn;
+ }
+
+ return err;
+}
+
+void SrsRtcFrameBuilderVideoFrameDetector::on_keyframe_detached()
+{
+ rtp_key_frame_ts_ = -1;
+}
+
+bool SrsRtcFrameBuilderVideoFrameDetector::is_lost_sn(uint16_t received)
+{
+ return lost_sn_ == received;
+}
+
SrsRtcFrameBuilder::SrsRtcFrameBuilder(ISrsStreamBridge* bridge)
{
bridge_ = bridge;
is_first_audio_ = true;
- codec_ = NULL;
- header_sn_ = 0;
- memset(cache_video_pkts_, 0, sizeof(cache_video_pkts_));
- rtp_key_frame_ts_ = -1;
+ audio_transcoder_ = NULL;
+ video_codec_ = SrsVideoCodecIdAVC;
+ video_cache_ = new SrsRtcFrameBuilderVideoPacketCache();
+ frame_detector_ = new SrsRtcFrameBuilderVideoFrameDetector(video_cache_);
sync_state_ = -1;
- obs_whip_sps_ = obs_whip_pps_ = NULL;
+ obs_whip_vps_ = obs_whip_sps_ = obs_whip_pps_ = NULL;
}
SrsRtcFrameBuilder::~SrsRtcFrameBuilder()
{
- srs_freep(codec_);
- clear_cached_video();
+ srs_freep(audio_transcoder_);
+ srs_freep(video_cache_);
+ srs_freep(frame_detector_);
+ srs_freep(obs_whip_vps_);
srs_freep(obs_whip_sps_);
srs_freep(obs_whip_pps_);
}
-srs_error_t SrsRtcFrameBuilder::initialize(SrsRequest* r)
+srs_error_t SrsRtcFrameBuilder::initialize(SrsRequest* r, SrsAudioCodecId audio_codec, SrsVideoCodecId video_codec)
{
srs_error_t err = srs_success;
- srs_freep(codec_);
- codec_ = new SrsAudioTranscoder();
+ srs_freep(audio_transcoder_);
+ audio_transcoder_ = new SrsAudioTranscoder();
- SrsAudioCodecId from = SrsAudioCodecIdOpus; // TODO: From SDP?
SrsAudioCodecId to = SrsAudioCodecIdAAC; // The output audio codec.
int channels = 2; // The output audio channels.
int sample_rate = 48000; // The output audio sample rate in HZ.
int bitrate = _srs_config->get_rtc_aac_bitrate(r->vhost); // The output audio bitrate in bps.
- if ((err = codec_->initialize(from, to, channels, sample_rate, bitrate)) != srs_success) {
+
+ // TODO: FIXME:
+ // In the future, when we support enhanced-RTMP with Opus format,
+ // this transcoding will no longer be necessary.
+ if ((err = audio_transcoder_->initialize(audio_codec, to, channels, sample_rate, bitrate)) != srs_success) {
return srs_error_wrap(err, "bridge initialize");
}
+ video_codec_ = video_codec;
+
return err;
}
@@ -1569,7 +1834,7 @@ srs_error_t SrsRtcFrameBuilder::transcode_audio(SrsRtpPacket *pkt)
if (is_first_audio_) {
int header_len = 0;
uint8_t* header = NULL;
- codec_->aac_codec_header(&header, &header_len);
+ audio_transcoder_->aac_codec_header(&header, &header_len);
SrsCommonMessage out_rtmp;
packet_aac(&out_rtmp, (char *)header, header_len, ts, is_first_audio_);
@@ -1594,13 +1859,14 @@ srs_error_t SrsRtcFrameBuilder::transcode_audio(SrsRtpPacket *pkt)
frame.dts = ts;
frame.cts = 0;
- err = codec_->transcode(&frame, out_pkts);
+ err = audio_transcoder_->transcode(&frame, out_pkts);
if (err != srs_success) {
return err;
}
for (std::vector::iterator it = out_pkts.begin(); it != out_pkts.end(); ++it) {
SrsCommonMessage out_rtmp;
+ // TODO: FIXME: Should never directly use it, please define a variable with class name.
out_rtmp.header.timestamp = (*it)->dts;
packet_aac(&out_rtmp, (*it)->samples[0].bytes, (*it)->samples[0].size, ts, is_first_audio_);
@@ -1614,7 +1880,7 @@ srs_error_t SrsRtcFrameBuilder::transcode_audio(SrsRtpPacket *pkt)
break;
}
}
- codec_->free_frames(out_pkts);
+ audio_transcoder_->free_frames(out_pkts);
return err;
}
@@ -1636,40 +1902,27 @@ void SrsRtcFrameBuilder::packet_aac(SrsCommonMessage* audio, char* data, int len
audio->size = rtmp_len;
}
-srs_error_t SrsRtcFrameBuilder::packet_video(SrsRtpPacket* src)
+srs_error_t SrsRtcFrameBuilder::packet_video(SrsRtpPacket* pkt)
{
srs_error_t err = srs_success;
- // TODO: Only copy when need
- SrsRtpPacket* pkt = src->copy();
-
- if (pkt->is_keyframe()) {
+ // For keyframe.
+ if (pkt->is_keyframe(video_codec_)) {
return packet_video_key_frame(pkt);
}
- // store in cache
- int index = cache_index(pkt->header.get_sequence());
- cache_video_pkts_[index].in_use = true;
- srs_freep(cache_video_pkts_[index].pkt);
- cache_video_pkts_[index].pkt = pkt;
- cache_video_pkts_[index].sn = pkt->header.get_sequence();
- cache_video_pkts_[index].ts = pkt->get_avsync_time();
- cache_video_pkts_[index].rtp_ts = pkt->header.get_timestamp();
+ // For non-keyframe.
+ video_cache_->store_packet(pkt->copy());
// check whether to recovery lost packet and can construct a video frame
- if (lost_sn_ == pkt->header.get_sequence()) {
- uint16_t tail_sn = 0;
- int sn = find_next_lost_sn(lost_sn_, tail_sn);
- if (-1 == sn ) {
- if (check_frame_complete(header_sn_, tail_sn)) {
- if ((err = packet_video_rtmp(header_sn_, tail_sn)) != srs_success) {
- err = srs_error_wrap(err, "fail to pack video frame");
- }
- }
- } else if (-2 == sn) {
- return srs_error_new(ERROR_RTC_RTP_MUXER, "video cache is overflow");
- } else {
- lost_sn_ = (uint16_t)sn;
+ uint16_t current_sn = pkt->header.get_sequence();
+ if (frame_detector_->is_lost_sn(current_sn)) {
+ uint16_t start, end; bool got_frame;
+ if ((err = frame_detector_->detect_frame(current_sn, start, end, got_frame)) != srs_success) {
+ return srs_error_wrap(err, "detect frame failed");
+ }
+ if (got_frame && (err = packet_video_rtmp(start, end)) != srs_success) {
+ err = srs_error_wrap(err, "fail to pack video frame, start=%u, end=%u", start, end);
}
}
@@ -1680,59 +1933,31 @@ srs_error_t SrsRtcFrameBuilder::packet_video_key_frame(SrsRtpPacket* pkt)
{
srs_error_t err = srs_success;
- err = packet_sequence_header_avc(pkt);
+ if (video_codec_ == SrsVideoCodecIdAVC) {
+ err = packet_sequence_header_avc(pkt);
+ } else if (video_codec_ == SrsVideoCodecIdHEVC) {
+ err = packet_sequence_header_hevc(pkt);
+ } else {
+ err = srs_error_new(ERROR_RTC_RTP_MUXER, "unsupported video codec %d", video_codec_);
+ }
+
if (err != srs_success) {
return srs_error_wrap(err, "packet video key frame");
}
- if (-1 == rtp_key_frame_ts_) {
- rtp_key_frame_ts_ = pkt->header.get_timestamp();
- header_sn_ = pkt->header.get_sequence();
- lost_sn_ = header_sn_ + 1;
- // Received key frame and clean cache of old p frame pkts
- clear_cached_video();
- srs_trace("set ts=%u, header=%hu, lost=%hu", (uint32_t)rtp_key_frame_ts_, header_sn_, lost_sn_);
- } else if (rtp_key_frame_ts_ != pkt->header.get_timestamp()) {
- //new key frame, clean cache
- int64_t old_ts = rtp_key_frame_ts_;
- uint16_t old_header_sn = header_sn_;
- uint16_t old_lost_sn = lost_sn_;
- rtp_key_frame_ts_ = pkt->header.get_timestamp();
- header_sn_ = pkt->header.get_sequence();
- lost_sn_ = header_sn_ + 1;
- clear_cached_video();
- srs_warn("drop old ts=%u, header=%hu, lost=%hu, set new ts=%u, header=%hu, lost=%hu",
- (uint32_t)old_ts, old_header_sn, old_lost_sn, (uint32_t)rtp_key_frame_ts_, header_sn_, lost_sn_);
- }
+ frame_detector_->on_keyframe_start(pkt);
- uint16_t index = cache_index(pkt->header.get_sequence());
- cache_video_pkts_[index].in_use = true;
- srs_freep(cache_video_pkts_[index].pkt);
- cache_video_pkts_[index].pkt = pkt;
- cache_video_pkts_[index].sn = pkt->header.get_sequence();
- cache_video_pkts_[index].ts = pkt->get_avsync_time();
- cache_video_pkts_[index].rtp_ts = pkt->header.get_timestamp();
+ video_cache_->store_packet(pkt->copy());
- int32_t sn = lost_sn_;
- uint16_t tail_sn = 0;
- if (srs_rtp_seq_distance(header_sn_, pkt->header.get_sequence()) < 0){
- // When receive previous pkt in the same frame, update header sn;
- header_sn_ = pkt->header.get_sequence();
- sn = find_next_lost_sn(header_sn_, tail_sn);
- } else if (lost_sn_ == pkt->header.get_sequence()) {
- sn = find_next_lost_sn(lost_sn_, tail_sn);
- }
-
- if (-1 == sn) {
- if (check_frame_complete(header_sn_, tail_sn)) {
- if ((err = packet_video_rtmp(header_sn_, tail_sn)) != srs_success) {
- err = srs_error_wrap(err, "fail to packet frame");
- }
+ uint16_t current_sn = pkt->header.get_sequence();
+ if (frame_detector_->is_lost_sn(current_sn)) {
+ uint16_t start, end; bool got_frame;
+ if ((err = frame_detector_->detect_frame(current_sn, start, end, got_frame)) != srs_success) {
+ return srs_error_wrap(err, "detect frame failed");
+ }
+ if (got_frame && (err = packet_video_rtmp(start, end)) != srs_success) {
+ err = srs_error_wrap(err, "fail to pack video frame, start=%u, end=%u", start, end);
}
- } else if (-2 == sn) {
- return srs_error_new(ERROR_RTC_RTP_MUXER, "video cache is overflow");
- } else {
- lost_sn_ = (uint16_t)sn;
}
return err;
@@ -1833,6 +2058,244 @@ srs_error_t SrsRtcFrameBuilder::do_packet_sequence_header_avc(SrsRtpPacket* pkt,
return err;
}
+srs_error_t SrsRtcFrameBuilder::packet_sequence_header_hevc(SrsRtpPacket* pkt)
+{
+ srs_error_t err = srs_success;
+
+ // For OBS WHIP, it uses RTP Raw packet with VPS/SPS/PPS/IDR frame. Note that not all
+ // raw payload is VPS/SPS/PPS.
+ bool has_vps_sps_pps_in_raw_payload = false;
+ SrsRtpRawPayload* raw_payload = dynamic_cast(pkt->payload());
+ if (raw_payload) {
+ if (pkt->nalu_type == SrsHevcNaluType_VPS) {
+ has_vps_sps_pps_in_raw_payload = true;
+ srs_freep(obs_whip_vps_);
+ obs_whip_vps_ = pkt->copy();
+ } else if (pkt->nalu_type == SrsHevcNaluType_SPS) {
+ has_vps_sps_pps_in_raw_payload = true;
+ srs_freep(obs_whip_sps_);
+ obs_whip_sps_ = pkt->copy();
+ } else if (pkt->nalu_type == SrsHevcNaluType_PPS) {
+ has_vps_sps_pps_in_raw_payload = true;
+ srs_freep(obs_whip_pps_);
+ obs_whip_pps_ = pkt->copy();
+ }
+ // Ignore if one of OBS WHIP VPS/SPS/PPS is not ready.
+ if (has_vps_sps_pps_in_raw_payload && (!obs_whip_vps_ || !obs_whip_sps_ || !obs_whip_pps_)) {
+ return err;
+ }
+ }
+
+ // Generally, there will be SPS+PPS+IDR in a STAP-A packet.
+ SrsRtpSTAPPayloadHevc* stap_payload_hevc = dynamic_cast(pkt->payload());
+ if (video_codec_ == SrsVideoCodecIdHEVC && (stap_payload_hevc || has_vps_sps_pps_in_raw_payload)) {
+ SrsSample* vps = stap_payload_hevc ? stap_payload_hevc->get_vps() : NULL;
+ if (!vps && obs_whip_vps_) vps = dynamic_cast(obs_whip_vps_->payload())->sample_;
+ SrsSample* sps = stap_payload_hevc ? stap_payload_hevc->get_sps() : NULL;
+ if (!sps && obs_whip_sps_) sps = dynamic_cast(obs_whip_sps_->payload())->sample_;
+ SrsSample* pps = stap_payload_hevc ? stap_payload_hevc->get_pps() : NULL;
+ if (!pps && obs_whip_pps_) pps = dynamic_cast(obs_whip_pps_->payload())->sample_;
+ if (!vps || !sps || !pps) {
+ return srs_error_new(ERROR_RTC_RTP_MUXER, "no vps/sps/pps in stap-a hevc rtp. vps: %p, sps:%p, pps:%p", vps, sps, pps);
+ }
+
+ err = do_packet_sequence_header_hevc(pkt, vps, sps, pps);
+
+ // Always reset the VPS/SPS/PPS cache after used it.
+ srs_freep(obs_whip_vps_);
+ srs_freep(obs_whip_sps_);
+ srs_freep(obs_whip_pps_);
+
+ if (err != srs_success) {
+ return srs_error_wrap(err, "packet vps/sps/pps");
+ }
+ }
+
+ return err;
+}
+
+srs_error_t SrsRtcFrameBuilder::do_packet_sequence_header_hevc(SrsRtpPacket* pkt, SrsSample* vps, SrsSample* sps, SrsSample* pps)
+{
+ srs_error_t err = srs_success;
+
+ std::string sh;
+ SrsUniquePtr hevc(new SrsRawHEVCStream());
+ std::vector h265_pps = { string(pps->bytes, pps->size) };
+ if ((err = hevc->mux_sequence_header(string(vps->bytes, vps->size), string(sps->bytes, sps->size), h265_pps, sh)) != srs_success) {
+ return srs_error_wrap(err, "mux sequence header");
+ }
+
+ char* flv = NULL;
+ int nb_flv = 0;
+ if ((err = hevc->mux_avc2flv_enhanced(sh, SrsVideoAvcFrameTypeKeyFrame, SrsVideoHEVCFrameTraitPacketTypeSequenceStart, pkt->get_avsync_time(),
+ pkt->get_avsync_time(), &flv, &nb_flv)) != srs_success) {
+ return srs_error_wrap(err, "mux sequence header");
+ }
+
+ SrsMessageHeader header;
+ header.initialize_video(nb_flv, pkt->get_avsync_time(), 1);
+ SrsCommonMessage rtmp;
+ if ((err = rtmp.create(&header, flv, nb_flv)) != srs_success) {
+ return srs_error_wrap(err, "create rtmp");
+ }
+
+ SrsSharedPtrMessage msg;
+ if ((err = msg.create(&rtmp)) != srs_success) {
+ return srs_error_wrap(err, "create message");
+ }
+
+ if ((err = bridge_->on_frame(&msg)) != srs_success) {
+ return err;
+ }
+
+ return err;
+}
+
+int SrsRtcFrameBuilder::calculate_packet_payload_size(SrsRtpPacket* pkt)
+{
+ if (!pkt || !pkt->payload()) {
+ return 0;
+ }
+
+ // H.264 FU-A payload
+ SrsRtpFUAPayload2* fua_payload = dynamic_cast(pkt->payload());
+ if (fua_payload && fua_payload->size > 0) {
+ int size = fua_payload->size;
+ if (fua_payload->start) {
+ size += 1 + 4; // NALU header + length prefix
+ }
+ return size;
+ }
+
+ // H.264 STAP-A payload
+ SrsRtpSTAPPayload* stap_payload = dynamic_cast(pkt->payload());
+ if (stap_payload) {
+ int size = 0;
+ for (int j = 0; j < (int)stap_payload->nalus.size(); ++j) {
+ SrsSample* sample = stap_payload->nalus.at(j);
+ if (sample->size > 0) {
+ size += 4 + sample->size; // length prefix + NALU
+ }
+ }
+ return size;
+ }
+
+ // H.265 FU-A payload
+ SrsRtpFUAPayloadHevc2* fua_payload_hevc = dynamic_cast(pkt->payload());
+ if (fua_payload_hevc && fua_payload_hevc->size > 0) {
+ int size = fua_payload_hevc->size;
+ if (fua_payload_hevc->start) {
+ size += 2 + 4; // HEVC NALU header + length prefix
+ }
+ return size;
+ }
+
+ // H.265 STAP payload
+ SrsRtpSTAPPayloadHevc* stap_payload_hevc = dynamic_cast(pkt->payload());
+ if (stap_payload_hevc) {
+ int size = 0;
+ for (int j = 0; j < (int)stap_payload_hevc->nalus.size(); ++j) {
+ SrsSample* sample = stap_payload_hevc->nalus.at(j);
+ if (sample->size > 0) {
+ size += 4 + sample->size; // length prefix + NALU
+ }
+ }
+ return size;
+ }
+
+ // Raw payload
+ SrsRtpRawPayload* raw_payload = dynamic_cast(pkt->payload());
+ if (raw_payload && raw_payload->nn_payload > 0) {
+ return 4 + raw_payload->nn_payload; // length prefix + payload
+ }
+
+ return 0;
+}
+
+void SrsRtcFrameBuilder::write_packet_payload_to_buffer(SrsRtpPacket* pkt, SrsBuffer& payload, int& nalu_len)
+{
+ if (!pkt || !pkt->payload()) {
+ return;
+ }
+
+ // H.264 FU-A payload
+ SrsRtpFUAPayload2* fua_payload = dynamic_cast(pkt->payload());
+ if (fua_payload && fua_payload->size > 0) {
+ if (fua_payload->start) {
+ nalu_len = fua_payload->size + 1;
+ payload.skip(4); // Skip 4 bytes to write nalu_len later
+ payload.write_1bytes(fua_payload->nri | fua_payload->nalu_type);
+ payload.write_bytes(fua_payload->payload, fua_payload->size);
+ } else {
+ nalu_len += fua_payload->size;
+ payload.write_bytes(fua_payload->payload, fua_payload->size);
+ if (fua_payload->end) {
+ // Write nalu_len back
+ payload.skip(-(4 + nalu_len));
+ payload.write_4bytes(nalu_len);
+ payload.skip(nalu_len);
+ }
+ }
+ return;
+ }
+
+ // H.264 STAP-A payload
+ SrsRtpSTAPPayload* stap_payload = dynamic_cast(pkt->payload());
+ if (stap_payload) {
+ for (int j = 0; j < (int)stap_payload->nalus.size(); ++j) {
+ SrsSample* sample = stap_payload->nalus.at(j);
+ if (sample->size > 0) {
+ payload.write_4bytes(sample->size);
+ payload.write_bytes(sample->bytes, sample->size);
+ }
+ }
+ return;
+ }
+
+ // H.265 FU-A payload
+ SrsRtpFUAPayloadHevc2* fua_payload_hevc = dynamic_cast(pkt->payload());
+ if (fua_payload_hevc && fua_payload_hevc->size > 0) {
+ if (fua_payload_hevc->start) {
+ nalu_len = fua_payload_hevc->size + 2;
+ payload.skip(4); // Skip 4 bytes to write nalu_len later
+ payload.write_1bytes(fua_payload_hevc->nalu_type << 1);
+ payload.write_1bytes(0x01);
+ payload.write_bytes(fua_payload_hevc->payload, fua_payload_hevc->size);
+ } else {
+ nalu_len += fua_payload_hevc->size;
+ payload.write_bytes(fua_payload_hevc->payload, fua_payload_hevc->size);
+ if (fua_payload_hevc->end) {
+ // Write nalu_len back
+ payload.skip(-(4 + nalu_len));
+ payload.write_4bytes(nalu_len);
+ payload.skip(nalu_len);
+ }
+ }
+ return;
+ }
+
+ // H.265 STAP payload
+ SrsRtpSTAPPayloadHevc* stap_payload_hevc = dynamic_cast(pkt->payload());
+ if (stap_payload_hevc) {
+ for (int j = 0; j < (int)stap_payload_hevc->nalus.size(); ++j) {
+ SrsSample* sample = stap_payload_hevc->nalus.at(j);
+ if (sample->size > 0) {
+ payload.write_4bytes(sample->size);
+ payload.write_bytes(sample->bytes, sample->size);
+ }
+ }
+ return;
+ }
+
+ // Raw payload
+ SrsRtpRawPayload* raw_payload = dynamic_cast(pkt->payload());
+ if (raw_payload && raw_payload->nn_payload > 0) {
+ payload.write_4bytes(raw_payload->nn_payload);
+ payload.write_bytes(raw_payload->payload, raw_payload->nn_payload);
+ return;
+ }
+}
+
srs_error_t SrsRtcFrameBuilder::packet_video_rtmp(const uint16_t start, const uint16_t end)
{
srs_error_t err = srs_success;
@@ -1841,40 +2304,23 @@ srs_error_t SrsRtcFrameBuilder::packet_video_rtmp(const uint16_t start, const ui
int16_t cnt = srs_rtp_seq_distance(start, end) + 1;
srs_assert(cnt >= 1);
+ // The start position packet may be null, so we need to find the actual first packet.
+ SrsRtpPacket* first_frame_pkt = NULL;
+
+ // First loop: Calculate total payload size and find first packet
for (uint16_t i = 0; i < (uint16_t)cnt; ++i) {
uint16_t sn = start + i;
- uint16_t index = cache_index(sn);
- SrsRtpPacket* pkt = cache_video_pkts_[index].pkt;
+ SrsRtpPacket* pkt = video_cache_->get_packet(sn);
- // fix crash when pkt->payload() if pkt is nullptr;
if (!pkt) continue;
- // calculate nalu len
- SrsRtpFUAPayload2* fua_payload = dynamic_cast(pkt->payload());
- if (fua_payload && fua_payload->size > 0) {
- if (fua_payload->start) {
- nb_payload += 1 + 4;
- }
- nb_payload += fua_payload->size;
- continue;
+ // Set the first available packet of the frame
+ if (!first_frame_pkt) {
+ first_frame_pkt = pkt;
}
- SrsRtpSTAPPayload* stap_payload = dynamic_cast(pkt->payload());
- if (stap_payload) {
- for (int j = 0; j < (int)stap_payload->nalus.size(); ++j) {
- SrsSample* sample = stap_payload->nalus.at(j);
- if (sample->size > 0) {
- nb_payload += 4 + sample->size;
- }
- }
- continue;
- }
-
- SrsRtpRawPayload* raw_payload = dynamic_cast(pkt->payload());
- if (raw_payload && raw_payload->nn_payload > 0) {
- nb_payload += 4 + raw_payload->nn_payload;
- continue;
- }
+ // Calculate payload size using helper function
+ nb_payload += calculate_packet_payload_size(pkt);
}
if (0 == nb_payload) {
@@ -1883,100 +2329,70 @@ srs_error_t SrsRtcFrameBuilder::packet_video_rtmp(const uint16_t start, const ui
// The chrome web browser send RTP packet with empty payload frequently,
// reset header_sn_, lost_sn_ and continue to found next frame in this case,
// otherwise, all the cached RTP packets are dropped before next key frame arrive.
- header_sn_ = end + 1;
- uint16_t tail_sn = 0;
- int sn = find_next_lost_sn(header_sn_, tail_sn);
- if (-1 == sn) {
- if (check_frame_complete(header_sn_, tail_sn)) {
- err = packet_video_rtmp(header_sn_, tail_sn);
- }
- } else if (-2 == sn) {
- return srs_error_new(ERROR_RTC_RTP_MUXER, "video cache is overflow");
- } else {
- lost_sn_ = sn;
+ uint16_t next_start, next_end; bool got_frame;
+ if ((err = frame_detector_->detect_next_frame(end + 1, next_start, next_end, got_frame)) != srs_success) {
+ return srs_error_wrap(err, "update frame detector failed");
+ }
+ if (got_frame && (err = packet_video_rtmp(next_start, next_end)) != srs_success) {
+ err = srs_error_wrap(err, "fail to pack video frame, start=%u, end=%u", next_start, next_end);
}
return err;
}
- //type_codec1 + avc_type + composition time + nalu size + nalu
- nb_payload += 1 + 1 + 3;
+ // If no first frame packet, it make no sense to continue.
+ if (!first_frame_pkt) {
+ return srs_error_new(ERROR_RTC_RTP_MUXER, "no available packets in frame range, start=%u, end=%u", start, end);
+ }
+
+ // h265: IsExHeader | FrameType | PacketType + Video FourCC
+ // h264: FrameType | CodecID + avc_type + composition time + nalu size + nalu
+ nb_payload += 5;
+
+ // Note that the start position may be null, so it's not the real correct start
+ // packet of a video frame, therefore we use the first available packet instead.
+ SrsRtpPacket* pkt = first_frame_pkt;
+
+ if (pkt->is_keyframe(video_codec_)) {
+ frame_detector_->on_keyframe_detached();
+ }
+
+ SrsVideoAvcFrameType frame_type = SrsVideoAvcFrameTypeInterFrame;
+ if (pkt->is_keyframe(video_codec_)) {
+ frame_type = SrsVideoAvcFrameTypeKeyFrame;
+ }
SrsCommonMessage rtmp;
- SrsRtpPacket* pkt = cache_video_pkts_[cache_index(start)].pkt;
rtmp.header.initialize_video(nb_payload, pkt->get_avsync_time(), 1);
rtmp.create_payload(nb_payload);
rtmp.size = nb_payload;
SrsBuffer payload(rtmp.payload, rtmp.size);
- if (pkt->is_keyframe()) {
- payload.write_1bytes(0x17); // type(4 bits): key frame; code(4bits): avc
- rtp_key_frame_ts_ = -1;
+ if (video_codec_ == SrsVideoCodecIdHEVC) {
+ // @see: https://veovera.org/docs/enhanced/enhanced-rtmp-v1.pdf, page 8
+ payload.write_1bytes(SRS_FLV_IS_EX_HEADER | (frame_type << 4) | SrsVideoHEVCFrameTraitPacketTypeCodedFramesX);
+ payload.write_4bytes(0x68766331); // 'h' 'v' 'c' '1'
} else {
- payload.write_1bytes(0x27); // type(4 bits): inter frame; code(4bits): avc
+ // @see: E.4.3 Video Tags, video_file_format_spec_v10_1.pdf, page 78
+ // Frame Type, Type of video frame.
+ // CodecID, Codec Identifier.
+ // set the rtmp header
+ payload.write_1bytes((frame_type << 4) | video_codec_);
+ payload.write_1bytes(0x01); // avc_type: nalu
+ payload.write_1bytes(0x0); // composition time
+ payload.write_1bytes(0x0);
+ payload.write_1bytes(0x0);
}
- payload.write_1bytes(0x01); // avc_type: nalu
- payload.write_1bytes(0x0); // composition time
- payload.write_1bytes(0x0);
- payload.write_1bytes(0x0);
+ // Second loop: Write payload data using helper function
int nalu_len = 0;
for (uint16_t i = 0; i < (uint16_t)cnt; ++i) {
- uint16_t index = cache_index((start + i));
- SrsRtpPacket* pkt = cache_video_pkts_[index].pkt;
+ uint16_t sequence_number = start + i;
+ SrsRtpPacket* pkt_raw = video_cache_->take_packet(sequence_number);
- // fix crash when pkt->payload() if pkt is nullptr;
- if (!pkt) continue;
+ if (!pkt_raw) continue;
- cache_video_pkts_[index].in_use = false;
- cache_video_pkts_[index].pkt = NULL;
- cache_video_pkts_[index].ts = 0;
- cache_video_pkts_[index].rtp_ts = 0;
- cache_video_pkts_[index].sn = 0;
-
- SrsRtpFUAPayload2* fua_payload = dynamic_cast(pkt->payload());
- if (fua_payload && fua_payload->size > 0) {
- if (fua_payload->start) {
- nalu_len = fua_payload->size + 1;
- //skip 4 bytes to write nalu_len future
- payload.skip(4);
- payload.write_1bytes(fua_payload->nri | fua_payload->nalu_type);
- payload.write_bytes(fua_payload->payload, fua_payload->size);
- } else {
- nalu_len += fua_payload->size;
- payload.write_bytes(fua_payload->payload, fua_payload->size);
- if (fua_payload->end) {
- //write nalu_len back
- payload.skip(-(4 + nalu_len));
- payload.write_4bytes(nalu_len);
- payload.skip(nalu_len);
- }
- }
- srs_freep(pkt);
- continue;
- }
-
- SrsRtpSTAPPayload* stap_payload = dynamic_cast(pkt->payload());
- if (stap_payload) {
- for (int j = 0; j < (int)stap_payload->nalus.size(); ++j) {
- SrsSample* sample = stap_payload->nalus.at(j);
- if (sample->size > 0) {
- payload.write_4bytes(sample->size);
- payload.write_bytes(sample->bytes, sample->size);
- }
- }
- srs_freep(pkt);
- continue;
- }
-
- SrsRtpRawPayload* raw_payload = dynamic_cast(pkt->payload());
- if (raw_payload && raw_payload->nn_payload > 0) {
- payload.write_4bytes(raw_payload->nn_payload);
- payload.write_bytes(raw_payload->payload, raw_payload->nn_payload);
- srs_freep(pkt);
- continue;
- }
-
- srs_freep(pkt);
+ SrsUniquePtr pkt(pkt_raw);
+ write_packet_payload_to_buffer(pkt.get(), payload, nalu_len);
}
SrsSharedPtrMessage msg;
@@ -1988,97 +2404,25 @@ srs_error_t SrsRtcFrameBuilder::packet_video_rtmp(const uint16_t start, const ui
srs_warn("fail to pack video frame");
}
- header_sn_ = end + 1;
- uint16_t tail_sn = 0;
- int sn = find_next_lost_sn(header_sn_, tail_sn);
- if (-1 == sn) {
- if (check_frame_complete(header_sn_, tail_sn)) {
- err = packet_video_rtmp(header_sn_, tail_sn);
- }
- } else if (-2 == sn) {
- return srs_error_new(ERROR_RTC_RTP_MUXER, "video cache is overflow");
- } else {
- lost_sn_ = sn;
+ // Try to detect and detach next RTMP packet.
+ uint16_t next_start, next_end; bool got_frame;
+ if ((err = frame_detector_->detect_next_frame(end + 1, next_start, next_end, got_frame)) != srs_success) {
+ return srs_error_wrap(err, "update frame detector failed");
+ }
+ if (got_frame && (err = packet_video_rtmp(next_start, next_end)) != srs_success) {
+ err = srs_error_wrap(err, "fail to pack video frame, start=%u, end=%u", next_start, next_end);
}
return err;
}
-int32_t SrsRtcFrameBuilder::find_next_lost_sn(uint16_t current_sn, uint16_t& end_sn)
-{
- uint32_t last_rtp_ts = cache_video_pkts_[cache_index(header_sn_)].rtp_ts;
- for (int i = 0; i < s_cache_size; ++i) {
- uint16_t lost_sn = current_sn + i;
- int index = cache_index(lost_sn);
-
- if (!cache_video_pkts_[index].in_use) {
- return lost_sn;
- }
- //check time first, avoid two small frame mixed case decode fail
- if (last_rtp_ts != cache_video_pkts_[index].rtp_ts) {
- end_sn = lost_sn - 1;
- return -1;
- }
-
- if (cache_video_pkts_[index].pkt->header.get_marker()) {
- end_sn = lost_sn;
- return -1;
- }
- }
-
- srs_error("cache overflow. the packet count of video frame is more than %u", s_cache_size);
- return -2;
-}
-
-void SrsRtcFrameBuilder::clear_cached_video()
-{
- for (size_t i = 0; i < s_cache_size; i++)
- {
- if (cache_video_pkts_[i].in_use) {
- srs_freep(cache_video_pkts_[i].pkt);
- cache_video_pkts_[i].sn = 0;
- cache_video_pkts_[i].ts = 0;
- cache_video_pkts_[i].rtp_ts = 0;
- cache_video_pkts_[i].in_use = false;
- }
- }
-}
-
-bool SrsRtcFrameBuilder::check_frame_complete(const uint16_t start, const uint16_t end)
-{
- int16_t cnt = srs_rtp_seq_distance(start, end) + 1;
- srs_assert(cnt >= 1);
-
- uint16_t fu_s_c = 0;
- uint16_t fu_e_c = 0;
- for (uint16_t i = 0; i < (uint16_t)cnt; ++i) {
- int index = cache_index((start + i));
- SrsRtpPacket* pkt = cache_video_pkts_[index].pkt;
-
- // fix crash when pkt->payload() if pkt is nullptr;
- if (!pkt) continue;
-
- SrsRtpFUAPayload2* fua_payload = dynamic_cast(pkt->payload());
- if (!fua_payload) continue;
-
- if (fua_payload->start) {
- ++fu_s_c;
- }
-
- if (fua_payload->end) {
- ++fu_e_c;
- }
- }
-
- return fu_s_c == fu_e_c;
-}
-
#endif
SrsCodecPayload::SrsCodecPayload()
{
pt_of_publisher_ = pt_ = 0;
sample_ = 0;
+ codec_ = -1;
}
SrsCodecPayload::SrsCodecPayload(uint8_t pt, std::string encode_name, int sample)
@@ -2086,12 +2430,32 @@ SrsCodecPayload::SrsCodecPayload(uint8_t pt, std::string encode_name, int sample
pt_of_publisher_ = pt_ = pt;
name_ = encode_name;
sample_ = sample;
+ codec_ = -1;
}
SrsCodecPayload::~SrsCodecPayload()
{
}
+int8_t SrsCodecPayload::codec(bool video)
+{
+ // Return cached value if already initialized
+ if (codec_ != -1) {
+ return codec_;
+ }
+
+ // Parse codec based on context (video or audio)
+ if (video) {
+ // For unknown video codecs like H.266, still return SrsVideoCodecIdReserved
+ // but it's correctly identified as a video codec due to the context
+ codec_ = srs_video_codec_str2id(name_);
+ } else {
+ codec_ = srs_audio_codec_str2id(name_);
+ }
+
+ return codec_;
+}
+
SrsCodecPayload* SrsCodecPayload::copy()
{
SrsCodecPayload* cp = new SrsCodecPayload();
@@ -2128,7 +2492,7 @@ SrsVideoPayload::SrsVideoPayload(uint8_t pt, std::string encode_name, int sample
type_ = "video";
h264_param_.profile_level_id = "";
h264_param_.packetization_mode = "";
- h264_param_.level_asymmerty_allow = "";
+ h264_param_.level_asymmetry_allow = "";
}
SrsVideoPayload::~SrsVideoPayload()
@@ -2162,8 +2526,8 @@ SrsMediaPayloadType SrsVideoPayload::generate_media_payload_type()
std::ostringstream format_specific_param;
bool has_param = false;
- if (!h264_param_.level_asymmerty_allow.empty()) {
- format_specific_param << "level-asymmetry-allowed=" << h264_param_.level_asymmerty_allow;
+ if (!h264_param_.level_asymmetry_allow.empty()) {
+ format_specific_param << "level-asymmetry-allowed=" << h264_param_.level_asymmetry_allow;
has_param = true;
}
if (!h264_param_.packetization_mode.empty()) {
@@ -2244,7 +2608,7 @@ srs_error_t SrsVideoPayload::set_h264_param_desc(std::string fmtp)
// @see https://tools.ietf.org/html/rfc6184#section-6.3
h264_param_.packetization_mode = kv[1];
} else if (kv[0] == "level-asymmetry-allowed") {
- h264_param_.level_asymmerty_allow = kv[1];
+ h264_param_.level_asymmetry_allow = kv[1];
} else {
return srs_error_new(ERROR_RTC_SDP_DECODE, "invalid h264 param=%s", kv[0].c_str());
}
@@ -2817,7 +3181,7 @@ SrsRtcAudioRecvTrack::~SrsRtcAudioRecvTrack()
{
}
-void SrsRtcAudioRecvTrack::on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtspPacketPayloadType* ppt)
+void SrsRtcAudioRecvTrack::on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtpPacketPayloadType* ppt)
{
// No payload, ignore.
if (buf->empty()) {
@@ -2825,7 +3189,7 @@ void SrsRtcAudioRecvTrack::on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer
}
*ppayload = new SrsRtpRawPayload();
- *ppt = SrsRtspPacketPayloadTypeRaw;
+ *ppt = SrsRtpPacketPayloadTypeRaw;
}
srs_error_t SrsRtcAudioRecvTrack::on_rtp(SrsSharedPtr& source, SrsRtpPacket* pkt)
@@ -2865,25 +3229,45 @@ SrsRtcVideoRecvTrack::~SrsRtcVideoRecvTrack()
{
}
-void SrsRtcVideoRecvTrack::on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtspPacketPayloadType* ppt)
+void SrsRtcVideoRecvTrack::on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtpPacketPayloadType* ppt)
{
// No payload, ignore.
if (buf->empty()) {
return;
}
- uint8_t v = (uint8_t)(buf->head()[0] & kNalTypeMask);
- pkt->nalu_type = v;
+ SrsVideoCodecId codec = (SrsVideoCodecId)track_desc_->media_->codec(true);
+ if (codec == SrsVideoCodecIdAVC) {
+ uint8_t v = SrsAvcNaluTypeParse(buf->head()[0]);
+ pkt->nalu_type = v;
- if (v == kStapA) {
- *ppayload = new SrsRtpSTAPPayload();
- *ppt = SrsRtspPacketPayloadTypeSTAP;
- } else if (v == kFuA) {
- *ppayload = new SrsRtpFUAPayload2();
- *ppt = SrsRtspPacketPayloadTypeFUA2;
+ if (v == kStapA) {
+ *ppayload = new SrsRtpSTAPPayload();
+ *ppt = SrsRtpPacketPayloadTypeSTAP;
+ } else if (v == kFuA) {
+ *ppayload = new SrsRtpFUAPayload2();
+ *ppt = SrsRtpPacketPayloadTypeFUA2;
+ } else {
+ *ppayload = new SrsRtpRawPayload();
+ *ppt = SrsRtpPacketPayloadTypeRaw;
+ }
+ } else if (codec == SrsVideoCodecIdHEVC) {
+ uint8_t v = SrsHevcNaluTypeParse(buf->head()[0]);
+ pkt->nalu_type = v;
+
+ if (v == kStapHevc) {
+ *ppayload = new SrsRtpSTAPPayloadHevc();
+ *ppt = SrsRtpPacketPayloadTypeSTAPHevc;
+ } else if (v == kFuHevc) {
+ *ppayload = new SrsRtpFUAPayloadHevc2();
+ *ppt = SrsRtpPacketPayloadTypeFUAHevc2;
+ } else {
+ *ppayload = new SrsRtpRawPayload();
+ *ppt = SrsRtpPacketPayloadTypeRaw;
+ }
} else {
- *ppayload = new SrsRtpRawPayload();
- *ppt = SrsRtspPacketPayloadTypeRaw;
+ *ppayload = NULL;
+ *ppt = SrsRtpPacketPayloadTypeUnknown;
}
}
diff --git a/trunk/src/app/srs_app_rtc_source.hpp b/trunk/src/app/srs_app_rtc_source.hpp
index 50eaa80f0..4822fbe62 100644
--- a/trunk/src/app/srs_app_rtc_source.hpp
+++ b/trunk/src/app/srs_app_rtc_source.hpp
@@ -312,18 +312,12 @@ private:
srs_error_t consume_packets(std::vector& pkts);
};
-// Collect and build WebRTC RTP packets to AV frames.
-class SrsRtcFrameBuilder
+// Video packet cache for RTP packet management
+// TODO: Maybe should use SrsRtpRingBuffer?
+class SrsRtcFrameBuilderVideoPacketCache
{
private:
- ISrsStreamBridge* bridge_;
-private:
- bool is_first_audio_;
- SrsAudioTranscoder *codec_;
-private:
- const static uint16_t s_cache_size = 512;
- //TODO:use SrsRtpRingBuffer
- //TODO:jitter buffer class
+ const static uint16_t cache_size_ = 512;
struct RtcPacketCache {
bool in_use;
uint16_t sn;
@@ -331,22 +325,74 @@ private:
uint32_t rtp_ts;
SrsRtpPacket* pkt;
};
- RtcPacketCache cache_video_pkts_[s_cache_size];
+ RtcPacketCache cache_pkts_[cache_size_];
+public:
+ SrsRtcFrameBuilderVideoPacketCache();
+ virtual ~SrsRtcFrameBuilderVideoPacketCache();
+public:
+ SrsRtpPacket* get_packet(uint16_t sequence_number);
+ void store_packet(SrsRtpPacket* pkt);
+ void clear_all();
+ SrsRtpPacket* take_packet(uint16_t sequence_number);
+public:
+ // Find next lost sequence number starting from current_sn
+ // Returns: lost_sn if found, -1 if complete frame found (sets end_sn), -2 if cache overflow
+ int32_t find_next_lost_sn(uint16_t current_sn, uint16_t header_sn, uint16_t& end_sn);
+ // Check if frame is complete by verifying FU-A start/end fragment counts match
+ bool check_frame_complete(const uint16_t start, const uint16_t end);
+private:
+ bool is_slot_in_use(uint16_t sequence_number);
+ uint32_t get_rtp_timestamp(uint16_t sequence_number);
+ inline uint16_t cache_index(uint16_t sequence_number) {
+ return sequence_number % cache_size_;
+ }
+};
+
+// Video frame detector for managing frame boundaries and packet loss detection
+class SrsRtcFrameBuilderVideoFrameDetector
+{
+private:
+ SrsRtcFrameBuilderVideoPacketCache* video_cache_;
uint16_t header_sn_;
uint16_t lost_sn_;
int64_t rtp_key_frame_ts_;
+public:
+ SrsRtcFrameBuilderVideoFrameDetector(SrsRtcFrameBuilderVideoPacketCache* cache);
+ virtual ~SrsRtcFrameBuilderVideoFrameDetector();
+public:
+ void on_keyframe_start(SrsRtpPacket* pkt);
+ srs_error_t detect_frame(uint16_t received, uint16_t& frame_start, uint16_t& frame_end, bool& frame_ready);
+ srs_error_t detect_next_frame(uint16_t next_head, uint16_t& next_start, uint16_t& next_end, bool& next_ready);
+ void on_keyframe_detached();
+ bool is_lost_sn(uint16_t received);
+};
+
+// Collect and build WebRTC RTP packets to AV frames.
+class SrsRtcFrameBuilder
+{
+private:
+ ISrsStreamBridge* bridge_;
+private:
+ bool is_first_audio_;
+ SrsAudioTranscoder *audio_transcoder_;
+
+ SrsVideoCodecId video_codec_;
+private:
+ SrsRtcFrameBuilderVideoPacketCache* video_cache_;
+ SrsRtcFrameBuilderVideoFrameDetector* frame_detector_;
private:
// The state for timestamp sync state. -1 for init. 0 not sync. 1 sync.
int sync_state_;
private:
- // For OBS WHIP, send SPS/PPS in dedicated RTP packet.
+ // For OBS WHIP, send (VPS/)SPS/PPS in dedicated RTP packet.
+ SrsRtpPacket* obs_whip_vps_;
SrsRtpPacket* obs_whip_sps_;
SrsRtpPacket* obs_whip_pps_;
public:
SrsRtcFrameBuilder(ISrsStreamBridge* bridge);
virtual ~SrsRtcFrameBuilder();
public:
- srs_error_t initialize(SrsRequest* r);
+ srs_error_t initialize(SrsRequest* r, SrsAudioCodecId audio_codec, SrsVideoCodecId video_codec);
virtual srs_error_t on_publish();
virtual void on_unpublish();
virtual srs_error_t on_rtp(SrsRtpPacket *pkt);
@@ -358,14 +404,12 @@ private:
srs_error_t packet_video_key_frame(SrsRtpPacket* pkt);
srs_error_t packet_sequence_header_avc(SrsRtpPacket* pkt);
srs_error_t do_packet_sequence_header_avc(SrsRtpPacket* pkt, SrsSample* sps, SrsSample* pps);
+ srs_error_t packet_sequence_header_hevc(SrsRtpPacket* pkt);
+ srs_error_t do_packet_sequence_header_hevc(SrsRtpPacket* pkt, SrsSample* vps, SrsSample* sps, SrsSample* pps);
private:
- inline uint16_t cache_index(uint16_t current_sn) {
- return current_sn % s_cache_size;
- }
- int32_t find_next_lost_sn(uint16_t current_sn, uint16_t& end_sn);
- bool check_frame_complete(const uint16_t start, const uint16_t end);
srs_error_t packet_video_rtmp(const uint16_t start, const uint16_t end);
- void clear_cached_video();
+ int calculate_packet_payload_size(SrsRtpPacket* pkt);
+ void write_packet_payload_to_buffer(SrsRtpPacket* pkt, SrsBuffer& payload, int& nalu_len);
};
#endif
@@ -383,11 +427,21 @@ public:
int sample_;
std::vector rtcp_fbs_;
+private:
+ // The cached codec ID, corresponding to name_.
+ // For video, you can convert it to type SrsVideoCodecId
+ // For audio, you can convert it to type SrsAudioCodecId
+ // Note: Set up to -1, which means not initialized/cached yet
+ // Note: Won't copy codec_, it will be recalculated when codec(bool) is called
+ int8_t codec_;
public:
SrsCodecPayload();
SrsCodecPayload(uint8_t pt, std::string encode_name, int sample);
virtual ~SrsCodecPayload();
public:
+ // Get codec ID with context information about whether it's video or audio
+ // Returns the numeric codec ID, with caching for performance
+ int8_t codec(bool video);
virtual SrsCodecPayload* copy();
virtual SrsMediaPayloadType generate_media_payload_type();
};
@@ -593,25 +647,25 @@ protected:
virtual srs_error_t do_check_send_nacks(uint32_t& timeout_nacks);
};
-class SrsRtcAudioRecvTrack : public SrsRtcRecvTrack, public ISrsRtspPacketDecodeHandler
+class SrsRtcAudioRecvTrack : public SrsRtcRecvTrack, public ISrsRtpPacketDecodeHandler
{
public:
SrsRtcAudioRecvTrack(SrsRtcConnection* session, SrsRtcTrackDescription* track_desc);
virtual ~SrsRtcAudioRecvTrack();
public:
- virtual void on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtspPacketPayloadType* ppt);
+ virtual void on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtpPacketPayloadType* ppt);
public:
virtual srs_error_t on_rtp(SrsSharedPtr& source, SrsRtpPacket* pkt);
virtual srs_error_t check_send_nacks();
};
-class SrsRtcVideoRecvTrack : public SrsRtcRecvTrack, public ISrsRtspPacketDecodeHandler
+class SrsRtcVideoRecvTrack : public SrsRtcRecvTrack, public ISrsRtpPacketDecodeHandler
{
public:
SrsRtcVideoRecvTrack(SrsRtcConnection* session, SrsRtcTrackDescription* stream_descs);
virtual ~SrsRtcVideoRecvTrack();
public:
- virtual void on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtspPacketPayloadType* ppt);
+ virtual void on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtpPacketPayloadType* ppt);
public:
virtual srs_error_t on_rtp(SrsSharedPtr& source, SrsRtpPacket* pkt);
virtual srs_error_t check_send_nacks();
diff --git a/trunk/src/app/srs_app_srt_source.cpp b/trunk/src/app/srs_app_srt_source.cpp
index 6f46d1b72..860cd01e1 100644
--- a/trunk/src/app/srs_app_srt_source.cpp
+++ b/trunk/src/app/srs_app_srt_source.cpp
@@ -708,7 +708,7 @@ srs_error_t SrsSrtFrameBuilder::on_hevc_frame(SrsTsMessage* msg, vector= SrsHevcNaluType_CODED_SLICE_BLA) && (nalu_type <= SrsHevcNaluType_RESERVED_23)) {
+ if (SrsIsIRAP(nalu_type)) {
frame_type = SrsVideoAvcFrameTypeKeyFrame;
}
}
diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp
index 2a5304999..3824a2a3a 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 42
+#define VERSION_REVISION 43
#endif
\ No newline at end of file
diff --git a/trunk/src/kernel/srs_kernel_codec.cpp b/trunk/src/kernel/srs_kernel_codec.cpp
index f1a73ae32..bcb516438 100644
--- a/trunk/src/kernel/srs_kernel_codec.cpp
+++ b/trunk/src/kernel/srs_kernel_codec.cpp
@@ -8,6 +8,7 @@
#include
#include
+#include
using namespace std;
#include
@@ -41,6 +42,26 @@ string srs_video_codec_id2str(SrsVideoCodecId codec)
}
}
+SrsVideoCodecId srs_video_codec_str2id(const std::string &codec)
+{
+ std::string upper_codec = codec;
+ std::transform(upper_codec.begin(), upper_codec.end(), upper_codec.begin(), ::toupper);
+
+ if (upper_codec == "H264" || upper_codec == "AVC") {
+ return SrsVideoCodecIdAVC;
+ } else if (upper_codec == "H265" || upper_codec == "HEVC") {
+ return SrsVideoCodecIdHEVC;
+ } else if (upper_codec == "AV1") {
+ return SrsVideoCodecIdAV1;
+ } else if (upper_codec == "VP6") {
+ return SrsVideoCodecIdOn2VP6;
+ } else if (upper_codec == "VP6A") {
+ return SrsVideoCodecIdOn2VP6WithAlphaChannel;
+ }
+
+ return SrsVideoCodecIdReserved;
+}
+
string srs_audio_codec_id2str(SrsAudioCodecId codec)
{
switch (codec) {
@@ -68,6 +89,25 @@ string srs_audio_codec_id2str(SrsAudioCodecId codec)
}
}
+SrsAudioCodecId srs_audio_codec_str2id(const std::string &codec)
+{
+ // to uppercase
+ std::string upper_codec = codec;
+ std::transform(upper_codec.begin(), upper_codec.end(), upper_codec.begin(), ::toupper);
+
+ if (upper_codec == "AAC") {
+ return SrsAudioCodecIdAAC;
+ } else if (upper_codec == "MP3") {
+ return SrsAudioCodecIdMP3;
+ } else if (upper_codec == "OPUS") {
+ return SrsAudioCodecIdOpus;
+ } else if (upper_codec == "SPEEX") {
+ return SrsAudioCodecIdSpeex;
+ }
+
+ return SrsAudioCodecIdReserved1;
+}
+
SrsAudioSampleRate srs_audio_sample_rate_from_number(uint32_t v)
{
if (v == 5512) return SrsAudioSampleRate5512;
@@ -667,7 +707,7 @@ srs_error_t SrsVideoFrame::add_sample(char* bytes, int size)
if (c && c->id == SrsVideoCodecIdHEVC) {
#ifdef SRS_H265
SrsHevcNaluType nalu_type = SrsHevcNaluTypeParse(bytes[0]);
- has_idr = (SrsHevcNaluType_CODED_SLICE_BLA <= nalu_type) && (nalu_type <= SrsHevcNaluType_RESERVED_23);
+ has_idr = SrsIsIRAP(nalu_type);
return err;
#else
return srs_error_new(ERROR_HEVC_DISABLED, "H.265 is disabled");
@@ -751,6 +791,7 @@ srs_error_t SrsVideoFrame::parse_avc_bframe(const SrsSample* sample, bool& is_b_
return err;
}
+#ifdef SRS_H265
srs_error_t SrsVideoFrame::parse_hevc_nalu_type(const SrsSample* sample, SrsHevcNaluType& hevc_nalu_type)
{
srs_error_t err = srs_success;
@@ -831,6 +872,7 @@ srs_error_t SrsVideoFrame::parse_hevc_bframe(const SrsSample* sample, SrsFormat
return err;
}
+#endif
SrsFormat::SrsFormat()
{
diff --git a/trunk/src/kernel/srs_kernel_codec.hpp b/trunk/src/kernel/srs_kernel_codec.hpp
index f267fdc01..b2f2b00b8 100644
--- a/trunk/src/kernel/srs_kernel_codec.hpp
+++ b/trunk/src/kernel/srs_kernel_codec.hpp
@@ -16,6 +16,12 @@ class SrsBuffer;
class SrsBitBuffer;
class SrsFormat;
+/* Extended VideoTagHeader
+ * defined in reference link:
+ * https://veovera.org/docs/enhanced/enhanced-rtmp-v1.pdf
+ * */
+#define SRS_FLV_IS_EX_HEADER 0x80
+
// @see: https://datatracker.ietf.org/doc/html/rfc6184#section-1.3
const int SrsAvcNaluHeaderSize = 1;
// @see: https://datatracker.ietf.org/doc/html/rfc7798#section-1.1.4
@@ -57,6 +63,7 @@ enum SrsVideoCodecId
SrsVideoCodecIdAV1 = 13,
};
std::string srs_video_codec_id2str(SrsVideoCodecId codec);
+SrsVideoCodecId srs_video_codec_str2id(const std::string& codec);
/**
* The video AVC frame trait(characteristic).
@@ -173,6 +180,7 @@ enum SrsAudioCodecId
SrsAudioCodecIdReservedDeviceSpecificSound = 15,
};
std::string srs_audio_codec_id2str(SrsAudioCodecId codec);
+SrsAudioCodecId srs_audio_codec_str2id(const std::string& codec);
/**
* The audio AAC frame trait(characteristic).
@@ -506,6 +514,7 @@ enum SrsHevcNaluType {
};
// @see https://datatracker.ietf.org/doc/html/rfc7798#section-1.1.4
#define SrsHevcNaluTypeParse(code) (SrsHevcNaluType)((code & 0x7E) >> 1)
+#define SrsIsIRAP(type) ((type >= SrsHevcNaluType_CODED_SLICE_BLA) && (type <= SrsHevcNaluType_RESERVED_23))
/**
* @see Table 7-7 – Name association to slice_type
@@ -1340,9 +1349,10 @@ public:
public:
static srs_error_t parse_avc_nalu_type(const SrsSample* sample, SrsAvcNaluType& avc_nalu_type);
static srs_error_t parse_avc_bframe(const SrsSample* sample, bool& is_b_frame);
-
+#ifdef SRS_H265
static srs_error_t parse_hevc_nalu_type(const SrsSample* sample, SrsHevcNaluType& hevc_nalu_type);
static srs_error_t parse_hevc_bframe(const SrsSample* sample, SrsFormat* format, bool& is_b_frame);
+#endif
};
/**
diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp
index 01a4c28b8..d2ab5a49a 100644
--- a/trunk/src/kernel/srs_kernel_rtc_rtp.cpp
+++ b/trunk/src/kernel/srs_kernel_rtc_rtp.cpp
@@ -750,18 +750,18 @@ ISrsRtpPayloader::~ISrsRtpPayloader()
{
}
-ISrsRtspPacketDecodeHandler::ISrsRtspPacketDecodeHandler()
+ISrsRtpPacketDecodeHandler::ISrsRtpPacketDecodeHandler()
{
}
-ISrsRtspPacketDecodeHandler::~ISrsRtspPacketDecodeHandler()
+ISrsRtpPacketDecodeHandler::~ISrsRtpPacketDecodeHandler()
{
}
SrsRtpPacket::SrsRtpPacket()
{
payload_ = NULL;
- payload_type_ = SrsRtspPacketPayloadTypeUnknown;
+ payload_type_ = SrsRtpPacketPayloadTypeUnknown;
shared_buffer_ = NULL;
actual_buffer_size_ = 0;
@@ -864,7 +864,7 @@ void SrsRtpPacket::add_padding(int size)
}
}
-void SrsRtpPacket::set_decode_handler(ISrsRtspPacketDecodeHandler* h)
+void SrsRtpPacket::set_decode_handler(ISrsRtpPacketDecodeHandler* h)
{
decode_handler = h;
}
@@ -936,7 +936,7 @@ srs_error_t SrsRtpPacket::decode(SrsBuffer* buf)
// By default, we always use the RAW payload.
if (!payload_) {
payload_ = new SrsRtpRawPayload();
- payload_type_ = SrsRtspPacketPayloadTypeRaw;
+ payload_type_ = SrsRtpPacketPayloadTypeRaw;
}
if ((err = payload_->decode(buf)) != srs_success) {
@@ -946,7 +946,6 @@ srs_error_t SrsRtpPacket::decode(SrsBuffer* buf)
return err;
}
-// Helper function to check if H.264 RTP packet is a keyframe
bool srs_rtp_packet_h264_is_keyframe(uint8_t nalu_type, ISrsRtpPayloader* payload)
{
if (nalu_type == kStapA) {
@@ -968,49 +967,45 @@ bool srs_rtp_packet_h264_is_keyframe(uint8_t nalu_type, ISrsRtpPayloader* payloa
return false;
}
-#ifdef SRS_H265
-// Helper function to check if H.265 RTP packet is a keyframe
bool srs_rtp_packet_h265_is_keyframe(uint8_t nalu_type, ISrsRtpPayloader* payload)
{
if(nalu_type == kStapHevc) {
SrsRtpSTAPPayloadHevc* stap_payload = dynamic_cast(payload);
- if(NULL != stap_payload->get_vps() || NULL != stap_payload->get_sps() || NULL != stap_payload->get_pps()) {
+ if (stap_payload->get_vps() || stap_payload->get_sps() || stap_payload->get_pps()) {
return true;
}
- } else if(nalu_type == kFuHevc) {
+ } else if (nalu_type == kFuHevc) {
SrsRtpFUAPayloadHevc2* fua_payload = dynamic_cast(payload);
- if(fua_payload->nalu_type >= SrsHevcNaluType_CODED_SLICE_BLA && fua_payload->nalu_type <= SrsHevcNaluType_RESERVED_23) {
+ if(SrsIsIRAP(fua_payload->nalu_type)) {
return true;
}
} else {
- if((SrsHevcNaluType_VPS == nalu_type) || (SrsHevcNaluType_SPS == nalu_type) || (SrsHevcNaluType_PPS == nalu_type)) {
+ if (SrsIsIRAP(nalu_type) || (SrsHevcNaluType_VPS == nalu_type) || (SrsHevcNaluType_SPS == nalu_type) || (SrsHevcNaluType_PPS == nalu_type)) {
return true;
}
}
-
+
return false;
}
-#endif
-bool SrsRtpPacket::is_keyframe()
+bool SrsRtpPacket::is_keyframe(SrsVideoCodecId codec_id)
{
// False if audio packet
- if(SrsFrameTypeAudio == frame_type) {
+ if (SrsFrameTypeAudio == frame_type) {
return false;
}
- // Check H.264 keyframe types
- if (nalu_type == kStapA || nalu_type == kFuA ||
- nalu_type == SrsAvcNaluTypeIDR || nalu_type == SrsAvcNaluTypeSPS || nalu_type == SrsAvcNaluTypePPS) {
+ // For H264 video rtp packet
+ if (codec_id == SrsVideoCodecIdAVC) {
return srs_rtp_packet_h264_is_keyframe(nalu_type, payload_);
}
+
+ // For H265 video rtp packet
+ if (codec_id == SrsVideoCodecIdHEVC) {
+ return srs_rtp_packet_h265_is_keyframe(nalu_type, payload_);
+ }
-#ifdef SRS_H265
- // Check H.265 keyframe types
- return srs_rtp_packet_h265_is_keyframe(nalu_type, payload_);
-#else
return false;
-#endif
}
SrsRtpRawPayload::SrsRtpRawPayload()
@@ -1573,6 +1568,7 @@ ISrsRtpPayloader* SrsRtpFUAPayload2::copy()
return cp;
}
+#ifdef SRS_H265
SrsRtpSTAPPayloadHevc::SrsRtpSTAPPayloadHevc()
{
++_srs_pps_objs_rothers->sugar;
@@ -1931,3 +1927,4 @@ ISrsRtpPayloader* SrsRtpFUAPayloadHevc2::copy()
return cp;
}
+#endif
\ No newline at end of file
diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp
index b4c4b9143..f69732b3c 100644
--- a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp
+++ b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp
@@ -264,27 +264,27 @@ public:
};
// The payload type, for performance to avoid dynamic cast.
-enum SrsRtspPacketPayloadType
+enum SrsRtpPacketPayloadType
{
- SrsRtspPacketPayloadTypeRaw,
- SrsRtspPacketPayloadTypeFUA2,
- SrsRtspPacketPayloadTypeFUAHevc2,
- SrsRtspPacketPayloadTypeFUA,
- SrsRtspPacketPayloadTypeFUAHevc,
- SrsRtspPacketPayloadTypeNALU,
- SrsRtspPacketPayloadTypeSTAP,
- SrsRtspPacketPayloadTypeSTAPHevc,
- SrsRtspPacketPayloadTypeUnknown,
+ SrsRtpPacketPayloadTypeRaw,
+ SrsRtpPacketPayloadTypeFUA2,
+ SrsRtpPacketPayloadTypeFUAHevc2,
+ SrsRtpPacketPayloadTypeFUA,
+ SrsRtpPacketPayloadTypeFUAHevc,
+ SrsRtpPacketPayloadTypeNALU,
+ SrsRtpPacketPayloadTypeSTAP,
+ SrsRtpPacketPayloadTypeSTAPHevc,
+ SrsRtpPacketPayloadTypeUnknown,
};
-class ISrsRtspPacketDecodeHandler
+class ISrsRtpPacketDecodeHandler
{
public:
- ISrsRtspPacketDecodeHandler();
- virtual ~ISrsRtspPacketDecodeHandler();
+ ISrsRtpPacketDecodeHandler();
+ virtual ~ISrsRtpPacketDecodeHandler();
public:
// We don't know the actual payload, so we depends on external handler.
- virtual void on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtspPacketPayloadType* ppt) = 0;
+ virtual void on_before_decode_payload(SrsRtpPacket* pkt, SrsBuffer* buf, ISrsRtpPayloader** ppayload, SrsRtpPacketPayloadType* ppt) = 0;
};
// The RTP packet with cached shared message.
@@ -295,7 +295,7 @@ public:
SrsRtpHeader header;
private:
ISrsRtpPayloader* payload_;
- SrsRtspPacketPayloadType payload_type_;
+ SrsRtpPacketPayloadType payload_type_;
private:
// The original shared message, all RTP packets can refer to its data.
// Note that the size of shared msg, is not the packet size, it's a larger aligned buffer.
@@ -315,7 +315,7 @@ private:
// The cached payload size for packet.
int cached_payload_size;
// The helper handler for decoder, use RAW payload if NULL.
- ISrsRtspPacketDecodeHandler* decode_handler;
+ ISrsRtpPacketDecodeHandler* decode_handler;
private:
int64_t avsync_time_;
public:
@@ -334,14 +334,14 @@ public:
void enable_twcc_decode() { header.enable_twcc_decode(); } // SrsRtpPacket::enable_twcc_decode
// Get and set the payload of packet.
// @remark Note that return NULL if no payload.
- void set_payload(ISrsRtpPayloader* p, SrsRtspPacketPayloadType pt) { payload_ = p; payload_type_ = pt; }
+ void set_payload(ISrsRtpPayloader* p, SrsRtpPacketPayloadType pt) { payload_ = p; payload_type_ = pt; }
ISrsRtpPayloader* payload() { return payload_; }
// Set the padding of RTP packet.
void set_padding(int size);
// Increase the padding of RTP packet.
void add_padding(int size);
// Set the decode handler.
- void set_decode_handler(ISrsRtspPacketDecodeHandler* h);
+ void set_decode_handler(ISrsRtpPacketDecodeHandler* h);
// Whether the packet is Audio packet.
bool is_audio();
// Set RTP header extensions for encoding or decoding header extension
@@ -352,7 +352,7 @@ public:
virtual srs_error_t encode(SrsBuffer* buf);
virtual srs_error_t decode(SrsBuffer* buf);
public:
- bool is_keyframe();
+ bool is_keyframe(SrsVideoCodecId codec_id);
// Get and set the packet sync time in milliseconds.
void set_avsync_time(int64_t avsync_time) { avsync_time_ = avsync_time; }
int64_t get_avsync_time() const { return avsync_time_; }
@@ -478,6 +478,7 @@ public:
virtual ISrsRtpPayloader* copy();
};
+#ifdef SRS_H265
class SrsRtpSTAPPayloadHevc : public ISrsRtpPayloader
{
public:
@@ -541,5 +542,6 @@ public:
virtual srs_error_t decode(SrsBuffer* buf);
virtual ISrsRtpPayloader* copy();
};
+#endif
#endif
diff --git a/trunk/src/kernel/srs_kernel_ts.cpp b/trunk/src/kernel/srs_kernel_ts.cpp
index eed8c5bac..9492852fb 100644
--- a/trunk/src/kernel/srs_kernel_ts.cpp
+++ b/trunk/src/kernel/srs_kernel_ts.cpp
@@ -3146,8 +3146,7 @@ srs_error_t SrsTsMessageCache::do_cache_hevc(SrsVideoFrame* frame)
// Insert aud before NALU for HEVC.
SrsHevcNaluType nalu_type = (SrsHevcNaluType)SrsHevcNaluTypeParse(sample->bytes[0]);
- bool is_idr = (SrsHevcNaluType_CODED_SLICE_BLA <= nalu_type) && (nalu_type <= SrsHevcNaluType_RESERVED_23);
- if (is_idr && !frame->has_sps_pps && !is_sps_pps_appended) {
+ if (SrsIsIRAP(nalu_type) && !frame->has_sps_pps && !is_sps_pps_appended) {
for (size_t i = 0; i < codec->hevc_dec_conf_record_.nalu_vec.size(); i++) {
const SrsHevcHvccNalu& nalu = codec->hevc_dec_conf_record_.nalu_vec[i];
if (nalu.num_nalus <= 0 || nalu.nal_data_vec.empty()) continue;
diff --git a/trunk/src/protocol/srs_protocol_raw_avc.cpp b/trunk/src/protocol/srs_protocol_raw_avc.cpp
index 1f8ca1d24..bc22e2520 100644
--- a/trunk/src/protocol/srs_protocol_raw_avc.cpp
+++ b/trunk/src/protocol/srs_protocol_raw_avc.cpp
@@ -583,6 +583,39 @@ srs_error_t SrsRawHEVCStream::mux_avc2flv(std::string video, int8_t frame_type,
return err;
}
+
+srs_error_t SrsRawHEVCStream::mux_avc2flv_enhanced(std::string video, int8_t frame_type, int8_t packet_type, uint32_t dts, uint32_t pts, char ** flv, int * nb_flv)
+{
+ srs_error_t err = srs_success;
+
+ // for h265 in RTMP video payload, there is 5bytes header:
+ // 1bytes, IsExHeader | FrameType | PacketType
+ // 4bytes, Video FourCC. AV1 = { 'a', 'v', '0', '1' }
+ // VP9 = { 'v', 'p', '0', '9' }
+ // HEVC = { 'h', 'v', 'c', '1' }
+ // @see: enhanced-rtmp-v1.pdf, page 9
+ int size = (int)video.length() + 5;
+ char *data = new char[size];
+ char *p = data;
+
+ // IsExHeader | FrameType | PacketType
+ *p++ = SRS_FLV_IS_EX_HEADER | (frame_type << 4) | packet_type;
+
+ // Video FourCC.
+ *p++ = 'h';
+ *p++ = 'v';
+ *p++ = 'c';
+ *p++ = '1';
+
+ // hevc raw data.
+ memcpy(p, video.data(), video.length());
+
+ *flv = data;
+ *nb_flv = size;
+
+ return err;
+}
+
#endif
SrsRawAacStream::SrsRawAacStream()
diff --git a/trunk/src/protocol/srs_protocol_raw_avc.hpp b/trunk/src/protocol/srs_protocol_raw_avc.hpp
index 809c82e8d..e9402cc56 100644
--- a/trunk/src/protocol/srs_protocol_raw_avc.hpp
+++ b/trunk/src/protocol/srs_protocol_raw_avc.hpp
@@ -95,6 +95,15 @@ public:
// @param flv output the muxed flv packet.
// @param nb_flv output the muxed flv size.
virtual srs_error_t mux_avc2flv(std::string video, int8_t frame_type, int8_t avc_packet_type, uint32_t dts, uint32_t pts, char **flv, int *nb_flv);
+ // Mux the hevc video packet to flv video packet, enhanced mode.
+ // @param packet_type, SrsVideoHEVCFrameTraitPacketTypeSequenceStart or SrsVideoHEVCFrameTraitPacketTypeCodedFrames.
+ // @param frame_type, SrsVideoAvcFrameTypeKeyFrame or SrsVideoAvcFrameTypeInterFrame.
+ // @param video the hevc raw data.
+ // @param flv output the muxed flv packet.
+ // @param nb_flv output the muxed flv size.
+ // TODO: Rename method to mux_hevc2flv_enhanced since AVC is an alias for H.264, not H.265/HEVC.
+ // This affects other modules like SRT and GB28181, so should be done in a separate refactoring.
+ virtual srs_error_t mux_avc2flv_enhanced(std::string video, int8_t frame_type, int8_t packet_type, uint32_t dts, uint32_t pts, char **flv, int *nb_flv);
};
#endif
diff --git a/trunk/src/utest/srs_utest_rtc2.cpp b/trunk/src/utest/srs_utest_rtc2.cpp
new file mode 100644
index 000000000..cb70fc5f5
--- /dev/null
+++ b/trunk/src/utest/srs_utest_rtc2.cpp
@@ -0,0 +1,1517 @@
+//
+// Copyright (c) 2013-2025 The SRS Authors
+//
+// SPDX-License-Identifier: MIT
+//
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+using namespace std;
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadVideoCodecCaching)
+{
+ // Test video codec caching mechanism
+ if (true) {
+ SrsCodecPayload payload;
+ payload.name_ = "H264";
+
+ // First call should parse and cache the codec
+ int8_t codec1 = payload.codec(true);
+ EXPECT_EQ(SrsVideoCodecIdAVC, codec1);
+
+ // Second call should return cached value
+ int8_t codec2 = payload.codec(true);
+ EXPECT_EQ(SrsVideoCodecIdAVC, codec2);
+ EXPECT_EQ(codec1, codec2);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadAudioCodecCaching)
+{
+ // Test audio codec caching mechanism
+ if (true) {
+ SrsCodecPayload payload;
+ payload.name_ = "AAC";
+
+ // First call should parse and cache the codec
+ int8_t codec1 = payload.codec(false);
+ EXPECT_EQ(SrsAudioCodecIdAAC, codec1);
+
+ // Second call should return cached value
+ int8_t codec2 = payload.codec(false);
+ EXPECT_EQ(SrsAudioCodecIdAAC, codec2);
+ EXPECT_EQ(codec1, codec2);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadVideoCodecTypes)
+{
+ // Test various video codec types
+ if (true) {
+ // H.264/AVC codec
+ SrsCodecPayload h264_payload;
+ h264_payload.name_ = "H264";
+ EXPECT_EQ(SrsVideoCodecIdAVC, h264_payload.codec(true));
+
+ // Alternative H.264 name
+ SrsCodecPayload avc_payload;
+ avc_payload.name_ = "AVC";
+ EXPECT_EQ(SrsVideoCodecIdAVC, avc_payload.codec(true));
+ }
+
+ if (true) {
+ // H.265/HEVC codec
+ SrsCodecPayload h265_payload;
+ h265_payload.name_ = "H265";
+ EXPECT_EQ(SrsVideoCodecIdHEVC, h265_payload.codec(true));
+
+ // Alternative H.265 name
+ SrsCodecPayload hevc_payload;
+ hevc_payload.name_ = "HEVC";
+ EXPECT_EQ(SrsVideoCodecIdHEVC, hevc_payload.codec(true));
+ }
+
+ if (true) {
+ // AV1 codec
+ SrsCodecPayload av1_payload;
+ av1_payload.name_ = "AV1";
+ EXPECT_EQ(SrsVideoCodecIdAV1, av1_payload.codec(true));
+ }
+
+ if (true) {
+ // VP6 codec
+ SrsCodecPayload vp6_payload;
+ vp6_payload.name_ = "VP6";
+ EXPECT_EQ(SrsVideoCodecIdOn2VP6, vp6_payload.codec(true));
+
+ // VP6 with alpha channel
+ SrsCodecPayload vp6a_payload;
+ vp6a_payload.name_ = "VP6A";
+ EXPECT_EQ(SrsVideoCodecIdOn2VP6WithAlphaChannel, vp6a_payload.codec(true));
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadAudioCodecTypes)
+{
+ // Test various audio codec types
+ if (true) {
+ // AAC codec
+ SrsCodecPayload aac_payload;
+ aac_payload.name_ = "AAC";
+ EXPECT_EQ(SrsAudioCodecIdAAC, aac_payload.codec(false));
+ }
+
+ if (true) {
+ // MP3 codec
+ SrsCodecPayload mp3_payload;
+ mp3_payload.name_ = "MP3";
+ EXPECT_EQ(SrsAudioCodecIdMP3, mp3_payload.codec(false));
+ }
+
+ if (true) {
+ // Opus codec
+ SrsCodecPayload opus_payload;
+ opus_payload.name_ = "OPUS";
+ EXPECT_EQ(SrsAudioCodecIdOpus, opus_payload.codec(false));
+ }
+
+ if (true) {
+ // Speex codec
+ SrsCodecPayload speex_payload;
+ speex_payload.name_ = "SPEEX";
+ EXPECT_EQ(SrsAudioCodecIdSpeex, speex_payload.codec(false));
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadCaseInsensitive)
+{
+ // Test case insensitive codec name parsing
+ if (true) {
+ // Video codecs - lowercase
+ SrsCodecPayload h264_lower;
+ h264_lower.name_ = "h264";
+ EXPECT_EQ(SrsVideoCodecIdAVC, h264_lower.codec(true));
+
+ SrsCodecPayload hevc_lower;
+ hevc_lower.name_ = "hevc";
+ EXPECT_EQ(SrsVideoCodecIdHEVC, hevc_lower.codec(true));
+
+ // Video codecs - mixed case
+ SrsCodecPayload h264_mixed;
+ h264_mixed.name_ = "H264";
+ EXPECT_EQ(SrsVideoCodecIdAVC, h264_mixed.codec(true));
+
+ SrsCodecPayload hevc_mixed;
+ hevc_mixed.name_ = "Hevc";
+ EXPECT_EQ(SrsVideoCodecIdHEVC, hevc_mixed.codec(true));
+ }
+
+ if (true) {
+ // Audio codecs - lowercase
+ SrsCodecPayload aac_lower;
+ aac_lower.name_ = "aac";
+ EXPECT_EQ(SrsAudioCodecIdAAC, aac_lower.codec(false));
+
+ SrsCodecPayload opus_lower;
+ opus_lower.name_ = "opus";
+ EXPECT_EQ(SrsAudioCodecIdOpus, opus_lower.codec(false));
+
+ // Audio codecs - mixed case
+ SrsCodecPayload mp3_mixed;
+ mp3_mixed.name_ = "Mp3";
+ EXPECT_EQ(SrsAudioCodecIdMP3, mp3_mixed.codec(false));
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadUnknownCodecs)
+{
+ // Test unknown/unsupported codec handling
+ if (true) {
+ // Unknown video codec
+ SrsCodecPayload unknown_video;
+ unknown_video.name_ = "H266"; // Future codec not yet supported
+ EXPECT_EQ(SrsVideoCodecIdReserved, unknown_video.codec(true));
+
+ // Completely unknown video codec
+ SrsCodecPayload invalid_video;
+ invalid_video.name_ = "UNKNOWN_VIDEO";
+ EXPECT_EQ(SrsVideoCodecIdReserved, invalid_video.codec(true));
+ }
+
+ if (true) {
+ // Unknown audio codec
+ SrsCodecPayload unknown_audio;
+ unknown_audio.name_ = "FLAC"; // Not supported in this context
+ EXPECT_EQ(SrsAudioCodecIdReserved1, unknown_audio.codec(false));
+
+ // Completely unknown audio codec
+ SrsCodecPayload invalid_audio;
+ invalid_audio.name_ = "UNKNOWN_AUDIO";
+ EXPECT_EQ(SrsAudioCodecIdReserved1, invalid_audio.codec(false));
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadEmptyName)
+{
+ // Test empty codec name handling
+ if (true) {
+ SrsCodecPayload empty_video;
+ empty_video.name_ = "";
+ EXPECT_EQ(SrsVideoCodecIdReserved, empty_video.codec(true));
+
+ SrsCodecPayload empty_audio;
+ empty_audio.name_ = "";
+ EXPECT_EQ(SrsAudioCodecIdReserved1, empty_audio.codec(false));
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadContextSensitive)
+{
+ // Test that the same codec name can be interpreted differently based on context
+ if (true) {
+ // This test demonstrates that the video/audio context parameter matters
+ SrsCodecPayload payload;
+ payload.name_ = "H264";
+
+ // When called with video=true, should return video codec ID
+ int8_t video_codec = payload.codec(true);
+ EXPECT_EQ(SrsVideoCodecIdAVC, video_codec);
+
+ // Reset the cached value to test audio context
+ payload.codec_ = -1;
+
+ // When called with video=false, H264 is not a valid audio codec
+ int8_t audio_codec = payload.codec(false);
+ EXPECT_EQ(SrsAudioCodecIdReserved1, audio_codec);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadConstructorInitialization)
+{
+ // Test that codec_ is properly initialized to -1
+ if (true) {
+ SrsCodecPayload payload1;
+ // codec_ should be initialized to -1 (not cached)
+ EXPECT_EQ(-1, payload1.codec_);
+
+ SrsCodecPayload payload2(96, "H264", 90000);
+ // codec_ should be initialized to -1 (not cached)
+ EXPECT_EQ(-1, payload2.codec_);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadMultipleCallsConsistency)
+{
+ // Test that multiple calls with the same context return consistent results
+ if (true) {
+ SrsCodecPayload payload;
+ payload.name_ = "HEVC";
+
+ // Multiple calls should return the same result
+ for (int i = 0; i < 10; i++) {
+ EXPECT_EQ(SrsVideoCodecIdHEVC, payload.codec(true));
+ }
+
+ // Reset and test audio context
+ payload.codec_ = -1;
+ payload.name_ = "OPUS";
+
+ for (int i = 0; i < 10; i++) {
+ EXPECT_EQ(SrsAudioCodecIdOpus, payload.codec(false));
+ }
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadCacheInvalidation)
+{
+ // Test that changing name_ doesn't automatically invalidate cache
+ // (This demonstrates the current behavior - cache is not automatically invalidated)
+ if (true) {
+ SrsCodecPayload payload;
+ payload.name_ = "H264";
+
+ // First call caches the result
+ int8_t codec1 = payload.codec(true);
+ EXPECT_EQ(SrsVideoCodecIdAVC, codec1);
+
+ // Change the name but cache should still return old value
+ payload.name_ = "HEVC";
+ int8_t codec2 = payload.codec(true);
+ EXPECT_EQ(SrsVideoCodecIdAVC, codec2); // Still returns cached H264 value
+
+ // Manual cache reset allows new parsing
+ payload.codec_ = -1;
+ int8_t codec3 = payload.codec(true);
+ EXPECT_EQ(SrsVideoCodecIdHEVC, codec3); // Now returns HEVC
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadSpecialCharacters)
+{
+ // Test codec names with special characters or whitespace
+ if (true) {
+ // Test with leading/trailing spaces (should still work due to uppercase conversion)
+ SrsCodecPayload payload_spaces;
+ payload_spaces.name_ = " H264 ";
+ // Note: The current implementation doesn't trim spaces, so this will be unknown
+ EXPECT_EQ(SrsVideoCodecIdReserved, payload_spaces.codec(true));
+
+ // Test with numbers and special characters
+ SrsCodecPayload payload_special;
+ payload_special.name_ = "H.264";
+ EXPECT_EQ(SrsVideoCodecIdReserved, payload_special.codec(true));
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadBoundaryValues)
+{
+ // Test boundary values and edge cases
+ if (true) {
+ // Test very long codec name
+ SrsCodecPayload long_name;
+ long_name.name_ = std::string(1000, 'A'); // 1000 character string
+ EXPECT_EQ(SrsVideoCodecIdReserved, long_name.codec(true));
+ // Already cached by video, so should return same result for audio.
+ EXPECT_EQ(SrsVideoCodecIdReserved, long_name.codec(false));
+ }
+
+ if (true) {
+ // Test single character names
+ SrsCodecPayload single_char;
+ single_char.name_ = "A";
+ EXPECT_EQ(SrsVideoCodecIdReserved, single_char.codec(true));
+ // Already cached by video, so should return same result for audio.
+ EXPECT_EQ(SrsVideoCodecIdReserved, single_char.codec(false));
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadPerformanceCache)
+{
+ // Test that caching provides performance benefit (conceptual test)
+ if (true) {
+ SrsCodecPayload payload;
+ payload.name_ = "H264";
+
+ // First call does parsing and caching
+ auto start = std::chrono::high_resolution_clock::now();
+ int8_t codec1 = payload.codec(true);
+ auto end1 = std::chrono::high_resolution_clock::now();
+
+ // Subsequent calls should be faster (cached)
+ auto start2 = std::chrono::high_resolution_clock::now();
+ int8_t codec2 = payload.codec(true);
+ auto end2 = std::chrono::high_resolution_clock::now();
+
+ // Both should return the same result
+ EXPECT_EQ(codec1, codec2);
+ EXPECT_EQ(SrsVideoCodecIdAVC, codec1);
+
+ // Note: In practice, the performance difference might be negligible
+ // for such simple string comparisons, but the caching mechanism is there
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsVideoPayloadInheritance)
+{
+ // Test that SrsVideoPayload inherits codec functionality correctly
+ if (true) {
+ SrsVideoPayload video_payload;
+ video_payload.name_ = "H265";
+
+ // Should work the same as base class
+ EXPECT_EQ(SrsVideoCodecIdHEVC, video_payload.codec(true));
+
+ // Test caching works in derived class
+ EXPECT_EQ(SrsVideoCodecIdHEVC, video_payload.codec(true));
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsAudioPayloadInheritance)
+{
+ // Test that SrsAudioPayload inherits codec functionality correctly
+ if (true) {
+ SrsAudioPayload audio_payload;
+ audio_payload.name_ = "AAC";
+
+ // Should work the same as base class
+ EXPECT_EQ(SrsAudioCodecIdAAC, audio_payload.codec(false));
+
+ // Test caching works in derived class
+ EXPECT_EQ(SrsAudioCodecIdAAC, audio_payload.codec(false));
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsCodecPayloadCopyBehavior)
+{
+ // Test that copy() method doesn't copy the cached codec_ value
+ if (true) {
+ SrsCodecPayload original;
+ original.name_ = "H264";
+ original.pt_ = 96;
+ original.sample_ = 90000;
+
+ // Cache the codec value
+ int8_t codec_original = original.codec(true);
+ EXPECT_EQ(SrsVideoCodecIdAVC, codec_original);
+
+ // Copy the payload
+ SrsCodecPayload* copied = original.copy();
+
+ // The copied payload should have the same name but uncached codec
+ EXPECT_EQ(original.name_, copied->name_);
+ EXPECT_EQ(original.pt_, copied->pt_);
+ EXPECT_EQ(original.sample_, copied->sample_);
+ EXPECT_EQ(-1, copied->codec_); // Should not copy cached value
+
+ // But calling codec() should return the same result
+ EXPECT_EQ(SrsVideoCodecIdAVC, copied->codec(true));
+
+ srs_freep(copied);
+ }
+}
+
+// Helper function to create a test RTP packet
+SrsRtpPacket* mock_create_test_rtp_packet(uint16_t sequence_number, uint32_t timestamp, bool marker = false)
+{
+ SrsRtpPacket* pkt = new SrsRtpPacket();
+ pkt->header.set_sequence(sequence_number);
+ pkt->header.set_timestamp(timestamp);
+ pkt->header.set_marker(marker);
+ pkt->header.set_ssrc(12345);
+ return pkt;
+}
+
+// Helper function to create a test FU-A payload
+SrsRtpFUAPayload2* mock_create_test_fua_payload(bool start, bool end, const char* payload_data, int size)
+{
+ SrsRtpFUAPayload2* fua = new SrsRtpFUAPayload2();
+ fua->start = start;
+ fua->end = end;
+ fua->nalu_type = SrsAvcNaluTypeNonIDR; // Use a common NALU type
+ fua->nri = SrsAvcNaluTypeNonIDR;
+
+ // Create a buffer for the payload
+ char* buf = new char[size];
+ memcpy(buf, payload_data, size);
+ fua->payload = buf;
+ fua->size = size;
+
+ return fua;
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheBasicOperations)
+{
+ // Test basic store and get operations
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Test storing and retrieving a packet
+ SrsRtpPacket* pkt1 = mock_create_test_rtp_packet(100, 1000);
+ cache.store_packet(pkt1);
+
+ SrsRtpPacket* retrieved = cache.get_packet(100);
+ EXPECT_TRUE(retrieved != NULL);
+ EXPECT_EQ(100, retrieved->header.get_sequence());
+ EXPECT_EQ(1000, retrieved->header.get_timestamp());
+
+ // Test getting non-existent packet
+ SrsRtpPacket* missing = cache.get_packet(200);
+ EXPECT_TRUE(missing == NULL);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheNullPacket)
+{
+ // Test handling of null packets
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Storing null packet should be ignored
+ cache.store_packet(NULL);
+
+ // Cache should remain empty
+ SrsRtpPacket* retrieved = cache.get_packet(100);
+ EXPECT_TRUE(retrieved == NULL);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheOverwrite)
+{
+ // Test overwriting packets in the same slot
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store first packet
+ SrsRtpPacket* pkt1 = mock_create_test_rtp_packet(100, 1000);
+ cache.store_packet(pkt1);
+
+ // Store second packet with same sequence (should overwrite)
+ SrsRtpPacket* pkt2 = mock_create_test_rtp_packet(100, 2000);
+ cache.store_packet(pkt2);
+
+ // Should get the second packet
+ SrsRtpPacket* retrieved = cache.get_packet(100);
+ EXPECT_TRUE(retrieved != NULL);
+ EXPECT_EQ(100, retrieved->header.get_sequence());
+ EXPECT_EQ(2000, retrieved->header.get_timestamp());
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheModuloIndexing)
+{
+ int cache_size = SrsRtcFrameBuilderVideoPacketCache::cache_size_;
+
+ // Test that cache uses modulo indexing (N slots)
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store packets that would map to the same cache slot (N apart)
+ SrsRtpPacket* pkt1 = mock_create_test_rtp_packet(100, 1000);
+ SrsRtpPacket* pkt2 = mock_create_test_rtp_packet(100 + cache_size, 2000);
+
+ cache.store_packet(pkt1);
+ cache.store_packet(pkt2);
+
+ // Should get the second packet (overwrote first)
+ SrsRtpPacket* retrieved1 = cache.get_packet(100);
+ EXPECT_TRUE(retrieved1 == NULL); // First packet was overwritten
+
+ SrsRtpPacket* retrieved2 = cache.get_packet(100 + cache_size);
+ EXPECT_TRUE(retrieved2 != NULL);
+ EXPECT_EQ(100 + cache_size, retrieved2->header.get_sequence());
+ EXPECT_EQ(2000, retrieved2->header.get_timestamp());
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheTakePacket)
+{
+ // Test take_packet functionality
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store a packet
+ SrsRtpPacket* pkt = mock_create_test_rtp_packet(100, 1000);
+ cache.store_packet(pkt);
+
+ // Take the packet (should remove from cache)
+ SrsRtpPacket* taken = cache.take_packet(100);
+ EXPECT_TRUE(taken != NULL);
+ EXPECT_EQ(100, taken->header.get_sequence());
+
+ // Clean up the taken packet
+ srs_freep(taken);
+
+ // Packet should no longer be in cache
+ SrsRtpPacket* retrieved = cache.get_packet(100);
+ EXPECT_TRUE(retrieved == NULL);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheTakeNonExistent)
+{
+ // Test taking non-existent packet
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Try to take packet that doesn't exist
+ SrsRtpPacket* taken = cache.take_packet(100);
+ EXPECT_TRUE(taken == NULL);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheClearAll)
+{
+ // Test clear_all functionality
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store multiple packets
+ for (int i = 0; i < 10; i++) {
+ SrsRtpPacket* pkt = mock_create_test_rtp_packet(100 + i, 1000 + i);
+ cache.store_packet(pkt);
+ }
+
+ // Verify packets are stored
+ SrsRtpPacket* retrieved = cache.get_packet(105);
+ EXPECT_TRUE(retrieved != NULL);
+
+ // Clear all packets
+ cache.clear_all();
+
+ // Verify all packets are gone
+ for (int i = 0; i < 10; i++) {
+ SrsRtpPacket* pkt = cache.get_packet(100 + i);
+ EXPECT_TRUE(pkt == NULL);
+ }
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheFindNextLostSnComplete)
+{
+ // Test find_next_lost_sn when frame is complete
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store a complete sequence of packets with same timestamp
+ uint32_t timestamp = 1000;
+ for (uint16_t i = 100; i <= 105; i++) {
+ SrsRtpPacket* pkt = mock_create_test_rtp_packet(i, timestamp, i == 105); // Last packet has marker
+ cache.store_packet(pkt);
+ }
+
+ uint16_t end_sn = 0;
+ int32_t result = cache.find_next_lost_sn(100, 100, end_sn);
+
+ // Should return -1 (complete frame) and set end_sn to marker packet
+ EXPECT_EQ(-1, result);
+ EXPECT_EQ(105, end_sn);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheFindNextLostSnMissing)
+{
+ // Test find_next_lost_sn when packet is missing
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store packets with a gap (missing 103)
+ uint32_t timestamp = 1000;
+ cache.store_packet(mock_create_test_rtp_packet(100, timestamp));
+ cache.store_packet(mock_create_test_rtp_packet(101, timestamp));
+ cache.store_packet(mock_create_test_rtp_packet(102, timestamp));
+ // Skip 103
+ cache.store_packet(mock_create_test_rtp_packet(104, timestamp));
+ cache.store_packet(mock_create_test_rtp_packet(105, timestamp, true)); // marker
+
+ uint16_t end_sn = 0;
+ int32_t result = cache.find_next_lost_sn(100, 100, end_sn);
+
+ // Should return the missing sequence number
+ EXPECT_EQ(103, result);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheFindNextLostSnDifferentTimestamp)
+{
+ // Test find_next_lost_sn when timestamp changes (frame boundary)
+ // NOTE: This tests the current implementation behavior, which uses timestamp changes
+ // to detect frame boundaries. According to RFC 6184, the marker bit should be the
+ // primary mechanism, but SRS also uses timestamp changes as a fallback.
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store packets with different timestamps but no marker bits
+ cache.store_packet(mock_create_test_rtp_packet(100, 1000, false)); // marker is false
+ cache.store_packet(mock_create_test_rtp_packet(101, 1000, false)); // marker is false
+ cache.store_packet(mock_create_test_rtp_packet(102, 2000, false)); // Different timestamp, marker false
+
+ uint16_t end_sn = 0;
+ int32_t result = cache.find_next_lost_sn(100, 100, end_sn);
+
+ // Current implementation: returns -1 (complete frame) and sets end_sn to last packet of same timestamp
+ // This is SRS-specific behavior that uses timestamp changes as frame boundary detection
+ EXPECT_EQ(-1, result);
+ EXPECT_EQ(101, end_sn);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheFindNextLostSnRfcCompliant)
+{
+ // Test RFC 6184 compliant frame boundary detection using marker bit
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store packets: 100 and 101 have same timestamp, 101 has marker bit, 102 has different timestamp
+ cache.store_packet(mock_create_test_rtp_packet(100, 1000, false)); // marker is false
+ cache.store_packet(mock_create_test_rtp_packet(101, 1000, true)); // marker is true (end of access unit)
+ cache.store_packet(mock_create_test_rtp_packet(102, 2000, false)); // different timestamp
+
+ uint16_t end_sn = 0;
+ int32_t result = cache.find_next_lost_sn(100, 100, end_sn);
+
+ // RFC 6184 compliant: Should return -1 (complete frame) and set end_sn to marker packet (101)
+ // The marker bit should take precedence over timestamp changes
+ EXPECT_EQ(-1, result);
+ EXPECT_EQ(101, end_sn);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheFindNextLostSnMarkerVsTimestamp)
+{
+ // Test the priority between marker bit and timestamp change
+ // This demonstrates the current SRS implementation behavior vs RFC 6184 compliance
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Critical scenario: 100 and 101 have same timestamp, 101 has marker bit, 102 has different timestamp
+ // This tests whether SRS respects the marker bit or prioritizes timestamp changes
+ cache.store_packet(mock_create_test_rtp_packet(100, 1000, false)); // marker is false
+ cache.store_packet(mock_create_test_rtp_packet(101, 1000, true)); // marker is true (should end frame here)
+ cache.store_packet(mock_create_test_rtp_packet(102, 2000, false)); // different timestamp
+
+ uint16_t end_sn = 0;
+ int32_t result = cache.find_next_lost_sn(100, 100, end_sn);
+
+ // Current SRS implementation behavior:
+ // The algorithm checks marker bit BEFORE timestamp change in the loop
+ // So it should detect the marker bit on packet 101 and end the frame there
+ EXPECT_EQ(-1, result);
+ EXPECT_EQ(101, end_sn); // Should end at marker packet, not at timestamp change
+
+ // This actually demonstrates that SRS IS RFC-compliant in this case!
+ // The marker bit is detected first and takes precedence
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheTimestampPriorityIssue)
+{
+ // Test the specific issue: timestamp check happens BEFORE marker check
+ // This demonstrates a potential RFC compliance issue
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Scenario where timestamp change occurs before we reach the marker bit
+ cache.store_packet(mock_create_test_rtp_packet(100, 1000, false)); // timestamp=1000, marker=false
+ cache.store_packet(mock_create_test_rtp_packet(101, 2000, false)); // timestamp=2000, marker=false (timestamp changed!)
+ cache.store_packet(mock_create_test_rtp_packet(102, 2000, true)); // timestamp=2000, marker=true (actual frame end)
+
+ uint16_t end_sn = 0;
+ int32_t result = cache.find_next_lost_sn(100, 100, end_sn);
+
+ // Current SRS implementation: timestamp check happens FIRST
+ // When it reaches packet 101, it detects timestamp change and ends frame at packet 100
+ // It never gets to check the marker bit on packet 102
+ EXPECT_EQ(-1, result);
+ EXPECT_EQ(100, end_sn); // Ends at last packet with original timestamp (100)
+
+ // This is NOT RFC-compliant! According to RFC 6184, it should continue until
+ // the marker bit (packet 102) and set end_sn = 102
+ // The comment in line 1591 "check time first, avoid two small frame mixed case decode fail"
+ // shows this is intentional for robustness, but it's not RFC-compliant
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheFindNextLostSnNoMarkerNoTimestampChange)
+{
+ // Test behavior when neither marker bit nor timestamp change occurs
+ // This tests the cache overflow detection
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store many packets with same timestamp and no marker bit
+ // This should eventually trigger cache overflow detection
+ uint32_t timestamp = 1000;
+ for (int i = 0; i < 10; i++) {
+ cache.store_packet(mock_create_test_rtp_packet(100 + i, timestamp, false));
+ }
+
+ uint16_t end_sn = 0;
+ int32_t result = cache.find_next_lost_sn(100, 100, end_sn);
+
+ // Should continue searching until cache limit or missing packet
+ // In this case, all packets are present, so it should continue until cache overflow
+ // The exact behavior depends on cache size (512), but for this small test it should work
+ EXPECT_EQ(110, result); // Should find missing packet after the stored range
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheCheckFrameCompleteSimple)
+{
+ // Test check_frame_complete with non-fragmented packets
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store simple packets (no FU-A payload)
+ for (uint16_t i = 100; i <= 103; i++) {
+ SrsRtpPacket* pkt = mock_create_test_rtp_packet(i, 1000);
+ cache.store_packet(pkt);
+ }
+
+ // When there are no FU-A payloads, fu_s_c == fu_e_c (both are 0)
+ bool complete = cache.check_frame_complete(100, 103);
+ EXPECT_TRUE(complete); // Expected: no FU-A fragments means complete (0 == 0)
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheCheckFrameCompleteFragmented)
+{
+ // Test check_frame_complete with fragmented packets (FU-A)
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Create fragmented packets with matching start/end counts
+ char payload_data[] = "test_payload";
+
+ // First fragment (start=true, end=false)
+ SrsRtpPacket* pkt1 = mock_create_test_rtp_packet(100, 1000);
+ SrsRtpFUAPayload2* fua1 = mock_create_test_fua_payload(true, false, payload_data, sizeof(payload_data));
+ pkt1->set_payload(fua1, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt1);
+
+ // Middle fragment (start=false, end=false)
+ SrsRtpPacket* pkt2 = mock_create_test_rtp_packet(101, 1000);
+ SrsRtpFUAPayload2* fua2 = mock_create_test_fua_payload(false, false, payload_data, sizeof(payload_data));
+ pkt2->set_payload(fua2, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt2);
+
+ // Last fragment (start=false, end=true)
+ SrsRtpPacket* pkt3 = mock_create_test_rtp_packet(102, 1000);
+ SrsRtpFUAPayload2* fua3 = mock_create_test_fua_payload(false, true, payload_data, sizeof(payload_data));
+ pkt3->set_payload(fua3, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt3);
+
+ // Should return true (1 start fragment == 1 end fragment = complete fragmented frame)
+ bool complete = cache.check_frame_complete(100, 102);
+ EXPECT_TRUE(complete);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheCheckFrameIncompleteFragmented)
+{
+ // Test check_frame_complete with incomplete fragmented packets
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ char payload_data[] = "test_payload";
+
+ // Two start fragments but only one end fragment (incomplete)
+ SrsRtpPacket* pkt1 = mock_create_test_rtp_packet(100, 1000);
+ SrsRtpFUAPayload2* fua1 = mock_create_test_fua_payload(true, false, payload_data, sizeof(payload_data));
+ pkt1->set_payload(fua1, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt1);
+
+ SrsRtpPacket* pkt2 = mock_create_test_rtp_packet(101, 1000);
+ SrsRtpFUAPayload2* fua2 = mock_create_test_fua_payload(true, false, payload_data, sizeof(payload_data));
+ pkt2->set_payload(fua2, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt2);
+
+ SrsRtpPacket* pkt3 = mock_create_test_rtp_packet(102, 1000);
+ SrsRtpFUAPayload2* fua3 = mock_create_test_fua_payload(false, true, payload_data, sizeof(payload_data));
+ pkt3->set_payload(fua3, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt3);
+
+ // Should return false (2 starts, 1 end - mismatch)
+ bool complete = cache.check_frame_complete(100, 102);
+ EXPECT_FALSE(complete);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheCheckFrameCompleteOneStartOneEnd)
+{
+ // Test check_frame_complete with exactly 1 start and 1 end fragment (correct case)
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ char payload_data[] = "test_payload";
+
+ // Single fragmented NALU: start fragment
+ SrsRtpPacket* pkt1 = mock_create_test_rtp_packet(100, 1000);
+ SrsRtpFUAPayload2* fua1 = mock_create_test_fua_payload(true, false, payload_data, sizeof(payload_data));
+ pkt1->set_payload(fua1, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt1);
+
+ // Middle fragments (no start, no end)
+ SrsRtpPacket* pkt2 = mock_create_test_rtp_packet(101, 1000);
+ SrsRtpFUAPayload2* fua2 = mock_create_test_fua_payload(false, false, payload_data, sizeof(payload_data));
+ pkt2->set_payload(fua2, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt2);
+
+ SrsRtpPacket* pkt3 = mock_create_test_rtp_packet(102, 1000);
+ SrsRtpFUAPayload2* fua3 = mock_create_test_fua_payload(false, false, payload_data, sizeof(payload_data));
+ pkt3->set_payload(fua3, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt3);
+
+ // End fragment
+ SrsRtpPacket* pkt4 = mock_create_test_rtp_packet(103, 1000);
+ SrsRtpFUAPayload2* fua4 = mock_create_test_fua_payload(false, true, payload_data, sizeof(payload_data));
+ pkt4->set_payload(fua4, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt4);
+
+ // Should return true (1 start == 1 end = complete fragmented frame)
+ bool complete = cache.check_frame_complete(100, 103);
+ EXPECT_TRUE(complete);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheCheckFrameCompleteMultipleNalus)
+{
+ // Test check_frame_complete with multiple complete fragmented NALUs (2 start == 2 end = complete)
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ char payload_data[] = "test_payload";
+
+ // First NALU: start fragment
+ SrsRtpPacket* pkt1 = mock_create_test_rtp_packet(100, 1000);
+ SrsRtpFUAPayload2* fua1 = mock_create_test_fua_payload(true, false, payload_data, sizeof(payload_data));
+ pkt1->set_payload(fua1, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt1);
+
+ // First NALU: end fragment
+ SrsRtpPacket* pkt2 = mock_create_test_rtp_packet(101, 1000);
+ SrsRtpFUAPayload2* fua2 = mock_create_test_fua_payload(false, true, payload_data, sizeof(payload_data));
+ pkt2->set_payload(fua2, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt2);
+
+ // Second NALU: start fragment
+ SrsRtpPacket* pkt3 = mock_create_test_rtp_packet(102, 1000);
+ SrsRtpFUAPayload2* fua3 = mock_create_test_fua_payload(true, false, payload_data, sizeof(payload_data));
+ pkt3->set_payload(fua3, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt3);
+
+ // Second NALU: end fragment
+ SrsRtpPacket* pkt4 = mock_create_test_rtp_packet(103, 1000);
+ SrsRtpFUAPayload2* fua4 = mock_create_test_fua_payload(false, true, payload_data, sizeof(payload_data));
+ pkt4->set_payload(fua4, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt4);
+
+ // Should return true (2 starts == 2 ends = complete fragmented frame)
+ bool complete = cache.check_frame_complete(100, 103);
+ EXPECT_TRUE(complete);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheCheckFrameCompleteNullPackets)
+{
+ // Test check_frame_complete with null packets in range
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store only some packets in the range
+ SrsRtpPacket* pkt1 = mock_create_test_rtp_packet(100, 1000);
+ cache.store_packet(pkt1);
+ // Skip 101 (will be null)
+ SrsRtpPacket* pkt3 = mock_create_test_rtp_packet(102, 1000);
+ cache.store_packet(pkt3);
+
+ // Should handle null packets gracefully and return true (no fragmentation, 0 == 0)
+ bool complete = cache.check_frame_complete(100, 102);
+ EXPECT_TRUE(complete);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheSequenceWrapAround)
+{
+ // Test sequence number wrap-around (16-bit overflow)
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Test near the 16-bit boundary
+ uint16_t seq_near_max = 65534;
+ SrsRtpPacket* pkt1 = mock_create_test_rtp_packet(seq_near_max, 1000);
+ cache.store_packet(pkt1);
+
+ uint16_t seq_wrapped = 1; // After wrap-around
+ SrsRtpPacket* pkt2 = mock_create_test_rtp_packet(seq_wrapped, 1000);
+ cache.store_packet(pkt2);
+
+ // Should be able to retrieve both packets
+ SrsRtpPacket* retrieved1 = cache.get_packet(seq_near_max);
+ EXPECT_TRUE(retrieved1 != NULL);
+ EXPECT_EQ(seq_near_max, retrieved1->header.get_sequence());
+
+ SrsRtpPacket* retrieved2 = cache.get_packet(seq_wrapped);
+ EXPECT_TRUE(retrieved2 != NULL);
+ EXPECT_EQ(seq_wrapped, retrieved2->header.get_sequence());
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoPacketCacheMemoryManagement)
+{
+ // Test that cache properly manages memory
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+
+ // Store a packet
+ SrsRtpPacket* pkt = mock_create_test_rtp_packet(100, 1000);
+ cache.store_packet(pkt);
+
+ // Overwrite with another packet (should free the first one)
+ SrsRtpPacket* pkt2 = mock_create_test_rtp_packet(100, 2000);
+ cache.store_packet(pkt2);
+
+ // Verify the second packet is stored
+ SrsRtpPacket* retrieved = cache.get_packet(100);
+ EXPECT_TRUE(retrieved != NULL);
+ EXPECT_EQ(2000, retrieved->header.get_timestamp());
+
+ // Clear all should free remaining packets
+ cache.clear_all();
+
+ // Cache should be empty
+ SrsRtpPacket* after_clear = cache.get_packet(100);
+ EXPECT_TRUE(after_clear == NULL);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorBasicConstruction)
+{
+ // Test basic construction and destruction
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Verify constructor initializes private members correctly
+ EXPECT_EQ(&cache, detector.video_cache_);
+ EXPECT_EQ(0, detector.header_sn_);
+ EXPECT_EQ(0, detector.lost_sn_);
+ EXPECT_EQ(-1, detector.rtp_key_frame_ts_);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorOnKeyframeStart)
+{
+ // Test on_keyframe_start functionality
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Verify initial state
+ EXPECT_EQ(-1, detector.rtp_key_frame_ts_);
+ EXPECT_EQ(0, detector.header_sn_);
+ EXPECT_EQ(0, detector.lost_sn_);
+
+ // Create a keyframe packet
+ SrsUniquePtr keyframe_pkt(mock_create_test_rtp_packet(100, 1000));
+
+ // Call on_keyframe_start - should initialize internal state
+ detector.on_keyframe_start(keyframe_pkt.get());
+
+ // Verify state after first keyframe
+ EXPECT_EQ(1000, detector.rtp_key_frame_ts_);
+ EXPECT_EQ(100, detector.header_sn_);
+ EXPECT_EQ(101, detector.lost_sn_); // header_sn_ + 1
+
+ // Test that subsequent calls with same timestamp don't reset
+ SrsUniquePtr same_keyframe_pkt(mock_create_test_rtp_packet(101, 1000));
+ detector.on_keyframe_start(same_keyframe_pkt.get());
+
+ // State should remain unchanged for same timestamp
+ EXPECT_EQ(1000, detector.rtp_key_frame_ts_);
+ EXPECT_EQ(100, detector.header_sn_);
+ EXPECT_EQ(101, detector.lost_sn_);
+
+ // Test that calls with different timestamp do reset
+ SrsUniquePtr new_keyframe_pkt(mock_create_test_rtp_packet(200, 2000));
+ detector.on_keyframe_start(new_keyframe_pkt.get());
+
+ // State should be reset for new timestamp
+ EXPECT_EQ(2000, detector.rtp_key_frame_ts_);
+ EXPECT_EQ(200, detector.header_sn_);
+ EXPECT_EQ(201, detector.lost_sn_); // header_sn_ + 1
+
+ // All keyframe packets will be automatically cleaned up by SrsUniquePtr destructors
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorDetectFrameBasic)
+{
+ srs_error_t err;
+
+ // Test basic detect_frame functionality
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Initialize with keyframe
+ SrsUniquePtr keyframe_pkt(mock_create_test_rtp_packet(100, 1000));
+ detector.on_keyframe_start(keyframe_pkt.get());
+
+ // Verify initial state after keyframe
+ EXPECT_EQ(100, detector.header_sn_);
+ EXPECT_EQ(101, detector.lost_sn_);
+
+ // Store some packets in cache to form a complete frame
+ cache.store_packet(mock_create_test_rtp_packet(100, 1000, false));
+ cache.store_packet(mock_create_test_rtp_packet(101, 1000, false));
+ cache.store_packet(mock_create_test_rtp_packet(102, 1000, true)); // marker bit
+
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ // Test detecting frame when receiving the last packet
+ HELPER_EXPECT_SUCCESS(detector.detect_frame(102, frame_start, frame_end, frame_ready));
+
+ // If frame is ready, verify the frame boundaries
+ if (frame_ready) {
+ EXPECT_EQ(100, frame_start);
+ EXPECT_EQ(102, frame_end);
+ }
+
+ // keyframe_pkt will be automatically cleaned up by SrsUniquePtr destructor
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorDetectFrameWithGaps)
+{
+ srs_error_t err;
+
+ // Test detect_frame with missing packets
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Initialize with keyframe
+ SrsUniquePtr keyframe_pkt(mock_create_test_rtp_packet(100, 1000));
+ detector.on_keyframe_start(keyframe_pkt.get());
+
+ // Verify initial state
+ EXPECT_EQ(100, detector.header_sn_);
+ EXPECT_EQ(101, detector.lost_sn_);
+
+ // Store packets with a gap (missing 101)
+ cache.store_packet(mock_create_test_rtp_packet(100, 1000, false));
+ // Skip 101 - this creates a gap
+ cache.store_packet(mock_create_test_rtp_packet(102, 1000, false));
+ cache.store_packet(mock_create_test_rtp_packet(103, 1000, true)); // marker bit
+
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ // Test detecting frame - should not be ready due to missing packet
+ HELPER_EXPECT_SUCCESS(detector.detect_frame(103, frame_start, frame_end, frame_ready));
+ EXPECT_FALSE(frame_ready); // Should not be ready due to gap
+
+ // Verify lost_sn_ is set to the missing packet
+ EXPECT_EQ(101, detector.lost_sn_);
+
+ // Test is_lost_sn functionality
+ bool is_lost = detector.is_lost_sn(101);
+ EXPECT_TRUE(is_lost); // 101 should be the lost sequence number
+ EXPECT_FALSE(detector.is_lost_sn(100)); // 100 should not be lost
+ EXPECT_FALSE(detector.is_lost_sn(102)); // 102 should not be lost
+
+ // keyframe_pkt will be automatically cleaned up by SrsUniquePtr destructor
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorDetectFrameRecovery)
+{
+ srs_error_t err;
+
+ // Test frame detection recovery after missing packet arrives
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Initialize with keyframe
+ SrsUniquePtr keyframe_pkt(mock_create_test_rtp_packet(100, 1000));
+ detector.on_keyframe_start(keyframe_pkt.get());
+
+ // Store packets with a gap (missing 101)
+ cache.store_packet(mock_create_test_rtp_packet(100, 1000, false));
+ cache.store_packet(mock_create_test_rtp_packet(102, 1000, false));
+ cache.store_packet(mock_create_test_rtp_packet(103, 1000, true)); // marker bit
+
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ // First detection should succeed but frame not ready due to missing packet
+ HELPER_EXPECT_SUCCESS(detector.detect_frame(103, frame_start, frame_end, frame_ready));
+ EXPECT_FALSE(frame_ready);
+
+ // Now add the missing packet
+ cache.store_packet(mock_create_test_rtp_packet(101, 1000, false));
+
+ // Detection should now succeed when we receive the missing packet
+ HELPER_EXPECT_SUCCESS(detector.detect_frame(101, frame_start, frame_end, frame_ready));
+
+ // If frame is ready, verify the frame boundaries
+ if (frame_ready) {
+ EXPECT_EQ(100, frame_start);
+ EXPECT_EQ(103, frame_end);
+ }
+
+ // keyframe_pkt will be automatically cleaned up by SrsUniquePtr destructor
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorDetectNextFrame)
+{
+ srs_error_t err;
+
+ // Test detect_next_frame functionality
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Verify initial state
+ EXPECT_EQ(0, detector.header_sn_);
+ EXPECT_EQ(0, detector.lost_sn_);
+
+ // Store packets for next frame
+ cache.store_packet(mock_create_test_rtp_packet(200, 2000, false));
+ cache.store_packet(mock_create_test_rtp_packet(201, 2000, false));
+ cache.store_packet(mock_create_test_rtp_packet(202, 2000, true)); // marker bit
+
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ // Test detecting next frame starting from sequence 200
+ HELPER_EXPECT_SUCCESS(detector.detect_next_frame(200, frame_start, frame_end, frame_ready));
+
+ // Verify header_sn_ is updated by detect_next_frame
+ EXPECT_EQ(200, detector.header_sn_);
+
+ if (frame_ready) {
+ EXPECT_EQ(200, frame_start);
+ EXPECT_EQ(202, frame_end);
+ }
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorDetectNextFrameWithGaps)
+{
+ srs_error_t err;
+
+ // Test detect_next_frame with missing packets
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Store packets with a gap (missing 201)
+ cache.store_packet(mock_create_test_rtp_packet(200, 2000, false));
+ // Skip 201 - this creates a gap
+ cache.store_packet(mock_create_test_rtp_packet(202, 2000, false));
+ cache.store_packet(mock_create_test_rtp_packet(203, 2000, true)); // marker bit
+
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ // Test detecting next frame - should not be ready due to missing packet
+ HELPER_EXPECT_SUCCESS(detector.detect_next_frame(200, frame_start, frame_end, frame_ready));
+ EXPECT_FALSE(frame_ready); // Should not be ready due to gap
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorOnKeyframeDetached)
+{
+ // Test on_keyframe_detached functionality
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Initialize with keyframe
+ SrsUniquePtr keyframe_pkt(mock_create_test_rtp_packet(100, 1000));
+ detector.on_keyframe_start(keyframe_pkt.get());
+
+ // Verify keyframe is set
+ EXPECT_EQ(1000, detector.rtp_key_frame_ts_);
+ EXPECT_EQ(100, detector.header_sn_);
+
+ // Detach keyframe
+ detector.on_keyframe_detached();
+
+ // Verify keyframe timestamp is reset to -1
+ EXPECT_EQ(-1, detector.rtp_key_frame_ts_);
+ // header_sn_ and lost_sn_ should remain unchanged
+ EXPECT_EQ(100, detector.header_sn_);
+
+ // After detaching, should be able to start new keyframe
+ SrsUniquePtr new_keyframe_pkt(mock_create_test_rtp_packet(200, 2000));
+ detector.on_keyframe_start(new_keyframe_pkt.get());
+
+ // Verify new keyframe is set
+ EXPECT_EQ(2000, detector.rtp_key_frame_ts_);
+ EXPECT_EQ(200, detector.header_sn_);
+ EXPECT_EQ(201, detector.lost_sn_);
+
+ // Both keyframe packets will be automatically cleaned up by SrsUniquePtr destructors
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorSequenceWrapAround)
+{
+ srs_error_t err;
+
+ // Test frame detection with sequence number wrap-around
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Initialize with keyframe near sequence wrap-around
+ uint16_t seq_near_max = 65534;
+ SrsUniquePtr keyframe_pkt(mock_create_test_rtp_packet(seq_near_max, 1000));
+ detector.on_keyframe_start(keyframe_pkt.get());
+
+ // Store packets across wrap-around boundary
+ cache.store_packet(mock_create_test_rtp_packet(seq_near_max, 1000, false));
+ cache.store_packet(mock_create_test_rtp_packet(65535, 1000, false));
+ cache.store_packet(mock_create_test_rtp_packet(0, 1000, false)); // wrapped
+ cache.store_packet(mock_create_test_rtp_packet(1, 1000, true)); // marker bit
+
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ // Test detecting frame across wrap-around
+ HELPER_EXPECT_SUCCESS(detector.detect_frame(1, frame_start, frame_end, frame_ready));
+
+ // If frame is ready, verify the frame boundaries
+ if (frame_ready) {
+ EXPECT_EQ(65534, frame_start);
+ EXPECT_EQ(1, frame_end);
+ }
+
+ // keyframe_pkt will be automatically cleaned up by SrsUniquePtr destructor
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorIsLostSnBasic)
+{
+ srs_error_t err;
+
+ // Test is_lost_sn functionality
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Initialize with keyframe
+ SrsUniquePtr keyframe_pkt(mock_create_test_rtp_packet(100, 1000));
+ detector.on_keyframe_start(keyframe_pkt.get());
+
+ // Verify initial lost_sn_
+ EXPECT_EQ(101, detector.lost_sn_);
+
+ // Store packets with a gap
+ cache.store_packet(mock_create_test_rtp_packet(100, 1000, false));
+ // Skip 101 - creates gap
+ cache.store_packet(mock_create_test_rtp_packet(102, 1000, true));
+
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ // Trigger detection to set lost_sn_
+ HELPER_EXPECT_SUCCESS(detector.detect_frame(102, frame_start, frame_end, frame_ready));
+
+ // Verify lost_sn_ is set to the missing packet
+ EXPECT_EQ(101, detector.lost_sn_);
+
+ // Test is_lost_sn
+ EXPECT_TRUE(detector.is_lost_sn(101)); // Should be lost
+ EXPECT_FALSE(detector.is_lost_sn(100)); // Should not be lost
+ EXPECT_FALSE(detector.is_lost_sn(102)); // Should not be lost
+ EXPECT_FALSE(detector.is_lost_sn(103)); // Should not be lost
+
+ // keyframe_pkt will be automatically cleaned up by SrsUniquePtr destructor
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorPrivateMemberStateTracking)
+{
+ srs_error_t err;
+
+ // Test detailed private member state changes during frame detection
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Verify initial state - constructor should set video_cache_
+ EXPECT_EQ(&cache, detector.video_cache_);
+ EXPECT_EQ(0, detector.header_sn_);
+ EXPECT_EQ(0, detector.lost_sn_);
+ EXPECT_EQ(-1, detector.rtp_key_frame_ts_);
+
+ // Test keyframe initialization
+ SrsUniquePtr keyframe_pkt(mock_create_test_rtp_packet(500, 5000));
+ detector.on_keyframe_start(keyframe_pkt.get());
+
+ EXPECT_EQ(5000, detector.rtp_key_frame_ts_);
+ EXPECT_EQ(500, detector.header_sn_);
+ EXPECT_EQ(501, detector.lost_sn_); // header_sn_ + 1
+
+ // Test previous packet handling (sequence < header_sn_)
+ cache.store_packet(mock_create_test_rtp_packet(499, 5000, false));
+ cache.store_packet(mock_create_test_rtp_packet(500, 5000, false));
+ cache.store_packet(mock_create_test_rtp_packet(501, 5000, true));
+
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ // Detect with previous packet (499 < 500)
+ HELPER_EXPECT_SUCCESS(detector.detect_frame(499, frame_start, frame_end, frame_ready));
+
+ // header_sn_ should be updated to the earlier packet
+ EXPECT_EQ(499, detector.header_sn_);
+
+ // keyframe_pkt will be automatically cleaned up by SrsUniquePtr destructor
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorCacheOverflow)
+{
+ srs_error_t err;
+
+ // Test behavior when cache overflows
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Store more than cache_size_ packets with same timestamp and no marker bit
+ // This will trigger cache overflow detection in find_next_lost_sn
+ uint32_t timestamp = 1000;
+ uint16_t start_sn = 100;
+
+ // Store N packets (more than cache_size_) to guarantee overflow
+ for (int i = 0; i < SrsRtcFrameBuilderVideoPacketCache::cache_size_; i++) {
+ cache.store_packet(mock_create_test_rtp_packet(start_sn + i, timestamp, false));
+ }
+
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ // Test detect_next_frame with cache overflow scenario
+ // This should trigger find_next_lost_sn to return -2 (cache overflow)
+ HELPER_EXPECT_FAILED(detector.detect_next_frame(start_sn, frame_start, frame_end, frame_ready));
+ EXPECT_FALSE(frame_ready);
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorPreviousPacketHandling)
+{
+ srs_error_t err;
+
+ // Test handling of previous packets in the same frame
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Initialize with keyframe
+ SrsUniquePtr keyframe_pkt(mock_create_test_rtp_packet(100, 1000));
+ detector.on_keyframe_start(keyframe_pkt.get());
+
+ // Store packets in order
+ cache.store_packet(mock_create_test_rtp_packet(100, 1000, false));
+ cache.store_packet(mock_create_test_rtp_packet(101, 1000, false));
+ cache.store_packet(mock_create_test_rtp_packet(102, 1000, true));
+
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ // First detect with later packet
+ HELPER_EXPECT_SUCCESS(detector.detect_frame(102, frame_start, frame_end, frame_ready));
+
+ // Then detect with earlier packet (should handle previous packet case)
+ HELPER_EXPECT_SUCCESS(detector.detect_frame(101, frame_start, frame_end, frame_ready));
+
+ // keyframe_pkt will be automatically cleaned up by SrsUniquePtr destructor
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorFragmentedFrames)
+{
+ srs_error_t err;
+
+ // Test frame detection with fragmented packets (FU-A)
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Initialize with keyframe
+ SrsUniquePtr keyframe_pkt(mock_create_test_rtp_packet(100, 1000));
+ detector.on_keyframe_start(keyframe_pkt.get());
+
+ // Create fragmented frame with FU-A payloads
+ char payload_data[] = "test_fragmented_payload";
+
+ // Start fragment
+ SrsRtpPacket* pkt1 = mock_create_test_rtp_packet(100, 1000);
+ SrsRtpFUAPayload2* fua1 = mock_create_test_fua_payload(true, false, payload_data, sizeof(payload_data));
+ pkt1->set_payload(fua1, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt1);
+
+ // Middle fragment
+ SrsRtpPacket* pkt2 = mock_create_test_rtp_packet(101, 1000);
+ SrsRtpFUAPayload2* fua2 = mock_create_test_fua_payload(false, false, payload_data, sizeof(payload_data));
+ pkt2->set_payload(fua2, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt2);
+
+ // End fragment with marker bit
+ SrsRtpPacket* pkt3 = mock_create_test_rtp_packet(102, 1000, true);
+ SrsRtpFUAPayload2* fua3 = mock_create_test_fua_payload(false, true, payload_data, sizeof(payload_data));
+ pkt3->set_payload(fua3, SrsRtpPacketPayloadTypeFUA2);
+ cache.store_packet(pkt3);
+
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ // Test detecting fragmented frame
+ HELPER_EXPECT_SUCCESS(detector.detect_frame(102, frame_start, frame_end, frame_ready));
+
+ // keyframe_pkt will be automatically cleaned up by SrsUniquePtr destructor
+ }
+}
+
+VOID TEST(KernelRTC2Test, SrsRtcFrameBuilderVideoFrameDetectorNullPacketHandling)
+{
+ srs_error_t err;
+
+ // Test handling of null packets and edge cases
+ if (true) {
+ SrsRtcFrameBuilderVideoPacketCache cache;
+ SrsRtcFrameBuilderVideoFrameDetector detector(&cache);
+
+ // Test on_keyframe_start with null packet (should not crash)
+ // Note: In real implementation, null packets should be handled gracefully
+
+ // Test detect_frame without initialization
+ uint16_t frame_start = 0, frame_end = 0;
+ bool frame_ready = false;
+
+ HELPER_EXPECT_SUCCESS(detector.detect_frame(100, frame_start, frame_end, frame_ready));
+ EXPECT_FALSE(frame_ready); // Should not be ready without proper initialization
+
+ // Test detect_next_frame without packets
+ HELPER_EXPECT_SUCCESS(detector.detect_next_frame(100, frame_start, frame_end, frame_ready));
+ EXPECT_FALSE(frame_ready); // Should not be ready without packets
+ }
+}
+
diff --git a/trunk/src/utest/srs_utest_rtc2.hpp b/trunk/src/utest/srs_utest_rtc2.hpp
new file mode 100644
index 000000000..1d796340a
--- /dev/null
+++ b/trunk/src/utest/srs_utest_rtc2.hpp
@@ -0,0 +1,15 @@
+//
+// Copyright (c) 2013-2025 The SRS Authors
+//
+// SPDX-License-Identifier: MIT
+//
+
+#ifndef SRS_UTEST_RTC2_HPP
+#define SRS_UTEST_RTC2_HPP
+
+/*
+#include
+*/
+#include
+
+#endif