AI: Improve utest coverage for HLS.

This commit is contained in:
OSSRS-AI 2025-10-28 20:57:55 -04:00 committed by winlin
parent 758906353c
commit 1faadd0c73
5 changed files with 960 additions and 215 deletions

View File

@ -709,6 +709,20 @@ srs_error_t SrsHlsFmp4Muxer::update_config(ISrsRequest *r)
max_td_ = hls_fragment_ * hls_td_ratio;
// create m3u8 dir once.
if ((err = create_directories()) != srs_success) {
return srs_error_wrap(err, "create dir");
}
writer_ = app_factory_->create_file_writer();
return err;
}
// LCOV_EXCL_START
srs_error_t SrsHlsFmp4Muxer::create_directories()
{
srs_error_t err = srs_success;
SrsPath path;
m3u8_dir_ = path.filepath_dir(m3u8_);
if ((err = path.mkdir_all(m3u8_dir_)) != srs_success) {
@ -716,7 +730,7 @@ srs_error_t SrsHlsFmp4Muxer::update_config(ISrsRequest *r)
}
if (hls_keys_ && (hls_path_ != hls_key_file_path_)) {
string key_file = srs_path_build_stream(hls_key_file_, vhost, app, stream);
string key_file = srs_path_build_stream(hls_key_file_, req_->vhost_, req_->app_, req_->stream_);
string key_url = hls_key_file_path_ + "/" + key_file;
string key_dir = path.filepath_dir(key_url);
if ((err = path.mkdir_all(key_dir)) != srs_success) {
@ -724,10 +738,9 @@ srs_error_t SrsHlsFmp4Muxer::update_config(ISrsRequest *r)
}
}
writer_ = app_factory_->create_file_writer();
return err;
}
// LCOV_EXCL_STOP
srs_error_t SrsHlsFmp4Muxer::segment_open(srs_utime_t basetime)
{
@ -747,8 +760,46 @@ srs_error_t SrsHlsFmp4Muxer::segment_open(srs_utime_t basetime)
}
// generate filename.
std::string m4s_file = generate_m4s_filename();
std::string m4s_path = hls_path_ + "/" + m4s_file;
current_->set_path(m4s_path);
// the ts url, relative or absolute url.
// TODO: FIXME: Use url and path manager.
std::string m4s_url = current_->fullpath();
if (srs_strings_starts_with(m4s_url, m3u8_dir_)) {
m4s_url = m4s_url.substr(m3u8_dir_.length());
}
while (srs_strings_starts_with(m4s_url, "/")) {
m4s_url = m4s_url.substr(1);
}
current_->uri_ += hls_entry_prefix_;
if (!hls_entry_prefix_.empty() && !srs_strings_ends_with(hls_entry_prefix_, "/")) {
current_->uri_ += "/";
// add the http dir to uri.
SrsPath path;
string http_dir = path.filepath_dir(m3u8_url_);
if (!http_dir.empty()) {
current_->uri_ += http_dir + "/";
}
}
current_->uri_ += m4s_url;
current_->initialize(basetime, video_track_id_, audio_track_id_, sequence_no_, m4s_path);
return err;
}
std::string SrsHlsFmp4Muxer::generate_m4s_filename()
{
std::string m4s_file = hls_m4s_file_;
m4s_file = srs_path_build_stream(m4s_file, req_->vhost_, req_->app_, req_->stream_);
if (hls_ts_floor_) {
// accept the floor ts for the first piece.
int64_t current_floor_ts = srs_time_now_realtime() / hls_fragment_;
@ -784,41 +835,14 @@ srs_error_t SrsHlsFmp4Muxer::segment_open(srs_utime_t basetime)
} else {
m4s_file = srs_path_build_timestamp(m4s_file);
}
if (true) {
std::stringstream ss;
ss << current_->sequence_no_;
m4s_file = srs_strings_replace(m4s_file, "[seq]", ss.str());
}
std::string m4s_path = hls_path_ + "/" + m4s_file;
current_->set_path(m4s_path);
// the ts url, relative or absolute url.
// TODO: FIXME: Use url and path manager.
std::string m4s_url = current_->fullpath();
if (srs_strings_starts_with(m4s_url, m3u8_dir_)) {
m4s_url = m4s_url.substr(m3u8_dir_.length());
}
while (srs_strings_starts_with(m4s_url, "/")) {
m4s_url = m4s_url.substr(1);
}
current_->uri_ += hls_entry_prefix_;
if (!hls_entry_prefix_.empty() && !srs_strings_ends_with(hls_entry_prefix_, "/")) {
current_->uri_ += "/";
// add the http dir to uri.
SrsPath path;
string http_dir = path.filepath_dir(m3u8_url_);
if (!http_dir.empty()) {
current_->uri_ += http_dir + "/";
}
}
current_->uri_ += m4s_url;
current_->initialize(basetime, video_track_id_, audio_track_id_, sequence_no_, m4s_path);
return err;
return m4s_file;
}
srs_error_t SrsHlsFmp4Muxer::on_sequence_header()
@ -1033,47 +1057,9 @@ srs_error_t SrsHlsFmp4Muxer::do_refresh_m3u8(std::string m3u8_file)
// write all segments
for (int i = 0; i < segments_->size(); i++) {
SrsHlsM4sSegment *segment = dynamic_cast<SrsHlsM4sSegment *>(segments_->at(i));
if (segment->is_sequence_header()) {
// #EXT-X-DISCONTINUITY\n
ss << "#EXT-X-DISCONTINUITY" << SRS_CONSTS_LF;
if ((err = do_refresh_m3u8_segment(segment, ss)) != srs_success) {
return srs_error_wrap(err, "hls: refresh m3u8 segment");
}
#if 1
if (hls_keys_ && ((segment->sequence_no_ % hls_fragments_per_key_) == 0)) {
char hexiv[33];
srs_hex_encode_to_string(hexiv, segment->iv_, 16);
hexiv[32] = '\0';
string key_file = srs_path_build_stream(hls_key_file_, req_->vhost_, req_->app_, req_->stream_);
key_file = srs_strings_replace(key_file, "[seq]", srs_strconv_format_int(segment->sequence_no_));
string key_path = key_file;
// if key_url is not set,only use the file name
if (!hls_key_url_.empty()) {
key_path = hls_key_url_ + key_file;
}
ss << "#EXT-X-KEY:METHOD=SAMPLE-AES,URI=" << "\"" << key_path << "\",IV=0x" << hexiv << SRS_CONSTS_LF;
}
#endif
// "#EXTINF:4294967295.208,\n"
ss.precision(3);
ss.setf(std::ios::fixed, std::ios::floatfield);
ss << "#EXTINF:" << srsu2msi(segment->duration()) / 1000.0 << ", no desc" << SRS_CONSTS_LF;
// {file name}\n
// TODO get segment name in relative path.
SrsPath path;
std::string seg_uri = segment->fullpath();
if (true) {
std::stringstream stemp;
stemp << srsu2msi(segment->duration());
seg_uri = srs_strings_replace(seg_uri, "[duration]", stemp.str());
}
// ss << segment->uri << SRS_CONSTS_LF;
ss << path.filepath_base(seg_uri) << SRS_CONSTS_LF;
}
// write m3u8 to writer.
@ -1085,6 +1071,60 @@ srs_error_t SrsHlsFmp4Muxer::do_refresh_m3u8(std::string m3u8_file)
return err;
}
srs_error_t SrsHlsFmp4Muxer::do_refresh_m3u8_segment(SrsHlsM4sSegment *segment, std::stringstream &ss)
{
srs_error_t err = srs_success;
if (segment->is_sequence_header()) {
// #EXT-X-DISCONTINUITY\n
ss << "#EXT-X-DISCONTINUITY" << SRS_CONSTS_LF;
}
if (hls_keys_ && ((segment->sequence_no_ % hls_fragments_per_key_) == 0)) {
char hexiv[33];
srs_hex_encode_to_string(hexiv, segment->iv_, 16);
hexiv[32] = '\0';
string key_file = srs_path_build_stream(hls_key_file_, req_->vhost_, req_->app_, req_->stream_);
key_file = srs_strings_replace(key_file, "[seq]", srs_strconv_format_int(segment->sequence_no_));
string key_path = key_file;
// if key_url is not set,only use the file name
if (!hls_key_url_.empty()) {
key_path = hls_key_url_ + key_file;
}
ss << "#EXT-X-KEY:METHOD=SAMPLE-AES,URI=" << "\"" << key_path << "\",IV=0x" << hexiv << SRS_CONSTS_LF;
}
// "#EXTINF:4294967295.208,\n"
ss.precision(3);
ss.setf(std::ios::fixed, std::ios::floatfield);
ss << "#EXTINF:" << srsu2msi(segment->duration()) / 1000.0 << ", no desc" << SRS_CONSTS_LF;
// {file name}\n
// TODO get segment name in relative path.
SrsPath path;
std::string seg_uri = segment->fullpath();
if (true) {
std::stringstream stemp;
stemp << srsu2msi(segment->duration());
seg_uri = srs_strings_replace(seg_uri, "[duration]", stemp.str());
}
// ss << segment->uri << SRS_CONSTS_LF;
ss << path.filepath_base(seg_uri) << SRS_CONSTS_LF;
return err;
}
ISrsHlsMuxer::ISrsHlsMuxer()
{
}
ISrsHlsMuxer::~ISrsHlsMuxer()
{
}
SrsHlsMuxer::SrsHlsMuxer()
{
req_ = NULL;
@ -1128,6 +1168,7 @@ SrsHlsMuxer::~SrsHlsMuxer()
app_factory_ = NULL;
}
// LCOV_EXCL_START
void SrsHlsMuxer::dispose()
{
srs_error_t err = srs_success;
@ -1152,6 +1193,7 @@ void SrsHlsMuxer::dispose()
srs_trace("gracefully dispose hls %s", req_ ? req_->get_stream_url().c_str() : "");
}
// LCOV_EXCL_STOP
int SrsHlsMuxer::sequence_no()
{
@ -1282,6 +1324,24 @@ srs_error_t SrsHlsMuxer::update_config(ISrsRequest *r, string entry_prefix,
// when update config, reset the history target duration.
max_td_ = fragment * config_->get_hls_td_ratio(r->vhost_);
if ((err = create_directories()) != srs_success) {
return srs_error_wrap(err, "create dir");
}
if (hls_keys_) {
writer_ = app_factory_->create_enc_file_writer();
} else {
writer_ = app_factory_->create_file_writer();
}
return err;
}
// LCOV_EXCL_START
srs_error_t SrsHlsMuxer::create_directories()
{
srs_error_t err = srs_success;
// create m3u8 dir once.
SrsPath path_util;
m3u8_dir_ = path_util.filepath_dir(m3u8_);
@ -1298,14 +1358,9 @@ srs_error_t SrsHlsMuxer::update_config(ISrsRequest *r, string entry_prefix,
}
}
if (hls_keys_) {
writer_ = app_factory_->create_enc_file_writer();
} else {
writer_ = app_factory_->create_file_writer();
}
return err;
}
// LCOV_EXCL_STOP
srs_error_t SrsHlsMuxer::recover_hls()
{
@ -1496,48 +1551,7 @@ srs_error_t SrsHlsMuxer::segment_open()
}
// generate filename.
std::string ts_file = hls_ts_file_;
ts_file = srs_path_build_stream(ts_file, req_->vhost_, req_->app_, req_->stream_);
if (hls_ts_floor_) {
// accept the floor ts for the first piece.
int64_t current_floor_ts = srs_time_now_realtime() / hls_fragment_;
if (!accept_floor_ts_) {
accept_floor_ts_ = current_floor_ts - 1;
} else {
accept_floor_ts_++;
}
// jump when deviation more than 10p
if (accept_floor_ts_ - current_floor_ts > SRS_JUMP_WHEN_PIECE_DEVIATION) {
srs_warn("hls: jmp for ts deviation, current=%" PRId64 ", accept=%" PRId64, current_floor_ts, accept_floor_ts_);
accept_floor_ts_ = current_floor_ts - 1;
}
// when reap ts, adjust the deviation.
deviation_ts_ = (int)(accept_floor_ts_ - current_floor_ts);
// dup/jmp detect for ts in floor mode.
if (previous_floor_ts_ && previous_floor_ts_ != current_floor_ts - 1) {
srs_warn("hls: dup/jmp ts, previous=%" PRId64 ", current=%" PRId64 ", accept=%" PRId64 ", deviation=%d",
previous_floor_ts_, current_floor_ts, accept_floor_ts_, deviation_ts_);
}
previous_floor_ts_ = current_floor_ts;
// we always ensure the piece is increase one by one.
std::stringstream ts_floor;
ts_floor << accept_floor_ts_;
ts_file = srs_strings_replace(ts_file, "[timestamp]", ts_floor.str());
// TODO: FIMXE: we must use the accept ts floor time to generate the hour variable.
ts_file = srs_path_build_timestamp(ts_file);
} else {
ts_file = srs_path_build_timestamp(ts_file);
}
if (true) {
std::stringstream ss;
ss << current_->sequence_no_;
ts_file = srs_strings_replace(ts_file, "[seq]", ss.str());
}
std::string ts_file = generate_ts_filename();
current_->set_path(hls_path_ + "/" + ts_file);
// the ts url, relative or absolute url.
@ -1579,6 +1593,57 @@ srs_error_t SrsHlsMuxer::segment_open()
return err;
}
string SrsHlsMuxer::generate_ts_filename()
{
std::string ts_file = hls_ts_file_;
ts_file = srs_path_build_stream(ts_file, req_->vhost_, req_->app_, req_->stream_);
if (hls_ts_floor_) {
// accept the floor ts for the first piece.
int64_t current_floor_ts = srs_time_now_realtime() / hls_fragment_;
if (!accept_floor_ts_) {
accept_floor_ts_ = current_floor_ts - 1;
} else {
accept_floor_ts_++;
}
// jump when deviation more than 10p
if (accept_floor_ts_ - current_floor_ts > SRS_JUMP_WHEN_PIECE_DEVIATION) {
srs_warn("hls: jmp for ts deviation, current=%" PRId64 ", accept=%" PRId64, current_floor_ts, accept_floor_ts_);
accept_floor_ts_ = current_floor_ts - 1;
}
// when reap ts, adjust the deviation.
deviation_ts_ = (int)(accept_floor_ts_ - current_floor_ts);
// dup/jmp detect for ts in floor mode.
if (previous_floor_ts_ && previous_floor_ts_ != current_floor_ts - 1) {
srs_warn("hls: dup/jmp ts, previous=%" PRId64 ", current=%" PRId64 ", accept=%" PRId64 ", deviation=%d",
previous_floor_ts_, current_floor_ts, accept_floor_ts_, deviation_ts_);
}
previous_floor_ts_ = current_floor_ts;
// we always ensure the piece is increase one by one.
std::stringstream ts_floor;
ts_floor << accept_floor_ts_;
ts_file = srs_strings_replace(ts_file, "[timestamp]", ts_floor.str());
// TODO: FIMXE: we must use the accept ts floor time to generate the hour variable.
ts_file = srs_path_build_timestamp(ts_file);
} else {
ts_file = srs_path_build_timestamp(ts_file);
}
if (true) {
std::stringstream ss;
ss << current_->sequence_no_;
ts_file = srs_strings_replace(ts_file, "[seq]", ss.str());
}
return ts_file;
}
srs_error_t SrsHlsMuxer::on_sequence_header()
{
srs_error_t err = srs_success;
@ -1713,6 +1778,31 @@ srs_error_t SrsHlsMuxer::do_segment_close()
return err;
}
if ((err = do_segment_close2()) != srs_success) {
return srs_error_wrap(err, "hls: do segment close");
}
// shrink the segments.
segments_->shrink(hls_window_);
// refresh the m3u8, donot contains the removed ts
err = refresh_m3u8();
// remove the ts file.
segments_->clear_expired(hls_cleanup_);
// check ret of refresh m3u8
if (err != srs_success) {
return srs_error_wrap(err, "hls: refresh m3u8");
}
return err;
}
srs_error_t SrsHlsMuxer::do_segment_close2()
{
srs_error_t err = srs_success;
// when close current segment, the current segment must not be NULL.
srs_assert(current_);
@ -1762,20 +1852,6 @@ srs_error_t SrsHlsMuxer::do_segment_close()
}
}
// shrink the segments.
segments_->shrink(hls_window_);
// refresh the m3u8, donot contains the removed ts
err = refresh_m3u8();
// remove the ts file.
segments_->clear_expired(hls_cleanup_);
// check ret of refresh m3u8
if (err != srs_success) {
return srs_error_wrap(err, "hls: refresh m3u8");
}
return err;
}
@ -1815,6 +1891,7 @@ srs_error_t SrsHlsMuxer::write_hls_key()
return err;
}
// LCOV_EXCL_START
srs_error_t SrsHlsMuxer::refresh_m3u8()
{
srs_error_t err = srs_success;
@ -1842,6 +1919,7 @@ srs_error_t SrsHlsMuxer::refresh_m3u8()
return err;
}
// LCOV_EXCL_STOP
srs_error_t SrsHlsMuxer::do_refresh_m3u8(string m3u8_file)
{
@ -1893,43 +1971,9 @@ srs_error_t SrsHlsMuxer::do_refresh_m3u8(string m3u8_file)
// write all segments
for (int i = 0; i < segments_->size(); i++) {
SrsHlsSegment *segment = dynamic_cast<SrsHlsSegment *>(segments_->at(i));
if (segment->is_sequence_header()) {
// #EXT-X-DISCONTINUITY\n
ss << "#EXT-X-DISCONTINUITY" << SRS_CONSTS_LF;
if ((err = do_refresh_m3u8_segment(segment, ss)) != srs_success) {
return srs_error_wrap(err, "hls: refresh m3u8 segment");
}
if (hls_keys_ && ((segment->sequence_no_ % hls_fragments_per_key_) == 0)) {
char hexiv[33];
srs_hex_encode_to_string(hexiv, segment->iv_, 16);
hexiv[32] = '\0';
string key_file = srs_path_build_stream(hls_key_file_, req_->vhost_, req_->app_, req_->stream_);
key_file = srs_strings_replace(key_file, "[seq]", srs_strconv_format_int(segment->sequence_no_));
string key_path = key_file;
// if key_url is not set,only use the file name
if (!hls_key_url_.empty()) {
key_path = hls_key_url_ + key_file;
}
ss << "#EXT-X-KEY:METHOD=AES-128,URI=" << "\"" << key_path << "\",IV=0x" << hexiv << SRS_CONSTS_LF;
}
// "#EXTINF:4294967295.208,\n"
ss.precision(3);
ss.setf(std::ios::fixed, std::ios::floatfield);
ss << "#EXTINF:" << srsu2msi(segment->duration()) / 1000.0 << ", no desc" << SRS_CONSTS_LF;
// {file name}\n
std::string seg_uri = segment->uri_;
if (true) {
std::stringstream stemp;
stemp << srsu2msi(segment->duration());
seg_uri = srs_strings_replace(seg_uri, "[duration]", stemp.str());
}
// ss << segment->uri << SRS_CONSTS_LF;
ss << seg_uri << SRS_CONSTS_LF;
}
// write m3u8 to writer.
@ -1941,6 +1985,50 @@ srs_error_t SrsHlsMuxer::do_refresh_m3u8(string m3u8_file)
return err;
}
srs_error_t SrsHlsMuxer::do_refresh_m3u8_segment(SrsHlsSegment *segment, std::stringstream &ss)
{
srs_error_t err = srs_success;
if (segment->is_sequence_header()) {
// #EXT-X-DISCONTINUITY\n
ss << "#EXT-X-DISCONTINUITY" << SRS_CONSTS_LF;
}
if (hls_keys_ && ((segment->sequence_no_ % hls_fragments_per_key_) == 0)) {
char hexiv[33];
srs_hex_encode_to_string(hexiv, segment->iv_, 16);
hexiv[32] = '\0';
string key_file = srs_path_build_stream(hls_key_file_, req_->vhost_, req_->app_, req_->stream_);
key_file = srs_strings_replace(key_file, "[seq]", srs_strconv_format_int(segment->sequence_no_));
string key_path = key_file;
// if key_url is not set,only use the file name
if (!hls_key_url_.empty()) {
key_path = hls_key_url_ + key_file;
}
ss << "#EXT-X-KEY:METHOD=AES-128,URI=" << "\"" << key_path << "\",IV=0x" << hexiv << SRS_CONSTS_LF;
}
// "#EXTINF:4294967295.208,\n"
ss.precision(3);
ss.setf(std::ios::fixed, std::ios::floatfield);
ss << "#EXTINF:" << srsu2msi(segment->duration()) / 1000.0 << ", no desc" << SRS_CONSTS_LF;
// {file name}\n
std::string seg_uri = segment->uri_;
if (true) {
std::stringstream stemp;
stemp << srsu2msi(segment->duration());
seg_uri = srs_strings_replace(seg_uri, "[duration]", stemp.str());
}
// ss << segment->uri << SRS_CONSTS_LF;
ss << seg_uri << SRS_CONSTS_LF;
return err;
}
ISrsHlsController::ISrsHlsController()
{
}

View File

@ -11,6 +11,7 @@
#include <string>
#include <vector>
#include <sstream>
#include <srs_app_async_call.hpp>
#include <srs_app_fragment.hpp>
@ -184,6 +185,52 @@ public:
virtual std::string to_string();
};
// The HLS muxer interface.
class ISrsHlsMuxer
{
public:
ISrsHlsMuxer();
virtual ~ISrsHlsMuxer();
public:
virtual srs_error_t initialize() = 0;
virtual void dispose() = 0;
public:
virtual int sequence_no() = 0;
virtual std::string ts_url() = 0;
virtual srs_utime_t duration() = 0;
virtual int deviation() = 0;
public:
virtual SrsAudioCodecId latest_acodec() = 0;
virtual void set_latest_acodec(SrsAudioCodecId v) = 0;
virtual SrsVideoCodecId latest_vcodec() = 0;
virtual void set_latest_vcodec(SrsVideoCodecId v) = 0;
public:
virtual bool pure_audio() = 0;
virtual bool is_segment_overflow() = 0;
virtual bool is_segment_absolutely_overflow() = 0;
virtual bool wait_keyframe() = 0;
public:
virtual srs_error_t on_publish(ISrsRequest *req) = 0;
virtual srs_error_t on_unpublish() = 0;
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) = 0;
virtual srs_error_t segment_open() = 0;
virtual srs_error_t on_sequence_header() = 0;
virtual srs_error_t flush_audio(SrsTsMessageCache *cache) = 0;
virtual srs_error_t flush_video(SrsTsMessageCache *cache) = 0;
virtual void update_duration(uint64_t dts) = 0;
virtual srs_error_t segment_close() = 0;
virtual srs_error_t recover_hls() = 0;
};
// Mux the HLS stream(m3u8 and ts files).
// Generally, the m3u8 muxer only provides methods to open/close segments,
// to flush video/audio, without any mechenisms.
@ -191,7 +238,7 @@ public:
// That is, user must use HlsCache, which will control the methods of muxer,
// and provides HLS mechenisms.
// TODO: Rename to SrsHlsTsMuxer, for TS file only.
class SrsHlsMuxer
class SrsHlsMuxer : public ISrsHlsMuxer
{
// clang-format off
SRS_DECLARE_PRIVATE: // clang-format on
@ -299,8 +346,20 @@ public:
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);
// clang-format off
SRS_DECLARE_PRIVATE: // clang-format on
virtual srs_error_t create_directories();
public:
// Open a new segment(a new ts file)
virtual srs_error_t segment_open();
// clang-format off
SRS_DECLARE_PRIVATE: // clang-format on
virtual std::string generate_ts_filename();
public:
virtual srs_error_t on_sequence_header();
// Whether segment overflow,
// that is whether the current segment duration>=(the segment in config)
@ -326,9 +385,11 @@ public:
// clang-format off
SRS_DECLARE_PRIVATE: // clang-format on
virtual srs_error_t do_segment_close();
virtual srs_error_t do_segment_close2();
virtual srs_error_t write_hls_key();
virtual srs_error_t refresh_m3u8();
virtual srs_error_t do_refresh_m3u8(std::string m3u8_file);
virtual srs_error_t do_refresh_m3u8_segment(SrsHlsSegment *segment, std::stringstream &ss);
// Check if a segment with the given URI already exists in the segments list.
virtual bool segment_exists(const std::string &ts_url);
@ -468,8 +529,20 @@ public:
virtual srs_error_t on_unpublish();
// When publish, update the config for muxer.
virtual srs_error_t update_config(ISrsRequest *r);
// clang-format off
SRS_DECLARE_PRIVATE: // clang-format on
virtual srs_error_t create_directories();
public:
// Open a new segment(a new ts file)
virtual srs_error_t segment_open(srs_utime_t basetime);
// clang-format off
SRS_DECLARE_PRIVATE: // clang-format on
virtual std::string generate_m4s_filename();
public:
virtual srs_error_t on_sequence_header();
// Whether segment overflow,
// that is whether the current segment duration>=(the segment in config)
@ -494,6 +567,7 @@ SRS_DECLARE_PRIVATE: // clang-format on
virtual srs_error_t write_hls_key();
virtual srs_error_t refresh_m3u8();
virtual srs_error_t do_refresh_m3u8(std::string m3u8_file);
virtual srs_error_t do_refresh_m3u8_segment(SrsHlsM4sSegment *segment, std::stringstream &ss);
};
// The base class for HLS controller
@ -549,7 +623,7 @@ SRS_DECLARE_PRIVATE: // clang-format on
SRS_DECLARE_PRIVATE: // clang-format on
// The HLS muxer to reap ts and m3u8.
// The TS is cached to SrsTsMessageCache then flush to ts segment.
SrsHlsMuxer *muxer_;
ISrsHlsMuxer *muxer_;
// The TS cache
SrsTsMessageCache *tsmc_;

