文档中心 > 芯片厂商开放

文档内容拷贝自:codebase_host/yunhal/opendoc/zh/yunhal_doc/video.md
联系人:@赵爱华

1. 概述(Introduction)

本文档简要描述了YunOS Multimedia Framework的整体设计。对于多媒体框架与硬件厂商的对接部分,本文着重阐述了基于Linux V4l2语义的Video Codec接口;我们可以称之为YunOS Video HAL。

2. 架构(Multimedia Framework Architecture)

2.1. Multimedia架构图

下图从模块构成的角度展示了YunOS Multiemdia的架构图:
01-multimedia-overview
绿色模块是YunOS多媒体相关的内容

2.2. Multimedia的模块

本文讨论的Multimedia,包含除Audio/Camera硬件封装外的所有多媒体相关内容,包括但不限于:MediaPlayer,MediaRecorder,硬件编解码(MediaCodec/V4L2Codec),MediaServer,SurfaceTexture, Image,MediaProvider,SoundPool,WifiDisplay, MediaSession,MTP,WebVideo,DRM,WebRtc,MDK(MediaDevKit),FFMpeg等。
作为与硬件厂商密切相关的部分,本文会着重阐述YunOS多媒体框架,硬件编解码适配;并概要介绍YunOS上视频通讯的解决方案(与系统中其他的厂商模块关系较为密切)。

2.3. YunOS多媒体框架概要

2.3.1. Multimedia Framework

02-cow-MMF-overview

上图是YunOS多媒体框架(Cow MMF)的框图:我们使用C++标准库,借鉴Gstreamer的模块化思想;实现了灵活但并不复杂的流媒体Pipeline Graph管理。
Cow MMF具备多媒体框架的三个显著特征:
https://en.wikipedia.org/wiki/Multimedia_framework
* 插件机制。每个媒体处理模块是一个独立的plugin;方便功能的扩展及模块的重用
* 直观无差别(intuitive)的编程接口。多媒体框架/模块的接口是通用的;不面向具体的业务,但可以支撑任何的多媒体业务。
* 数据在模块间自动流转。组件连接后,数据在模块间自动流转,业务层不需要关心buffer运转的细节。

除此之外,Cow MMF还具有满足多媒体业务的一些常见特点:

  • 业务层的接口是异步的。应用调用多媒体接口不会被阻塞,方便对用户进行及时响应
  • 模块/plugin的API(主要是状态切换)是异步的,从而确保各模块的并发运行,提高响应速度
  • 为了确保状态转换和数据处理一致性,在业务层和模块之间有层Pipeline确保宏观操作的顺序执行。(比如,后触发的stop不能先于之前的seek被响应)
  • 多媒体框架(Pipeline)通过factory管理组件,避免了业务与组件之间的硬关联。

2.3.2. Multimedia Component

YunOS多媒体组件的接口定义如下所示:

class Component {
    struct Reader {
        virtual mm_status_t read(MediaBufferSP & buffer) = 0;
        virtual MediaMetaSP getMetaData() = 0;
    };
    typedef MMSharedPtr<Reader> ReaderSP;

    struct Writer {
        virtual mm_status_t write(const MediaBufferSP & buffer) = 0;
        virtual mm_status_t setMetaData(const MediaMetaSP & metaData) = 0;
    };
    typedef MMSharedPtr<Writer> WriterSP;

    virtual ReaderSP getReader(MediaType mediaType) = 0;
    virtual WriterSP getWriter(MediaType mediaType) = 0;

    virtual mm_status_t addSource(Component * component, MediaType mediaType) = 0;
    virtual mm_status_t addSink(Component * component, MediaType mediaType) = 0;

    virtual mm_status_t setListener(ListenerSP listener);

    virtual mm_status_t prepare() { return MM_ERROR_SUCCESS; }
    virtual mm_status_t start() { return MM_ERROR_SUCCESS; }
    virtual mm_status_t stop() { return MM_ERROR_SUCCESS; }
    virtual mm_status_t pause() { return MM_ERROR_SUCCESS; }
    virtual mm_status_t resume() { return MM_ERROR_SUCCESS; }
    virtual mm_status_t seek(int msec, int seekSequence);
    virtual mm_status_t reset() { return MM_ERROR_SUCCESS; }
    virtual mm_status_t flush() { return MM_ERROR_SUCCESS; }
    virtual mm_status_t drain() { return MM_ERROR_SUCCESS; }

    virtual mm_status_t pushData(MediaBufferSP & buffer) { return MM_ERROR_UNSUPPORTED; }
    virtual mm_status_t setClock(ClockSP clock) { return MM_ERROR_SUCCESS; }
    virtual ClockSP provideClock() { return ClockSP((Clock*)NULL); }
}

