diff --git a/conf/config.ini b/conf/config.ini index cad6d64579..0272f502e7 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -166,6 +166,15 @@ segKeep=0 #如果设置为1,则第一个切片长度强制设置为1个GOP。当GOP小于segDur,可以提高首屏速度 fastRegister=0 +# HLS自适应流配置,由于此功能是基于视频转码来实现的,因此也必须打开此功能前,也必须设置transcode_size=1 +# 索引文件个数 +# 之前HLS URL逻辑为 http://vhost/app/stream/hls.m3u8; +# 当kIndexCount>0后,会在http://vhost/app/stream.m3u8下生成支持多流切换的m3u8索引文件 +# 具体生成多少个流,由baseWidth和indexCount决定 +indexCount=2 +# 基准宽度, 大于此宽度的Hls流会生成indexCount个hls子流 +baseWidth=640 + [hook] #是否启用hook事件,启用后,推拉流都将进行鉴权 enable=0 diff --git a/src/Common/config.cpp b/src/Common/config.cpp index 2cc2ee720e..6be5ce3ad3 100644 --- a/src/Common/config.cpp +++ b/src/Common/config.cpp @@ -333,6 +333,8 @@ const string kFileBufSize = HLS_FIELD "fileBufSize"; const string kBroadcastRecordTs = HLS_FIELD "broadcastRecordTs"; const string kDeleteDelaySec = HLS_FIELD "deleteDelaySec"; const string kFastRegister = HLS_FIELD "fastRegister"; +const string kIndexCount = HLS_FIELD "indexCount"; +const string kBaseWidth = HLS_FIELD "baseWidth"; static onceToken token([]() { mINI::Instance()[kSegmentDuration] = 2; @@ -344,6 +346,8 @@ static onceToken token([]() { mINI::Instance()[kBroadcastRecordTs] = false; mINI::Instance()[kDeleteDelaySec] = 10; mINI::Instance()[kFastRegister] = false; + mINI::Instance()[kIndexCount] = 2; + mINI::Instance()[kBaseWidth] = 640; }); } // namespace Hls diff --git a/src/Common/config.h b/src/Common/config.h index 2b99a5e60a..93aa9d55b7 100644 --- a/src/Common/config.h +++ b/src/Common/config.h @@ -386,6 +386,14 @@ extern const std::string kBroadcastRecordTs; extern const std::string kDeleteDelaySec; // 如果设置为1,则第一个切片长度强制设置为1个GOP extern const std::string kFastRegister; +/* 索引文件个数 +之前HLS URL逻辑为 http://vhost/app/stream/hls.m3u8; +当kIndexCount>0后,会在http://vhost/app/stream.m3u8下生成支持多流切换的m3u8索引文件 +具体生成多少个流,由baseWidth和indexCount决定 +*/ +extern const std::string kIndexCount; +// 基准宽度, 大于此宽度的Hls流会生成indexCount个hls子流 +extern const std::string kBaseWidth; } // namespace Hls ////////////Rtp代理相关配置/////////// diff --git a/src/Record/HlsMediaSource.cpp b/src/Record/HlsMediaSource.cpp index 382e277946..5edd748b24 100644 --- a/src/Record/HlsMediaSource.cpp +++ b/src/Record/HlsMediaSource.cpp @@ -10,7 +10,7 @@ #include "HlsMediaSource.h" #include "Common/config.h" - +#include "Util/File.h" using namespace toolkit; namespace mediakit { @@ -71,6 +71,10 @@ void HlsCookieData::setMediaSource(const HlsMediaSource::Ptr &src) { _src = src; } +HlsMediaSource::~HlsMediaSource() { + removeIndexFile(); +} + HlsMediaSource::Ptr HlsCookieData::getMediaSource() const { return _src.lock(); } @@ -88,6 +92,40 @@ void HlsMediaSource::setIndexFile(std::string index_file) }; _ring = std::make_shared(0, std::move(lam)); regist(); + + GET_CONFIG(bool, transcode_size, General::kTranscodeSize); + GET_CONFIG(uint32_t, indexCount, Hls::kIndexCount); + GET_CONFIG(uint32_t, baseWidth, Hls::kBaseWidth); + auto video = std::dynamic_pointer_cast(getTrack(TrackVideo)); + // 有视频,且非转码的文件 + if (transcode_size && indexCount > 0 && video && -1 == getMediaTuple().stream.find('_')) { + std::list lst; + // 增加原始流 + M3u8Item mi; + mi.id = getMediaTuple().stream; + mi.width = video->getVideoWidth(); + mi.height = video->getVideoHeight(); + lst.push_back(mi); + + int step = (video->getVideoWidth() - baseWidth) / indexCount; + if (step < 100) step = 100; + int width = baseWidth; + for (int i = 1; i< indexCount; i++) { + if (width >= video->getVideoWidth()) { + break; + } + // 生成由转码生成的缩小的hls文件 + mi.id = getMediaTuple().stream + "_" + std::to_string(width); + mi.width = width; + if (mi.width % 2) mi.width++; + // 保持长宽比,并确保偶数 + mi.height = width * video->getVideoHeight() / video->getVideoWidth(); + if (mi.height % 2) mi.height++; + lst.push_back(mi); + width += step; + } + makeM3u8Index(lst); + } } //赋值m3u8索引文件内容 @@ -111,4 +149,55 @@ void HlsMediaSource::getIndexFile(std::function cb _list_cb.emplace_back(std::move(cb)); } +void HlsMediaSource::removeIndexFile() +{ + if (_index_m3u8.length()) { + GET_CONFIG(uint32_t, delay, Hls::kDeleteDelaySec); + if (!delay) { + File::delete_file(_index_m3u8.data()); + } + else { + auto path_prefix = _index_m3u8; + EventPoller::getCurrentPoller()->doDelayTask(delay * 1000, [path_prefix]() { + File::delete_file(path_prefix.data()); + return 0; + }); + } + _index_m3u8.clear(); + } +} + +void HlsMediaSource::makeM3u8Index(const std::list& substeams, const std::string& hls_save_path) +{ + auto dstPath = Recorder::getRecordPath(Recorder::type_hls, getMediaTuple(), hls_save_path); + toolkit::replace(dstPath, "/hls", ""); + InfoL << "refresh index m3u8: " << dstPath; + FILE* fp = toolkit::File::create_file(dstPath.c_str(), "wb"); + if (fp) { + _index_m3u8 = dstPath; + /* + #EXTM3U + #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=358400,RESOLUTION=1280x720 + 11.m3u8 + #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=972800[,RESOLUTION=1280x720] + 22.m3u8 + */ + const int prog_id = 1; + fprintf(fp, "#EXTM3U\n"); + for (auto it : substeams) + { + auto& mi = it; + int bitrate = mi.bitrate; + if (!bitrate) { + bitrate = mi.width * mi.height; + InfoL << mi.id << " guess bitrate " << bitrate; + } + fprintf(fp, "#EXT-X-STREAM-INF:PROGRAM-ID=%d,BANDWIDTH=%d,RESOLUTION=%dx%d\n", prog_id, + bitrate, mi.width, mi.height); + fprintf(fp, "%s/hls.m3u8\n", mi.id.c_str()); + } + fclose(fp); + } +} + } // namespace mediakit diff --git a/src/Record/HlsMediaSource.h b/src/Record/HlsMediaSource.h index f9c4c56de5..44140dafb2 100644 --- a/src/Record/HlsMediaSource.h +++ b/src/Record/HlsMediaSource.h @@ -26,6 +26,7 @@ class HlsMediaSource : public MediaSource { using Ptr = std::shared_ptr; HlsMediaSource(const std::string &schema, const MediaTuple &tuple) : MediaSource(schema, tuple) {} + ~HlsMediaSource() override; /** * 获取媒体源的环形缓冲 @@ -41,6 +42,15 @@ class HlsMediaSource : public MediaSource { * 设置或清空m3u8索引文件内容 */ void setIndexFile(std::string index_file); + struct M3u8Item { + std::string id; + int width = 0; + int height = 0; + int bitrate = 0; + }; + std::string _index_m3u8; + void removeIndexFile(); + void makeM3u8Index(const std::list& substeam, const std::string& hls_save_path = ""); /** * 异步获取m3u8文件 diff --git a/trancode.md b/trancode.md new file mode 100644 index 0000000000..304f87a3f8 --- /dev/null +++ b/trancode.md @@ -0,0 +1,61 @@ +# 使用场景 +- webrtc音频转码: 标准webrtc不支持AAC音频,而标准RTMP也只支持AAC音频,当RTMP推流遇到webrtc拉流时,会听不到声音; +- RTMP音频转码: 标准RTMP默认只支持AAC格式音频和H264视频,当收到zlm支持的其他媒体格式时,也会导致没声音或视频; +- 高清视频的接收:假如用户推了一路1080p的视频流(rtmp://vhost/app/stream),播放器接收和解码这路流是需要一定的带宽和CPU的, +当机器性能不够时,可采用订阅rtmp://vhost/app/stream_720方式,来获取720p的小视频流,服务器会在必要时(StreamNotFound)启动转码,并在不需要时(StreamNoReader)停止转码; +- HLS自适应流传输:前面的机制需要手工实现客户端来进行码流切换,但HLS有个hls adaptive streaming技术,可做到自动切换, +注意HLS自适应流地址为 http://vhost/app/stream.m3u8, 而非旧的 http://vhost/app/stream/hls.m3u8,旧地址只能获取到某一特定分辨率的流 + +# 相对于ffmpeg推拉流转码的优缺点 +之前zlm推荐的转码场景是通过启动ffmpeg进程进行拉流,后经转码后,并重新推向zlm中,这有个天然的好处是 +- 可利用多台机器资源来实现并发转码 +- 可应用FFmpeg中各种各样的滤镜 +这是本方案所不及的;但进程内转码的方案也有如下优点: +- 省去启动多个进程方式,能节约点资源占用 +- 可与MediaServer实现更紧密和结合,如实现无人观看不转码等功能; +- 能通过开发实现复杂的二次开发,这些功能可能无法很简单地通过FFmpeg命令行来实现; +当然,转码分支,也仍支持原有的推拉实现,用户可根据自身场景选择最合适的实现; + +# 如何编译 +## FFMPEG +转码底层使用FFMPEG来实现,需要打开FFMPEG, 即编译时必须指定 -DENABLE_FFMPEG=1, 当前已知支持FFMPEG 4.x 5.x 和 6.0, + 在ubuntu中可通过以下指令来安装: + ``` + apt-get install libavcodec-dev libavutil-dev libswscale-dev libresample-dev + ``` + 由于 ffmpeg 内置的opus编码器帧大小比较小2ms,建议自己编译ffmpeg时打开libopus集成 + +## WEBRTC可选 +此外转码分支最早用于解决webrtc播放AAC音频没声音的问题,因此一般也会同时开启WEBRTC功能, 即-DENABLE_WEBRTC=1, +此时必须先装好libsrtp库, 安装过程详见[wiki](https://github.com/ZLMediaKit/ZLMediaKit/wiki/zlm%E5%90%AF%E7%94%A8webrtc%E7%BC%96%E8%AF%91%E6%8C%87%E5%8D%97) + +# 配置开关 +- 音频转码项可通过audio_transcode配置项来配置,或是hook来打开,默认打开 +- 宽度转码功能可通过transcode_size来配置,默认打开 +- hls自适应流,通过indexCount和baseWidth来配置 + +``` +[protocol] +# 开启音频自动转码功能 +audio_transcode=1 + +[general] +# 转码成opus音频时的比特率 +opusBitrate=64000 +# 转码成AAC音频时的比特率 +aacBitrate=64000 +# 开启指定宽度转码功能 +transcode_size=1 + +[hls] +# HLS自适应流配置,由于此功能是基于视频转码来实现的,因此也必须打开此功能前,也必须设置transcode_size=1 +# 索引文件个数 +# 之前HLS URL逻辑为 http://vhost/app/stream/hls.m3u8; +# 当kIndexCount>0后,会在http://vhost/app/stream.m3u8下生成m3u8的索引文件,用于多流切换 +# 具体生成多少个流,取决于baseWidth和indexCount +indexCount=2 +# 基础宽度, 大于此宽度的Hls流会生成indexCount个hls子流 +baseWidth=640 + +``` +注意如果编译时没启用FFMPEG,这些选项会自动关闭,使用此分支前得先确保启用FFMPEG! diff --git a/webrtc/WebRtcPusher.cpp b/webrtc/WebRtcPusher.cpp index cde0799259..9b0545157c 100644 --- a/webrtc/WebRtcPusher.cpp +++ b/webrtc/WebRtcPusher.cpp @@ -11,7 +11,7 @@ #include "WebRtcPusher.h" #include "Common/config.h" #include "Rtsp/RtspMediaSourceImp.h" - +#include "Record/HlsMediaSource.h" using namespace std; namespace mediakit { @@ -35,6 +35,7 @@ WebRtcPusher::WebRtcPusher(const EventPoller::Ptr &poller, const MediaInfo &info, const ProtocolOption &option) : WebRtcTransportImp(poller) { _media_info = info; + _option = option; _push_src = src; _push_src_ownership = ownership; _continue_push_ms = option.continue_push_ms; @@ -96,17 +97,40 @@ void WebRtcPusher::onRecvRtp(MediaTrack &track, const string &rid, RtpPacket::Pt pr.second->onWrite(rtp, false); } } else { + std::string stream; //视频 std::lock_guard lock(_mtx); auto &src = _push_src_sim[rid]; if (!src) { - const auto& stream = _push_src->getMediaTuple().stream; + stream = _push_src->getMediaTuple().stream; auto src_imp = _push_src->clone(rid.empty() ? stream : stream + '_' + rid); _push_src_sim_ownership[rid] = src_imp->getOwnership(); src_imp->setListener(static_pointer_cast(shared_from_this())); src = src_imp; } src->onWrite(std::move(rtp), false); + +#if defined(ENABLE_HLS) + if (stream.length() && _option.enable_hls) { + std::list files; + for (auto it = _push_src_sim.begin(); it != _push_src_sim.end(); it++) { + RtspMediaSource::Ptr src = it->second; + auto video = std::dynamic_pointer_cast(src->getTrack(TrackVideo)); + if (!video) continue; + + HlsMediaSource::M3u8Item mi; + mi.id = src->getMediaTuple().stream; + mi.bitrate = video->getBitRate(); + mi.width = video->getVideoHeight(); + mi.height = video->getVideoHeight(); + files.push_back(mi); + } + // 生成hls主索引文件 + if (!_hls_index) + _hls_index = std::make_shared(HLS_SCHEMA, _push_src->getMediaTuple()); + _hls_index->makeM3u8Index(files, _option.hls_save_path); + } +#endif } } diff --git a/webrtc/WebRtcPusher.h b/webrtc/WebRtcPusher.h index 19b0460897..e2da175ec8 100644 --- a/webrtc/WebRtcPusher.h +++ b/webrtc/WebRtcPusher.h @@ -15,7 +15,7 @@ #include "Rtsp/RtspMediaSource.h" namespace mediakit { - +class HlsMediaSource; class WebRtcPusher : public WebRtcTransportImp, public MediaSourceEvent { public: using Ptr = std::shared_ptr; @@ -64,6 +64,8 @@ class WebRtcPusher : public WebRtcTransportImp, public MediaSourceEvent { RtspMediaSource::Ptr _push_src; //推流所有权 std::shared_ptr _push_src_ownership; + std::shared_ptr _hls_index; + ProtocolOption _option; //推流的rtsp源,支持simulcast std::recursive_mutex _mtx; std::unordered_map _push_src_sim;