live555 livemedia库结构分析[原创]

来源:互联网 发布:淘宝小号出租 编辑:程序博客网 时间:2024/05/22 12:48

live555是国内视频项目开发经常会用到的一个开源项目,该项目体积小巧,功能全面,尤其适合移植到嵌入式平台。可惜的是的相关文档太少,除了mail list 和faq之外几无可寻,几天前刚好有个项目需要用到livemedia库,于是笔者花了几个天时间把livemedia的主要部分做了点分析。不足之处,希望有兴趣的朋友补充。

整体框架

先简单介绍下live555,项目分为两个大部分:

  1. 底层livemedia库;提供各种功能实现,包括视频文件读写,rtsp协议,rtp协议,等等。
  2. 基于livemedia开发的几个应用,包括rtsp服务器live555 media server,接收视频的客户端openrtsp,playsip等。

可以看出livemedia库是各种应用的基础,其内部由各种class组成,结构庞杂。我们先分析它的基本框架:livemedia与一般类库区别最大的一点,就是它的运行结构,整个运行过程就是一个消息循环,类似窗口机制。不同的是它采用select()循环处理socket读写和各种定时任务、触发任务。下面即是内部循环的代码:

void BasicTaskScheduler0::doEventLoop(char* watchVariable) {  // Repeatedly loop, handling readble sockets and timed events:  while (1) {    if (watchVariable != NULL && *watchVariable != 0) break;    SingleStep();  }}
可以看出,watchVariable是用户设置的循环退出条件,当其不等于0时即退出。SingleStep()内调用select(),根据select()返回结果调用各种回调函数。

livemedia库内部的类关系结构,可以从Medium类的继承关系图中清楚的反映出来。
里面有几个重要的类:Medium, FramedSource, MediaSession, RTSPclient等,以下重点分析这个类的作用和用法。

重要类分析

Medium类几乎是所有其他类的基类。并且每个继承自Medium的类创建时都会自动加入到UsageEnvironment中的对象列表中,以后可以根据对象名字索引。下面重点介绍几个重要的Medium子类。

从视频数据传送的角度上来看,各个类可分为三种:Source, Filter, Sink。Source是数据源,Filter做数据中间处理,而Sink是数据传输终点,也是整个数据流的发起者,可以说一切数据传输从Sink->startplaying()开始。这三者构成整个数据链。读者可从类名大致判断,某个类的角色,如FileSink是将视频写入文件 ,RTPSource是客户端接受RTP视频数据作为整个链条的源头。

FramedSource类

首先看源头,FramedSource基本上是所有源头类的抽象基类或者说interface,在所有类中占有相当重要的地位。这个类的接口很简单:

void getNextFrame(unsigned char* to, unsigned maxSize,    afterGettingFunc* afterGettingFunc,    void* afterGettingClientData,    onCloseFunc* onCloseFunc,    void* onCloseClientData);void stopGettingFrames();
getNextFrame()函数由链条的 下一环节(Caller)调用,功能是获得单帧。(注意这里的帧frame与packet的区别是,一个frame可能由多个packet组成,如multiframedrtpsource类的功能就是把多个packet组成一个frame传送给caller) 前两个参数指定存帧地址,afterGettingFunc指定回调函数,当帧存入指定地址后调用。onCloseFunc同样也是回调函数,当视频源关闭,不再有数据时调用,如视频结束。
stopGettingFrames()函数由Caller告诉视频源,停止数据传送。

凡继承自FramedSource的类,必须重载两个实现函数:
virtual void doGetNextFrame();virtual void doStopGettingFrames();

FramedFilter类

FramedFilter继承自FramedSource作为中间环节,一方面获得上一环节的源数据,另一方面提供下一环节数据。因此在原有接口的基础上,增加以下接口:

  FramedSource* inputSource() const { return fInputSource; }  void reassignInputSource(FramedSource* newInputSource) { fInputSource = newInputSource; }  // Call before destruction if you want to prevent the destructor from closing the input source  void detachInputSource() { reassignInputSource(NULL); }  
FramedFilter往往通过其他类的AddFilter()接口挂载,需要主要注意的是,其析构 过程,往往不需要主动调用,因为AddFilter()后inputSource被FramedFilter彻底替换,最后被当作inputSource删除,而FramedFilter的析构函数会自动删除inputSource。所以不需要主动析构。

Sink类

最后Sink类的基类都以MediaSink作为基类, (AVIFileSink和QuickTimeFileSink除外,作为视频Container,可能会记录多路视频音频,故不直接继承自MediaSink)。此类最重要的两个接口:

  Boolean startPlaying(MediaSource& source,       afterPlayingFunc* afterFunc,       void* afterClientData);  virtual void stopPlaying();  
前文已经提过,startPlaying()是一切数据流动的开关,当调用startPlaying()后,一级级视频传送才真正开始。

类RTSPClient和MediaSession

最后,再简略地描述两个类:RTSPClientMediaSession,RTSPClient处理RTSP会话协议;而MediaSession处理一次视频会话,其封装了RTCP和RTP,可以包括多个MediaSubsession, 每个MediaSubsession指一路视频或音频流。
接下来以RTSPClient和 MediaSession两个类结合用于视频接收为例,描述整个运行流程:

  1. 首先创建RTSPClient对象,完成后立刻执行RTSPClient::sendDescribeCommand()。在回调函数中获得服务器SDP Description;
  2. 根据SDP Description创建MediaSession对象。MediaSession对象内部会分析SDP Description,自动创建MediaSubsession,一个MediaSubsession对应一路音视频流。
  3. 遍历每个MediaSubsession执行MediaSubsession::initiate()。此过程会创建RTCPinstance和RTPsource,做好接受数据的准备。
  4. 针对每路MediaSubsession,执行RTSPClient::sendSetupCommand()。
  5. 创建Sink类,并执行Sink->Startplaying(MediaSubsession->readSource(),Onclosure),启动接受数据,Onclosure()是回调函数,当服务器视频结束时自动调用。
  6. 执行RTSPClient::sendPlayCommand()。告诉服务器开始传送视频,此时视频传输真正开始。
  7. 传输过程中,如果外部想中断,只需设置BasicTaskScheduler0::doEventLoop(char* watchVariable)中的watchVariable置1,中断循环,然后析构各类对象(RTSPClient,MediaSession,Sink然后退出。当然优雅的做法还需向服务器发送sendTeardownCommand();
  8. 如果任其自然传输,最后的结果是服务器视频结束,内部自动执行上面设置的回调函数Sink->Onclosure(),收到后做扫尾工作。

几个注意点

  • 各个类基本的使用方法只需要参照testPrg目录下的代码,大多都能看懂,依样画芦不难。不过要做商用级产品,还需要注意内部的出错处理,遗憾的是,库内部对各种出错情况考虑不周,如接受视频帧过程中,网络中断居然内部没有任何处理;又如读写文件时如果发生异常,也没有任何处理措施。使用者不得不采用一些非常规的补救措施。
  • 因为特殊的运行结构,很多操作均是异步完成,优点是性能优异,响应及时,无阻塞,但是缺点也很明显 ,使用者必须使用众多的回调函数才能实现期望功能,造成代码可读性比较差,维护成本高。所以必须控制代码规模,复杂的逻辑最好放到上层实现。这点也是笔者失误后的经验之谈
  • 关于库本身不支持多线程的问题,在FAQ中也提到,有几种方案:1. 放弃多线程,因为库本身就支持多路视频;2.将用到的对象(如BasicUsageEnvironment,BasicTaskScheduler,RTSPclient 等等)打包,一个线程一份,互不干扰。我的项目即使用这个方法,经测试 确实可行。 3. 采用多进程
  • 关于对象的释放问题,因为特殊的结构,造成对象必须在heap上构建,在heap上释放,库内部的对象几乎都只能采用CreateNew()方法创建,Medium::Close()方法释放。所以,如果自己定义的子类,必须声明私有构造函数,私有虚析构函数。另外一个原则是,自己创建的类,自己负责释放,库内部不会自动释放(除了一些特殊情况,如FramedFilter)
  • 最后补充一句。因为回调函数必须声明为static, 造成不能使用this指针,总之尽量避免回调函数。