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.](656e4e296d)

---------

Co-authored-by: Haibo Chen <495810242@qq.com>
Co-authored-by: winlin <winlinvip@gmail.com>
Co-authored-by: OSSRS-AI <winlinam@gmail.com>
This commit is contained in:
chundonglinlin 2025-08-20 12:09:54 +08:00 committed by GitHub
parent ebcaef43c6
commit 664e868972
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 200 additions and 13 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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.

View File

@ -17,5 +17,6 @@ vhost __defaultVhost__ {
hls_path ./objs/nginx/html;
hls_fragment 10;
hls_window 60;
hls_recover on;
}
}

View File

@ -7,6 +7,7 @@ The changelog for SRS.
<a name="v7-changes"></a>
## 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)

View File

@ -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)

View File

@ -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);

View File

@ -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.

View File

@ -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<SrsHlsSegment *>(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");
}

View File

@ -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).

View File

@ -9,6 +9,6 @@
#define VERSION_MAJOR 7
#define VERSION_MINOR 0
#define VERSION_REVISION 56
#define VERSION_REVISION 57
#endif