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