鸿蒙媒体子系统解读-编码录像流程解读

Posted 润和HiHope社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了鸿蒙媒体子系统解读-编码录像流程解读相关的知识,希望对你有一定的参考价值。

鸿蒙媒体子系统解读-编码录像流程解读

本文作者:江苏润和软件股份有限公司 王高浩


一、鸿蒙媒体子系统简介
媒体子系统旨在为多媒体应用开发者开发者提供统一的开发接口,使得开发者可以专注于应用业务的开发,轻松使用多媒体的资源。下图分别展现媒体子系统的框架及业务流程。

图 1 媒体子系统框架图

如图1,多媒体框架支持相机、录像和播放业务功能,这些功能支持鸿蒙JS应用开发及各种使用媒体能力的KIT模块开发,系统框架包括framework层,framework对外提供应用调用的native接口及其对应的业务实现,针对相机、录像及播放业务,framework实现了音视频输入输出,音视频编解码,视频文件的打包及解复用等功能。core service层,core service利用平台提供的能力去实现对底层硬件及相关驱动使用,另外core server实现文件管理,存储管理及日志管理。

图 2 多媒体业务流程图

如图2,多媒体包括Camera,Recorder和Player,Camera提供YUV、RGB、JPEG以及H264,H265数据到共享内存surface中,Recorder模块将surface中h264/h265数据和音频aac数据打包成mp4文件,Player模块把mp4文件解复用成音频和视频数据,分别送入对应解码器解码,然后进行播放。

目录结构

表1 轻量级多媒体子系统源代码目录结构

名称描述
foundation/multimedia/frameworks内部框架实现,包括Audio,Camera,Player,Recorder
foundation/multimedia/interfaces/kits应用接口对外头文件
foundation/multimedia/services/media_lite应用接口底层服务实现
foundation/multimedia/utils/lite应用接口通用模块实现
foundation/multimedia/hals硬件平台相关媒体适配接口头文件

使用
Native应用接口调用可以参考applications/sample/camera/media下demo实现。应用开发者使用多媒体接口实现录像、预览和播放音视频,使用可以参考《多媒体开发指南》。
本文解读媒体子系统里面的编码录像流程。

二、编码录像流程涉及的各模块

1、录像部分(Recorder子系统)的各功能模块

模块(类)名称功能
Recorder用于实现recorder功能的对外接口类,针对录像的各个方面做设置:设置源、音视频编码格式、视频尺寸、视频帧率、视频码率、音频通道数、最大录像时长、录像文件格式。。。以及一些操作:准备、开始、停止、暂停、继续、重置、释放。
RecorderImplRecorder类的具体实现。该类有两个成员变量:sourceManager_和recorderSink_,前者负责音视频的码流的获取,后者负责录像文件的写入。对Recorder类的所有设置和操作其实都由这两个变量实现。
RecorderSink与写录像文件功能相关。设置文件句柄、最大时长、最大字节数、创建复用器(muxer)、增加track、写文件、设置文件格式、用回调处理一般性消息和错误消息等。
RecorderVideoSource基本上的功能就是设置buffer和获取视频码流
RecorderAudiosource基本上的功能就是设置创建音频源和音频编码器,以及读取音频流。

2、视频编码部分的各功能模块

模块(类)名称功能
CameraDevice作用把CameraAbility(帧率和分辨率)里的参数传入设置摄像头,并初始化成员变量prcessorHdls_(处理器句柄)和prcessorAttrs_(处理能力属性)。并在运行之前把摄像头的输出状态设置为以下三者之一:Record、Preview、Capture,并根据输出状态设置对应的assistant。以及执行获取码流。
RecordAssistantCameraDevice类所拥有的用于录像的类,它的功能有设置视频编码器句柄、设置视频码流缓冲区、设置响应码流生成完毕消息的回调函数、以及开始执行编码。

需要注意的是音频的采集和编码功能是在Recorder子系统独立完成的,而视频的采集编码功能在CameraDevice和RecordAssistant模块中完成。

三、编码录像流程代码解读

applications\\sample\\camera\\media\\camera_sample.cpp是一个打开摄像头并且进行录像、预览、截图的例子,通过例子程序可以深入了解媒体子系统的编码录像流程。下文先对代码做尽可能的精简,再对剩余代码做简要的解释。

