目录
0. 前言
公司项目中需要一个小型的实时流推流服务器,在之前的代码中是使用ffserver来完成这个功能的,但由于种种原因,需要重新实现实时流服务这一块的内容。经过一段时间的尝试,终于有了结果,在这里分享一下。
代码平台是ubuntu,使用了ffmpeg(编译时带libx264)、opencv、live555三个库。
首先,说一下这个推流服务器的使用场景和要求:
1. 在针对图片做算法时,往往会用到opencv库,此时一张图片就是一个cv::Mat,因此要求推流以cv::Mat为单位,即手动地将一张张图片推流为一个视频流。
2. 可以保存这个视频流中的任意一部分到本地文件。
以live555实现实时流推送,在网上有很多例子,其结构大致都是一样的:
1. 以FramedSource为基类,自定义一个子类,用于获取要推送的媒体流的数据。在这个类中,一般使用ffmpeg将raw video编码为对应格式的ES流。
2. 如果要实现的是多播场景,比如投屏,会议等情况,那么实现一个 PassiveServerMediaSubsession 的实例,指定多播地址,主动调用startPlay来推送数据。
3. 如果要实现的是单播场景(注意:rtp-over-udp和rtp-over-tcp都是单播),那么就要以 OnDemandServerMediaSubsession 为基类,自定义一个SubSession,我们会将上述自定义的Source关联到这个subSession中。这样,当我们访问这个SubSession时,就会创建对应的Source和Sink,然后开始推送数据。
主体的框架大家都是一样的,但其中有不少的问题需要一一解决。
1. 选择要使用的场景
当实现点播时,自然需要使用单播场景,因为单播本来就是各自独立的;而当实现实时流时,有两种选择:(1)选择多播,优点是一路流只需要发送到一个多播地址上即可,同时可以有N个client访问这个流,缺点是由于其基于UDP传输数据,因此可能会发送丢包,尤其是网络环境不佳的情况下,这样会对客户的体验造成非常不好的影响。(2)使用单播,特别是使用rtp-over-tcp,这样由于基于TCP传输,就不会有丢包的问题发生,但问题在于有N个Client,就需要将同一路流分别发送到N个TCP中,对带宽的负担非常的重。
由于公司的使用场景中,连接的客户端不会太多,因此采用单播场景,使用rtp-over-tcp,以保证质量。
2. 如何提高实时流的实时性?
方法一:在代码中,使用的是libx264编码器,为了提高实时性,我们对它做以下设置,以便提供编码的速度:
AVDictionary *pH264Dict = NULL;
av_dict_set(&pH264Dict, "preset", "ultrafast", 0);
av_dict_set(&pH264Dict, "tune", "zerolatency", 0);
int nRet = avcodec_open2(m_psH264CodecCtx, pH264Codec, &pH264Dict);
if (nRet < 0) {
fprintf(stderr, "Could not open H.264\n");
return -9;
}
方法二:由于是实时流,编码时只使用I帧和P帧,而不能存在B帧。
#define GOPSIZE 20
//在对Frame进行编码之前
static int nIndex = 0;
if(nIndex == 0)
{
m_psYUVFrame->pict_type = AV_PICTURE_TYPE_I;
}
else
{
m_psYUVFrame->pict_type = AV_PICTURE_TYPE_P;
}
nIndex++;
if(nIndex >= GOPSIZE)
{
nIndex = 0;
}
方法三:在客户端,我们可以选择使用第三方的流播放器,或者可以自己使用ffmpeg来实现一个简易的播放器。我使用的是自己写的简易的播放器,这是因为一般的播放器中都会有解码缓冲区和播放的帧缓冲区,这会导致实时性下降。
3.如何提高实时流的响应?
我们一般在打开一个流的时候,都必须等待一段时间才会看到相关的画面,如何对这个响应做优化?
3.1 方法一
一个实时流,由I帧,P帧,B帧组成,当然,由于我们要传输的是实时流,这里不应该使用B帧。其中,I帧的数量是非常少的,当一个client突然访问实时流时,其开始传输的数据,必然不会是I帧的起始数据,而是其他部分的数据,客户端直到获取到下一个I帧的数据才能正确解码,这样就会造成Client端迟迟不能显示画面。而由于实时流的编码是发生在当下的,由我们所控制的,因此如果我们可以在Client访问的时刻,将当前帧设置为编码为I帧(为了保险,可以设置为连续3帧都编码为I帧),那么客户端就可以立刻接收到I帧的数据,然后正确解码。而且这样做,仅在少数时刻有影响,对于整个流来说数据量不会增加太多。当然,对于每秒都有数十以上的大型直播流而言,这种方法当然不能使用。
以下是在live555中的实现方法:
在OnDemandServerMediaSubsession构造中有一个参数为reuseFirstSource,当其为True时,所有Client都公用同一个Source,但此时live555中没有接口可以通知我们Client的到来,因此有如下两种方法:
1、修改live555源码,添加可以通知有新Client的接口;可惜,出于其他考虑,并未采用这种方法,这个方法其实最为简洁。
有新Client时,创建其对应的StreamState的函数在这里:
virtual void OnDemandServerMediaSubsession::
getStreamParameters(unsigned clientSessionId,
netAddressBits clientAddress,
Port const& clientRTPPort,
Port const& clientRTCPPort,
int tcpSocketNum,
unsigned char rtpChannelId,
unsigned char rtcpChannelId,
netAddressBits& destinationAddress,
u_int8_t& destinationTTL,
Boolean& isMulticast,
Port& serverRTPPort,
Port& serverRTCPPort,
void*& streamToken);
2、设置reuseFirstSource为false,此时每次有新的Client,都会调用:
virtual FramedSource* OnDemandServerMediaSubsession::createNewStreamSource(unsigned clientSessionId,unsigned& estBitrate);
这样我们就知道有新的Client连接了。但这样的话,一个Client就会对应一个Source,而不是所有Client共用一个Source,幸好,live555为我们考虑到了这种情况,它由一个类型 StreamReplicator ,它可以为一个InputSource创建任意数量的“副本流”。
首先,说一下它的工作原理,我们知道,source和sink之间的数据获取,其“主动轮”是sink,sink主动调用source的接口获取数据,source调用sink传入的回调通知sink数据获取到了,然后sink再次调用source的接口获取数据,这样一直循环,就可以源源不断的获取到一个流的数据。而在StreamReplicator中,它的“主动轮”是其第一个“副本流”对应的sink,如果第一个副本流获取不到数据,那么其他副本流将不会获取到数据,这是因为StreamReplicator设计为这样的,主副本Source获取到数据后,再将其拷贝给其他的副本Source。因此第一个副本流对于的sink就非常关键,它应该从一开始就不断获取source的数据,而且一直不能关闭,这样其他的副本才能很好的工作。
因此,自定义了一个Sink,它和SimpleUDPSink完全一样,只不过去掉了其中处理数据的部分:
#ifndef NULLSINK_H
#define NULLSINK_H
#include <MediaSink.hh>
class CNULLSink: public MediaSink {
public:
static CNULLSink* createNew(UsageEnvironment& env);
protected:
CNULLSink(UsageEnvironment& env);
// called only by createNew()
virtual ~CNULLSink();
private: // redefined virtual functions:
virtual Boolean continuePlaying();
private:
void continuePlaying1();
static void afterGettingFrame(void* clientData, unsigned frameSize,
unsigned numTruncatedBytes,
struct timeval presentationTime,
unsigned durationInMicroseconds);
void afterGettingFrame1(unsigned frameSize, unsigned numTruncatedBytes,
unsigned durationInMicroseconds);
static void sendNext(void* firstArg);
private:
unsigned fMaxPayloadSize;
unsigned char* fOutputBuffer;
struct timeval fNextSendTime;
};
#include "NULLSink.h"
CNULLSink* CNULLSink::createNew(UsageEnvironment& env) {
return new CNULLSink(env);
}
CNULLSink::CNULLSink(UsageEnvironment& env)
: MediaSink(env){
fMaxPayloadSize = 2000 * 1024;
fOutputBuffer = new unsigned char[fMaxPayloadSize];
}
CNULLSink::~CNULLSink() {
delete[] fOutputBuffer;
}
Boolean CNULLSink::continuePlaying() {
gettimeofday(&fNextSendTime, NULL);
continuePlaying1();
return True;
}
void CNULLSink::continuePlaying1() {
nextTask() = NULL;
if (fSource != NULL) {
fSource->getNextFrame(fOutputBuffer, fMaxPayloadSize,
afterGettingFrame, this,
onSourceClosure, this);
}
}
void CNULLSink::afterGettingFrame(void* clientData, unsigned frameSize,
unsigned numTruncatedBytes,
struct timeval /*presentationTime*/,
unsigned durationInMicroseconds) {
CNULLSink* sink = (CNULLSink*)clientData;
sink->afterGettingFrame1(frameSize, numTruncatedBytes, durationInMicroseconds);
}
void CNULLSink::afterGettingFrame1(unsigned frameSize, unsigned numTruncatedBytes,
unsigned durationInMicroseconds) {
fNextSendTime.tv_usec += durationInMicroseconds;
fNextSendTime.tv_sec += fNextSendTime.tv_usec/1000000;
fNextSendTime.tv_usec %= 1000000;
struct timeval timeNow;
gettimeofday(&timeNow, NULL);
int secsDiff = fNextSendTime.tv_sec - timeNow.tv_sec;
int64_t uSecondsToGo = secsDiff*1000000 + (fNextSendTime.tv_usec - timeNow.tv_usec);
if (uSecondsToGo < 0 || secsDiff < 0) { // sanity check: Make sure that the time-to-delay is non-negative:
uSecondsToGo = 0;
}
nextTask() = envir().taskScheduler().scheduleDelayedTask(uSecondsToGo,
(TaskFunc*)sendNext, this);
}
void CNULLSink::sendNext(void* firstArg) {
CNULLSink* sink = (CNULLSink*)firstArg;
sink->continuePlaying1();
}
然后就是自定义OnDemandSubSession的子类:
#ifndef LIVESTREAMMEDIASUBSESSION_H
#define LIVESTREAMMEDIASUBSESSION_H
#include <OnDemandServerMediaSubsession.hh>
#include <FramedSource.hh>
#include <RTPSink.hh>
#include "ImageHandler.h"
#include "CFramedLiveSource.h"
#include <H264VideoStreamFramer.hh>
#include <H264VideoRTPSink.hh>
#include <StreamReplicator.hh>
#include <StreamReplicator.hh>
#include <SimpleRTPSink.hh>
#include "NULLSink.h"
#include <H265VideoRTPSink.hh>
#include <H265VideoStreamFramer.hh>
class CLiveStreamMediaSubSession : public OnDemandServerMediaSubsession
{
public:
static CLiveStreamMediaSubSession*
createNew(UsageEnvironment& env,CImageIO *piImgIO);
public:
CLiveStreamMediaSubSession(UsageEnvironment& env,
CImageIO *piImgIO);
virtual ~CLiveStreamMediaSubSession();
void CreateNullStream();
private:
// redefined virtual functions
virtual FramedSource* createNewStreamSource(unsigned clientSessionId,
unsigned& estBitrate);
virtual RTPSink* createNewRTPSink(Groupsock* rtpGroupsock,
unsigned char rtpPayloadTypeIfDynamic,
FramedSource* inputSource);
protected:
virtual char const* sdpLines();
CImageIO *m_piImgIO;
StreamReplicator *m_pReplicator;
MediaSink *m_pNullSink;
};
#endif // LIVESTREAMMEDIASUBSESSION_H
#include "LiveStreamMediaSubSession.h"
CLiveStreamMediaSubSession *CLiveStreamMediaSubSession::createNew(UsageEnvironment& env,CImageIO *piImgIO) {
return new CLiveStreamMediaSubSession(env, piImgIO);
}
CLiveStreamMediaSubSession::CLiveStreamMediaSubSession(UsageEnvironment& env,CImageIO *piImgIO)
:OnDemandServerMediaSubsession(env, false)
{
m_piImgIO = piImgIO;
m_pReplicator = nullptr;
m_pNullSink = nullptr;
CFramedLiveSource *pH264Source = CFramedLiveSource::createNew(envir(),m_piImgIO);
m_pReplicator = StreamReplicator::createNew(envir(),pH264Source,false);
CreateNullStream();
}
CLiveStreamMediaSubSession::~CLiveStreamMediaSubSession()
{
m_pNullSink->stopPlaying();
Medium::close(m_pNullSink);
Medium::close(m_pReplicator);
}
void CLiveStreamMediaSubSession::CreateNullStream()
{
FramedSource* pFirstReplicator = m_pReplicator->createStreamReplica();
m_pNullSink = CNULLSink::createNew(envir());
m_pNullSink->startPlaying(*pFirstReplicator,NULL,NULL);
}
FramedSource *CLiveStreamMediaSubSession::createNewStreamSource(unsigned /*clientSessionId*/, unsigned& estBitrate) {
estBitrate = 5120;
FramedSource* pReplicator = m_pReplicator->createStreamReplica();
H264VideoStreamFramer *pVideoSource =
H264VideoStreamFramer::createNew(envir(),pReplicator );
m_piImgIO->SetKeyFrameFlag();
return pVideoSource;
}
RTPSink* CLiveStreamMediaSubSession::createNewRTPSink(Groupsock* rtpGroupsock,
unsigned char rtpPayloadTypeIfDynamic,
FramedSource* /*inputSource*/) {
H264VideoRTPSink *pSink = H264VideoRTPSink::createNew(envir(),rtpGroupsock, 96);
return pSink;
}
char const* CLiveStreamMediaSubSession::sdpLines()
{
const char *strSDPLines =
"m=video 0 RTP/AVP 96\r\n"
"c=IN IP4 0.0.0.0\r\n"
"b=AS:96\r\n"
"a=rtpmap:96 H264/90000\r\n"
"a=fmtp:96 packetization-mode=1;profile-level-id=000000;sprop-parameter-sets=H264\r\n"
"a=control:track1\r\n";
fSDPLines = strDup(strSDPLines);
return fSDPLines;
}
3.2 方法二
在客户端使用ffmpeg打开一个流时,省略 avformat_find_stream_info() 函数。这个函数要获取足够的数据来分析流的相关信息,因此也会影响流的响应时间,如果将它去掉,客户端可以更加快速的响应。然而,在实际使用中,我发现当打开我们推送的实时流时去掉这个函数是可以工作的,而且响应会大大加快,但当使用同样的代码打开某些本地文件时,会报can't fine video stream的错误。总之,需要实践中验证一下能不能使用它。
4. 其他问题
1. 在server端各处有一些缓冲区,如source中有,sink中有,要注意这些缓冲区的大小,将其设置为一些恰当的值,否则当ffmpeg编码后的流的大小超过这些缓冲区的大小时,就会导致奔溃或者数据丢失。尤其是当流的尺寸大幅改变时,预设的缓冲区大小就可能不够用。
2. 在客户端使用ffmpeg解析实时流时,为了防止网络性能太差,而导致 av_read_frame() 阻塞,因此都会使用中断机制来保证可以跳出这个阻塞;但是,如果中断机制的时间设置的过短,那么会导致 av_read_frame() 在解析实时流时会返回AVERROR_EOF的错误,而且再次调用 av_read_frame()都会返回AVERROR_EOF ,按照道理,实时流是不会结束的,因此不可能返回这个错误,但由于我们使用了中断机制,当某个时段网络太差,导致给定的中断时间内未读取到一帧数据,那么就会发送这种情况,因此要慎重设置中断时间的长度,而且在真的发生了上述这种情况后,应该立刻再次打开这个流。
3. 要注意推流的cv::Mat的尺寸,并不是所有的尺寸都可以被libx264编码的,最好选用常见的标准尺寸。
5. 具体的代码