diff --git a/README.md b/README.md index a4f3af865..f59c2ec2f 100755 --- a/README.md +++ b/README.md @@ -566,6 +566,7 @@ Supported operating systems and hardware: ### SRS 2.0 history +* v2.0, 2015-03-31, enhanced hls, support deviation for duration. 2.0.151. * v2.0, 2015-03-30, for [#351](https://github.com/winlinvip/simple-rtmp-server/issues/351), support config the m3u8/ts path for hls. 2.0.149. * v2.0, 2015-03-17, for [#155](https://github.com/winlinvip/simple-rtmp-server/issues/155), osx(darwin) support demo with nginx and ffmpeg. 2.0.143. * v2.0, 2015-03-15, start [2.0release branch](https://github.com/winlinvip/simple-rtmp-server/tree/2.0release), 80773 lines. diff --git a/trunk/conf/full.conf b/trunk/conf/full.conf index 7441cad6f..dddecc20a 100644 --- a/trunk/conf/full.conf +++ b/trunk/conf/full.conf @@ -560,6 +560,11 @@ vhost with-hls.srs.com { # @see https://github.com/winlinvip/simple-rtmp-server/wiki/v2_CN_DeliveryHLS#hls-config # default: [app]/[stream]-[seq].ts hls_ts_file [app]/[stream]-[seq].ts; + # whether use floor for the hls_ts_file path generation. + # if on, use floor(timestamp/hls_fragment) as the variable [timestamp], + # and use enahanced algorithm to calc deviation for segment. + # default: off + hls_ts_floor off; # the hls entry prefix, which is base url of ts url. # if specified, the ts path in m3u8 will be like: # http://your-server/live/livestream-0.ts @@ -588,6 +593,11 @@ vhost with-hls.srs.com { # h264, vn # default: h264 hls_vcodec h264; + + # on_hls, never config in here, should config in http_hooks. + # for the hls http callback, @see http_hooks.on_hls of vhost hooks.callback.srs.com + # @read https://github.com/winlinvip/simple-rtmp-server/wiki/v2_CN_DeliveryHLS#http-callback + # @read https://github.com/winlinvip/simple-rtmp-server/wiki/v2_EN_DeliveryHLS#http-callback } } # the vhost with hls disabled. @@ -722,6 +732,20 @@ vhost hooks.callback.srs.com { # an int value specifies the error code(0 corresponding to success): # 0 on_dvr http://127.0.0.1:8085/api/v1/dvrs http://localhost:8085/api/v1/dvrs; + # when srs reap a ts file of hls, call the hook, + # the request in the POST data string is a object encode by json: + # { + # "action": "on_hls", + # "client_id": 1985, + # "ip": "192.168.1.10", "vhost": "video.test.com", "app": "live", + # "stream": "livestream", + # "cwd": "/usr/local/srs", + # "file": "./objs/nginx/html/live/livestream.1420254068776-100.ts" + # } + # if valid, the hook must return HTTP code 200(Stauts OK) and response + # an int value specifies the error code(0 corresponding to success): + # 0 + on_hls http://127.0.0.1:8085/api/v1/hls http://localhost:8085/api/v1/hls; } } diff --git a/trunk/src/app/srs_app_config.cpp b/trunk/src/app/srs_app_config.cpp index 9f3c9d4a0..34ca326e2 100644 --- a/trunk/src/app/srs_app_config.cpp +++ b/trunk/src/app/srs_app_config.cpp @@ -1482,7 +1482,7 @@ int SrsConfig::check_config() string m = conf->at(j)->name.c_str(); 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_m3u8_file" && m != "hls_ts_file" && m != "hls_ts_floor" ) { ret = ERROR_SYSTEM_CONFIG_INVALID; srs_error("unsupported vhost hls directive %s, ret=%d", m.c_str(), ret); @@ -3207,6 +3207,23 @@ string SrsConfig::get_hls_ts_file(string vhost) return conf->arg0(); } +bool SrsConfig::get_hls_ts_floor(string vhost) +{ + SrsConfDirective* hls = get_hls(vhost); + + if (!hls) { + return SRS_CONF_DEFAULT_HLS_TS_FLOOR; + } + + SrsConfDirective* conf = hls->get("hls_ts_floor"); + + if (!conf || conf->arg0() != "on") { + return SRS_CONF_DEFAULT_HLS_TS_FLOOR; + } + + return true; +} + double SrsConfig::get_hls_fragment(string vhost) { SrsConfDirective* hls = get_hls(vhost); diff --git a/trunk/src/app/srs_app_config.hpp b/trunk/src/app/srs_app_config.hpp index 631498539..0ab5717e0 100644 --- a/trunk/src/app/srs_app_config.hpp +++ b/trunk/src/app/srs_app_config.hpp @@ -48,6 +48,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #define SRS_CONF_DEFAULT_HLS_PATH "./objs/nginx/html" #define SRS_CONF_DEFAULT_HLS_M3U8_FILE "[app]/[stream].m3u8" #define SRS_CONF_DEFAULT_HLS_TS_FILE "[app]/[stream]-[seq].ts" +#define SRS_CONF_DEFAULT_HLS_TS_FLOOR "off" #define SRS_CONF_DEFAULT_HLS_FRAGMENT 10 #define SRS_CONF_DEFAULT_HLS_TD_RATIO 1.5 #define SRS_CONF_DEFAULT_HLS_AOF_RATIO 2.0 @@ -884,6 +885,10 @@ public: * get the HLS ts file path template. */ virtual std::string get_hls_ts_file(std::string vhost); + /** + * whether enable the floor(timestamp/hls_fragment) for variable timestamp. + */ + virtual bool get_hls_ts_floor(std::string vhost); /** * get the hls fragment time, in seconds. */ diff --git a/trunk/src/app/srs_app_hls.cpp b/trunk/src/app/srs_app_hls.cpp index 8a2e7336d..6f3a76c71 100644 --- a/trunk/src/app/srs_app_hls.cpp +++ b/trunk/src/app/srs_app_hls.cpp @@ -58,6 +58,11 @@ using namespace std; // drop the segment when duration of ts too small. #define SRS_AUTO_HLS_SEGMENT_MIN_DURATION_MS 100 +// startup piece, the first piece, fragment percent to reap. +#define SRS_HLS_FLOOR_STARTUP_PERCENT 0.1 +// fragment plus the deviation percent. +#define SRS_HLS_FLOOR_REAP_PERCENT 0.2 + ISrsHlsHandler::ISrsHlsHandler() { } @@ -170,6 +175,9 @@ SrsHlsMuxer::SrsHlsMuxer() handler = NULL; hls_fragment = hls_window = 0; hls_aof_ratio = 1.0; + hls_fragment_deviation = 0; + previous_floor_ts = 0; + hls_ts_floor = false; target_duration = 0; _sequence_no = 0; current = NULL; @@ -205,8 +213,24 @@ int SrsHlsMuxer::sequence_no() return _sequence_no; } +string SrsHlsMuxer::ts_url() +{ + return current? current->uri:""; +} + +double SrsHlsMuxer::duration() +{ + return current? current->duration:0; +} + +double SrsHlsMuxer::deviation() +{ + return hls_fragment_deviation; +} + int SrsHlsMuxer::update_config(SrsRequest* r, string entry_prefix, - string path, string m3u8_file, string ts_file, int fragment, int window, double aof_ratio + string path, string m3u8_file, string ts_file, double fragment, double window, + bool ts_floor, double aof_ratio ) { int ret = ERROR_SUCCESS; @@ -218,7 +242,12 @@ int SrsHlsMuxer::update_config(SrsRequest* r, string entry_prefix, hls_ts_file = ts_file; hls_fragment = fragment; hls_aof_ratio = aof_ratio; + hls_ts_floor = ts_floor; + previous_floor_ts = 0; hls_window = window; + // for the first time, we set to -N% of fragment, + // that is, the first piece always smaller. + hls_fragment_deviation = -1 * (fragment * SRS_HLS_FLOOR_STARTUP_PERCENT); // generate the m3u8 dir and path. m3u8 = path + "/" + m3u8_file; @@ -301,6 +330,19 @@ int SrsHlsMuxer::segment_open(int64_t segment_start_dts) // 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) { + int64_t floor_ts = (int64_t)(srs_get_system_time_ms() / (1000 * hls_fragment)); + std::stringstream ts_floor; + ts_floor << floor_ts; + ts_file = srs_string_replace(ts_file, "[timestamp]", ts_floor.str()); + + // dup/jmp detect for ts in floor mode. + if (previous_floor_ts && previous_floor_ts != floor_ts - 1) { + srs_warn("hls: dup or jmp for floor ts, previous=%"PRId64", current=%"PRId64", ts=%s, deviation=%.2f", + previous_floor_ts, floor_ts, ts_file.c_str(), hls_fragment_deviation); + } + previous_floor_ts = floor_ts; + } ts_file = srs_path_build_timestamp(ts_file); if (true) { std::stringstream ss; @@ -308,6 +350,7 @@ int SrsHlsMuxer::segment_open(int64_t segment_start_dts) ts_file = srs_string_replace(ts_file, "[seq]", ss.str()); } current->full_path = hls_path + "/" + ts_file; + srs_info("hls: generate ts path %s, tmpl=%s, floor=%d", ts_file.c_str(), hls_ts_file.c_str(), hls_ts_floor); // the ts url, relative or absolute url. std::string ts_url = current->full_path; @@ -336,7 +379,7 @@ int SrsHlsMuxer::segment_open(int64_t segment_start_dts) srs_error("open hls muxer failed. ret=%d", ret); return ret; } - srs_info("open HLS muxer success. path=%s, tmp=%s", + srs_info("open HLS muxer success. path=%s, tmp=%s", current->full_path.c_str(), tmp_file.c_str()); // set the segment muxer audio codec. @@ -363,14 +406,22 @@ int SrsHlsMuxer::on_sequence_header() bool SrsHlsMuxer::is_segment_overflow() { srs_assert(current); - return current->duration >= hls_fragment; + + // use N% deviation, to smoother. + double deviation = hls_ts_floor? SRS_HLS_FLOOR_REAP_PERCENT * hls_fragment_deviation : 0.0; + + return current->duration >= hls_fragment + deviation; } bool SrsHlsMuxer::is_segment_absolutely_overflow() { // @see https://github.com/winlinvip/simple-rtmp-server/issues/151#issuecomment-83553950 srs_assert(current); - return current->duration >= hls_aof_ratio * hls_fragment; + + // use N% deviation, to smoother. + double deviation = hls_ts_floor? SRS_HLS_FLOOR_REAP_PERCENT * hls_fragment_deviation : 0.0; + + return current->duration >= hls_aof_ratio * (hls_fragment + deviation); } int SrsHlsMuxer::update_acodec(SrsCodecAudio ac) @@ -457,10 +508,15 @@ int SrsHlsMuxer::segment_close(string log_desc) // valid, add to segments if segment duration is ok if (current->duration * 1000 >= SRS_AUTO_HLS_SEGMENT_MIN_DURATION_MS) { segments.push_back(current); + + // when reap ts, adjust the deviation. + if (hls_ts_floor) { + hls_fragment_deviation += (double)(hls_fragment - current->duration); + } - srs_info("%s reap ts segment, sequence_no=%d, uri=%s, duration=%.2f, start=%"PRId64"", + srs_info("%s reap ts segment, sequence_no=%d, uri=%s, duration=%.2f, start=%"PRId64", deviation=%.2f", log_desc.c_str(), current->sequence_no, current->uri.c_str(), current->duration, - current->segment_start_dts); + current->segment_start_dts, hls_fragment_deviation); // notify handler for update ts. srs_assert(current->writer); @@ -505,7 +561,7 @@ int SrsHlsMuxer::segment_close(string log_desc) // shrink the segments. double duration = 0; int remove_index = -1; - for (int i = segments.size() - 1; i >= 0; i--) { + for (int i = (int)segments.size() - 1; i >= 0; i--) { SrsHlsSegment* segment = segments[i]; duration += segment->duration; @@ -662,8 +718,8 @@ int SrsHlsCache::on_publish(SrsHlsMuxer* muxer, SrsRequest* req, int64_t segment std::string stream = req->stream; std::string app = req->app; - int hls_fragment = (int)_srs_config->get_hls_fragment(vhost); - int hls_window = (int)_srs_config->get_hls_window(vhost); + double hls_fragment = _srs_config->get_hls_fragment(vhost); + double hls_window = _srs_config->get_hls_window(vhost); // get the hls m3u8 ts list entry prefix config std::string entry_prefix = _srs_config->get_hls_entry_prefix(vhost); @@ -673,12 +729,16 @@ int SrsHlsCache::on_publish(SrsHlsMuxer* muxer, SrsRequest* req, int64_t segment std::string ts_file = _srs_config->get_hls_ts_file(vhost); // the audio overflow, for pure audio to reap segment. double hls_aof_ratio = _srs_config->get_hls_aof_ratio(vhost); + // whether use floor(timestamp/hls_fragment) for variable timestamp + bool ts_floor = _srs_config->get_hls_ts_floor(vhost); // TODO: FIXME: support load exists m3u8, to continue publish stream. // for the HLS donot requires the EXT-X-MEDIA-SEQUENCE be monotonically increase. // open muxer - if ((ret = muxer->update_config(req, entry_prefix, path, m3u8_file, ts_file, hls_fragment, hls_window, hls_aof_ratio)) != ERROR_SUCCESS) { + if ((ret = muxer->update_config(req, entry_prefix, + path, m3u8_file, ts_file, hls_fragment, hls_window, ts_floor, hls_aof_ratio)) != ERROR_SUCCESS + ) { srs_error("m3u8 muxer update config failed. ret=%d", ret); return ret; } @@ -687,6 +747,9 @@ int SrsHlsCache::on_publish(SrsHlsMuxer* muxer, SrsRequest* req, int64_t segment srs_error("m3u8 muxer open segment failed. ret=%d", ret); return ret; } + srs_trace("hls: win=%.2f, frag=%.2f, prefix=%s, path=%s, m3u8=%s, ts=%s, aof=%.2f, floor=%d", + hls_window, hls_fragment, entry_prefix.c_str(), path.c_str(), m3u8_file.c_str(), + ts_file.c_str(), hls_aof_ratio, ts_floor); return ret; } @@ -1057,9 +1120,9 @@ void SrsHls::hls_show_mux_log() // the run time is not equals to stream time, // @see: https://github.com/winlinvip/simple-rtmp-server/issues/81#issuecomment-48100994 // it's ok. - srs_trace("-> "SRS_CONSTS_LOG_HLS - " time=%"PRId64", stream dts=%"PRId64"(%"PRId64"ms), sequence_no=%d", - pprint->age(), stream_dts, stream_dts / 90, muxer->sequence_no()); + srs_trace("-> "SRS_CONSTS_LOG_HLS" time=%"PRId64", stream dts=%"PRId64"(%"PRId64"ms), sequence_no=%d, ts=%s, duration=%.2f, deviation=%.2f", + pprint->age(), stream_dts, stream_dts / 90, muxer->sequence_no(), muxer->ts_url().c_str(), + muxer->duration(), muxer->deviation()); } } diff --git a/trunk/src/app/srs_app_hls.hpp b/trunk/src/app/srs_app_hls.hpp index 916b0cc2b..05f8ce3ee 100644 --- a/trunk/src/app/srs_app_hls.hpp +++ b/trunk/src/app/srs_app_hls.hpp @@ -172,8 +172,17 @@ private: std::string hls_ts_file; std::string m3u8_dir; double hls_aof_ratio; - int hls_fragment; - int hls_window; + double hls_fragment; + double hls_window; +private: + // whether use floor algorithm for timestamp. + bool hls_ts_floor; + // the deviation in seconds to adjust the fragment to be more + // bigger or smaller. + double hls_fragment_deviation; + // the previous reap floor timestamp, + // used to detect the dup or jmp or ts. + int64_t previous_floor_ts; private: int _sequence_no; int target_duration; @@ -203,6 +212,9 @@ public: virtual ~SrsHlsMuxer(); public: virtual int sequence_no(); + virtual std::string ts_url(); + virtual double duration(); + virtual double deviation(); public: /** * initialize the hls muxer. @@ -213,7 +225,7 @@ public: */ virtual int update_config(SrsRequest* r, std::string entry_prefix, std::string path, std::string m3u8_file, std::string ts_file, - int fragment, int window, double aof_ratio); + double fragment, double window, bool ts_floor, double aof_ratio); /** * open a new segment(a new ts file), * @param segment_start_dts use to calc the segment duration,