static int32_t SampleGetRecordFd()

	//创建一个后缀为mp4的文件并返回文件句柄,用于保存录像文件。

Recorder *SampleCreateRecorder()

//1、规定各种音视频编码参数
//2、Recorder *recorder = new Recorder();
    //3、用这些音视频参数设置recorder
    //4、return recorder;

//该类可以处理相机状态变化的回调;同时该类还可以执行record、preview、capture操作。
class SampleCameraStateMng : public CameraStateCallback 
public:
    int PrepareRecorder()
    
recorder_ = SampleCreateRecorder();		//创建Recorder对象
recordFd_ = SampleGetRecordFd();		//创建录像文件句柄
    
void StartRecord()		//执行Record
    
        int ret = PrepareRecorder();			//创建Recoder
        ret = recorder_->SetOutputFile(recordFd_);
        ret = recorder_->Prepare();			//设置Muxer
        ret = recorder_->Start();				//等待audioSource/videoSource里面的码流数据,写入Muxer
        //创建并设置FrameConfig *fc,主要是把surface设置进去
        ret = cam_->TriggerLoopingCapture(*fc);	//触发录像
    
;
int main()

    char input;
    while (cin >> input) 
        switch (input) 
            case '2':
                CamStateMng.StartRecord();		//执行record
                break; 
            default:
                SampleHelp();
                break;
        
    
    return 0;


关于Camera架构的部分已经在之前的文章中分析过了,所以现在只考虑在Camera的框架下,编码录像流程是如何工作的。由以上的代码可知,当用户输入‘2’的时候,会执行SampleCameraStateMng类的StartRecord函数,此时就会开始录像。因此从这个函数开始分析。

图3 StartRecord函数

StartRecord函数的核心就是图3的5个函数,接下来逐一分析。

3.1、PrepareRecorder函数
StartRecord函数首先会执行PrepareRecorder函数,它的功能有两个,第一个是创建和设置Recorder对象,第二个是创建用于保存录像文件的文件句柄。第二个非常简单,因此我们只分析第一个。第一个功能的代码如下图。

图4 创建和设置Recorder对象
可以看到该代码分为2个部分,第一个部分是红框前的代码,是为了满足业务需要而设置的各种音视频采集和编码参数;第二个部分是红框内的代码,创建一个Recorder类的对象,并用之前确定的各种参数来设置这个对象。至此recorder_对象创建和设置完成。不过recorder_对象只是一个包装,真正被设置的是包含在里面的sourceManager_变量,以及sourceManager_里面的videoSource变量和audioSource变量。sourceManager_的数据结构如下图所示。

图5 sourceManager_变量的数据结构

可以看到sourceManager_变量的核心部分是videoSource和audioSource,sourceManager_变量的功能就是维护这两个音视频的编码码流。

3.2、recorder_->SetOutputFile(recordFd_)函数
有了recorder_对象后,该函数将录像文件句柄recordFd_赋值给recorder_里的recorderSink_变量的outputFd_变量。留作备用。

3.3、recorder_->Prepare()函数
如前所述,Recorder类由RecorderImpl类具体实现,而RecorderImpl类又由内部的sourceManager_和recorderSink_变量来实现具体的设置和操作。其中sourceManager_又分为video和audio两部分。因此整个Prepare函数也就分为recorderSink_、video、audio这3个部分,分别做prepare,如下图所示。

图6 Prepare函数

第一个红框处的PrepareRecorderSink由recorderSink_->Prepare()来实现,它的作用就是将收集到的写录像文件句柄、录像文件最大时长、录像文件最大文件容量设置到recorderSink_的成员变量formatMuxerHandle_中,也就是创建了一个用于写录像文件的音视频复用器(Muxer)。
第二个红框处的PrepareVideoSource函数的功能是读出sourceManager_变量里的videoSource(视频源)的一系列设置:编码格式、图像宽高、编码带宽、帧率、关键帧间隔,并把这些设置加入formatMuxerHandle_中,作为要写入的录像文件的一个视频track。
第三个红框处的PrepareAudioSource函数的功能与PrepareVideoSource函数的功能大体类似,先利用audioSource的一系列设置做音频初始化,分为音频源初始化和音频编码器初始化。然后再把这些设置加入formatMuxerHandle_中,作为要写入的录像文件的一个音频track。