前半部分API管理组件连接。addSource表示当连接其他组件的时候,该组件主动从上游组件拉取(pull)数据进行处理;addSink表示连接其他组件的时候,该组件主动将处理完的数据推送(push)给下游组件。连接后,组件之间可以自主地通过ReaderSP和WriterSP交换数据。
后半部分支持流水线的状态切换,规范了状态切换时组件需要完成的任务。比如,stop与start相对,断开组件之间的数据连接:释放引用的外部对象(buffer/ReaderSP, WriterSP)。reset与prepare相对,释放组件内部分配的资源(引用计数减为0,真正释放资源)。这样可以保证状态转换过程并发有序。如果组件stop的时候没有释放peer的资源,或者在stop的时候释放自己的资源,都会导致SP对象析构时,所属的组件已经卸载了;产生异常。这部分接口一般是异步的,从而使得各组件的状态切换并发进行。

不同功能的组件可以从这个接口继承,完成具体的任务,如下图所示。编解码组件一般从FilterComponent继承来完成数据的处理。
03-cow_comp

3. 实现(Implement Video HAL)

3.1 Legacy编解码接口的支持

对于厂商BSP中提供的OMXIL编解码功能,YunOS模块实现了类似android MediaCodec的模块进行加载;该模块采用了不同于android的实现方式,基于标准C++实现。MediaCodec被封装为Cow的plugin,从而实现了对android BSP的兼容。
与GStreamer的plugin类似,其他HW Codec也可以被封装为Cow的plugin;Cow中有对Intel/TI等厂家Codec接口的plugin封装。

3.2 YunOS Video HAL

厂商Codec的接口五花八门,对每一个codec进行封装对于厂商和操作系统来说都是繁杂的工作。我们需要统一的接口来简化厂商和操作系统的工作,如下图所示:
04-video-codec-common-if

那么,这个标准应该是什么样子的呢?

3.2.1. OpenMax-IL不是一个好的选择

05-omx-il-diagram

OpenMax-IL本应该是个不错的选择,但事实上它把简单的问题复杂化了:

OpenMAX-IL是在2005年前后参考Linux的GStreamer制定的多媒体框架,它定义了多媒体模块连接/状态切换/流水线管理方式等细节。将它用作Codec的标准是大材小用,OMX的Codec背上了不必要的包袱(支持组件连接)。

OpenMAX-IL的接口是异步的,实现这些异步的接口,以及将异步的接口封装为MediaCodec的同步接口都需要花费较大的额外工作。同步转换为异步,然后异步又转换为同步,相当于做了无用功。

综合上面两点,我们还可以看到:OMX-IL虽然是国际标准,但是它被MediaCodec遮盖了–没有应用直接使用OMX-IL的接口。

3.2.2. V4L2Codec是更好的选择

V4L2(Video for Linux 2)是Linux kernel的视频采集(camera,v4l2src)和视频输出(v4l2sink, 类似directFB)接口;一个用于产生视频输出,一个用于消费视频数据。那么把这两个接口放到一个模块里面,有视频的输入和输出,使用已有的V4L2的语义就可以规范编解码的接口。它的优势在于:

我们不需要制定一个新的标准。制定新的标准需要花费很大的精力,又需要花费更大的精力去说服别人来接受这个标准。而Linux kernel的头文件是广泛使用的,它的语义完备并被大家所接受。

  • V4L2的接口是同步的,对它的实现和使用都比较简单。配合事件通知的机制(device poll)可以方便地实现数据处理的调度。
  • V4l2Codec的语义比OMX更加丰富。比如在input/output buffer上分别支持mmap和user_ptr方式,从而尽可能地避免数据的拷贝。通过S_FMT接口可以指定ES stream的类型,避免逐Byte的sync code的扫描。
  • V4L2Codec的实现复杂度远低于OMX方案。与android上面的实现对比,V4l2Codec相当于OMX-IL+MediaCodec两层封装合并为一层,避免了不必要的同步/异步/同步的转换,避免了不必要的复杂性(框架属性)。

另一方面,为了支持编解码中的特定语义;我们对编解码的一些细节进行了规范:
* flush()通过STREAM_OFF/STREAM_ON来实现
* 额外的视频格式信息(extra_data)通过S_FMT操作来传递,
* 添加新的event用于支持resolution的动态变化
* 支持YunOS的surface buffer handle
* 区分了buffer点名(类似OMX_UseBuffer)和buffer输入(类似OMX_FillThisBuffer)的区别;在STREAM_OFF之前输入的buffer用于buffer点名而不是真正的输入。

我们还进一步规范了厂商动态库的加载流程,将相应的接口提交到了开源项目中:https://github.com/01org/libyami/blob/master/v4l2/v4l2codec_device_ops.h。

简单来讲,厂商只要支持下述数据结构中的函数就可以了:

typedef struct V4l2CodecOps {
    uint32_t mSize;
    V4l2CodecVersion mVersion;
    char mVendorString[V4L2CODEC_VENDOR_STRING_SIZE];

    V4l2OpenFunc mOpenFunc;
    V4l2CloseFunc mCloseFunc;
    // useful operation happens with different io command
    V4l2IoctlFunc mIoctlFunc;
    // poll for codec notification, for example: available of input/output buffer, video resolution change etc
    V4l2PollFunc mPollFunc;
    // escape the blocked poll thread above during exiting
    V4l2SetDevicePollInterruptFunc mSetDevicePollInterruptFunc;
    // make mPollFunc() blocked when there is no event from codec
    V4l2ClearDevicePollInterruptFunc mClearDevicePollInterruptFunc;
    V4l2MmapFunc mMmapFunc;
    V4l2MunmapFunc mMunmapFunc;

    // pass vendor specific control with key-value pair
    V4l2SetParameterFunc mSetParameterFunc;
    // deprecate
    V4l2UseEglImageFunc mUseEglImageFunc;
} V4l2CodecOps;

通常来讲V4l2Codec的实现复杂度只有OMX Codec的1/10,CPU占用率也可以低1/3左右。

下图是在MediaPlayer中使用V4l2Codec进行视频播放的大致流程:V4L2Codec和Vl42__poll_thread是硬件厂商需要实现的部分。在多媒体框架层,V4l2Codec被封装成VDV4L2组件(包括VideoDecodeV4l2/VDV4L2__MMMsgThread/VDV4L2__poll_thread多个线程);视频显示通过VideoSink控制。
06-V4L2Codec

4. 测试(Vendor Test Suite)

4.1. Build Test Suite

Yalloc的gtest测试程序位于yunhal/modules/video目录中,默认在编译image时会参于编译。

$ xmake yunhal_tests

此时v4l2 video codec的测试用例会参与编译

4.2. Run Test Suite

如需执行测试用例,先进入设备shell,然后执行以下命令即可自动化执行所有相关单元测试:

如果需要查看当前gest中的所有video部分测试用例,运行如下命令:

$ yunhal_tests --gtest_list_tests

当前video支持的测试用例如下:

v4l2DecoderTest.
v4l2DecoderTest
v4l2EncoderTest.
v4l2EncoderTest_load_unload
v4l2EncoderTest_color_format
v4l2EncoderTest_size
v4l2EncoderTest_frame_count

5. 附录(Appendix)

5.1 参考网站(References)

5.2 视频通讯解决方案

视频通讯中牵涉的模块较多,与厂商的关系较为密切。以WebRTC作为例子,我们简要介绍一下YunOS上的实现方案;可以作为ViLTE等实现方案的参考。视频通讯中的硬件加速部分,平台相关性较大;而这部分功能对应多媒体播放和录制的基本功能,通过重用多媒体系统已有的模块可以简化实现方案。框架如下图所示:
07-werbrtc-yunos

5.2.1 多媒体接口

为了完善全栈的Buffer生命周期管理,避免在模块接口处进行数据的拷贝。我们在定义WebRTC和Multimedia之间的数据接口时采用了如下规则:
* 为了避免拷贝,完善frame data的生命周期管理;需要定义基类VideoFrame,它是生产者和消费者之间数据共享的接口
* Frame的使用分为生产者和消费者。生产者对Frame负有更大的管理责任;消费者通过基类的接口使用Frame。

在录制本地视频的场景中,VideoFrame的生产者是Encoder,消费者是WebRTC

