业务场景是:有一个rtmp的源,对外提供rtmp的直播节目,地址rtmp://abc.com/live/tv,
现在的需求是要将此节目拉过来,生成HLS对外发布,或对外还是rtmp发布,比如rtmp://my.com/live/tv。
作用嘛,肯定是你懂的!
此时需要一个把rtmp数据拉过来,再推出去的动作,一般推给SRS流媒体服务器后,即可随意对外分发提供rtmp或hls。
如何实现呢?
一般的流媒体服务器,像SRS提供许多流媒体格式的输出,可以用于进行流媒体处理,但此时需要一个拉流的程序。SRS里面用的是ffmpeg程序,但是个人感觉并不是特别好用,比如在某种情况下,ffmpeg卡死了,但是SRS并不会让ffmpeg重新拉流。
这里推荐两种方法:
一、使用zlmedia 的json API实现
最简单的办法就是使用zlmedia的mediaproxy API接口,即可完成,具体可参考
MediaServer支持的HTTP API · ZLMediaKit/ZLMediaKit Wiki · GitHub
"/index/api/addStreamProxy"
二、使用zlmedia进行二次开发
如果需要用程序进行自定义实现,可以参照
https://github.com/xia-chu/ZLMediaKit/blob/master/tests/test_pusher.cpp
假定SRS运行在本机的1935端口,推给SRS后,由SRS再处理对外发布。
int main(int argc, char *argv[]) {
return domain("rtmp://abc.com/live/hks1", "rtmp://127.0.0.1/live/tv");
}
以下是我根据ZLMediaKit中的 test_pusher修改后的代码,可以从外界传递参数到程序中,实现自定义拉流并转推。
实测此功能正常运行,稳定性还不错。
程序的编译方法:在编译zlmedia的时候会自动编译test目录下的所有.cpp。
#include <signal.h>
#include <iostream>
#include "Util/logger.h"
#include "Util/NoticeCenter.h"
#include "Poller/EventPoller.h"
#include "Player/PlayerProxy.h"
#include "Rtmp/RtmpPusher.h"
#include "Common/config.h"
#include "Pusher/MediaPusher.h"
#include "Util/CMD.h"
using namespace std;
using namespace toolkit;
using namespace mediakit;
//推流器,保持强引用
MediaPusher::Ptr pusher;
Timer::Ptr g_timer;
//声明函数
void rePushDelay(const EventPoller::Ptr &poller,const string &schema,const string &vhost,const string &app, const string &stream, const string &url);
//创建推流器并开始推流
void createPusher(const EventPoller::Ptr &poller, const string &schema,const string &vhost,const string &app, const string &stream, const string &url) {
//创建推流器并绑定一个MediaSource
pusher.reset(new MediaPusher(schema,vhost, app, stream,poller));
//可以指定rtsp推流方式,支持tcp和udp方式,默认tcp
// (*pusher)[Client::kRtpType] = Rtsp::RTP_UDP;
//设置推流中断处理逻辑
pusher->setOnShutdown([poller,schema,vhost, app, stream, url](const SockException &ex) {
WarnL << "Server connection is closed:" << ex.getErrCode() << " " << ex.what();
//重试
rePushDelay(poller,schema,vhost,app, stream, url);
});
//设置发布结果处理逻辑
pusher->setOnPublished([poller,schema,vhost, app, stream, url](const SockException &ex) {
if (ex) {
WarnL << "Publish fail:" << ex.getErrCode() << " " << ex.what();
//如果发布失败,就重试
rePushDelay(poller,schema,vhost,app, stream, url);
} else {
InfoL << "Publish success,Please play with player:" << url;
}
});
pusher->publish(url);
}
//推流失败或断开延迟2秒后重试推流
void rePushDelay(const EventPoller::Ptr &poller,const string &schema,const string &vhost,const string &app, const string &stream, const string &url) {
g_timer = std::make_shared<Timer>(2,[poller,schema,vhost,app, stream, url]() {
InfoL << "Re-Publishing...";
//重新推流
createPusher(poller,schema,vhost,app, stream, url);
//此任务不重复
return false;
}, poller);
}
//这里才是真正执行main函数,你可以把函数名(domain)改成main,然后就可以输入自定义url了
int domain(const string &playUrl, const string &pushUrl) {
auto poller = EventPollerPool::Instance().getPoller();
//拉一个流,生成一个RtmpMediaSource,源的名称是"app/stream"
//你也可以以其他方式生成RtmpMediaSource,比如说MP4文件(请查看test_rtmpPusherMp4.cpp代码)
MediaInfo info(pushUrl);
PlayerProxy::Ptr player(new PlayerProxy(DEFAULT_VHOST, "app", "stream",false,false,-1 , poller));
//可以指定rtsp拉流方式,支持tcp和udp方式,默认tcp
// (*player)[Client::kRtpType] = Rtsp::RTP_UDP;
player->play(playUrl.data());
//监听RtmpMediaSource注册事件,在PlayerProxy播放成功后触发
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastMediaChanged,
[pushUrl,poller](BroadcastMediaChangedArgs) {
//媒体源"app/stream"已经注册,这时方可新建一个RtmpPusher对象并绑定该媒体源
if(bRegist && pushUrl.find(sender.getSchema()) == 0){
createPusher(poller,sender.getSchema(),sender.getVhost(),sender.getApp(), sender.getId(), pushUrl);
}
});
//设置退出信号处理函数
static semaphore sem;
signal(SIGINT, [](int) { sem.post(); });// 设置退出信号
sem.wait();
pusher.reset();
g_timer.reset();
return 0;
}
int main(int argc, char *argv[])
{
// ./ffmpeg -loglevel info -f flv -i rtmp://test.yunyingtx.com/live/PLTV/88888888/tv
// -vcodec copy -acodec copy -f flv -y rtmp://127.0.0.1:1936/live/tv
string input;
string output;
try {
for (int i = 0; i < argc-1; i++)
{
if (string(argv[i]) == "-i")
input = argv[i + 1];
}
for (int i = 0; i < argc - 1; i++)
{
if (string(argv[i]) == "-y")
output = argv[i + 1];
}
}
catch (std::exception &ex) {
cout << ex.what() << endl;
return -1;
}
if (input.find("rtmp://") == string::npos ||
output.find("rtmp://") == string::npos)
{
return -1;
}
EventPollerPool::setPoolSize(1);
//设置日志
LogLevel logLevel = (LogLevel)LDebug;//
Logger::Instance().add(std::make_shared<ConsoleChannel>());
Logger::Instance().setWriter(std::make_shared<AsyncLogWriter>());
auto fileChannel = std::make_shared<FileChannel>("FileChannel", exeDir() + "../logs/", logLevel);
fileChannel->setMaxDay(7);
Logger::Instance().add(fileChannel);
DebugL << "program started." << endl;
DebugL << input<<endl;
DebugL << output<<endl;
return domain(input, output);//"rtmp://127.0.0.1/live/cctv13"
}
zlmedia本身存在一个问题:拉流时有中断的现象,就是说rtmp源还在播,但是我们的这个程序不知道因为什么原因停止工作了,导致整个媒体处理流程中断。
经过长时间的跟踪和调试,发现ZLMediaKit在处理 rtmp的流程存在问题
https://github.com/xia-chu/ZLMediaKit/issues/455
解决办法是在ZLMediaKit的 src/Rtmp/RtmpPlayerImp.h 文件中
对onCheckMeta() 函数加入对 _delegate为空的判断,只有为空的时候才reset()。
修改前:
bool onCheckMeta(const AMFValue &val) override {
_rtmp_src = dynamic_pointer_cast<RtmpMediaSource>(_pMediaSrc);
if (_rtmp_src) {
_rtmp_src->setMetaData(val);
_set_meta_data = true;
}
_delegate.reset(new RtmpDemuxer);
_delegate->loadMetaData(val);
return true;
}
修改后 :
bool onCheckMeta(const AMFValue &val) override {
_rtmp_src = dynamic_pointer_cast<RtmpMediaSource>(_pMediaSrc);
if (_rtmp_src) {
_rtmp_src->setMetaData(val);
_set_meta_data = true;
}
if (!_delegate) {
_delegate.reset(new RtmpDemuxer);
_delegate->loadMetaData(val);
}
return true;
}