3.4、recorder_->Start()函数
与前面的函数类似,该Start函数同样分成三个部分:recorderSink、videoSource、audioSource分别做start,如下图所示。

图7 Start函数

第一个红框的recorderSink_->Start();比较简单,功能是1、设置Muxer(音视频复用器)的一般性消息和错误消息的回调。2、启动该Muxer,执行写录像文件的操作。
第二个红框的StartVideoSource();函数,作用是启动了一个新的线程,该线程执行VideoSourceProcess函数,该函数如下图所示。

图8 VideoSourceProcess函数

这个函数的功能是,它在一个线程中,不停的用第一个红框里的videoSource来获取视频码流,再用第二个红框里的recorderSink把码流写入Muxer(音视频复用器)里,也就是写到录像文件中。第二个红框的内容比较清晰,所以我们只分析第一个红框里的内容,也就是AcquireBuffer函数的功能。该函数代码如下图所示。

图9 AcquireBuffer函数

可以看出AcquireBuffer函数从RecorderVideoSource的surface_里面获取码流的起始地址和长度,然后把它们设置到buffer里面。那么surface_又是从哪里得到码流的呢?但是RecorderVideoSource类里面并没有明确的和编码器绑定的部分,这是需要弄清楚的。
第三个红框的StartAudioSource();函数,也是同样的,启动了一个新的线程,该线程执行AudioSourceProcess函数。该函数的功能也类似于VideoSourceProcess函数,它在一个线程中不停的用audioSource来获取音频码流,再用recorderSink把码流写入Muxer(音视频复用器)里,也就是写到录像文件中。然而不同之处在于audioSource包含自己的拾音器、编码器、缓冲区,整过过程是完整的,无需像videoSource那样从外界获取码流。

至此,录像部分的流程(Recorder子系统)就开始工作了。总结一下该流程:
a、先创建一个Recorder类的对象,它统领整个Recorder架构,并用满足业务需要的音视频采集和编码参数去设置它。
b、设置好该Recorder对象的Muxer(音视频复用器),并用上面设置好的音视频采集和编码参数创建和设置音视频2条track,再把这2条track加入到这个Muxer里面。并且还要设置好音频的采样源和编码器,但是无需对视频部分做设置。
c、最后启动这个Recorder对象,让它里面的audioSource(音频源)和videoSource(视频源)在2个单独的线程里面不断的获取编码数据,并将数据写入Muxer,最终写入录像文件。

虽然这个流程看上去是成立的,但是还不够完整。只有音频部分是完整的,视频部分没有设置和启动编码器,也没有告诉我们videoSource的surface_是如何初始化的,也没有告诉我们surface_是从哪里获取到视频码流的,以及Recorder架构和Camera架构是如何关联的。这些问题的答案在图3的第5个红框里面,也就是cam_->TriggerLoopingCapture(*fc)函数。

3.5、cam_->TriggerLoopingCapture(*fc)函数
重新贴一下图3的代码,再增加一个红框,观察一下fc变量是如何创建和初始化的。如下图所示。

图10 与编码和获取码流相关代码

图10的第一个红框里的recorder_->GetSurface(0)才真正的创建了recorder_里的surface_,接着surface_被设置了分辨率(1920*1080)等参数后,又被放置到fc中,而第二个红框则表明fc又被CameraImpl类的cam_对象使用。因此,Recorder架构和Camera架构发生关联,既Camera架构将Recorder架构所拥有的surface_对象作为自己的码流缓冲区,产生的码流放入这个缓冲区;而Recorder架构从这个缓冲区读取视频码流,写入录像文件。分析图10的第二个红框的代码可以得到更多的架构细节。

图11 CameraImpl类和CameraDevice类的TriggerLoopingCapture函数

