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