Merge remote-tracking branch 'upstream/master' into master

This commit is contained in:
monktan
2020-10-09 09:41:43 +08:00
22 changed files with 307 additions and 113 deletions

View File

@@ -13,7 +13,7 @@
## 项目特点 ## 项目特点
- 基于C++11开发避免使用裸指针代码稳定可靠性能优越。 - 基于C++11开发避免使用裸指针代码稳定可靠性能优越。
- 支持多种协议(RTSP/RTMP/HLS/HTTP-FLV/Websocket-FLV/GB28181/HTTP-TS/Websocket-TS/HTTP-fMP4/Websocket-fMP4/MP4),支持协议互转。 - 支持多种协议(RTSP/RTMP/HLS/HTTP-FLV/WebSocket-FLV/GB28181/HTTP-TS/WebSocket-TS/HTTP-fMP4/WebSocket-fMP4/MP4),支持协议互转。
- 使用多路复用/多线程/异步网络IO模式开发并发性能优越支持海量客户端连接。 - 使用多路复用/多线程/异步网络IO模式开发并发性能优越支持海量客户端连接。
- 代码经过长期大量的稳定性、性能测试,已经在线上商用验证已久。 - 代码经过长期大量的稳定性、性能测试,已经在线上商用验证已久。
- 支持linux、macos、ios、android、windows全平台。 - 支持linux、macos、ios、android、windows全平台。
@@ -65,10 +65,12 @@
- TS - TS
- 支持http[s]-ts直播 - 支持http[s]-ts直播
- 支持ws[s]-ts直播 - 支持ws[s]-ts直播
- 支持H264/H265/AAC/G711/OPUS编码
- fMP4 - fMP4
- 支持http[s]-fmp4直播 - 支持http[s]-fmp4直播
- 支持ws[s]-fmp4直播 - 支持ws[s]-fmp4直播
- 支持H264/H265/AAC/G711/OPUS编码
- HTTP[S]与WebSocket - HTTP[S]与WebSocket
- 服务器支持`目录索引生成`,`文件下载`,`表单提交请求` - 服务器支持`目录索引生成`,`文件下载`,`表单提交请求`
@@ -131,26 +133,30 @@ docker run -id -p 1935:1935 -p 8080:80 -p 8554:554 -p 10000:10000 -p 10000:10000
bash build_docker_images.sh bash build_docker_images.sh
``` ```
## 参考案例 ## 合作项目
- [IOS摄像头实时录制,生成rtsp/rtmp/hls/http-flv](https://gitee.com/xia-chu/IOSMedia) - 可视化管理网站
- [IOS rtmp/rtsp播放器视频推流器](https://gitee.com/xia-chu/IOSPlayer) - [一个非常漂亮的可视化后台管理系统](https://github.com/MingZhuLiu/ZLMediaServerManagent)
- [支持linux、windows、mac的rtmp/rtsp播放器](https://github.com/xia-chu/ZLMediaPlayer) - [基于ZLMediaKit主线的管理WEB网站](https://gitee.com/kkkkk5G/MediaServerUI)
- [基于ZLMediaKit分支的管理WEB网站](https://github.com/chenxiaolei/ZLMediaKit_NVR_UI) - [基于ZLMediaKit分支的管理WEB网站](https://github.com/chenxiaolei/ZLMediaKit_NVR_UI)
- [基于ZLMediaKit主线的管理WEB网站](https://gitee.com/kkkkk5G/MediaServerUI)
- [DotNetCore的RESTful客户端](https://github.com/MingZhuLiu/ZLMediaKit.DotNetCore.Sdk)
- [C#版本的Http API与Hook](https://github.com/chengxiaosheng/ZLMediaKit.HttpApi)
- [GB28181-2016网络视频平台](https://github.com/swwheihei/wvp)
- [node-js版本的GB28181平台](https://gitee.com/hfwudao/GB28181_Node_Http)
- [基于C SDK实现的推流客户端](https://github.com/hctym1995/ZLM_ApiDemo)
- [Go实现的海康ehome服务器](https://github.com/tsingeye/FreeEhome)
- 流媒体管理平台
- [功能强大的流媒体控制管理接口平台,支持GB28181](https://github.com/chatop2020/StreamNode)
- [GB28181-2016网络视频平台](https://github.com/swwheihei/wvp)
- [node-js版本的GB28181平台](https://gitee.com/hfwudao/GB28181_Node_Http)
- [Go实现的海康ehome服务器](https://github.com/tsingeye/FreeEhome)
- 客户端
- [基于C SDK实现的推流客户端](https://github.com/hctym1995/ZLM_ApiDemo)
- [C#版本的Http API与Hook](https://github.com/chengxiaosheng/ZLMediaKit.HttpApi)
- [DotNetCore的RESTful客户端](https://github.com/MingZhuLiu/ZLMediaKit.DotNetCore.Sdk)
## 授权协议 ## 授权协议
本项目自有代码使用宽松的MIT协议在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 本项目自有代码使用宽松的MIT协议在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。
但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除;
由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。
在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议。
## 联系方式 ## 联系方式
@@ -166,6 +172,12 @@ bash build_docker_images.sh
- 3、有些问题如果不具备参考性的无需在issue提的可以在qq群提. - 3、有些问题如果不具备参考性的无需在issue提的可以在qq群提.
- 4、QQ私聊一般不接受无偿技术咨询和支持([为什么不提倡QQ私聊](https://github.com/xia-chu/ZLMediaKit/wiki/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%BB%BA%E8%AE%AEQQ%E7%A7%81%E8%81%8A%E5%92%A8%E8%AF%A2%E9%97%AE%E9%A2%98%EF%BC%9F)). - 4、QQ私聊一般不接受无偿技术咨询和支持([为什么不提倡QQ私聊](https://github.com/xia-chu/ZLMediaKit/wiki/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%BB%BA%E8%AE%AEQQ%E7%A7%81%E8%81%8A%E5%92%A8%E8%AF%A2%E9%97%AE%E9%A2%98%EF%BC%9F)).
## 特别感谢
本项目采用了[老陈](https://github.com/ireader) 的 [media-server](https://github.com/ireader/media-server) 库,
本项目的 ts/fmp4/mp4/ps 容器格式的复用解复用都依赖media-server库。在实现本项目诸多功能时老陈多次给予了无私热情关键的帮助
特此对他表示诚挚的感谢!
## 致谢 ## 致谢
感谢以下各位对本项目包括但不限于代码贡献、问题反馈、资金捐赠等各种方式的支持!以下排名不分先后: 感谢以下各位对本项目包括但不限于代码贡献、问题反馈、资金捐赠等各种方式的支持!以下排名不分先后:
@@ -192,11 +204,26 @@ bash build_docker_images.sh
[swwheihei](https://github.com/swwheihei) [swwheihei](https://github.com/swwheihei)
[KKKKK5G](https://gitee.com/kkkkk5G) [KKKKK5G](https://gitee.com/kkkkk5G)
[Zhou Weimin](<zhouweimin@supremind.com>) [Zhou Weimin](<zhouweimin@supremind.com>)
[Jim Jin](https://github.com/jim-king-2000)
[西瓜丶](<392293307@qq.com>)
[MingZhuLiu](https://github.com/MingZhuLiu)
[chengxiaosheng](https://github.com/chengxiaosheng)
[big panda](<2381267071@qq.com>)
[tanningzhong](https://github.com/tanningzhong)
[hctym1995](https://github.com/hctym1995)
## 使用案例
本项目已经得到不少公司和个人开发者的认可,据作者不完全统计,
使用本项目的公司包括知名的互联网巨头、国内排名前列的云服务公司、多家知名的AI独角兽公司
以及一系列中小型公司。使用者可以通过在 [issue](https://github.com/xia-chu/ZLMediaKit/issues/511) 上粘贴公司的大名和相关项目介绍为本项目背书,感谢支持!
## 捐赠 ## 捐赠
您的捐赠将用于支付该项目的一些费用支出以及激励开发者,
欢迎捐赠以便更好的推动项目的发展,谢谢您的支持! 欢迎捐赠以便更好的推动项目的发展,谢谢您的支持!
同时欢迎捐赠公网服务器用于在线展示效果。
[支付宝](https://gitee.com/xia-chu/other/raw/master/IMG_3919.JPG) [支付宝](https://gitee.com/xia-chu/other/raw/master/IMG_3919.JPG)

View File

@@ -52,6 +52,8 @@ mergeWriteMS=0
#会直接影响rtsp/rtmp/hls/mp4/flv等协议的时间戳 #会直接影响rtsp/rtmp/hls/mp4/flv等协议的时间戳
#同协议情况下不影响(例如rtsp/rtmp推流那么播放rtsp/rtmp时不会影响时间戳) #同协议情况下不影响(例如rtsp/rtmp推流那么播放rtsp/rtmp时不会影响时间戳)
modifyStamp=0 modifyStamp=0
#服务器唯一id用于触发hook时区别是哪台服务器
mediaServerId=your_server_id
[hls] [hls]
#hls写文件的buf大小调整参数可以提高文件io性能 #hls写文件的buf大小调整参数可以提高文件io性能

View File

@@ -463,16 +463,6 @@
"value": "rtmp://live.hkstv.hk.lxdns.com/live/hks2", "value": "rtmp://live.hkstv.hk.lxdns.com/live/hks2",
"description": "拉流地址例如rtmp://live.hkstv.hk.lxdns.com/live/hks2" "description": "拉流地址例如rtmp://live.hkstv.hk.lxdns.com/live/hks2"
}, },
{
"key": "enable_rtsp",
"value": "1",
"description": "是否转rtsp"
},
{
"key": "enable_rtmp",
"value": "1",
"description": "是否转rtmp"
},
{ {
"key": "enable_hls", "key": "enable_hls",
"value": null, "value": null,
@@ -555,7 +545,7 @@
}, },
{ {
"key": "dst_url", "key": "dst_url",
"value": "rtmp://10.8.9.115:8554/live/hks2", "value": "rtmp://127.0.0.1/live/hks2",
"description": "FFmpeg rtmp推流地址一般都是推给自己例如rtmp://127.0.0.1/live/stream_form_ffmpeg" "description": "FFmpeg rtmp推流地址一般都是推给自己例如rtmp://127.0.0.1/live/stream_form_ffmpeg"
}, },
{ {
@@ -780,7 +770,7 @@
{ {
"key": "customized_path", "key": "customized_path",
"value": null, "value": null,
"description": "录像保存目录" "description": "录像文件保存自定义根目录,为空则采用配置文件设置"
} }
] ]
} }

View File

@@ -197,8 +197,8 @@ void FFmpegSource::startTimer(int timeout_ms) {
strongSelf->findAsync(0, [&](const MediaSource::Ptr &src) { strongSelf->findAsync(0, [&](const MediaSource::Ptr &src) {
//同步查找流 //同步查找流
if (!src) { if (!src) {
//流不在线,重新拉流 //流不在线,重新拉流, 这里原先是10秒超时实际发现10秒不够改成20秒了
if(strongSelf->_replay_ticker.elapsedTime() > 10 * 1000){ if(strongSelf->_replay_ticker.elapsedTime() > 20 * 1000){
//上次重试时间超过10秒那么再重试FFmpeg拉流 //上次重试时间超过10秒那么再重试FFmpeg拉流
strongSelf->_replay_ticker.resetTime(); strongSelf->_replay_ticker.resetTime();
strongSelf->play(strongSelf->_src_url, strongSelf->_dst_url, timeout_ms, [](const SockException &) {}); strongSelf->play(strongSelf->_src_url, strongSelf->_dst_url, timeout_ms, [](const SockException &) {});
@@ -276,20 +276,27 @@ void FFmpegSnap::makeSnap(const string &play_url, const string &save_path, float
GET_CONFIG(string,ffmpeg_bin,FFmpeg::kBin); GET_CONFIG(string,ffmpeg_bin,FFmpeg::kBin);
GET_CONFIG(string,ffmpeg_snap,FFmpeg::kSnap); GET_CONFIG(string,ffmpeg_snap,FFmpeg::kSnap);
GET_CONFIG(string,ffmpeg_log,FFmpeg::kLog); GET_CONFIG(string,ffmpeg_log,FFmpeg::kLog);
Ticker ticker;
std::shared_ptr<Process> process = std::make_shared<Process>(); WorkThreadPool::Instance().getPoller()->async([timeout_sec, play_url,save_path,cb, ticker](){
auto delayTask = EventPollerPool::Instance().getPoller()->doDelayTask(timeout_sec * 1000,[process,cb](){ auto elapsed_ms = ticker.elapsedTime();
if(process->wait(false)){ if (elapsed_ms > timeout_sec * 1000) {
//FFmpeg进程还在运行超时就关闭它 //超时,后台线程负载太高,当代太久才启动该任务
process->kill(2000); cb(false);
return;
} }
return 0;
});
WorkThreadPool::Instance().getPoller()->async([process,play_url,save_path,delayTask,cb](){
char cmd[1024] = {0}; char cmd[1024] = {0};
snprintf(cmd, sizeof(cmd),ffmpeg_snap.data(),ffmpeg_bin.data(),play_url.data(),save_path.data()); snprintf(cmd, sizeof(cmd),ffmpeg_snap.data(),ffmpeg_bin.data(),play_url.data(),save_path.data());
std::shared_ptr<Process> process = std::make_shared<Process>();
process->run(cmd,ffmpeg_log.empty() ? "" : File::absolutePath("",ffmpeg_log)); process->run(cmd,ffmpeg_log.empty() ? "" : File::absolutePath("",ffmpeg_log));
//定时器延时应该减去后台任务启动的延时
auto delayTask = EventPollerPool::Instance().getPoller()->doDelayTask(timeout_sec * 1000 - elapsed_ms,[process,cb](){
if(process->wait(false)){
//FFmpeg进程还在运行超时就关闭它
process->kill(2000);
}
return 0;
});
//等待FFmpeg进程退出 //等待FFmpeg进程退出
process->wait(true); process->wait(true);
//FFmpeg进程退出了可以取消定时器了 //FFmpeg进程退出了可以取消定时器了

View File

@@ -25,7 +25,7 @@
#include "Util/File.h" #include "Util/File.h"
#include "Util/logger.h" #include "Util/logger.h"
#include "Util/uv_errno.h" #include "Util/uv_errno.h"
#include "Thread/WorkThreadPool.h" #include "Poller/EventPoller.h"
#include "Process.h" #include "Process.h"
using namespace toolkit; using namespace toolkit;
@@ -261,7 +261,7 @@ static void s_kill(pid_t pid, void *handle, int max_delay, bool force) {
} }
//发送SIGTERM信号后2秒后检查子进程是否已经退出 //发送SIGTERM信号后2秒后检查子进程是否已经退出
WorkThreadPool::Instance().getPoller()->doDelayTask(max_delay, [pid, handle]() { EventPollerPool::Instance().getPoller()->doDelayTask(max_delay, [pid, handle]() {
if (!s_wait(pid, handle, nullptr, false)) { if (!s_wait(pid, handle, nullptr, false)) {
//进程已经退出了 //进程已经退出了
return 0; return 0;

View File

@@ -403,6 +403,9 @@ void installWebApi() {
item["vhost"] = media->getVhost(); item["vhost"] = media->getVhost();
item["app"] = media->getApp(); item["app"] = media->getApp();
item["stream"] = media->getId(); item["stream"] = media->getId();
item["createStamp"] = (Json::UInt64) media->getCreateStamp();
item["aliveSecond"] = (Json::UInt64) media->getAliveSecond();
item["bytesSpeed"] = media->getBytesSpeed();
item["readerCount"] = media->readerCount(); item["readerCount"] = media->readerCount();
item["totalReaderCount"] = media->totalReaderCount(); item["totalReaderCount"] = media->totalReaderCount();
item["originType"] = (int) media->getOriginType(); item["originType"] = (int) media->getOriginType();

View File

@@ -134,6 +134,9 @@ const char *getContentType(const HttpArgs &value){
} }
static void do_http_hook(const string &url,const ArgsType &body,const function<void(const Value &,const string &)> &fun){ static void do_http_hook(const string &url,const ArgsType &body,const function<void(const Value &,const string &)> &fun){
GET_CONFIG(string,mediaServerId,General::kMediaServerId);
const_cast<ArgsType &>(body)["mediaServerId"] = mediaServerId;
GET_CONFIG(float,hook_timeoutSec,Hook::kTimeoutSec); GET_CONFIG(float,hook_timeoutSec,Hook::kTimeoutSec);
HttpRequester::Ptr requester(new HttpRequester); HttpRequester::Ptr requester(new HttpRequester);
requester->setMethod("POST"); requester->setMethod("POST");

View File

@@ -43,6 +43,7 @@ MediaSource::MediaSource(const string &schema, const string &vhost, const string
_schema = schema; _schema = schema;
_app = app; _app = app;
_stream_id = stream_id; _stream_id = stream_id;
_create_stamp = time(NULL);
} }
MediaSource::~MediaSource() { MediaSource::~MediaSource() {
@@ -66,6 +67,19 @@ const string& MediaSource::getId() const {
return _stream_id; return _stream_id;
} }
int MediaSource::getBytesSpeed(){
return _speed.getSpeed();
}
uint64_t MediaSource::getCreateStamp() const {
return _create_stamp;
}
uint64_t MediaSource::getAliveSecond() const {
//使用Ticker对象获取存活时间的目的是防止修改系统时间导致回退
return _ticker.createdTime() / 1000;
}
vector<Track::Ptr> MediaSource::getTracks(bool ready) const { vector<Track::Ptr> MediaSource::getTracks(bool ready) const {
auto listener = _listener.lock(); auto listener = _listener.lock();
if(!listener){ if(!listener){

View File

@@ -137,6 +137,52 @@ public:
string _param_strs; string _param_strs;
}; };
class BytesSpeed {
public:
BytesSpeed() = default;
~BytesSpeed() = default;
/**
* 添加统计字节
*/
BytesSpeed& operator += (uint64_t bytes) {
_bytes += bytes;
if (_bytes > 1024 * 1024) {
//数据大于1MB就计算一次网速
computeSpeed();
}
return *this;
}
/**
* 获取速度单位bytes/s
*/
int getSpeed() {
if (_ticker.elapsedTime() < 1000) {
//获取频率小于1秒那么返回上次计算结果
return _speed;
}
return computeSpeed();
}
private:
uint64_t computeSpeed() {
auto elapsed = _ticker.elapsedTime();
if (!elapsed) {
return _speed;
}
_speed = _bytes * 1000 / elapsed;
_ticker.resetTime();
_bytes = 0;
return _speed;
}
private:
int _speed = 0;
uint64_t _bytes = 0;
Ticker _ticker;
};
/** /**
* 媒体源任何rtsp/rtmp的直播流都源自该对象 * 媒体源任何rtsp/rtmp的直播流都源自该对象
*/ */
@@ -170,6 +216,13 @@ public:
// 设置时间戳 // 设置时间戳
virtual void setTimeStamp(uint32_t stamp) {}; virtual void setTimeStamp(uint32_t stamp) {};
// 获取数据速率单位bytes/s
int getBytesSpeed();
// 获取流创建GMT unix时间戳单位秒
uint64_t getCreateStamp() const;
// 获取流上线时间,单位秒
uint64_t getAliveSecond() const;
////////////////MediaSourceEvent相关接口实现//////////////// ////////////////MediaSourceEvent相关接口实现////////////////
// 设置监听者 // 设置监听者
@@ -229,7 +282,12 @@ private:
//触发媒体事件 //触发媒体事件
void emitEvent(bool regist); void emitEvent(bool regist);
protected:
BytesSpeed _speed;
private: private:
time_t _create_stamp;
Ticker _ticker;
string _schema; string _schema;
string _vhost; string _vhost;
string _app; string _app;

View File

@@ -60,6 +60,7 @@ const string kBroadcaseProxyPusherFailed = "kBroadcaseProxyPusherFailed";
//通用配置项目 //通用配置项目
namespace General{ namespace General{
#define GENERAL_FIELD "general." #define GENERAL_FIELD "general."
const string kMediaServerId = GENERAL_FIELD"mediaServerId";
const string kFlowThreshold = GENERAL_FIELD"flowThreshold"; const string kFlowThreshold = GENERAL_FIELD"flowThreshold";
const string kStreamNoneReaderDelayMS = GENERAL_FIELD"streamNoneReaderDelayMS"; const string kStreamNoneReaderDelayMS = GENERAL_FIELD"streamNoneReaderDelayMS";
const string kMaxStreamWaitTimeMS = GENERAL_FIELD"maxStreamWaitMS"; const string kMaxStreamWaitTimeMS = GENERAL_FIELD"maxStreamWaitMS";
@@ -82,6 +83,7 @@ onceToken token([](){
mINI::Instance()[kPublishToMP4] = 0; mINI::Instance()[kPublishToMP4] = 0;
mINI::Instance()[kMergeWriteMS] = 0; mINI::Instance()[kMergeWriteMS] = 0;
mINI::Instance()[kModifyStamp] = 0; mINI::Instance()[kModifyStamp] = 0;
mINI::Instance()[kMediaServerId] = makeRandStr(16);
},nullptr); },nullptr);
}//namespace General }//namespace General

View File

@@ -162,6 +162,8 @@ extern const string kBroadcastReloadConfig;
////////////通用配置/////////// ////////////通用配置///////////
namespace General{ namespace General{
//每个流媒体服务器的IDGUID
extern const string kMediaServerId;
//流量汇报事件流量阈值,单位KB默认1MB //流量汇报事件流量阈值,单位KB默认1MB
extern const string kFlowThreshold; extern const string kFlowThreshold;
//流无人观看并且超过若干时间后才触发kBroadcastStreamNoneReader事件 //流无人观看并且超过若干时间后才触发kBroadcastStreamNoneReader事件

View File

@@ -99,6 +99,7 @@ public:
if (key) { if (key) {
_have_video = true; _have_video = true;
} }
_speed += packet->size();
PacketCache<FMP4Packet, FMP4FlushPolicy>::inputPacket(true, packet, key); PacketCache<FMP4Packet, FMP4FlushPolicy>::inputPacket(true, packet, key);
} }

View File

@@ -105,6 +105,9 @@ void HlsMakerImp::onWriteSegment(const char *data, int len) {
if (_file) { if (_file) {
fwrite(data, len, 1, _file.get()); fwrite(data, len, 1, _file.get());
} }
if (_media_src) {
_media_src->onSegmentSize(len);
}
} }
void HlsMakerImp::onWriteHls(const char *data, int len) { void HlsMakerImp::onWriteHls(const char *data, int len) {

View File

@@ -79,6 +79,10 @@ public:
_list_cb.emplace_back(std::move(cb)); _list_cb.emplace_back(std::move(cb));
} }
void onSegmentSize(uint64_t bytes) {
_speed += bytes;
}
private: private:
bool _is_regist = false; bool _is_regist = false;
RingType::Ptr _ring; RingType::Ptr _ring;

View File

@@ -32,7 +32,7 @@ string Recorder::getRecordPath(Recorder::type type, const string &vhost, const s
} }
//Here we use the customized file path. //Here we use the customized file path.
if (!customized_path.empty()) { if (!customized_path.empty()) {
m3u8FilePath = customized_path + "/hls.m3u8"; return File::absolutePath(m3u8FilePath, customized_path);
} }
return File::absolutePath(m3u8FilePath, hlsPath); return File::absolutePath(m3u8FilePath, hlsPath);
} }
@@ -47,7 +47,7 @@ string Recorder::getRecordPath(Recorder::type type, const string &vhost, const s
} }
//Here we use the customized file path. //Here we use the customized file path.
if (!customized_path.empty()) { if (!customized_path.empty()) {
mp4FilePath = customized_path + "/"; return File::absolutePath(mp4FilePath, customized_path);
} }
return File::absolutePath(mp4FilePath, recordPath); return File::absolutePath(mp4FilePath, recordPath);
} }

View File

@@ -47,7 +47,7 @@ public:
* @param vhost 虚拟主机 * @param vhost 虚拟主机
* @param app 应用名 * @param app 应用名
* @param stream_id 流id * @param stream_id 流id
* @param customized_path 录像文件保存自定义目录,默认为空则自动生成 * @param customized_path 录像文件保存自定义目录,为空则采用配置文件设置
* @return 录制文件绝对路径 * @return 录制文件绝对路径
*/ */
static string getRecordPath(type type, const string &vhost, const string &app, const string &stream_id,const string &customized_path = ""); static string getRecordPath(type type, const string &vhost, const string &app, const string &stream_id,const string &customized_path = "");
@@ -58,7 +58,7 @@ public:
* @param vhost 虚拟主机 * @param vhost 虚拟主机
* @param app 应用名 * @param app 应用名
* @param stream_id 流id * @param stream_id 流id
* @param customized_path 录像文件保存自定义目录,默认为空则自动生成 * @param customized_path 录像文件保存自定义目录,为空则采用配置文件设置
* @return 对象指针可能为nullptr * @return 对象指针可能为nullptr
*/ */
static std::shared_ptr<MediaSinkInterface> createRecorder(type type, const string &vhost, const string &app, const string &stream_id, const string &customized_path = ""); static std::shared_ptr<MediaSinkInterface> createRecorder(type type, const string &vhost, const string &app, const string &stream_id, const string &customized_path = "");
@@ -79,7 +79,7 @@ public:
* @param vhost 虚拟主机 * @param vhost 虚拟主机
* @param app 应用名 * @param app 应用名
* @param stream_id 流id * @param stream_id 流id
* @param customized_path 录像文件保存自定义目录,默认为空则自动生成 * @param customized_path 录像文件保存自定义目录,为空则采用配置文件设置
* @return 成功与否 * @return 成功与否
*/ */
static bool startRecord(type type, const string &vhost, const string &app, const string &stream_id,const string &customized_path); static bool startRecord(type type, const string &vhost, const string &app, const string &stream_id,const string &customized_path);

View File

@@ -119,6 +119,7 @@ public:
* @param pkt rtmp包 * @param pkt rtmp包
*/ */
void onWrite(const RtmpPacket::Ptr &pkt, bool = true) override { void onWrite(const RtmpPacket::Ptr &pkt, bool = true) override {
_speed += pkt->size();
//保存当前时间戳 //保存当前时间戳
switch (pkt->type_id) { switch (pkt->type_id) {
case MSG_VIDEO : _track_stamps[TrackVideo] = pkt->time_stamp, _have_video = true; break; case MSG_VIDEO : _track_stamps[TrackVideo] = pkt->time_stamp, _have_video = true; break;

View File

@@ -11,11 +11,6 @@
#include "Common/config.h" #include "Common/config.h"
#include "RtpReceiver.h" #include "RtpReceiver.h"
#define POP_HEAD(trackidx) \
auto it = _rtp_sort_cache_map[trackidx].begin(); \
onRtpSorted(it->second, trackidx); \
_rtp_sort_cache_map[trackidx].erase(it);
#define AV_RB16(x) \ #define AV_RB16(x) \
((((const uint8_t*)(x))[0] << 8) | \ ((((const uint8_t*)(x))[0] << 8) | \
((const uint8_t*)(x))[1]) ((const uint8_t*)(x))[1])
@@ -24,7 +19,18 @@
namespace mediakit { namespace mediakit {
RtpReceiver::RtpReceiver() {} RtpReceiver::RtpReceiver() {
GET_CONFIG(uint32_t, clearCount, Rtp::kClearCount);
GET_CONFIG(uint32_t, maxRtpCount, Rtp::kMaxRtpCount);
int index = 0;
for (auto &sortor : _rtp_sortor) {
sortor.setup(maxRtpCount, clearCount);
sortor.setOnSort([this, index](uint16_t seq, const RtpPacket::Ptr &packet) {
onRtpSorted(packet, index);
});
++index;
}
}
RtpReceiver::~RtpReceiver() {} RtpReceiver::~RtpReceiver() {}
bool RtpReceiver::handleOneRtp(int track_index, TrackType type, int samplerate, unsigned char *rtp_raw_ptr, unsigned int rtp_raw_len) { bool RtpReceiver::handleOneRtp(int track_index, TrackType type, int samplerate, unsigned char *rtp_raw_ptr, unsigned int rtp_raw_len) {
@@ -80,7 +86,7 @@ bool RtpReceiver::handleOneRtp(int track_index, TrackType type, int samplerate,
if (_ssrc_err_count[track_index]++ > 10) { if (_ssrc_err_count[track_index]++ > 10) {
//ssrc切换后清除老数据 //ssrc切换后清除老数据
WarnL << "ssrc更换:" << _ssrc[track_index] << " -> " << rtp.ssrc; WarnL << "ssrc更换:" << _ssrc[track_index] << " -> " << rtp.ssrc;
_rtp_sort_cache_map[track_index].clear(); _rtp_sortor[track_index].clear();
_ssrc[track_index] = rtp.ssrc; _ssrc[track_index] = rtp.ssrc;
} }
return false; return false;
@@ -127,56 +133,15 @@ bool RtpReceiver::handleOneRtp(int track_index, TrackType type, int samplerate,
} }
void RtpReceiver::sortRtp(const RtpPacket::Ptr &rtp,int track_index){ void RtpReceiver::sortRtp(const RtpPacket::Ptr &rtp,int track_index){
if(rtp->sequence != _last_seq[track_index] + 1 && _last_seq[track_index] != 0){ _rtp_sortor[track_index].sortPacket(rtp->sequence, rtp);
//包乱序或丢包
_seq_ok_count[track_index] = 0;
_sort_started[track_index] = true;
if(_last_seq[track_index] > rtp->sequence && _last_seq[track_index] - rtp->sequence > 0xFF){
//sequence回环清空所有排序缓存
while (_rtp_sort_cache_map[track_index].size()) {
POP_HEAD(track_index)
}
++_seq_cycle_count[track_index];
}
}else{
//正确序列的包
_seq_ok_count[track_index]++;
}
_last_seq[track_index] = rtp->sequence;
//开始排序缓存
if (_sort_started[track_index]) {
_rtp_sort_cache_map[track_index].emplace(rtp->sequence, rtp);
GET_CONFIG(uint32_t,clearCount,Rtp::kClearCount);
GET_CONFIG(uint32_t,maxRtpCount,Rtp::kMaxRtpCount);
if (_seq_ok_count[track_index] >= clearCount) {
//网络环境改善,需要清空排序缓存
_seq_ok_count[track_index] = 0;
_sort_started[track_index] = false;
while (_rtp_sort_cache_map[track_index].size()) {
POP_HEAD(track_index)
}
} else if (_rtp_sort_cache_map[track_index].size() >= maxRtpCount) {
//排序缓存溢出
POP_HEAD(track_index)
}
}else{
//正确序列
onRtpSorted(rtp, track_index);
}
} }
void RtpReceiver::clear() { void RtpReceiver::clear() {
CLEAR_ARR(_last_seq);
CLEAR_ARR(_ssrc); CLEAR_ARR(_ssrc);
CLEAR_ARR(_ssrc_err_count); CLEAR_ARR(_ssrc_err_count);
CLEAR_ARR(_seq_ok_count); for (auto &sortor : _rtp_sortor) {
CLEAR_ARR(_sort_started); sortor.clear();
CLEAR_ARR(_seq_cycle_count); }
_rtp_sort_cache_map[0].clear();
_rtp_sort_cache_map[1].clear();
} }
void RtpReceiver::setPoolSize(int size) { void RtpReceiver::setPoolSize(int size) {
@@ -184,11 +149,11 @@ void RtpReceiver::setPoolSize(int size) {
} }
int RtpReceiver::getJitterSize(int track_index){ int RtpReceiver::getJitterSize(int track_index){
return _rtp_sort_cache_map[track_index].size(); return _rtp_sortor[track_index].getJitterSize();
} }
int RtpReceiver::getCycleCount(int track_index){ int RtpReceiver::getCycleCount(int track_index){
return _seq_cycle_count[track_index]; return _rtp_sortor[track_index].getCycleCount();
} }

View File

@@ -11,24 +11,141 @@
#ifndef ZLMEDIAKIT_RTPRECEIVER_H #ifndef ZLMEDIAKIT_RTPRECEIVER_H
#define ZLMEDIAKIT_RTPRECEIVER_H #define ZLMEDIAKIT_RTPRECEIVER_H
#include <map> #include <map>
#include <string> #include <string>
#include <memory> #include <memory>
#include "RtpCodec.h" #include "RtpCodec.h"
#include "RtspMediaSource.h" #include "RtspMediaSource.h"
using namespace std; using namespace std;
using namespace toolkit; using namespace toolkit;
namespace mediakit { namespace mediakit {
template<typename T, typename SEQ = uint16_t>
class PacketSortor {
public:
PacketSortor() = default;
~PacketSortor() = default;
/**
* 设置参数
* @param max_sort_size 最大排序缓存长度
* @param clear_sort_size seq连续次数超过该值后清空并关闭排序缓存
*/
void setup(uint32_t max_sort_size, uint32_t clear_sort_size) {
_max_sort_size = max_sort_size;
_clear_sort_size = clear_sort_size;
}
void setOnSort(function<void(SEQ seq, const T &packet)> cb){
_cb = std::move(cb);
}
/**
* 清空状态
*/
void clear() {
_last_seq = 0;
_seq_ok_count = 0;
_sort_started = 0;
_seq_cycle_count = 0;
_rtp_sort_cache_map.clear();
}
/**
* 获取排序缓存长度
*/
int getJitterSize(){
return _rtp_sort_cache_map.size();
}
/**
* 获取seq回环次数
*/
int getCycleCount(){
return _seq_cycle_count;
}
/**
* 输入并排序
* @param seq 序列号
* @param packet 包负载
*/
void sortPacket(SEQ seq, const T &packet){
if (seq != _last_seq + 1 && _last_seq != 0) {
//包乱序或丢包
_seq_ok_count = 0;
_sort_started = true;
if (_last_seq > seq && _last_seq - seq > 0xFF) {
//sequence回环清空所有排序缓存
while (_rtp_sort_cache_map.size()) {
popPacket();
}
++_seq_cycle_count;
}
} else {
//正确序列的包
_seq_ok_count++;
}
_last_seq = seq;
//开始排序缓存
if (_sort_started) {
_rtp_sort_cache_map.emplace(seq, packet);
if (_seq_ok_count >= _clear_sort_size) {
//网络环境改善,需要清空排序缓存
_seq_ok_count = 0;
_sort_started = false;
while (_rtp_sort_cache_map.size()) {
popPacket();
}
} else if (_rtp_sort_cache_map.size() >= _max_sort_size) {
//排序缓存溢出
popPacket();
}
} else {
//正确序列
onPacketSorted(seq, packet);
}
}
private:
void popPacket() {
auto it = _rtp_sort_cache_map.begin();
onPacketSorted(it->first, it->second);
_rtp_sort_cache_map.erase(it);
}
void onPacketSorted(SEQ seq, const T &packet) {
_cb(seq, packet);
}
private:
//是否开始seq排序
bool _sort_started = false;
//上次seq
SEQ _last_seq = 0;
//seq连续次数计数
uint32_t _seq_ok_count = 0;
//seq回环次数计数
uint32_t _seq_cycle_count = 0;
//排序缓存长度
uint32_t _max_sort_size;
//seq连续次数超过该值后清空并关闭排序缓存
uint32_t _clear_sort_size;
//rtp排序缓存根据seq排序
map<SEQ, T> _rtp_sort_cache_map;
//回调
function<void(SEQ seq, const T &packet)> _cb;
};
class RtpReceiver { class RtpReceiver {
public: public:
RtpReceiver(); RtpReceiver();
virtual ~RtpReceiver(); virtual ~RtpReceiver();
protected:
protected:
/** /**
* 输入数据指针生成并排序rtp包 * 输入数据指针生成并排序rtp包
* @param track_index track下标索引 * @param track_index track下标索引
@@ -46,6 +163,7 @@ protected:
* @param track_index track索引 * @param track_index track索引
*/ */
virtual void onRtpSorted(const RtpPacket::Ptr &rtp, int track_index){} virtual void onRtpSorted(const RtpPacket::Ptr &rtp, int track_index){}
void clear(); void clear();
void setPoolSize(int size); void setPoolSize(int size);
int getJitterSize(int track_index); int getJitterSize(int track_index);
@@ -58,16 +176,8 @@ private:
uint32_t _ssrc[2] = { 0, 0 }; uint32_t _ssrc[2] = { 0, 0 };
//ssrc不匹配计数 //ssrc不匹配计数
uint32_t _ssrc_err_count[2] = { 0, 0 }; uint32_t _ssrc_err_count[2] = { 0, 0 };
//上次seq
uint16_t _last_seq[2] = { 0 , 0 };
//seq连续次数计数
uint32_t _seq_ok_count[2] = { 0 , 0};
//seq回环次数计数
uint32_t _seq_cycle_count[2] = { 0 , 0};
//是否开始seq排序
bool _sort_started[2] = { 0 , 0};
//rtp排序缓存根据seq排序 //rtp排序缓存根据seq排序
map<uint16_t , RtpPacket::Ptr> _rtp_sort_cache_map[2]; PacketSortor<RtpPacket::Ptr> _rtp_sortor[2];
//rtp循环池 //rtp循环池
RtspMediaSource::PoolType _rtp_pool; RtspMediaSource::PoolType _rtp_pool;
}; };

View File

@@ -157,6 +157,7 @@ public:
* @param keyPos 该包是否为关键帧的第一个包 * @param keyPos 该包是否为关键帧的第一个包
*/ */
void onWrite(const RtpPacket::Ptr &rtp, bool keyPos) override { void onWrite(const RtpPacket::Ptr &rtp, bool keyPos) override {
_speed += rtp->size();
assert(rtp->type >= 0 && rtp->type < TrackMax); assert(rtp->type >= 0 && rtp->type < TrackMax);
auto track = _tracks[rtp->type]; auto track = _tracks[rtp->type];
if (track) { if (track) {

View File

@@ -76,6 +76,7 @@ public:
* @param key 是否为关键帧第一个包 * @param key 是否为关键帧第一个包
*/ */
void onWrite(const TSPacket::Ptr &packet, bool key) override { void onWrite(const TSPacket::Ptr &packet, bool key) override {
_speed += packet->size();
if (!_ring) { if (!_ring) {
createRing(); createRing();
} }