使用live555实现实时流的推流服务器

目录

0. 前言

1. 选择要使用的场景

2. 如何提高实时流的实时性?

3.如何提高实时流的响应?

3.1 方法一

3.2 方法二

4. 其他问题

5. 具体的代码


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. 具体的代码

live555实现的实时流推流服务器

 

 

 

 

 

 

  • 3
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: live555是一个基于C++的开源媒体服务器,是由美国的Live Networks公司开发并维护。它的主要功能是在网络上实现实时传输,支持RTSP、RTP/RTCP和SIP等协议。 在使用live555推送实时之前,我们需要先设置好服务器地址、端口号以及媒体文件等参数。然后,我们通过live555中的MediaSession类,创建一个媒体会话,将需要传输的媒体加入到会话中。媒体可以是音频或视频,也可以是音视频混合的多媒体。 接着,我们使用live555提供的MediaSubsession类,将加入媒体会话的媒体进行分割。分割后的媒体将按照RTSP或RTP协议进行传输,可以通过RTSP或RTP服务器进行接收。其中,RTSP用于控制媒体的播放、暂停、停止等操作;而RTP则是在网络传输中实现实时数据传输和同步的协议。 总的来说,通过使用live555的媒体会话和媒体分割功能,我们可以轻松地实现实时的推送和传输,为视频监控、视频会议、实时视频直播等应用提供了非常可靠和高效的技术支持。 ### 回答2: Live555是一个开源的C++多媒体框架,可用于实现实时的推送。Live555提供了一套完整的库和工具,能够支持常见的视频和音频协议,例如RTSP,RTP,RTCP等。 实时推送的过程可以概括为以下几个步骤: 1. 创建一个`RTSPServer`实例,用于接收客户端的连接请求。 2. 为需要推送的媒体资源创建一个`MediaSession`实例,并将其添加到`RTSPServer`中。 3. 创建一个`RTSPClientConnection`实例,用于处理客户端的连接和请求。 4. 在`RTSPSession`中添加需要推送的媒体资源,并为其创建一个`RTPSink`实例,用于将媒体数据发送到客户端。 5. 创建一个`MediaSource`实例,用于从媒体源(例如摄像头或音频设备)中获取实时数据。 6. 将`MediaSource`连接到`RTPSink`,并启动数据传输。 7. 开始监听客户端的连接请求,并响应相应的RTSP请求。 8. 当有客户端连接成功后,将媒体数据通过RTP协议发送给客户端。 9. 如果有多个客户端连接,可以使用多线程或多进程来处理并发连接。 通过以上步骤,Live555可以实现实时从媒体源推送到客户端。实时推送广泛应用于视频直播、视频会议等领域,能够实现高效的实时数据传输和播放。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值