srs/trunk/src/utest/srs_utest_ai24.cpp
2025-10-31 19:10:37 -04:00

1117 lines
36 KiB
C++

//
// Copyright (c) 2013-2025 The SRS Authors
//
// SPDX-License-Identifier: MIT
//
#include <srs_utest_ai24.hpp>
#include <srs_app_config.hpp>
#include <srs_app_fragment.hpp>
#include <srs_app_hls.hpp>
#include <srs_app_http_hooks.hpp>
#include <srs_app_rtc_source.hpp>
#include <srs_app_server.hpp>
#include <srs_app_utility.hpp>
#include <srs_kernel_codec.hpp>
#include <srs_kernel_error.hpp>
#include <srs_kernel_packet.hpp>
#include <srs_kernel_rtc_rtcp.hpp>
#include <srs_kernel_utility.hpp>
#include <srs_protocol_sdp.hpp>
#include <srs_protocol_utility.hpp>
#include <srs_utest_manual_kernel.hpp>
#include <srs_utest_manual_mock.hpp>
#ifdef SRS_FFMPEG_FIT
#include <srs_app_rtc_codec.hpp>
#ifdef __cplusplus
extern "C" {
#endif
#include <libavutil/log.h>
#ifdef __cplusplus
}
#endif
#endif
using namespace std;
// Mock class to access protected members of SrsRtcRecvTrack
class MockSrsRtcRecvTrackForAVSync : public SrsRtcRecvTrack
{
SRS_DECLARE_PRIVATE:
static SrsRtcTrackDescription* create_track_desc(const string& type, uint32_t ssrc, int sample_rate)
{
SrsRtcTrackDescription *desc = new SrsRtcTrackDescription();
desc->type_ = type;
desc->id_ = "test_track";
desc->ssrc_ = ssrc;
desc->is_active_ = true;
// Create media description with sample rate
desc->media_ = new SrsAudioPayload();
desc->media_->sample_ = sample_rate;
return desc;
}
public:
MockSrsRtcRecvTrackForAVSync(const string &type, uint32_t ssrc, int sample_rate, bool is_audio)
: SrsRtcRecvTrack(NULL, create_track_desc(type, ssrc, sample_rate), is_audio, true)
{
}
// Expose protected methods for testing
double get_rate() const { return rate_; }
void set_rate(double rate) { rate_ = rate; }
int64_t test_cal_avsync_time(uint32_t rtp_time)
{
return cal_avsync_time(rtp_time);
}
void test_update_send_report_time(const SrsNtp &ntp, uint32_t rtp_time)
{
update_send_report_time(ntp, rtp_time);
}
// Implement pure virtual methods
virtual srs_error_t on_rtp(SrsSharedPtr<SrsRtcSource> &source, SrsRtpPacket *pkt)
{
return srs_success;
}
virtual srs_error_t check_send_nacks()
{
return srs_success;
}
};
// Test: Rate initialization from SDP for audio track (48kHz)
VOID TEST(RtcAVSyncTest, AudioRateInitFromSDP)
{
MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true);
// Rate should be initialized to 48 (48000 Hz / 1000 = 48 RTP units per ms)
EXPECT_DOUBLE_EQ(48.0, track.get_rate());
}
// Test: Rate initialization from SDP for video track (90kHz)
VOID TEST(RtcAVSyncTest, VideoRateInitFromSDP)
{
MockSrsRtcRecvTrackForAVSync track("video", 67890, 90000, false);
// Rate should be initialized to 90 (90000 Hz / 1000 = 90 RTP units per ms)
EXPECT_DOUBLE_EQ(90.0, track.get_rate());
}
// Test: cal_avsync_time with SDP rate (before receiving SR)
VOID TEST(RtcAVSyncTest, CalAVSyncTimeWithSDPRate)
{
MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true);
// Simulate first SR received
SrsNtp ntp;
ntp.system_ms_ = 1000; // 1000 ms
uint32_t rtp_time = 48000; // 48000 RTP units
track.test_update_send_report_time(ntp, rtp_time);
// Calculate avsync time for a later RTP packet
// RTP time: 48000 + 4800 = 52800 (100ms later at 48kHz)
// Expected avsync_time: 1000 + (52800 - 48000) / 48 = 1000 + 100 = 1100 ms
int64_t avsync_time = track.test_cal_avsync_time(52800);
EXPECT_EQ(1100, avsync_time);
}
// Test: cal_avsync_time returns -1 when rate is 0
VOID TEST(RtcAVSyncTest, CalAVSyncTimeWithZeroRate)
{
MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true);
// Manually set rate to 0
track.set_rate(0.0);
// Should return -1 when rate is too small
int64_t avsync_time = track.test_cal_avsync_time(1000);
EXPECT_EQ(-1, avsync_time);
}
// Test: Rate update after receiving 2nd SR (audio)
VOID TEST(RtcAVSyncTest, AudioRateUpdateAfter2ndSR)
{
MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true);
// Initial rate from SDP
EXPECT_DOUBLE_EQ(48.0, track.get_rate());
// First SR
SrsNtp ntp1;
ntp1.system_ms_ = 1000;
uint32_t rtp_time1 = 48000;
track.test_update_send_report_time(ntp1, rtp_time1);
// Rate should still be 48 (from SDP)
EXPECT_DOUBLE_EQ(48.0, track.get_rate());
// Second SR (20ms later, RTP increased by 960)
SrsNtp ntp2;
ntp2.system_ms_ = 1020; // 20ms later
uint32_t rtp_time2 = 48960; // 960 RTP units later (48 * 20)
track.test_update_send_report_time(ntp2, rtp_time2);
// Rate should be updated to calculated value: 960 / 20 = 48
EXPECT_DOUBLE_EQ(48.0, track.get_rate());
}
// Test: Rate update after receiving 2nd SR (video)
VOID TEST(RtcAVSyncTest, VideoRateUpdateAfter2ndSR)
{
MockSrsRtcRecvTrackForAVSync track("video", 67890, 90000, false);
// Initial rate from SDP
EXPECT_DOUBLE_EQ(90.0, track.get_rate());
// First SR
SrsNtp ntp1;
ntp1.system_ms_ = 2000;
uint32_t rtp_time1 = 180000;
track.test_update_send_report_time(ntp1, rtp_time1);
// Rate should still be 90 (from SDP)
EXPECT_DOUBLE_EQ(90.0, track.get_rate());
// Second SR (100ms later, RTP increased by 9000)
SrsNtp ntp2;
ntp2.system_ms_ = 2100; // 100ms later
uint32_t rtp_time2 = 189000; // 9000 RTP units later (90 * 100)
track.test_update_send_report_time(ntp2, rtp_time2);
// Rate should be updated to calculated value: 9000 / 100 = 90
EXPECT_DOUBLE_EQ(90.0, track.get_rate());
}
// Test: Rate calculation with clock drift (slightly off from SDP)
VOID TEST(RtcAVSyncTest, RateUpdateWithClockDrift)
{
MockSrsRtcRecvTrackForAVSync track("video", 67890, 90000, false);
// Initial rate from SDP
EXPECT_DOUBLE_EQ(90.0, track.get_rate());
// First SR
SrsNtp ntp1;
ntp1.system_ms_ = 1000;
uint32_t rtp_time1 = 90000;
track.test_update_send_report_time(ntp1, rtp_time1);
// Second SR with slight clock drift
// Expected: 100ms -> 9000 RTP units
// Actual: 100ms -> 9010 RTP units (slight drift)
SrsNtp ntp2;
ntp2.system_ms_ = 1100;
uint32_t rtp_time2 = 99010; // Slightly more than expected
track.test_update_send_report_time(ntp2, rtp_time2);
// Rate should be updated to: round(9010 / 100) = 90
EXPECT_DOUBLE_EQ(90.0, track.get_rate());
}
// Test: Rate calculation with larger time interval
VOID TEST(RtcAVSyncTest, RateUpdateWithLargeInterval)
{
MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true);
// First SR
SrsNtp ntp1;
ntp1.system_ms_ = 5000;
uint32_t rtp_time1 = 240000;
track.test_update_send_report_time(ntp1, rtp_time1);
// Second SR (1000ms later)
SrsNtp ntp2;
ntp2.system_ms_ = 6000;
uint32_t rtp_time2 = 288000; // 48000 RTP units later (48 * 1000)
track.test_update_send_report_time(ntp2, rtp_time2);
// Rate should be: 48000 / 1000 = 48
EXPECT_DOUBLE_EQ(48.0, track.get_rate());
}
// Test: cal_avsync_time with precise rate after 2nd SR
VOID TEST(RtcAVSyncTest, CalAVSyncTimeAfter2ndSR)
{
MockSrsRtcRecvTrackForAVSync track("video", 67890, 90000, false);
// First SR
SrsNtp ntp1;
ntp1.system_ms_ = 1000;
uint32_t rtp_time1 = 90000;
track.test_update_send_report_time(ntp1, rtp_time1);
// Second SR
SrsNtp ntp2;
ntp2.system_ms_ = 1100;
uint32_t rtp_time2 = 99000;
track.test_update_send_report_time(ntp2, rtp_time2);
// Now calculate avsync time for a packet
// RTP time: 99000 + 4500 = 103500 (50ms later at 90kHz)
// Expected: 1100 + (103500 - 99000) / 90 = 1100 + 50 = 1150 ms
int64_t avsync_time = track.test_cal_avsync_time(103500);
EXPECT_EQ(1150, avsync_time);
}
// Test: Immediate A/V sync availability (issue #4418 fix)
VOID TEST(RtcAVSyncTest, ImmediateAVSyncAvailability)
{
MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true);
// Before any SR, rate should be available from SDP
EXPECT_DOUBLE_EQ(48.0, track.get_rate());
// First SR received
SrsNtp ntp1;
ntp1.system_ms_ = 1000;
uint32_t rtp_time1 = 48000;
track.test_update_send_report_time(ntp1, rtp_time1);
// Should be able to calculate avsync_time immediately (not -1)
int64_t avsync_time = track.test_cal_avsync_time(48480); // 10ms later
EXPECT_GT(avsync_time, 0); // Should be > 0, not -1
EXPECT_EQ(1010, avsync_time); // Should be 1000 + 10 = 1010
}
// Test: RTP timestamp wraparound handling
VOID TEST(RtcAVSyncTest, RTPTimestampWraparound)
{
MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true);
// First SR near wraparound
SrsNtp ntp1;
ntp1.system_ms_ = 1000;
uint32_t rtp_time1 = 0xFFFFF000; // Near max uint32_t
track.test_update_send_report_time(ntp1, rtp_time1);
// Second SR after wraparound
SrsNtp ntp2;
ntp2.system_ms_ = 1020; // 20ms later
uint32_t rtp_time2 = 0x000003C0; // Wrapped around, 960 units after wraparound
track.test_update_send_report_time(ntp2, rtp_time2);
// Note: Current implementation may not handle wraparound correctly
// This test documents the current behavior
// Rate calculation: (0x000003C0 - 0xFFFFF000) will underflow
// This is a known limitation that may need fixing in the future
}
// Test: Zero time elapsed between SRs (edge case)
VOID TEST(RtcAVSyncTest, ZeroTimeElapsedBetweenSRs)
{
MockSrsRtcRecvTrackForAVSync track("audio", 12345, 48000, true);
// First SR
SrsNtp ntp1;
ntp1.system_ms_ = 1000;
uint32_t rtp_time1 = 48000;
track.test_update_send_report_time(ntp1, rtp_time1);
double rate_before = track.get_rate();
// Second SR with same timestamp (0ms elapsed)
SrsNtp ntp2;
ntp2.system_ms_ = 1000; // Same time
uint32_t rtp_time2 = 48000; // Same RTP time
track.test_update_send_report_time(ntp2, rtp_time2);
// Rate should remain unchanged (SDP rate)
EXPECT_DOUBLE_EQ(rate_before, track.get_rate());
}
// Test: SrsParsedPacket::copy() method
VOID TEST(ParsedPacketTest, CopyParsedPacket)
{
srs_error_t err;
// Create a parsed packet with sample data
SrsParsedPacket packet;
SrsVideoCodecConfig codec;
HELPER_EXPECT_SUCCESS(packet.initialize(&codec));
// Set packet properties
packet.dts_ = 1000;
packet.cts_ = 100;
// Add sample data
char sample_data1[] = {0x01, 0x02, 0x03};
char sample_data2[] = {0x04, 0x05, 0x06, 0x07};
HELPER_EXPECT_SUCCESS(packet.add_sample(sample_data1, sizeof(sample_data1)));
HELPER_EXPECT_SUCCESS(packet.add_sample(sample_data2, sizeof(sample_data2)));
// Copy the packet
SrsParsedPacket *copied = packet.copy();
ASSERT_TRUE(copied != NULL);
// Verify all fields are copied correctly
EXPECT_EQ(packet.dts_, copied->dts_);
EXPECT_EQ(packet.cts_, copied->cts_);
EXPECT_EQ(packet.codec_, copied->codec_);
EXPECT_EQ(packet.nb_samples_, copied->nb_samples_);
// Verify samples are copied (shared pointers)
for (int i = 0; i < packet.nb_samples_; i++) {
EXPECT_EQ(packet.samples_[i].bytes_, copied->samples_[i].bytes_);
EXPECT_EQ(packet.samples_[i].size_, copied->samples_[i].size_);
}
srs_freep(copied);
}
// Test: SrsParsedVideoPacket::copy() method
VOID TEST(ParsedPacketTest, CopyParsedVideoPacket)
{
srs_error_t err;
// Create a parsed video packet with sample data
SrsParsedVideoPacket packet;
SrsVideoCodecConfig codec;
HELPER_EXPECT_SUCCESS(packet.initialize(&codec));
// Set packet properties
packet.dts_ = 2000;
packet.cts_ = 200;
packet.frame_type_ = SrsVideoAvcFrameTypeKeyFrame;
packet.avc_packet_type_ = SrsVideoAvcFrameTraitNALU;
packet.has_idr_ = true;
packet.has_aud_ = false;
packet.has_sps_pps_ = true;
packet.first_nalu_type_ = SrsAvcNaluTypeIDR;
// Add sample data
uint8_t sample_data[] = {0x65, 0x88, 0x84, 0x00};
HELPER_EXPECT_SUCCESS(packet.add_sample((char *)sample_data, sizeof(sample_data)));
// Copy the packet
SrsParsedVideoPacket *copied = packet.copy();
ASSERT_TRUE(copied != NULL);
// Verify base class fields are copied
EXPECT_EQ(packet.dts_, copied->dts_);
EXPECT_EQ(packet.cts_, copied->cts_);
EXPECT_EQ(packet.codec_, copied->codec_);
EXPECT_EQ(packet.nb_samples_, copied->nb_samples_);
// Verify video-specific fields are copied
EXPECT_EQ(packet.frame_type_, copied->frame_type_);
EXPECT_EQ(packet.avc_packet_type_, copied->avc_packet_type_);
EXPECT_EQ(packet.has_idr_, copied->has_idr_);
EXPECT_EQ(packet.has_aud_, copied->has_aud_);
EXPECT_EQ(packet.has_sps_pps_, copied->has_sps_pps_);
EXPECT_EQ(packet.first_nalu_type_, copied->first_nalu_type_);
// Verify samples are copied (shared pointers)
for (int i = 0; i < packet.nb_samples_; i++) {
EXPECT_EQ(packet.samples_[i].bytes_, copied->samples_[i].bytes_);
EXPECT_EQ(packet.samples_[i].size_, copied->samples_[i].size_);
}
srs_freep(copied);
}
#ifdef SRS_FFMPEG_FIT
// Helper function to call ffmpeg_log_callback with formatted string
static void call_ffmpeg_log(int level, const char *fmt, ...)
{
va_list vl;
va_start(vl, fmt);
SrsFFmpegLogHelper::ffmpeg_log_callback(NULL, level, fmt, vl);
va_end(vl);
}
// Test: SrsFFmpegLogHelper::ffmpeg_log_callback() method
VOID TEST(FFmpegLogHelperTest, LogCallback)
{
// Save original disabled state
bool original_disabled = SrsFFmpegLogHelper::disabled_;
// Test 1: Callback should work when not disabled
SrsFFmpegLogHelper::disabled_ = false;
// AV_LOG_WARNING level
call_ffmpeg_log(AV_LOG_WARNING, "Test warning message\n");
// AV_LOG_INFO level
call_ffmpeg_log(AV_LOG_INFO, "Test info message\n");
// AV_LOG_VERBOSE/DEBUG/TRACE levels
call_ffmpeg_log(AV_LOG_VERBOSE, "Test verbose message\n");
call_ffmpeg_log(AV_LOG_DEBUG, "Test debug message\n");
call_ffmpeg_log(AV_LOG_TRACE, "Test trace message\n");
// Test message without newline (should not strip last character)
call_ffmpeg_log(AV_LOG_INFO, "Test message without newline");
// Test message with newline (should strip newline)
call_ffmpeg_log(AV_LOG_INFO, "Test message with newline\n");
// Test 2: Callback should return early when disabled
SrsFFmpegLogHelper::disabled_ = true;
// These calls should return immediately without processing
call_ffmpeg_log(AV_LOG_ERROR, "This should not be logged\n");
call_ffmpeg_log(AV_LOG_WARNING, "This should not be logged\n");
call_ffmpeg_log(AV_LOG_INFO, "This should not be logged\n");
// Test 3: Test edge cases
SrsFFmpegLogHelper::disabled_ = false;
// Empty message
call_ffmpeg_log(AV_LOG_INFO, "");
// Very long message (should be truncated to buffer size)
std::string long_msg(5000, 'x');
call_ffmpeg_log(AV_LOG_INFO, "%s", long_msg.c_str());
// Restore original disabled state
SrsFFmpegLogHelper::disabled_ = original_disabled;
// If we reach here without crashing, the test passes
EXPECT_TRUE(true);
}
#endif
// Test SrsDvrAsyncCallOnHls::call() method
VOID TEST(DvrAsyncCallOnHlsTest, CallWithMultipleHooks)
{
srs_error_t err;
// Create mock config with HTTP hooks enabled
MockAppConfig mock_config;
mock_config.http_hooks_enabled_ = true;
// Create on_hls directive with multiple hook URLs
mock_config.on_hls_directive_ = new SrsConfDirective();
mock_config.on_hls_directive_->name_ = "on_hls";
mock_config.on_hls_directive_->args_.push_back("http://example.com/hook1");
mock_config.on_hls_directive_->args_.push_back("http://example.com/hook2");
// Create mock hooks
MockHttpHooks mock_hooks;
// Create mock request
MockRequest mock_req("test_vhost", "live", "stream");
// Create SrsDvrAsyncCallOnHls instance
SrsContextId cid;
SrsDvrAsyncCallOnHls call(cid, &mock_req, "/path/to/file.ts", "http://example.com/file.ts",
"m3u8_content", "http://example.com/playlist.m3u8", 1, 10 * SRS_UTIME_SECONDS);
// Replace global config and hooks with mocks
call.config_ = &mock_config;
call.hooks_ = &mock_hooks;
// Call should succeed and invoke hooks for each URL
HELPER_EXPECT_SUCCESS(call.call());
}
// Mock HLS muxer for testing SrsHlsController::reap_segment
class MockHlsMuxerForReapSegment : public ISrsHlsMuxer
{
SRS_DECLARE_PRIVATE:
int segment_close_count_;
int segment_open_count_;
int flush_video_count_;
int flush_audio_count_;
srs_error_t segment_close_error_;
srs_error_t segment_open_error_;
srs_error_t flush_video_error_;
srs_error_t flush_audio_error_;
public:
MockHlsMuxerForReapSegment()
{
segment_close_count_ = 0;
segment_open_count_ = 0;
flush_video_count_ = 0;
flush_audio_count_ = 0;
segment_close_error_ = srs_success;
segment_open_error_ = srs_success;
flush_video_error_ = srs_success;
flush_audio_error_ = srs_success;
}
virtual ~MockHlsMuxerForReapSegment()
{
srs_freep(segment_close_error_);
srs_freep(segment_open_error_);
srs_freep(flush_video_error_);
srs_freep(flush_audio_error_);
}
// ISrsHlsMuxer interface - only implement methods used by reap_segment
virtual srs_error_t initialize() { return srs_success; }
virtual void dispose() {}
virtual int sequence_no() { return 0; }
virtual std::string ts_url() { return ""; }
virtual srs_utime_t duration() { return 0; }
virtual int deviation() { return 0; }
virtual SrsAudioCodecId latest_acodec() { return SrsAudioCodecIdForbidden; }
virtual void set_latest_acodec(SrsAudioCodecId v) {}
virtual SrsVideoCodecId latest_vcodec() { return SrsVideoCodecIdForbidden; }
virtual void set_latest_vcodec(SrsVideoCodecId v) {}
virtual bool pure_audio() { return false; }
virtual bool is_segment_overflow() { return false; }
virtual bool is_segment_absolutely_overflow() { return false; }
virtual bool wait_keyframe() { return false; }
virtual srs_error_t on_publish(ISrsRequest *req) { return srs_success; }
virtual srs_error_t on_unpublish() { return srs_success; }
virtual srs_error_t update_config(ISrsRequest *r, std::string entry_prefix,
std::string path, std::string m3u8_file, std::string ts_file,
srs_utime_t fragment, srs_utime_t window, bool ts_floor, double aof_ratio,
bool cleanup, bool wait_keyframe, bool keys, int fragments_per_key,
std::string key_file, std::string key_file_path, std::string key_url)
{
return srs_success;
}
virtual srs_error_t on_sequence_header() { return srs_success; }
virtual void update_duration(uint64_t dts) {}
virtual srs_error_t recover_hls() { return srs_success; }
// Methods used by reap_segment
virtual srs_error_t segment_close()
{
segment_close_count_++;
return srs_error_copy(segment_close_error_);
}
virtual srs_error_t segment_open()
{
segment_open_count_++;
return srs_error_copy(segment_open_error_);
}
virtual srs_error_t flush_video(SrsTsMessageCache *cache)
{
flush_video_count_++;
return srs_error_copy(flush_video_error_);
}
virtual srs_error_t flush_audio(SrsTsMessageCache *cache)
{
flush_audio_count_++;
return srs_error_copy(flush_audio_error_);
}
// Test helpers
void set_segment_close_error(srs_error_t err)
{
srs_freep(segment_close_error_);
segment_close_error_ = srs_error_copy(err);
}
void set_segment_open_error(srs_error_t err)
{
srs_freep(segment_open_error_);
segment_open_error_ = srs_error_copy(err);
}
void set_flush_video_error(srs_error_t err)
{
srs_freep(flush_video_error_);
flush_video_error_ = srs_error_copy(err);
}
void set_flush_audio_error(srs_error_t err)
{
srs_freep(flush_audio_error_);
flush_audio_error_ = srs_error_copy(err);
}
int get_segment_close_count() const { return segment_close_count_; }
int get_segment_open_count() const { return segment_open_count_; }
int get_flush_video_count() const { return flush_video_count_; }
int get_flush_audio_count() const { return flush_audio_count_; }
};
// Test: SrsHlsController::reap_segment success path
VOID TEST(HlsControllerTest, ReapSegmentSuccess)
{
srs_error_t err;
// Create controller
SrsHlsController controller;
// Replace muxer with mock
MockHlsMuxerForReapSegment *mock_muxer = new MockHlsMuxerForReapSegment();
srs_freep(controller.muxer_);
controller.muxer_ = mock_muxer;
// Call reap_segment - should succeed
HELPER_EXPECT_SUCCESS(controller.reap_segment());
// Verify the sequence of operations
EXPECT_EQ(1, mock_muxer->get_segment_close_count());
EXPECT_EQ(1, mock_muxer->get_segment_open_count());
EXPECT_EQ(1, mock_muxer->get_flush_video_count());
EXPECT_EQ(1, mock_muxer->get_flush_audio_count());
}
// Mock HLS segment for testing do_segment_close
class MockHlsSegmentForSegmentClose : public SrsHlsSegment
{
SRS_DECLARE_PRIVATE:
srs_error_t rename_error_;
srs_utime_t mock_duration_;
bool rename_called_;
public:
MockHlsSegmentForSegmentClose() : SrsHlsSegment(NULL, SrsAudioCodecIdAAC, SrsVideoCodecIdAVC, NULL)
{
rename_error_ = srs_success;
mock_duration_ = 10 * SRS_UTIME_SECONDS; // Default 10 seconds
rename_called_ = false;
sequence_no_ = 1;
uri_ = "segment-1.ts";
// tscw_ is already NULL from base class, leave it NULL
}
virtual ~MockHlsSegmentForSegmentClose()
{
srs_freep(rename_error_);
}
virtual srs_error_t rename()
{
rename_called_ = true;
return srs_error_copy(rename_error_);
}
virtual srs_utime_t duration()
{
return mock_duration_;
}
void set_rename_error(srs_error_t err)
{
srs_freep(rename_error_);
rename_error_ = srs_error_copy(err);
}
void set_duration(srs_utime_t dur)
{
mock_duration_ = dur;
}
bool is_rename_called() const
{
return rename_called_;
}
};
// Test: SrsHlsMuxer::do_segment_close2 success path with valid duration
VOID TEST(HlsMuxerTest, DoSegmentCloseSuccess)
{
srs_error_t err;
// Create HLS muxer
SrsHlsMuxer muxer;
HELPER_EXPECT_SUCCESS(muxer.initialize());
// Create mock request
MockRequest req("test_vhost", "live", "stream");
muxer.req_ = &req;
// Create mock segment with valid duration (10 seconds)
MockHlsSegmentForSegmentClose *mock_segment = new MockHlsSegmentForSegmentClose();
mock_segment->set_duration(10 * SRS_UTIME_SECONDS);
muxer.current_ = mock_segment;
// Set max_td_ to 10 seconds (fragment duration)
muxer.max_td_ = 10 * SRS_UTIME_SECONDS;
// Call do_segment_close2 - should succeed
HELPER_EXPECT_SUCCESS(muxer.do_segment_close2());
// Verify segment was renamed
EXPECT_TRUE(mock_segment->is_rename_called());
// Verify segment was added to segments window
EXPECT_EQ(1, muxer.segments_->size());
// Verify current_ is set to NULL
EXPECT_TRUE(muxer.current_ == NULL);
// Cleanup
muxer.req_ = NULL;
}
// Test: SrsHlsMuxer::generate_ts_filename with hls_ts_floor enabled
VOID TEST(HlsMuxerTest, GenerateTsFilenameWithFloor)
{
// Create HLS muxer
SrsHlsMuxer muxer;
// Create mock request
MockRequest req("test_vhost", "live", "stream");
muxer.req_ = &req;
// Set up muxer configuration with ts_floor enabled
muxer.hls_ts_file_ = "[vhost]/[app]/[stream]-[timestamp]-[seq].ts";
muxer.hls_ts_floor_ = true;
muxer.hls_fragment_ = 10 * SRS_UTIME_SECONDS;
muxer.accept_floor_ts_ = 0;
muxer.previous_floor_ts_ = 0;
muxer.deviation_ts_ = 0;
// Create a mock segment with sequence number
SrsHlsSegment *segment = new SrsHlsSegment(muxer.context_, SrsAudioCodecIdAAC, SrsVideoCodecIdDisabled, new MockSrsFileWriter());
segment->sequence_no_ = 100;
muxer.current_ = segment;
// Call generate_ts_filename
std::string ts_filename = muxer.generate_ts_filename();
// Verify the filename contains replaced variables
EXPECT_TRUE(ts_filename.find("test_vhost") != std::string::npos);
EXPECT_TRUE(ts_filename.find("live") != std::string::npos);
EXPECT_TRUE(ts_filename.find("stream") != std::string::npos);
EXPECT_TRUE(ts_filename.find("100") != std::string::npos); // sequence number
// Verify accept_floor_ts_ was initialized (should be current_floor_ts - 1 on first call)
EXPECT_TRUE(muxer.accept_floor_ts_ > 0);
// Verify previous_floor_ts_ was set
EXPECT_TRUE(muxer.previous_floor_ts_ > 0);
// Verify deviation_ts_ was calculated
EXPECT_TRUE(muxer.deviation_ts_ <= 0); // Should be negative or zero since accept_floor_ts_ starts at current_floor_ts - 1
// Call again to test the increment logic
int64_t first_accept_floor_ts = muxer.accept_floor_ts_;
segment->sequence_no_ = 101;
std::string ts_filename2 = muxer.generate_ts_filename();
// Verify accept_floor_ts_ was incremented
EXPECT_EQ(first_accept_floor_ts + 1, muxer.accept_floor_ts_);
// Verify sequence number was replaced
EXPECT_TRUE(ts_filename2.find("101") != std::string::npos);
// Cleanup
muxer.req_ = NULL;
muxer.current_ = NULL;
srs_freep(segment);
}
// Mock segment for testing do_refresh_m3u8_segment
class MockHlsM4sSegment : public SrsHlsM4sSegment
{
public:
bool is_sequence_header_;
srs_utime_t duration_;
std::string fullpath_;
MockHlsM4sSegment() : SrsHlsM4sSegment(NULL)
{
is_sequence_header_ = false;
duration_ = 5000 * SRS_UTIME_MILLISECONDS; // 5 seconds
fullpath_ = "/path/to/segment-[duration].m4s";
sequence_no_ = 0;
memset(iv_, 0, 16);
// Set a test IV value
for (int i = 0; i < 16; i++) {
iv_[i] = i;
}
}
virtual ~MockHlsM4sSegment() {}
virtual bool is_sequence_header() { return is_sequence_header_; }
virtual srs_utime_t duration() { return duration_; }
virtual std::string fullpath() { return fullpath_; }
};
// Test: do_refresh_m3u8_segment with encryption enabled
VOID TEST(HlsFmp4MuxerTest, DoRefreshM3u8SegmentWithEncryption)
{
srs_error_t err;
// Create muxer and set up encryption
SrsHlsFmp4Muxer muxer;
// Set up request
MockRequest req("test_vhost", "test_app", "test_stream");
muxer.req_ = &req;
// Enable encryption
muxer.hls_keys_ = true;
muxer.hls_fragments_per_key_ = 5;
muxer.hls_key_file_ = "key-[seq].key";
muxer.hls_key_url_ = "https://example.com/keys/";
// Create mock segment
MockHlsM4sSegment segment;
segment.sequence_no_ = 10; // 10 % 5 == 0, so key should be written
segment.is_sequence_header_ = true; // Should write discontinuity
segment.duration_ = 5000 * SRS_UTIME_MILLISECONDS; // 5 seconds
segment.fullpath_ = "/path/to/segment-[duration].m4s";
// Call do_refresh_m3u8_segment
std::stringstream ss;
HELPER_EXPECT_SUCCESS(muxer.do_refresh_m3u8_segment(&segment, ss));
// Verify output
std::string output = ss.str();
// Should contain discontinuity tag
EXPECT_TRUE(output.find("#EXT-X-DISCONTINUITY") != std::string::npos);
// Should contain encryption key tag
EXPECT_TRUE(output.find("#EXT-X-KEY:METHOD=SAMPLE-AES") != std::string::npos);
EXPECT_TRUE(output.find("https://example.com/keys/") != std::string::npos);
EXPECT_TRUE(output.find("key-10.key") != std::string::npos);
EXPECT_TRUE(output.find("IV=0x") != std::string::npos);
// Should contain EXTINF tag with duration
EXPECT_TRUE(output.find("#EXTINF:5.000") != std::string::npos);
// Should contain segment filename
EXPECT_TRUE(output.find("segment-5000.m4s") != std::string::npos);
// Cleanup
muxer.req_ = NULL;
}
// Mock HLS segment for testing SrsHlsMuxer::do_refresh_m3u8_segment
class MockHlsSegmentForRefreshM3u8 : public SrsHlsSegment
{
SRS_DECLARE_PRIVATE:
bool is_sequence_header_;
srs_utime_t duration_;
public:
MockHlsSegmentForRefreshM3u8() : SrsHlsSegment(NULL, SrsAudioCodecIdAAC, SrsVideoCodecIdAVC, NULL)
{
is_sequence_header_ = false;
duration_ = 5000 * SRS_UTIME_MILLISECONDS; // 5 seconds
sequence_no_ = 0;
uri_ = "segment-[duration].ts";
// Set a test IV value
for (int i = 0; i < 16; i++) {
iv_[i] = i;
}
}
virtual ~MockHlsSegmentForRefreshM3u8() {}
virtual bool is_sequence_header() { return is_sequence_header_; }
virtual srs_utime_t duration() { return duration_; }
void set_is_sequence_header(bool v) { is_sequence_header_ = v; }
void set_duration(srs_utime_t dur) { duration_ = dur; }
};
// Test: SrsHlsMuxer::do_refresh_m3u8_segment with encryption enabled
VOID TEST(HlsMuxerTest, DoRefreshM3u8SegmentWithEncryption)
{
srs_error_t err;
// Create muxer
SrsHlsMuxer muxer;
// Set up request
MockRequest req("test_vhost", "test_app", "test_stream");
muxer.req_ = &req;
// Enable encryption
muxer.hls_keys_ = true;
muxer.hls_fragments_per_key_ = 5;
muxer.hls_key_file_ = "key-[seq].key";
muxer.hls_key_url_ = "https://example.com/keys/";
// Create mock segment
MockHlsSegmentForRefreshM3u8 segment;
segment.sequence_no_ = 10; // 10 % 5 == 0, so key should be written
segment.set_is_sequence_header(true); // Should write discontinuity
segment.set_duration(5000 * SRS_UTIME_MILLISECONDS); // 5 seconds
// Call do_refresh_m3u8_segment
std::stringstream ss;
HELPER_EXPECT_SUCCESS(muxer.do_refresh_m3u8_segment(&segment, ss));
// Verify output
std::string output = ss.str();
// Should contain discontinuity tag
EXPECT_TRUE(output.find("#EXT-X-DISCONTINUITY") != std::string::npos);
// Should contain encryption key tag with AES-128 method
EXPECT_TRUE(output.find("#EXT-X-KEY:METHOD=AES-128") != std::string::npos);
EXPECT_TRUE(output.find("https://example.com/keys/") != std::string::npos);
EXPECT_TRUE(output.find("key-10.key") != std::string::npos);
EXPECT_TRUE(output.find("IV=0x") != std::string::npos);
// Should contain EXTINF tag with duration
EXPECT_TRUE(output.find("#EXTINF:5.000") != std::string::npos);
// Should contain segment filename with duration replaced
EXPECT_TRUE(output.find("segment-5000.ts") != std::string::npos);
// Cleanup
muxer.req_ = NULL;
}
// Mock segment for testing SrsHlsFmp4Muxer::generate_m4s_filename
class MockHlsM4sSegmentForFilename : public SrsHlsM4sSegment
{
public:
MockHlsM4sSegmentForFilename() : SrsHlsM4sSegment(NULL)
{
sequence_no_ = 0;
}
virtual ~MockHlsM4sSegmentForFilename() {}
};
// Test: SrsHlsFmp4Muxer::generate_m4s_filename with hls_ts_floor enabled
VOID TEST(HlsFmp4MuxerTest, GenerateM4sFilenameWithFloor)
{
// Create HLS fmp4 muxer
SrsHlsFmp4Muxer muxer;
// Create mock request
MockRequest req("test_vhost", "live", "stream");
muxer.req_ = &req;
// Set up muxer configuration with ts_floor enabled
muxer.hls_m4s_file_ = "[vhost]/[app]/[stream]-[timestamp]-[seq].m4s";
muxer.hls_ts_floor_ = true;
muxer.hls_fragment_ = 10 * SRS_UTIME_SECONDS;
muxer.accept_floor_ts_ = 0;
muxer.previous_floor_ts_ = 0;
muxer.deviation_ts_ = 0;
// Create a mock segment with sequence number
MockHlsM4sSegmentForFilename *segment = new MockHlsM4sSegmentForFilename();
segment->sequence_no_ = 100;
muxer.current_ = segment;
// Call generate_m4s_filename
std::string m4s_filename = muxer.generate_m4s_filename();
// Verify the filename contains replaced variables
EXPECT_TRUE(m4s_filename.find("test_vhost") != std::string::npos);
EXPECT_TRUE(m4s_filename.find("live") != std::string::npos);
EXPECT_TRUE(m4s_filename.find("stream") != std::string::npos);
EXPECT_TRUE(m4s_filename.find("100") != std::string::npos); // sequence number
// Verify accept_floor_ts_ was initialized (should be current_floor_ts - 1 on first call)
EXPECT_TRUE(muxer.accept_floor_ts_ > 0);
// Verify previous_floor_ts_ was set
EXPECT_TRUE(muxer.previous_floor_ts_ > 0);
// Verify deviation_ts_ was calculated
EXPECT_TRUE(muxer.deviation_ts_ <= 0); // Should be negative or zero since accept_floor_ts_ starts at current_floor_ts - 1
// Call again to test the increment logic
int64_t first_accept_floor_ts = muxer.accept_floor_ts_;
segment->sequence_no_ = 101;
std::string m4s_filename2 = muxer.generate_m4s_filename();
// Verify accept_floor_ts_ was incremented
EXPECT_EQ(first_accept_floor_ts + 1, muxer.accept_floor_ts_);
// Verify sequence number was replaced
EXPECT_TRUE(m4s_filename2.find("101") != std::string::npos);
// Cleanup
muxer.req_ = NULL;
muxer.current_ = NULL;
srs_freep(segment);
}
// Test: SrsServer::initialize_st with asprocess enabled and ppid == 1 (should fail)
VOID TEST(ServerTest, InitializeStAsprocessWithPpid1)
{
srs_error_t err;
// Create server
SrsServer server;
// Create mock config with asprocess enabled
MockAppConfig mock_config;
mock_config.asprocess_ = true;
// Replace config with mock
server.config_ = &mock_config;
// Set ppid to 1 (init process)
server.ppid_ = 1;
// Call initialize_st - should fail because asprocess is true and ppid is 1
HELPER_EXPECT_FAILED(server.initialize_st());
}
// Test: srs_hex_encode_to_string_lowercase converts bytes to lowercase hex string
VOID TEST(KernelUtilityTest, HexEncodeToStringLowercase)
{
// Test normal case: convert bytes to lowercase hex
uint8_t src[] = {0xAB, 0xCD, 0xEF, 0x12, 0x34};
char des[11] = {0}; // 5 bytes * 2 chars + 1 null terminator
char *result = srs_hex_encode_to_string_lowercase(des, src, 5);
EXPECT_TRUE(result != NULL);
EXPECT_STREQ("abcdef1234", des);
// Test NULL source
EXPECT_TRUE(NULL == srs_hex_encode_to_string_lowercase(des, NULL, 5));
// Test zero length
EXPECT_TRUE(NULL == srs_hex_encode_to_string_lowercase(des, src, 0));
// Test NULL destination
EXPECT_TRUE(NULL == srs_hex_encode_to_string_lowercase(NULL, src, 5));
}
// Test: srs_strings_dumps_hex(const std::string &str) dumps string to hex format
VOID TEST(KernelUtilityTest, StringsDumpsHexWithString)
{
// Test normal case: dump string to hex
std::string input = "ABC";
std::string hex_result = srs_strings_dumps_hex(input);
// Should contain hex values for 'A' (0x41), 'B' (0x42), 'C' (0x43)
EXPECT_TRUE(hex_result.find("41") != std::string::npos);
EXPECT_TRUE(hex_result.find("42") != std::string::npos);
EXPECT_TRUE(hex_result.find("43") != std::string::npos);
// Test empty string
std::string empty_input = "";
std::string empty_result = srs_strings_dumps_hex(empty_input);
EXPECT_TRUE(empty_result.empty());
}
// Test: srs_is_boolean checks if string is "true" or "false"
VOID TEST(AppUtilityTest, IsBoolean)
{
// Test "true" string
EXPECT_TRUE(srs_is_boolean("true"));
// Test "false" string
EXPECT_TRUE(srs_is_boolean("false"));
// Test non-boolean strings
EXPECT_FALSE(srs_is_boolean("True"));
EXPECT_FALSE(srs_is_boolean("False"));
EXPECT_FALSE(srs_is_boolean("TRUE"));
EXPECT_FALSE(srs_is_boolean("FALSE"));
EXPECT_FALSE(srs_is_boolean("yes"));
EXPECT_FALSE(srs_is_boolean("no"));
EXPECT_FALSE(srs_is_boolean("1"));
EXPECT_FALSE(srs_is_boolean("0"));
EXPECT_FALSE(srs_is_boolean(""));
EXPECT_FALSE(srs_is_boolean("random"));
}