Skip to content

Commit

Permalink
http://127.0.0.1/live/test.m3u8 自动生成自适应hls索引文件:
Browse files Browse the repository at this point in the history
- webrtc simulicast生成多流索引文件
- 根据配置文件生成Hls自适应流索引文件
- 更新说明文件
  • Loading branch information
mtdxc authored and cqm committed Apr 8, 2024
1 parent 7791f34 commit 83717f1
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 4 deletions.
9 changes: 9 additions & 0 deletions conf/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/Common/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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

Expand Down
8 changes: 8 additions & 0 deletions src/Common/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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代理相关配置///////////
Expand Down
91 changes: 90 additions & 1 deletion src/Record/HlsMediaSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

#include "HlsMediaSource.h"
#include "Common/config.h"

#include "Util/File.h"
using namespace toolkit;

namespace mediakit {
Expand Down Expand Up @@ -71,6 +71,10 @@ void HlsCookieData::setMediaSource(const HlsMediaSource::Ptr &src) {
_src = src;
}

HlsMediaSource::~HlsMediaSource() {
removeIndexFile();
}

HlsMediaSource::Ptr HlsCookieData::getMediaSource() const {
return _src.lock();
}
Expand All @@ -88,6 +92,40 @@ void HlsMediaSource::setIndexFile(std::string index_file)
};
_ring = std::make_shared<RingType>(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<VideoTrack>(getTrack(TrackVideo));
// 有视频,且非转码的文件
if (transcode_size && indexCount > 0 && video && -1 == getMediaTuple().stream.find('_')) {
std::list<M3u8Item> 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索引文件内容
Expand All @@ -111,4 +149,55 @@ void HlsMediaSource::getIndexFile(std::function<void(const std::string& str)> 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<M3u8Item>& 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
10 changes: 10 additions & 0 deletions src/Record/HlsMediaSource.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class HlsMediaSource : public MediaSource {
using Ptr = std::shared_ptr<HlsMediaSource>;

HlsMediaSource(const std::string &schema, const MediaTuple &tuple) : MediaSource(schema, tuple) {}
~HlsMediaSource() override;

/**
* 获取媒体源的环形缓冲
Expand All @@ -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<M3u8Item>& substeam, const std::string& hls_save_path = "");

/**
* 异步获取m3u8文件
Expand Down
61 changes: 61 additions & 0 deletions trancode.md
Original file line number Diff line number Diff line change
@@ -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!
28 changes: 26 additions & 2 deletions webrtc/WebRtcPusher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#include "WebRtcPusher.h"
#include "Common/config.h"
#include "Rtsp/RtspMediaSourceImp.h"

#include "Record/HlsMediaSource.h"
using namespace std;

namespace mediakit {
Expand All @@ -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;
Expand Down Expand Up @@ -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<std::recursive_mutex> 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<WebRtcPusher>(shared_from_this()));
src = src_imp;
}
src->onWrite(std::move(rtp), false);

#if defined(ENABLE_HLS)
if (stream.length() && _option.enable_hls) {
std::list<HlsMediaSource::M3u8Item> 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<VideoTrack>(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<HlsMediaSource>(HLS_SCHEMA, _push_src->getMediaTuple());
_hls_index->makeM3u8Index(files, _option.hls_save_path);
}
#endif
}
}

Expand Down
4 changes: 3 additions & 1 deletion webrtc/WebRtcPusher.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#include "Rtsp/RtspMediaSource.h"

namespace mediakit {

class HlsMediaSource;
class WebRtcPusher : public WebRtcTransportImp, public MediaSourceEvent {
public:
using Ptr = std::shared_ptr<WebRtcPusher>;
Expand Down Expand Up @@ -64,6 +64,8 @@ class WebRtcPusher : public WebRtcTransportImp, public MediaSourceEvent {
RtspMediaSource::Ptr _push_src;
//推流所有权
std::shared_ptr<void> _push_src_ownership;
std::shared_ptr<HlsMediaSource> _hls_index;
ProtocolOption _option;
//推流的rtsp源,支持simulcast
std::recursive_mutex _mtx;
std::unordered_map<std::string/*rid*/, RtspMediaSource::Ptr> _push_src_sim;
Expand Down

0 comments on commit 83717f1

Please sign in to comment.