在播放远端视频的场景中,VideoFrame的生产者是WebRTC,消费者是Decoder
  • 生产者责任

    VideoFrame的创建由生产者负责,并通过SetFrameInfo()初始化信息。

    生产者内部一般有一个实体buffer与VideoFrame相关联;它在继承VideoFrame的时候,会维护这个实体buffer的reference。

    VideoFrame的生产者通过重载CleanUp()回收这个实体buffer

  • 消费者责任

    消费者在使用完VideoFrame后,通过ReleaseVideoFrame()销毁VideoFrame,自动触发生产者重载的CleanUp()函数完成buffer的回收。

  • 为VideoFrame添加了更多的Flag,便于今后的扩展
  • 避免模块依赖和保持接口ABI兼容性:

    接口类的实现包含在接口文件中,避免增加额外的模块依赖

    使用简单数据类型保持较好的ABI兼容性

    class VideoFrame {
      public:
    VideoFrame()
        : data_(NULL)
        , data_size_(0)
        , time_stamp_(-1)
        , flags_(0)
        { }
    
    // Called by VideoFrame producer to set video frame information.
    bool SetFrameInfo(uint8_t* data, size_t data_size, int64_t time_stamp) {
        data_ = data;
        data_size_ = data_size;
        time_stamp_ = time_stamp;
        return true;
    }
    
    // Called by consumer to retrieve video frame information of compressed data
    // uses ‘&’ as much as possible, make the code simpler
    bool GetFrameInfo(uint8_t* &data, size_t &data_size, int64_t &time_stamp) {
        data = data_;
        data_size = data_size_;
        time_stamp = time_stamp_;
        return true;
    }
    
    // flags for VideoFrame
    enum VideoFrameFlag {
        VFF_KeyFrame   = 1,        // IDR frame
        VFF_NoneRef    = 1 << 1,   // can be discarded w/o impact to other frame
        VFF_EOS        = 1 << 2,   // the final video frame
    };
    
    void SetFlag(VideoFrameFlag flag) { flags_ |= flag; }
    void ClearFlag(VideoFrameFlag flag) { flags_ &= !flag; }
    bool IsFlagSet(VideoFrameFlag flag) { return flags_ & flag; }
    void ClearFlags() { flags_ = 0; }
    
    // cleanup resources allocated by sub-class
    // A pure virtual func ensures the base class not be used by mistake, since the base class isn't useful enough.
    virtual bool CleanUp() = 0;
    static void ReleaseVieoFrame(VideoFrame* frame) {
        if (!frame)
            return;
        // CleanUp() can’t be done inside ~VideoFrame(), since sub-class has already released there
        frame->CleanUp();
        delete frame;
    }
    
    // make sure VideoFrame can't be freed directly, but through ReleaseVideoFrame()
      protected:
    virtual ~VideoFrame() { }
      private:
    uint8_t* data_;
    size_t data_size_;
    int64_t time_stamp_;
    uint32_t flags_;
    int32_t padding[11]; // for future extension
    };
    class VideoDecoderInterface {
      public:
    // construction function is interface between app and Multimedia, not list here
    // explicit VideoDecoderInterface (void * surface, int surfaceType = 0/* platform dependent surface type */);
    virtual ~VideoDecoderInterface() {}
    virtual bool InitDecodeContext(MultimediaCodec::VideoCodec video_codec) = 0;
    virtual bool Release() = 0;
    // decode one frame
    virtual bool OnFrame(VideoFrame* frame) = 0;
    // update playback time for video sync: pair absolute time (real_time) with media time stamp
    virtual bool UpdateMediaClock(int64_t pts, int64_t real_time) = 0;
    };
    class VideoEncoderInterface {
      public:
    // listener for Encoder
    class VideoEncoderListener {
        // triggerd when a new encoded video frame is ready
        virtual bool EncodedFrameReady(VideoFrame *frame) = 0;
    };
      public:
    // construction function is interface between app and Multimedia, not list here
    // explicit VideoEncoderInterface (void* camera, void* preview_surface)
    virtual ~VideoEncoderInterface() {}
    virtual bool InitEncodeContext(MultimediaCodec::VideoCodec video_codec, size_t width, size_t height,
                                  uint32_t start_bitrate, uint32_t max_framerate) = 0;
    // create the customized encoder for each peer connetion
    virtual VideoEncoderInterface* Copy() = 0;
    virtual bool Release() = 0;
    
    // encoded video frame will be emitted to listener after SetListener
    virtual bool SetListener(VideoEncoderListener *listener) = 0;
    // Retrieves one video frame from encoder if listener is not used.
    virtual bool EncodeOneFrame(VideoFrame* &frame, bool request_key_frame) = 0;
    // change target bitrate and framerate on the fly
    virtual bool RequestParameterUpdate(uint32_t bitrate, uint32_t framerate) = 0;
    };

对于VideoFrame在实际场景中的应用,以encode pipeline为例:

class VideoEncodeFrame : public VideoFrame {
  public:
    // not important in this case
    virtual bool CleanUp() { mMediaBuffer.reset(); return true; }
    bool setMediaBuffer(MediaBufferSP MediaBuffer);
  private:
    MediaBufferSP mMediaBuffer;
};
bool VideoEncodeFrame::setMediaBuffer(MediaBufferSP mediaBuf)
{
    if (!mediaBuf)
        return false;

    uint8_t * data = NULL;
    int32_t offset = 0;
    int32_t size = 0;
    // get video info of internal buffer
    mediaBuf ->getBufferInfo((uintptr_t*)&data, &offset, &size, 1);

    size -= offset;
    data += offset;
    if (size <=0) {
        MMLOGE("invalid size: %d\n", size);
        return false;
    }

    // populate buffer info to VideoFrame
    SetFrameInfo(data, size, mediaBuf ->pts());
    if (mediaBuf ->isFlagSet(MultimediaBuffer::MBFT_KeyFrame))
        SetFlag(VideoFrame::VFF_KeyFrame);

    // hold an reference of mediaBuf to extend the buffer life time
    mMediaBuffer = mediaBuf;
    return true;
}

FAQ

关于此文档暂时还没有FAQ
返回
顶部