diff --git a/trunk/src/app/srs_app_hls.cpp b/trunk/src/app/srs_app_hls.cpp index 5394256d8..4c96ff911 100644 --- a/trunk/src/app/srs_app_hls.cpp +++ b/trunk/src/app/srs_app_hls.cpp @@ -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(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(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() { } diff --git a/trunk/src/app/srs_app_hls.hpp b/trunk/src/app/srs_app_hls.hpp index 2623980f0..a34df591e 100644 --- a/trunk/src/app/srs_app_hls.hpp +++ b/trunk/src/app/srs_app_hls.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -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_; diff --git a/trunk/src/utest/srs_utest_ai13.cpp b/trunk/src/utest/srs_utest_ai13.cpp index 5a1e07be9..93851a4da 100644 --- a/trunk/src/utest/srs_utest_ai13.cpp +++ b/trunk/src/utest/srs_utest_ai13.cpp @@ -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(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(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(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(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(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 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 diff --git a/trunk/src/utest/srs_utest_ai24.cpp b/trunk/src/utest/srs_utest_ai24.cpp index 233c9f07f..24a7d61ba 100644 --- a/trunk/src/utest/srs_utest_ai24.cpp +++ b/trunk/src/utest/srs_utest_ai24.cpp @@ -11,6 +11,15 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #ifdef SRS_FFMPEG_FIT #include @@ -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); +} diff --git a/trunk/src/utest/srs_utest_manual_mock.hpp b/trunk/src/utest/srs_utest_manual_mock.hpp index f1c5564f8..98a508cae 100644 --- a/trunk/src/utest/srs_utest_manual_mock.hpp +++ b/trunk/src/utest/srs_utest_manual_mock.hpp @@ -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; }