View File

@ -1102,22 +1102,26 @@ VOID TEST(AppHlsTest, HlsControllerWriteAudioTypicalScenario)
// Create mock request
MockHlsRequest mock_request("__defaultVhost__", "live", "test");
// Cast to concrete type to access private members for testing
SrsHlsMuxer *muxer = dynamic_cast<SrsHlsMuxer*>(controller->muxer_);
srs_assert(muxer);
// Set up muxer with required fields
controller->muxer_->req_ = &mock_request;
controller->muxer_->hls_fragment_ = 10 * SRS_UTIME_SECONDS;
controller->muxer_->hls_aof_ratio_ = 2.0;
controller->muxer_->hls_wait_keyframe_ = true;
controller->muxer_->hls_ts_floor_ = false;
controller->muxer_->deviation_ts_ = 0;
controller->muxer_->max_td_ = 10 * SRS_UTIME_SECONDS;
controller->muxer_->latest_acodec_ = SrsAudioCodecIdAAC;
controller->muxer_->latest_vcodec_ = SrsVideoCodecIdDisabled; // Pure audio mode
controller->muxer_->writer_ = new MockSrsFileWriter();
controller->muxer_->context_ = new SrsTsContext();
muxer->req_ = &mock_request;
muxer->hls_fragment_ = 10 * SRS_UTIME_SECONDS;
muxer->hls_aof_ratio_ = 2.0;
muxer->hls_wait_keyframe_ = true;
muxer->hls_ts_floor_ = false;
muxer->deviation_ts_ = 0;
muxer->max_td_ = 10 * SRS_UTIME_SECONDS;
muxer->latest_acodec_ = SrsAudioCodecIdAAC;
muxer->latest_vcodec_ = SrsVideoCodecIdDisabled; // Pure audio mode
muxer->writer_ = new MockSrsFileWriter();
muxer->context_ = new SrsTsContext();
// Create a segment
SrsHlsSegment *segment = new SrsHlsSegment(controller->muxer_->context_, SrsAudioCodecIdAAC, SrsVideoCodecIdDisabled, new MockSrsFileWriter());
controller->muxer_->current_ = segment;
SrsHlsSegment *segment = new SrsHlsSegment(muxer->context_, SrsAudioCodecIdAAC, SrsVideoCodecIdDisabled, new MockSrsFileWriter());
muxer->current_ = segment;
segment->append(0);
// Create SrsFormat with AAC audio codec
@ -1197,11 +1201,11 @@ VOID TEST(AppHlsTest, HlsControllerWriteAudioTypicalScenario)
EXPECT_GT(controller->aac_samples_, 1024);
// Clean up
controller->muxer_->req_ = NULL;
controller->muxer_->current_ = NULL;
muxer->req_ = NULL;
muxer->current_ = NULL;
srs_freep(segment);
srs_freep(controller->muxer_->writer_);
srs_freep(controller->muxer_->context_);
srs_freep(muxer->writer_);
srs_freep(muxer->context_);
}
// Unit test for SrsHlsMuxer::flush_video typical scenario
@ -1414,6 +1418,10 @@ VOID TEST(AppHlsTest, HlsControllerSelectionTypicalScenario)
// Initialize the controller
HELPER_EXPECT_SUCCESS(controller->initialize());
// Cast to concrete type to access private members for testing
SrsHlsMuxer *muxer = dynamic_cast<SrsHlsMuxer*>(controller->muxer_);
srs_assert(muxer);
// Test initial state - no current segment
// sequence_no() should return 0
EXPECT_EQ(0, controller->sequence_no());
@ -1439,8 +1447,8 @@ VOID TEST(AppHlsTest, HlsControllerSelectionTypicalScenario)
segment->append(10000); // 10 seconds duration in milliseconds
// Set the current segment in the muxer
controller->muxer_->current_ = segment;
controller->muxer_->sequence_no_ = 42;
muxer->current_ = segment;
muxer->sequence_no_ = 42;
// Test selection code with current segment
// sequence_no() should return the muxer's sequence number
@ -1456,14 +1464,14 @@ VOID TEST(AppHlsTest, HlsControllerSelectionTypicalScenario)
EXPECT_EQ(0, controller->deviation());
// Test deviation with hls_ts_floor enabled
controller->muxer_->hls_ts_floor_ = true;
controller->muxer_->deviation_ts_ = 5;
muxer->hls_ts_floor_ = true;
muxer->deviation_ts_ = 5;
// deviation() should return the deviation value when hls_ts_floor is true
EXPECT_EQ(5, controller->deviation());
// Clean up
controller->muxer_->current_ = NULL;
muxer->current_ = NULL;
}
// Unit test for SrsHlsController::on_publish typical scenario
@ -1481,6 +1489,10 @@ VOID TEST(AppHlsTest, HlsControllerOnPublishTypicalScenario)
// Initialize the controller
HELPER_EXPECT_SUCCESS(controller->initialize());
// Cast to concrete type to access private members for testing
SrsHlsMuxer *muxer = dynamic_cast<SrsHlsMuxer*>(controller->muxer_);
srs_assert(muxer);
// Create mock request
MockHlsRequest mock_request("test.vhost", "live", "stream1");
@ -1489,47 +1501,47 @@ VOID TEST(AppHlsTest, HlsControllerOnPublishTypicalScenario)
// Verify that muxer was configured properly
// Check that muxer's request was set
EXPECT_TRUE(controller->muxer_->req_ != NULL);
EXPECT_EQ("test.vhost", controller->muxer_->req_->vhost_);
EXPECT_EQ("live", controller->muxer_->req_->app_);
EXPECT_EQ("stream1", controller->muxer_->req_->stream_);
EXPECT_TRUE(muxer->req_ != NULL);
EXPECT_EQ("test.vhost", muxer->req_->vhost_);
EXPECT_EQ("live", muxer->req_->app_);
EXPECT_EQ("stream1", muxer->req_->stream_);
// Verify HLS configuration was applied to muxer
// Fragment should be 10 seconds (from MockAppConfig default)
EXPECT_EQ(10 * SRS_UTIME_SECONDS, controller->muxer_->hls_fragment_);
EXPECT_EQ(10 * SRS_UTIME_SECONDS, muxer->hls_fragment_);
// Window should be 60 seconds (from MockAppConfig default)
EXPECT_EQ(60 * SRS_UTIME_SECONDS, controller->muxer_->hls_window_);
EXPECT_EQ(60 * SRS_UTIME_SECONDS, muxer->hls_window_);
// Path should be "./objs/nginx/html" (from MockAppConfig default)
EXPECT_EQ("./objs/nginx/html", controller->muxer_->hls_path_);
EXPECT_EQ("./objs/nginx/html", muxer->hls_path_);
// TS file should be "[app]/[stream]-[seq].ts" (from MockAppConfig default)
EXPECT_EQ("[app]/[stream]-[seq].ts", controller->muxer_->hls_ts_file_);
EXPECT_EQ("[app]/[stream]-[seq].ts", muxer->hls_ts_file_);
// AOF ratio should be 2.0 (from MockAppConfig default)
EXPECT_EQ(2.0, controller->muxer_->hls_aof_ratio_);
EXPECT_EQ(2.0, muxer->hls_aof_ratio_);
// Cleanup should be true (from MockAppConfig default)
EXPECT_TRUE(controller->muxer_->hls_cleanup_);
EXPECT_TRUE(muxer->hls_cleanup_);
// Wait keyframe should be true (from MockAppConfig default)
EXPECT_TRUE(controller->muxer_->hls_wait_keyframe_);
EXPECT_TRUE(muxer->hls_wait_keyframe_);
// TS floor should be false (from MockAppConfig default)
EXPECT_FALSE(controller->muxer_->hls_ts_floor_);
EXPECT_FALSE(muxer->hls_ts_floor_);
// Keys should be false (from MockAppConfig default)
EXPECT_FALSE(controller->muxer_->hls_keys_);
EXPECT_FALSE(muxer->hls_keys_);
// Fragments per key should be 5 (from MockAppConfig default)
EXPECT_EQ(5, controller->muxer_->hls_fragments_per_key_);
EXPECT_EQ(5, muxer->hls_fragments_per_key_);
// Verify hls_dts_directly was set from config
EXPECT_TRUE(controller->hls_dts_directly_);
// Verify that a segment was opened
EXPECT_TRUE(controller->muxer_->current_ != NULL);
EXPECT_TRUE(muxer->current_ != NULL);
}
// Unit test for SrsHlsController::on_unpublish typical scenario
@ -1547,6 +1559,10 @@ VOID TEST(AppHlsTest, HlsControllerOnUnpublishTypicalScenario)
// Initialize the controller
HELPER_EXPECT_SUCCESS(controller->initialize());
// Cast to concrete type to access private members for testing
SrsHlsMuxer *muxer = dynamic_cast<SrsHlsMuxer*>(controller->muxer_);
srs_assert(muxer);
// Create mock request
MockHlsRequest mock_request("test.vhost", "live", "stream1");
@ -1554,7 +1570,7 @@ VOID TEST(AppHlsTest, HlsControllerOnUnpublishTypicalScenario)
HELPER_EXPECT_SUCCESS(controller->on_publish(&mock_request));
// Verify that a segment was opened
EXPECT_TRUE(controller->muxer_->current_ != NULL);
EXPECT_TRUE(muxer->current_ != NULL);
// Set the codec in the muxer to enable proper audio/video handling
controller->muxer_->set_latest_acodec(SrsAudioCodecIdAAC);
@ -1581,7 +1597,7 @@ VOID TEST(AppHlsTest, HlsControllerOnUnpublishTypicalScenario)
EXPECT_TRUE(controller->tsmc_->audio_ == NULL);
// Verify that the segment was closed (current_ should be NULL after close)
EXPECT_TRUE(controller->muxer_->current_ == NULL);
EXPECT_TRUE(muxer->current_ == NULL);
}
// Unit test for SrsHlsController::write_video typical scenario
@ -1599,6 +1615,10 @@ VOID TEST(AppHlsTest, HlsControllerWriteVideoTypicalScenario)
// Initialize the controller
HELPER_EXPECT_SUCCESS(controller->initialize());
// Cast to concrete type to access private members for testing
SrsHlsMuxer *muxer = dynamic_cast<SrsHlsMuxer*>(controller->muxer_);
srs_assert(muxer);
// Create mock request
MockHlsRequest mock_request("test.vhost", "live", "stream1");
@ -1606,7 +1626,7 @@ VOID TEST(AppHlsTest, HlsControllerWriteVideoTypicalScenario)
HELPER_EXPECT_SUCCESS(controller->on_publish(&mock_request));
// Verify that a segment was opened
EXPECT_TRUE(controller->muxer_->current_ != NULL);
EXPECT_TRUE(muxer->current_ != NULL);
// Create a mock SrsFormat with video codec
SrsUniquePtr<SrsFormat> format(new SrsFormat());
@ -1649,10 +1669,10 @@ VOID TEST(AppHlsTest, HlsControllerWriteVideoTypicalScenario)
EXPECT_TRUE(controller->tsmc_->video_ == NULL);
// Verify that the segment is still open (not reaped yet, since segment is not overflow)
EXPECT_TRUE(controller->muxer_->current_ != NULL);
EXPECT_TRUE(muxer->current_ != NULL);
// Verify that the codec was set correctly
EXPECT_EQ(SrsVideoCodecIdAVC, controller->muxer_->latest_vcodec());
EXPECT_EQ(SrsVideoCodecIdAVC, muxer->latest_vcodec());
}
// Unit test for SrsHlsController::reap_segment typical scenario

