From 664e8689724b55abb2d4e4f7e9f33bed3b0724f5 Mon Sep 17 00:00:00 2001 From: chundonglinlin Date: Wed, 20 Aug 2025 12:09:54 +0800 Subject: [PATCH] HLS: restore HLS information when republish stream.(#3088). v7.0.57 (#3126) ### Feature HLS continuous mode: In this mode HLS sequence number is started from where it stopped last time. Old fragments are kept. Default is on. ### Configuration ``` vhost __defaultVhost__ { hls { enabled on; hls_path ./objs/nginx/html; hls_fragment 10; hls_window 60; hls_continuous on; } } ``` Contributed by AI: * [AI: Refine and extract HLS recover.](https://github.com/ossrs/srs/pull/3126/commits/656e4e296d8f93ff7e27df5c18e1f26cf5afcf82) --------- Co-authored-by: Haibo Chen <495810242@qq.com> Co-authored-by: winlin Co-authored-by: OSSRS-AI --- trunk/3rdparty/gtest-fit/CMakeLists.txt | 2 +- .../gtest-fit/googlemock/CMakeLists.txt | 2 +- .../gtest-fit/googletest/CMakeLists.txt | 2 +- trunk/conf/full.conf | 7 + trunk/conf/hls.conf | 1 + trunk/doc/CHANGELOG.md | 1 + trunk/ide/srs_clion/CMakeLists.txt | 12 +- trunk/src/app/srs_app_config.cpp | 21 ++- trunk/src/app/srs_app_config.hpp | 4 + trunk/src/app/srs_app_hls.cpp | 153 +++++++++++++++++- trunk/src/app/srs_app_hls.hpp | 6 + trunk/src/core/srs_core_version7.hpp | 2 +- 12 files changed, 200 insertions(+), 13 deletions(-) diff --git a/trunk/3rdparty/gtest-fit/CMakeLists.txt b/trunk/3rdparty/gtest-fit/CMakeLists.txt index ea81ab129..e4ecfd769 100644 --- a/trunk/3rdparty/gtest-fit/CMakeLists.txt +++ b/trunk/3rdparty/gtest-fit/CMakeLists.txt @@ -1,7 +1,7 @@ # Note: CMake support is community-based. The maintainers do not use CMake # internally. -cmake_minimum_required(VERSION 2.8.12) +cmake_minimum_required(VERSION 3.10) if (POLICY CMP0048) cmake_policy(SET CMP0048 NEW) diff --git a/trunk/3rdparty/gtest-fit/googlemock/CMakeLists.txt b/trunk/3rdparty/gtest-fit/googlemock/CMakeLists.txt index e7df8ec53..c1b6dab07 100644 --- a/trunk/3rdparty/gtest-fit/googlemock/CMakeLists.txt +++ b/trunk/3rdparty/gtest-fit/googlemock/CMakeLists.txt @@ -42,7 +42,7 @@ else() cmake_policy(SET CMP0048 NEW) project(gmock VERSION ${GOOGLETEST_VERSION} LANGUAGES CXX C) endif() -cmake_minimum_required(VERSION 2.8.12) +cmake_minimum_required(VERSION 3.10) if (COMMAND set_up_hermetic_build) set_up_hermetic_build() diff --git a/trunk/3rdparty/gtest-fit/googletest/CMakeLists.txt b/trunk/3rdparty/gtest-fit/googletest/CMakeLists.txt index abdd98b79..3887d6114 100644 --- a/trunk/3rdparty/gtest-fit/googletest/CMakeLists.txt +++ b/trunk/3rdparty/gtest-fit/googletest/CMakeLists.txt @@ -53,7 +53,7 @@ else() cmake_policy(SET CMP0048 NEW) project(gtest VERSION ${GOOGLETEST_VERSION} LANGUAGES CXX C) endif() -cmake_minimum_required(VERSION 2.8.12) +cmake_minimum_required(VERSION 3.10) if (POLICY CMP0063) # Visibility cmake_policy(SET CMP0063 NEW) diff --git a/trunk/conf/full.conf b/trunk/conf/full.conf index a0e03b219..8ee1aee62 100644 --- a/trunk/conf/full.conf +++ b/trunk/conf/full.conf @@ -1870,6 +1870,13 @@ vhost hls.srs.com { # Overwrite by env SRS_VHOST_HLS_HLS_WINDOW for all vhosts. # default: 60 hls_window 60; + # Whether to enable HLS recover mode. When enabled, HLS sequence numbers + # continue from where they stopped last time instead of restarting from 0. + # This preserves old fragments and provides better player compatibility. + # Useful for stream interruption recovery and live event continuity. + # Overwrite by env SRS_VHOST_HLS_HLS_RECOVER for all vhosts. + # Default: on + hls_recover on; # the error strategy. can be: # ignore, disable the hls. # disconnect, require encoder republish. diff --git a/trunk/conf/hls.conf b/trunk/conf/hls.conf index fb3f92966..03a827284 100644 --- a/trunk/conf/hls.conf +++ b/trunk/conf/hls.conf @@ -17,5 +17,6 @@ vhost __defaultVhost__ { hls_path ./objs/nginx/html; hls_fragment 10; hls_window 60; + hls_recover on; } } diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index 8775868f9..87b0a48df 100644 --- a/trunk/doc/CHANGELOG.md +++ b/trunk/doc/CHANGELOG.md @@ -7,6 +7,7 @@ The changelog for SRS. ## SRS 7.0 Changelog +* v7.0, 2025-08-19, Merge [#3126](https://github.com/ossrs/srs/pull/3126): HLS: restore HLS information when republish stream.(#3088). v7.0.57 (#3126) * v7.0, 2025-08-18, Merge [#4443](https://github.com/ossrs/srs/pull/4443): Support RTMPS server. v7.0.56 (#4443) * v7.0, 2025-08-16, Merge [#4441](https://github.com/ossrs/srs/pull/4441): fix err memory leak in rtc to rtmp bridge. v7.0.55 (#4441) * v7.0, 2025-08-14, Merge [#4161](https://github.com/ossrs/srs/pull/4161): fix hls & dash segments cleanup. v7.0.54 (#4161) diff --git a/trunk/ide/srs_clion/CMakeLists.txt b/trunk/ide/srs_clion/CMakeLists.txt index f5bf2ba70..68d4614fc 100755 --- a/trunk/ide/srs_clion/CMakeLists.txt +++ b/trunk/ide/srs_clion/CMakeLists.txt @@ -1,12 +1,10 @@ +# CMake minimum version should be called first +cmake_minimum_required(VERSION 3.10) + # Name of the project. # Language "C" is required for find_package(Threads). -if (CMAKE_VERSION VERSION_LESS 3.0) - project(srs CXX C) -else() - cmake_policy(SET CMP0048 NEW) - project(srs VERSION 4.0.0 LANGUAGES CXX C) -endif() -cmake_minimum_required(VERSION 2.8.12) +cmake_policy(SET CMP0048 NEW) +project(srs VERSION 4.0.0 LANGUAGES CXX C) # For utest required C++11. set (CMAKE_CXX_STANDARD 11) diff --git a/trunk/src/app/srs_app_config.cpp b/trunk/src/app/srs_app_config.cpp index 9e7461d0d..b0ba36b52 100644 --- a/trunk/src/app/srs_app_config.cpp +++ b/trunk/src/app/srs_app_config.cpp @@ -2693,7 +2693,7 @@ srs_error_t SrsConfig::check_normal_config() } else if (n == "hls") { for (int j = 0; j < (int)conf->directives.size(); j++) { string m = conf->at(j)->name; - if (m != "enabled" && m != "hls_entry_prefix" && m != "hls_path" && m != "hls_fragment" && m != "hls_window" && m != "hls_on_error" && m != "hls_storage" && m != "hls_mount" && m != "hls_td_ratio" && m != "hls_aof_ratio" && m != "hls_acodec" && m != "hls_vcodec" && m != "hls_m3u8_file" && m != "hls_ts_file" && m != "hls_ts_floor" && m != "hls_cleanup" && m != "hls_nb_notify" && m != "hls_wait_keyframe" && m != "hls_dispose" && m != "hls_keys" && m != "hls_fragments_per_key" && m != "hls_key_file" && m != "hls_key_file_path" && m != "hls_key_url" && m != "hls_dts_directly" && m != "hls_ctx" && m != "hls_ts_ctx" && m != "hls_use_fmp4" && m != "hls_fmp4_file" && m != "hls_init_file") { + if (m != "enabled" && m != "hls_entry_prefix" && m != "hls_path" && m != "hls_fragment" && m != "hls_window" && m != "hls_on_error" && m != "hls_storage" && m != "hls_mount" && m != "hls_td_ratio" && m != "hls_aof_ratio" && m != "hls_acodec" && m != "hls_vcodec" && m != "hls_m3u8_file" && m != "hls_ts_file" && m != "hls_ts_floor" && m != "hls_cleanup" && m != "hls_nb_notify" && m != "hls_wait_keyframe" && m != "hls_dispose" && m != "hls_keys" && m != "hls_fragments_per_key" && m != "hls_key_file" && m != "hls_key_file_path" && m != "hls_key_url" && m != "hls_dts_directly" && m != "hls_ctx" && m != "hls_ts_ctx" && m != "hls_use_fmp4" && m != "hls_fmp4_file" && m != "hls_init_file" && m != "hls_recover") { return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal vhost.hls.%s of %s", m.c_str(), vhost->arg0().c_str()); } @@ -7523,6 +7523,25 @@ string SrsConfig::get_hls_key_url(std::string vhost) return conf->arg0(); } +bool SrsConfig::get_hls_recover(string vhost) +{ + SRS_OVERWRITE_BY_ENV_BOOL2("srs.vhost.hls.hls_recover"); // SRS_VHOST_HLS_HLS_RECOVER + + static bool DEFAULT = true; + + SrsConfDirective *conf = get_hls(vhost); + if (!conf) { + return DEFAULT; + } + + conf = conf->get("hls_recover"); + if (!conf || conf->arg0().empty()) { + return DEFAULT; + } + + return SRS_CONF_PREFER_TRUE(conf->arg0()); +} + SrsConfDirective *SrsConfig::get_hds(const string &vhost) { SrsConfDirective *conf = get_vhost(vhost); diff --git a/trunk/src/app/srs_app_config.hpp b/trunk/src/app/srs_app_config.hpp index 890a31045..c9a6d5042 100644 --- a/trunk/src/app/srs_app_config.hpp +++ b/trunk/src/app/srs_app_config.hpp @@ -1032,6 +1032,10 @@ public: // Whether enable session for ts file. // The ts file including .ts file for MPEG-ts segment, .m4s file and init.mp4 file for fmp4 segment. virtual bool get_hls_ts_ctx_enabled(std::string vhost); + // Toggles HLS recover mode. + // In this mode HLS sequence number is started from where it stopped last time. + // Old fragments are kept. Default is on. + virtual bool get_hls_recover(std::string vhost); // hds section private: // Get the hds directive of vhost. diff --git a/trunk/src/app/srs_app_hls.cpp b/trunk/src/app/srs_app_hls.cpp index 8de16f60f..f98054671 100644 --- a/trunk/src/app/srs_app_hls.cpp +++ b/trunk/src/app/srs_app_hls.cpp @@ -1254,6 +1254,152 @@ srs_error_t SrsHlsMuxer::update_config(SrsRequest *r, string entry_prefix, return err; } +srs_error_t SrsHlsMuxer::recover_hls() +{ + srs_error_t err = srs_success; + + // exist the m3u8 file. + if (!srs_path_exists(m3u8)) { + return err; + } + + srs_trace("hls: recover stream m3u8=%s, m3u8_url=%s, hls_path=%s", + m3u8.c_str(), m3u8_url.c_str(), hls_path.c_str()); + + // read whole m3u8 file content as a string + SrsFileReader fr; + if ((err = fr.open(m3u8)) != srs_success) { + return srs_error_wrap(err, "open file"); + } + + std::string body; + if ((err = srs_ioutil_read_all(&fr, body)) != srs_success) { + return srs_error_wrap(err, "read data"); + } + if (body.empty()) { + return srs_error_wrap(err, "read empty m3u8"); + } + + bool discon = false; + + std::string ptl; + while (!body.empty()) { + size_t pos = string::npos; + + std::string line; + if ((pos = body.find("\n")) != string::npos) { + line = body.substr(0, pos); + body = body.substr(pos + 1); + } else { + line = body; + body = ""; + } + + line = srs_string_replace(line, "\r", ""); + line = srs_string_replace(line, " ", ""); + + // #EXT-X-VERSION:3 + // the version must be 3.0 + if (srs_string_starts_with(line, "#EXT-X-VERSION:")) { + if (!srs_string_ends_with(line, ":3")) { + srs_warn("m3u8 3.0 required, actual is %s", line.c_str()); + } + continue; + } + + // #EXT-X-PLAYLIST-TYPE:VOD + // the playlist type, vod or nothing. + if (srs_string_starts_with(line, "#EXT-X-PLAYLIST-TYPE:")) { + ptl = line; + continue; + } + + // #EXT-X-MEDIA-SEQUENCE:4294967295 + // the media sequence no. + if (srs_string_starts_with(line, "#EXT-X-MEDIA-SEQUENCE:")) { + _sequence_no = ::atof(line.substr(string("#EXT-X-MEDIA-SEQUENCE:").length()).c_str()); + } + + // #EXT-X-DISCONTINUITY + // the discontinuity tag. + if (srs_string_starts_with(line, "#EXT-X-DISCONTINUITY")) { + discon = true; + } + + // #EXTINF:11.401, + // livestream-5.ts + // parse each ts entry, expect current line is inf. + if (!srs_string_starts_with(line, "#EXTINF:")) { + continue; + } + + // expect next line is url. + std::string ts_url; + if ((pos = body.find("\n")) != string::npos) { + ts_url = body.substr(0, pos); + body = body.substr(pos + 1); + } else { + srs_warn("ts entry unexpected eof, inf=%s", line.c_str()); + break; + } + + // parse the ts duration. + line = line.substr(string("#EXTINF:").length()); + if ((pos = line.find(",")) != string::npos) { + line = line.substr(0, pos); + } + + double ts_duration = ::atof(line.c_str()); + + // Only create new segment if it doesn't already exist + if (!segment_exists(ts_url)) { + // load the default acodec, use the same logic as segment_open(). + SrsAudioCodecId default_acodec = SrsAudioCodecIdDisabled; + + // Now that we know the latest audio codec in stream, use it. + if (latest_acodec_ != SrsAudioCodecIdForbidden) + default_acodec = latest_acodec_; + + // load the default vcodec, use the same logic as segment_open(). + SrsVideoCodecId default_vcodec = SrsVideoCodecIdDisabled; + + // Now that we know the latest video codec in stream, use it. + if (latest_vcodec_ != SrsVideoCodecIdForbidden) + default_vcodec = latest_vcodec_; + + // new segment. + SrsHlsSegment *seg = new SrsHlsSegment(context, default_acodec, default_vcodec, writer); + seg->sequence_no = _sequence_no++; + seg->set_path(hls_path + "/" + req->app + "/" + ts_url); + seg->uri = ts_url; + seg->set_sequence_header(discon); + + seg->append(0); + seg->append(ts_duration * 1000); + + segments->append(seg); + } else { + // Segment already exists, just increment sequence number to maintain consistency + _sequence_no++; + } + + discon = false; + } + + return err; +} + +bool SrsHlsMuxer::segment_exists(const std::string &ts_url) +{ + for (int i = 0; i < segments->size(); i++) { + SrsHlsSegment *existing_seg = dynamic_cast(segments->at(i)); + if (existing_seg && existing_seg->uri == ts_url) { + return true; + } + } + return false; +} + srs_error_t SrsHlsMuxer::segment_open() { srs_error_t err = srs_success; @@ -1822,8 +1968,9 @@ srs_error_t SrsHlsController::on_publish(SrsRequest *req) string hls_key_file_path = _srs_config->get_hls_key_file_path(vhost); string hls_key_url = _srs_config->get_hls_key_url(vhost); - // TODO: FIXME: support load exists m3u8, to continue publish stream. + // TODO: FIXME: support load exists m3u8, to recover publish stream. // for the HLS donot requires the EXT-X-MEDIA-SEQUENCE be monotonically increase. + bool recover = _srs_config->get_hls_recover(vhost); if ((err = muxer->on_publish(req)) != srs_success) { return srs_error_wrap(err, "muxer publish"); @@ -1835,6 +1982,10 @@ srs_error_t SrsHlsController::on_publish(SrsRequest *req) return srs_error_wrap(err, "hls: update config"); } + if (recover && (err = muxer->recover_hls()) != srs_success) { + return srs_error_wrap(err, "hls: recover stream"); + } + if ((err = muxer->segment_open()) != srs_success) { return srs_error_wrap(err, "hls: segment open"); } diff --git a/trunk/src/app/srs_app_hls.hpp b/trunk/src/app/srs_app_hls.hpp index e65df376e..8b26dbffa 100644 --- a/trunk/src/app/srs_app_hls.hpp +++ b/trunk/src/app/srs_app_hls.hpp @@ -294,6 +294,12 @@ private: virtual srs_error_t write_hls_key(); virtual srs_error_t refresh_m3u8(); virtual srs_error_t _refresh_m3u8(std::string m3u8_file); + // Check if a segment with the given URI already exists in the segments list. + virtual bool segment_exists(const std::string &ts_url); + +public: + // HLS recover mode. + srs_error_t recover_hls(); }; // Mux the HLS stream(m3u8 and m4s files). diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp index 1b8835518..8d9488e31 100644 --- a/trunk/src/core/srs_core_version7.hpp +++ b/trunk/src/core/srs_core_version7.hpp @@ -9,6 +9,6 @@ #define VERSION_MAJOR 7 #define VERSION_MINOR 0 -#define VERSION_REVISION 56 +#define VERSION_REVISION 57 #endif \ No newline at end of file