diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 4622097d..c57d7a3e 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -1,7 +1,7 @@ --- name: bug 反馈 about: 反馈 ZLMediaKit 代码本身的 bug -title: "[BUG]: BUG 现象描述" +title: "[BUG] BUG现象描述(必填)" labels: bug assignees: '' diff --git a/.github/ISSUE_TEMPLATE/compile.md b/.github/ISSUE_TEMPLATE/compile.md index 4e052196..434398fa 100644 --- a/.github/ISSUE_TEMPLATE/compile.md +++ b/.github/ISSUE_TEMPLATE/compile.md @@ -1,7 +1,7 @@ --- name: 编译问题反馈 about: 反馈 ZLMediaKit 编译相关的问题 -title: "[编译问题]: " +title: "[编译问题] 编译问题描述(必填)" labels: 编译问题 assignees: '' diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md index 4660cf0c..edfe2487 100644 --- a/.github/ISSUE_TEMPLATE/feature.md +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -1,7 +1,7 @@ --- name: 新增功能请求 about: 请求新增某些新功能或新特性,或者对已有功能的改进 -title: "[功能请求]" +title: "[功能请求] 需求描述(必填)" labels: 意见建议 assignees: '' diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 66d235c7..0e503340 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,7 +1,7 @@ --- name: 技术咨询 about: 使用咨询、技术咨询等 -title: "[技术咨询]" +title: "[技术咨询] 咨询描述(必填)" labels: 技术咨询 assignees: '' @@ -16,4 +16,4 @@ assignees: '' **注意事项** - 技术咨询前请先认真阅读readme, [wiki](https://github.com/xia-chu/ZLMediaKit/wiki),如有必要,您也可以同时搜索已经答复的issue,如果没找到答案才在此提issue -- 技术咨询不属于bug缺陷,建议先star本项目,否则可能会降低答复优先级 +- 技术咨询不属于bug缺陷,要求用户先star(收藏)本项目,否则会直接关闭issue diff --git a/3rdpart/ZLToolKit b/3rdpart/ZLToolKit index aea48a14..ca26e43a 160000 --- a/3rdpart/ZLToolKit +++ b/3rdpart/ZLToolKit @@ -1 +1 @@ -Subproject commit aea48a14f9619d292789b6ba66d2922e1ed36e71 +Subproject commit ca26e43a5f62986bb8a007226e0bad148d154abc diff --git a/3rdpart/media-server b/3rdpart/media-server index 539f579d..5aa98846 160000 --- a/3rdpart/media-server +++ b/3rdpart/media-server @@ -1 +1 @@ -Subproject commit 539f579d59b39b386d8f2d3b59df8f56f9946025 +Subproject commit 5aa9884660df1c193d730a90835af36ee411668c diff --git a/AUTHORS b/AUTHORS index 6941936e..77f7ea9f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -66,4 +66,8 @@ WuPeng [KevinZang](https://github.com/ZSC714725) [gongluck](https://github.com/gongluck) [a-ucontrol](https://github.com/a-ucontrol) -[TalusL](https://github.com/TalusL) \ No newline at end of file +[TalusL](https://github.com/TalusL) +[ahaooahaz](https://github.com/AHAOAHA) +[TempoTian](https://github.com/TempoTian) +[Derek Liu](https://github.com/yjkhtddx) +[ljx0305](https://github.com/ljx0305) \ No newline at end of file diff --git a/README.md b/README.md index 28ab9b45..6a467d01 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ bash build_docker_images.sh ## 联系方式 - 邮箱:<1213642868@qq.com>(本项目相关或流媒体相关问题请走issue流程,否则恕不邮件答复) - - QQ群:qq群号在wiki中,请阅读wiki后再加群 + - QQ群:两个qq群已满员(共4000人),后续将不再新建qq群,用户可加入[知识星球](https://t.zsxq.com/0cVcuquPJ)提问以支持本项目。 ## 怎么提问? @@ -295,6 +295,10 @@ bash build_docker_images.sh [gongluck](https://github.com/gongluck) [a-ucontrol](https://github.com/a-ucontrol) [TalusL](https://github.com/TalusL) +[ahaooahaz](https://github.com/AHAOAHA) +[TempoTian](https://github.com/TempoTian) +[Derek Liu](https://github.com/yjkhtddx) +[ljx0305](https://github.com/ljx0305) ## 使用案例 diff --git a/api/include/mk_common.h b/api/include/mk_common.h index e8871949..33a3790e 100755 --- a/api/include/mk_common.h +++ b/api/include/mk_common.h @@ -33,7 +33,7 @@ # endif # endif #elif !defined(GENERATE_EXPORT) -# define API_EXPORT +# define API_EXPORT __attribute__((visibility("default"))) #endif #ifdef __cplusplus diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 2ea229c0..20ce4a88 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -537,7 +537,7 @@ void addStreamProxy(const string &vhost, const string &app, const string &stream return; } //添加拉流代理 - auto player = std::make_shared(vhost, app, stream, option, retry_count >=0 ? retry_count : -1); + auto player = std::make_shared(vhost, app, stream, option, retry_count); s_proxyMap[key] = player; //指定RTP over TCP(播放rtsp时有效) @@ -952,7 +952,7 @@ void installWebApi() { } //添加推流代理 - PusherProxy::Ptr pusher(new PusherProxy(src, retry_count>=0 ? retry_count : -1)); + auto pusher = std::make_shared(src, retry_count); s_proxyPusherMap[key] = pusher; //指定RTP over TCP(播放rtsp时有效) @@ -988,7 +988,7 @@ void installWebApi() { CHECK_SECRET(); CHECK_ARGS("schema", "vhost", "app", "stream", "dst_url"); auto dst_url = allArgs["dst_url"]; - auto retry_count = allArgs["retry_count"].empty()? -1: allArgs["retry_count"].as(); + auto retry_count = allArgs["retry_count"].empty() ? -1 : allArgs["retry_count"].as(); addStreamPusherProxy(allArgs["schema"], allArgs["vhost"], allArgs["app"], @@ -1327,7 +1327,7 @@ void installWebApi() { invoker(200, headerOut, val.toStyledString()); }); }); - + //设置录像流播放速度 api_regist("/index/api/setRecordSpeed", [](API_ARGS_MAP_ASYNC) { CHECK_SECRET(); @@ -1407,7 +1407,7 @@ void installWebApi() { invoker(200, headerOut, val.toStyledString()); }); }); - + // 删除录像文件夹 // http://127.0.0.1/index/api/deleteRecordDirectroy?vhost=__defaultVhost__&app=live&stream=ss&period=2020-01-01 api_regist("/index/api/deleteRecordDirectory", [](API_ARGS_MAP) { @@ -1424,7 +1424,7 @@ void installWebApi() { val["path"] = record_path; val["code"] = result; }); - + //获取录像文件夹列表或mp4文件列表 //http://127.0.0.1/index/api/getMp4RecordFile?vhost=__defaultVhost__&app=live&stream=ss&period=2020-01 api_regist("/index/api/getMp4RecordFile", [](API_ARGS_MAP){ diff --git a/server/WebHook.cpp b/server/WebHook.cpp index b66707f5..3e038c2d 100755 --- a/server/WebHook.cpp +++ b/server/WebHook.cpp @@ -161,7 +161,7 @@ void do_http_hook(const string &url, const ArgsType &body, const function(body)["mediaServerId"] = mediaServerId; - HttpRequester::Ptr requester(new HttpRequester); + auto requester = std::make_shared(); requester->setMethod("POST"); auto bodyStr = to_string(body); requester->setBody(bodyStr); diff --git a/server/main.cpp b/server/main.cpp index 8d3b0353..27f0b422 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -108,7 +108,7 @@ onceToken token1([](){ class CMD_main : public CMD { public: CMD_main() { - _parser.reset(new OptionParser(nullptr)); + _parser = std::make_shared(nullptr); #if !defined(_WIN32) (*_parser) << Option('d',/*该选项简称,如果是\x00则说明无简称*/ diff --git a/src/Http/HlsParser.cpp b/src/Http/HlsParser.cpp index ff9c7856..c7b45e3e 100644 --- a/src/Http/HlsParser.cpp +++ b/src/Http/HlsParser.cpp @@ -98,17 +98,14 @@ bool HlsParser::parse(const string &http_url, const string &m3u8) { continue; } - if (_is_m3u8) { - onParsed(_is_m3u8_inner, _sequence, ts_map); - } - return _is_m3u8; + return _is_m3u8 && onParsed(_is_m3u8_inner, _sequence, ts_map); } bool HlsParser::isM3u8() const { return _is_m3u8; } -bool HlsParser::isLive() const{ +bool HlsParser::isLive() const { return _is_live; } diff --git a/src/Http/HlsParser.h b/src/Http/HlsParser.h index eafaf338..c54fc6e5 100644 --- a/src/Http/HlsParser.h +++ b/src/Http/HlsParser.h @@ -36,8 +36,9 @@ typedef struct{ class HlsParser { public: - HlsParser(){} - ~HlsParser(){} + HlsParser() = default; + ~HlsParser() = default; + bool parse(const std::string &http_url,const std::string &m3u8); /** @@ -79,10 +80,16 @@ public: * 得到总时间 */ float getTotalDuration() const; - + protected: - //解析出ts文件地址回调 - virtual void onParsed(bool is_m3u8_inner,int64_t sequence,const std::map &ts_list) {}; + /** + * 解析m3u8文件回调 + * @param is_m3u8_inner 该m3u8文件中是否包含多个hls地址 + * @param sequence ts序号 + * @param ts_list ts地址列表 + * @return 是否解析成功,返回false时,将导致HlsParser::parse返回false + */ + virtual bool onParsed(bool is_m3u8_inner, int64_t sequence, const std::map &ts_list) = 0; private: bool _is_m3u8 = false; diff --git a/src/Http/HlsPlayer.cpp b/src/Http/HlsPlayer.cpp index ad2cadef..b87826c4 100644 --- a/src/Http/HlsPlayer.cpp +++ b/src/Http/HlsPlayer.cpp @@ -51,7 +51,7 @@ void HlsPlayer::teardown_l(const SockException &ex) { } else { _try_fetch_index_times += 1; shutdown(ex); - WarnL << "重新尝试拉取索引文件[" << _try_fetch_index_times << "]:" << _play_url; + WarnL << "Attempt to pull the m3u8 file again[" << _try_fetch_index_times << "]:" << _play_url; fetchIndexFile(); return; } @@ -118,7 +118,7 @@ void HlsPlayer::fetchSegment() { return; } if (err) { - WarnL << "download ts segment " << url << " failed:" << err.what(); + WarnL << "Download ts segment " << url << " failed:" << err.what(); if (err.getErrCode() == Err_timeout) { strong_self->_timeout_multiple = MAX(strong_self->_timeout_multiple + 1, MAX_TIMEOUT_MULTIPLE); }else{ @@ -147,30 +147,41 @@ void HlsPlayer::fetchSegment() { _http_ts_player->sendRequest(url); } -void HlsPlayer::onParsed(bool is_m3u8_inner, int64_t sequence, const map &ts_map) { +bool HlsPlayer::onParsed(bool is_m3u8_inner, int64_t sequence, const map &ts_map) { if (!is_m3u8_inner) { - //这是ts播放列表 + // 这是ts播放列表 if (_last_sequence == sequence) { - return; + // 如果是重复的ts列表,那么忽略 + // 但是需要注意, 如果当前ts列表为空了, 那么表明直播结束了或者m3u8文件有问题,需要重新拉流 + // 这里的5倍是为了防止m3u8文件有问题导致的无限重试 + if (_last_sequence > 0 && _ts_list.empty() && HlsParser::isLive() + && _wait_index_update_ticker.elapsedTime() > (uint64_t)HlsParser::getTargetDur() * 1000 * 5) { + _wait_index_update_ticker.resetTime(); + WarnL << "Fetch new ts list from m3u8 timeout"; + return false; + } + return true; } + _last_sequence = sequence; + _wait_index_update_ticker.resetTime(); for (auto &pr : ts_map) { auto &ts = pr.second; if (_ts_url_cache.emplace(ts.url).second) { - //该ts未重复 + // 该ts未重复 _ts_list.emplace_back(ts); - //按时间排序 + // 按时间排序 _ts_url_sort.emplace_back(ts.url); } } if (_ts_url_sort.size() > 2 * ts_map.size()) { - //去除防重列表中过多的数据 + // 去除防重列表中过多的数据 _ts_url_cache.erase(_ts_url_sort.front()); _ts_url_sort.pop_front(); } fetchSegment(); } else { - //这是m3u8列表,我们播放最高清的子hls + // 这是m3u8列表,我们播放最高清的子hls if (ts_map.empty()) { throw invalid_argument("empty sub hls list:" + getUrl()); } @@ -184,6 +195,7 @@ void HlsPlayer::onParsed(bool is_m3u8_inner, int64_t sequence, const map(headers)["Content-Type"]); if (content_type.find("application/vnd.apple.mpegurl") != 0 && content_type.find("/x-mpegurl") == _StrPrinter::npos) { - WarnL << "may not a hls video: " << content_type << ", url: " << getUrl(); + WarnL << "May not a hls video: " << content_type << ", url: " << getUrl(); } _m3u8.clear(); } @@ -208,7 +220,7 @@ void HlsPlayer::onResponseCompleted(const SockException &ex) { return; } if (!HlsParser::parse(getUrl(), _m3u8)) { - teardown_l(SockException(Err_other, "parse m3u8 failed:" + _m3u8)); + teardown_l(SockException(Err_other, "parse m3u8 failed:" + _play_url)); return; } if (!_play_result) { diff --git a/src/Http/HlsPlayer.h b/src/Http/HlsPlayer.h index 4970087a..422fe3f5 100644 --- a/src/Http/HlsPlayer.h +++ b/src/Http/HlsPlayer.h @@ -73,11 +73,11 @@ protected: virtual void onPacket(const char *data, size_t len) = 0; private: - void onParsed(bool is_m3u8_inner,int64_t sequence,const map &ts_map) override; - void onResponseHeader(const std::string &status,const HttpHeader &headers) override; - void onResponseBody(const char *buf,size_t size) override; + bool onParsed(bool is_m3u8_inner, int64_t sequence, const map &ts_map) override; + void onResponseHeader(const std::string &status, const HttpHeader &headers) override; + void onResponseBody(const char *buf, size_t size) override; void onResponseCompleted(const toolkit::SockException &e) override; - bool onRedirectUrl(const std::string &url,bool temporary) override; + bool onRedirectUrl(const std::string &url, bool temporary) override; private: void playDelay(); @@ -101,6 +101,7 @@ private: std::string _play_url; toolkit::Timer::Ptr _timer; toolkit::Timer::Ptr _timer_ts; + toolkit::Ticker _wait_index_update_ticker; std::list _ts_list; std::list _ts_url_sort; std::set _ts_url_cache; diff --git a/src/Http/HttpRequester.cpp b/src/Http/HttpRequester.cpp index 669d17fe..ddc73631 100644 --- a/src/Http/HttpRequester.cpp +++ b/src/Http/HttpRequester.cpp @@ -271,7 +271,7 @@ static void sendReport() { } static toolkit::onceToken s_token([]() { - NoticeCenter::Instance().addListener(nullptr, EventPollerPool::kOnStarted, [](EventPollerPool &pool, size_t &size) { + NoticeCenter::Instance().addListener(nullptr, "kBroadcastEventPollerPoolStarted", [](EventPollerPool &pool, size_t &size) { // 第一次汇报在程序启动后5分钟 pool.getPoller()->doDelayTask(5 * 60 * 1000, []() { sendReport(); diff --git a/src/Rtp/RtpSender.cpp b/src/Rtp/RtpSender.cpp index 39a0e907..de840633 100644 --- a/src/Rtp/RtpSender.cpp +++ b/src/Rtp/RtpSender.cpp @@ -246,6 +246,10 @@ void RtpSender::onConnect(){ } bool RtpSender::addTrack(const Track::Ptr &track){ + if (_args.only_audio && track->getTrackType() == TrackVideo) { + // 如果只发送音频则忽略视频 + return false; + } return _interface->addTrack(track); } @@ -265,6 +269,10 @@ void RtpSender::flush() { //此函数在其他线程执行 bool RtpSender::inputFrame(const Frame::Ptr &frame) { + if (_args.only_audio && frame->getTrackType() == TrackVideo) { + // 如果只发送音频则忽略视频 + return false; + } //连接成功后才做实质操作(节省cpu资源) return _is_connect ? _interface->inputFrame(frame) : false; } diff --git a/www/webrtc/index.html b/www/webrtc/index.html index d4eb1776..0ff074f4 100644 --- a/www/webrtc/index.html +++ b/www/webrtc/index.html @@ -18,11 +18,19 @@ -
+
+ 已存在的流列表,点击自动播放: +
    +
  1. 初始演示
  2. +
  3. 每秒自动刷新
  4. +
+
+ +

- +

@@ -252,6 +260,64 @@ } } + + function on_click_to_play(app, stream) { + console.log(`on_click_to_play: ${app}/${stream}`); + var url = `${document.location.protocol}//${window.location.host}/index/api/webrtc?app=${app}&stream=${stream}&type=play`; + document.getElementById('streamUrl').value = url; + start(); + } + function clearStreamList() { + let content = document.getElementById("olstreamlist"); + while (content.hasChildNodes()) { + content.removeChild(content.firstChild); + } + } + function fillStreamList(json) { + clearStreamList(); + if (json.code != 0) { + return; + } + let ss = {}; + for (let o of json.data) { + if (ss[o.app]) { + ss[o.app].add(o.stream); + } else { + let set = new Set(); + set.add(o.steram); + ss[o.app] = set; + } + } + + for (let o in ss) { + let app = o; + for (let s of ss[o]) { + if (s) { + //console.log(app, s); + let content = document.getElementById("olstreamlist"); + let child = `

  • ${app}/${s}
  • `; + content.insertAdjacentHTML("beforeend", child); + } + } + } + } + async function getData(url) { + const response = await fetch(url, { + method: 'GET' + }); + const data = await response.json(); + //console.log(data); + return data; + } + function get_media_list() { + let url = document.location.protocol+"//"+window.location.host+"/index/api/getMediaList?secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc"; + let json = getData(url); + json.then((json)=> fillStreamList(json)); + } + setInterval(() => { + get_media_list(); + }, 1000); +