图11的红色箭头表明cam_->TriggerLoopingCapture(*fc)函数是由device_对象的TriggerLoopingCapture(FrameConfig &fc)函数具体实现的。第一个红框的意思是如果fc被设置为FRAME_CONFIG_RECORD(录像)模式,那就会使用recordAssistant_对象来完成TriggerLoopingCapture函数。
第二个红框则是本函数的核心,它是将Recorder架构和Camera架构连接在一起的关键。它的代码如图12。

图12 SetFrameConfig函数

图12第一个红框的意思是根据我们在fc的surface_中设置的分辨率,在可用的分辨率组合attrs中找到最接近的那个,并以此来决定需要用到的那个处理器的Id(ProcessorIdx),最终得到处理器的设备Id(deviceId)。
第二个红框的意思是根据fc(分辨率和输出码流缓冲区)、attrs(最接近的帧率和分辨率)、deviceId(处理器的设备Id)来生成一个编码器句柄codecHdl。
第三个红框的意思是将回调函数列表recordCodecCb_和编码器句柄codecHdl绑定在一起。recordCodecCb_这个回调函数列表里面只有RecordAssistant::OnVencBufferAvailble这个函数是有效的,它的作用是当编码器句柄codecHdl有了码流以后,就把这段码流拷贝到codecHdl对应的缓冲区vencSurfaces_。
第四个红框的意思则表示所谓的vencSurfaces_里面包含的就是surface_。这样Recorder架构和Camera架构就连接在了一起。

回到图11的第三个红框,它最终调用了CodecStart(vencHdls_[i]);,意思就是启动编码器开始编码。至此,图11的TriggerLoopingCapture函数完成,Camera架构开始采集和编码,并且编码数据会被放到Recorder架构的surface_里面。同时,图10的StartRecord函数也一并完成,Recorder架构也开始工作。于是整个编码录像流程在两个架构的合作下就开始工作了。

四、整个编码录像流程总结

通过上文的分析,我们已经知道了整个编码录像的大体细节,但是不太连贯,因此现在我们来总结一下整个流程。
4.1、Camera架构初始化完成,cam_、device_、recordAssistant_这些视频编码相关对象已经创建完毕。
4.2、device_执行Initialize函数,用系统提供的各种可用的帧率+分辨率的组合来初始化device_里的prcessorAttrs_和prcessorHdls_,前者表示了这个组合对处理器算力的要求,后者表示为了完成这个任务需要分配什么样的处理器。
4.3、这时用户输入‘2’,执行SampleCameraStateMng类的StartRecord函数。该函数涉及Camera架构的部分会设定device_的工作模式为FRAME_CONFIG_RECORD,也就是录像模式。并且设定把Recorder架构的surface_变量传入device_。
4.4、根据在4.2初始化的prcessorAttrs_和prcessorHdls_,以及在4.3中设置的fc,创建处理器设备句柄deviceId和编码器句柄vencHdls_。并用回调函数规定当vencHdls_产生码流时,surface_变量接收该码流。
4.5、与此同时,4.3会创建和初始化Recorder类的对象recorder,它代表整个Recorder架构。然后设置recorder里面的Muxer(音视频复用器)、audioSource(音频采集、编码、音频码流获取和传送)、videoSource(视频码流获取和传送)。
4.6、启动recorder,于是audioSource和videoSource会在两个单独的线程里面等待各自的码流,然后写入Muxer,最终保存为文件。
4.7、启动编码器,产生码流。

编码录像流程的各模块各变量之间的关系如下图所示。

图13 编码录像流程的各模块各变量之间的关系

图13表示了各模块各变量之间的关系,其中2个紫色的粗线箭头表示中间的surface被左边的surface_赋值,而右边的vencSurfaces_又被中间的surface赋值;橙色的细线箭头表示变量之间的隶属关系,箭头所在变量隶属于箭尾所在变量。

以上是关于鸿蒙媒体子系统解读-编码录像流程解读的主要内容,如果未能解决你的问题,请参考以下文章

深度解读鸿蒙轻内核CPU占用率

从结构体内存池初始化到申请释放,详细解读鸿蒙轻内核的动态内存管理

从结构体内存池初始化到申请释放,详细解读鸿蒙轻内核的动态内存管理

鸿蒙OS开源代码精要解读之——init

万字解读鸿蒙轻内核物理内存模块

解读鸿蒙轻内核的监控器:异常钩子函数