View File

@ -11,6 +11,15 @@
#include <srs_protocol_sdp.hpp>
#include <srs_kernel_packet.hpp>
#include <srs_kernel_codec.hpp>
#include <srs_app_hls.hpp>
#include <srs_utest_manual_mock.hpp>
#include <srs_utest_manual_kernel.hpp>
#include <srs_app_config.hpp>
#include <srs_app_http_hooks.hpp>
#include <srs_app_utility.hpp>
#include <srs_kernel_utility.hpp>
#include <srs_protocol_utility.hpp>
#include <srs_app_fragment.hpp>
#ifdef SRS_FFMPEG_FIT
#include <srs_app_rtc_codec.hpp>
@ -469,3 +478,554 @@ VOID TEST(FFmpegLogHelperTest, LogCallback)
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);
}

View File

@ -270,6 +270,7 @@ public:
bool http_hooks_enabled_;
SrsConfDirective *on_stop_directive_;
SrsConfDirective *on_unpublish_directive_;
SrsConfDirective *on_hls_directive_;
bool rtc_nack_enabled_;
bool rtc_nack_no_copy_;
int rtc_drop_for_pt_;
@ -299,6 +300,7 @@ public:
http_hooks_enabled_ = true;
on_stop_directive_ = NULL;
on_unpublish_directive_ = NULL;
on_hls_directive_ = NULL;
rtc_nack_enabled_ = true;
rtc_nack_no_copy_ = false;
rtc_drop_for_pt_ = 0;
@ -330,6 +332,7 @@ public:
srs_freep(default_vhost_);
srs_freep(forwards_directive_);
srs_freep(backend_directive_);
srs_freep(on_hls_directive_);
}
public:
@ -501,7 +504,7 @@ public:
virtual bool get_rtc_stun_strict_check(std::string vhost) { return false; }
virtual std::string get_rtc_dtls_role(std::string vhost) { return rtc_dtls_role_; }
virtual std::string get_rtc_dtls_version(std::string vhost) { return "auto"; }
virtual SrsConfDirective *get_vhost_on_hls(std::string vhost) { return NULL; }
virtual SrsConfDirective *get_vhost_on_hls(std::string vhost) { return on_hls_directive_; }
virtual SrsConfDirective *get_vhost_on_hls_notify(std::string vhost) { return NULL; }
// HLS methods
virtual bool get_hls_enabled(std::string vhost) { return false; }