文档内容拷贝自:
codebase_host/yunhal/opendoc/zh/yunhal_doc/video.md
联系人:@赵爱华
本文档简要描述了YunOS Multimedia Framework的整体设计。对于多媒体框架与硬件厂商的对接部分,本文着重阐述了基于Linux V4l2语义的Video Codec接口;我们可以称之为YunOS Video HAL。
下图从模块构成的角度展示了YunOS Multiemdia的架构图:
绿色模块是YunOS多媒体相关的内容
本文讨论的Multimedia,包含除Audio/Camera硬件封装外的所有多媒体相关内容,包括但不限于:MediaPlayer,MediaRecorder,硬件编解码(MediaCodec/V4L2Codec),MediaServer,SurfaceTexture, Image,MediaProvider,SoundPool,WifiDisplay, MediaSession,MTP,WebVideo,DRM,WebRtc,MDK(MediaDevKit),FFMpeg等。
作为与硬件厂商密切相关的部分,本文会着重阐述YunOS多媒体框架,硬件编解码适配;并概要介绍YunOS上视频通讯的解决方案(与系统中其他的厂商模块关系较为密切)。
上图是YunOS多媒体框架(Cow MMF)的框图:我们使用C++标准库,借鉴Gstreamer的模块化思想;实现了灵活但并不复杂的流媒体Pipeline Graph管理。
Cow MMF具备多媒体框架的三个显著特征:
https://en.wikipedia.org/wiki/Multimedia_framework
* 插件机制。每个媒体处理模块是一个独立的plugin;方便功能的扩展及模块的重用
* 直观无差别(intuitive)的编程接口。多媒体框架/模块的接口是通用的;不面向具体的业务,但可以支撑任何的多媒体业务。
* 数据在模块间自动流转。组件连接后,数据在模块间自动流转,业务层不需要关心buffer运转的细节。
除此之外,Cow MMF还具有满足多媒体业务的一些常见特点:
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继承来完成数据的处理。
对于厂商BSP中提供的OMXIL编解码功能,YunOS模块实现了类似android MediaCodec的模块进行加载;该模块采用了不同于android的实现方式,基于标准C++实现。MediaCodec被封装为Cow的plugin,从而实现了对android BSP的兼容。
与GStreamer的plugin类似,其他HW Codec也可以被封装为Cow的plugin;Cow中有对Intel/TI等厂家Codec接口的plugin封装。
厂商Codec的接口五花八门,对每一个codec进行封装对于厂商和操作系统来说都是繁杂的工作。我们需要统一的接口来简化厂商和操作系统的工作,如下图所示:
那么,这个标准应该是什么样子的呢?
OpenMax-IL本应该是个不错的选择,但事实上它把简单的问题复杂化了:
OpenMAX-IL是在2005年前后参考Linux的GStreamer制定的多媒体框架,它定义了多媒体模块连接/状态切换/流水线管理方式等细节。将它用作Codec的标准是大材小用,OMX的Codec背上了不必要的包袱(支持组件连接)。
OpenMAX-IL的接口是异步的,实现这些异步的接口,以及将异步的接口封装为MediaCodec的同步接口都需要花费较大的额外工作。同步转换为异步,然后异步又转换为同步,相当于做了无用功。
综合上面两点,我们还可以看到:OMX-IL虽然是国际标准,但是它被MediaCodec遮盖了–没有应用直接使用OMX-IL的接口。
V4L2(Video for Linux 2)是Linux kernel的视频采集(camera,v4l2src)和视频输出(v4l2sink, 类似directFB)接口;一个用于产生视频输出,一个用于消费视频数据。那么把这两个接口放到一个模块里面,有视频的输入和输出,使用已有的V4L2的语义就可以规范编解码的接口。它的优势在于:
我们不需要制定一个新的标准。制定新的标准需要花费很大的精力,又需要花费更大的精力去说服别人来接受这个标准。而Linux kernel的头文件是广泛使用的,它的语义完备并被大家所接受。
另一方面,为了支持编解码中的特定语义;我们对编解码的一些细节进行了规范:
* 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控制。
Yalloc的gtest测试程序位于yunhal/modules/video目录中,默认在编译image时会参于编译。
$ xmake yunhal_tests
此时v4l2 video codec的测试用例会参与编译
如需执行测试用例,先进入设备shell,然后执行以下命令即可自动化执行所有相关单元测试:
如果需要查看当前gest中的所有video部分测试用例,运行如下命令:
$ yunhal_tests --gtest_list_tests
当前video支持的测试用例如下:
v4l2DecoderTest. v4l2DecoderTest v4l2EncoderTest. v4l2EncoderTest_load_unload v4l2EncoderTest_color_format v4l2EncoderTest_size v4l2EncoderTest_frame_count
V4L2 Linux Kernel Header
/usr/include/linux/videodev2.h
/usr/include/linux/types.h
视频通讯中牵涉的模块较多,与厂商的关系较为密切。以WebRTC作为例子,我们简要介绍一下YunOS上的实现方案;可以作为ViLTE等实现方案的参考。视频通讯中的硬件加速部分,平台相关性较大;而这部分功能对应多媒体播放和录制的基本功能,通过重用多媒体系统已有的模块可以简化实现方案。框架如下图所示:
为了完善全栈的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的回收。
接口类的实现包含在接口文件中,避免增加额外的模块依赖
使用简单数据类型保持较好的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; }