本文作者:江苏润和软件股份有限公司 王高浩
一、简介 媒体子系统旨在为多媒体应用开发者提供统一的开发接口,使得开发者可以专注于应用业务的开发,轻松使用多媒体的资源。下图分别展现媒体子系统的框架及业务流程。
如图1,多媒体框架支持相机、录像和播放业务功能,这些功能支持鸿蒙JS应用开发及各种使用媒体能力的KIT模块开发,系统框架包括framework层,framework对外提供应用调用的native接口及其对应的业务实现,针对相机、录像及播放业务,framework实现了音视频输入输出,音视频编解码,视频文件的打包及解复用等功能。core service层,core service利用平台提供的能力去实现对底层硬件及相关驱动使用,另外core server实现文件管理,存储管理及日志管理。
如图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实现。应用开发者使用多媒体接口实现录像、预览和播放音视频,使用可以参考《多媒体开发指南》。 本文解读媒体子系统里面的Camera子系统。
二、Camera子系统解读 1、Camera的各功能模块
模块(类)名称 功能 CameraAbility 摄像头的能力相关,主要是摄像头所产生的图像的分辨率和帧率 CameraConfigImpl 摄像头配置功能,主要是设置帧状态回调 CameraDevice 实现RecordAssistant、PreviewAssistant、CaptureAssistant、CameraDevice四个类。主要是将前三个类的对象与摄像头设备相关联:Record设置 vencHdls_、vencSurfaces_、vencAttr_这3个属性、设置产生码流后的回调、启动和停止编码器;Preview则没有这3个属性,也没有回调;Capture有2个属性,但是没有回调,以阻塞方式获取图片。CameraDevice类则是把CameraAbility里的参数传入设置摄像头,并初始化成员变量prcessorHdls_和prcessorAttrs_。并在运行之前把摄像头的输出状态设置为以下三者之一:Record、Preview、Capture,并根据输出状态设置对应的assistant。 CameraImpl 实现一个类CameraImpl,主要的功能把CameraDevice对象和CameraConfig对象、FrameConfig对象、EventHandler对象、CameraStateCallback对象关联在一起。并且用这些对象对CameraDevice进行设置和操纵,并用EventHandler对象发送一些回调消息。 CameraKit 全局唯一变量,是整个架构的入口,同时里面隐含了cameraManager_变量。 CameraManagerImpl 全局唯一变量,该变量维护了一个空闲摄像头池,整个摄像头架构都围绕着对这些摄像头的操作展开。 CameraService 全局唯一变量,该变量维护着摄像头的ability和device CameraStateCallback 该类设置和维护用户要摄像头执行的业务功能,比如record、preview、capture。该类用回调函数响应消息的方式来执行。该类为纯虚类,需要开发者自己派生新类来实现自己的业务逻辑。2、对Camera的例子程序的分析 applications\sample\camera\media\camera_sample.cpp是一个打开摄像头并且进行录像、预览、截图的例子,通过例子程序可以深入了解camera各模块的功能和大体流程。本文先对代码做尽可能的精简,再对剩余代码做简要的解释。
static int32_t SampleGetRecordFd() { //创建一个后缀为mp4的文件并返回文件句柄,用于record。 } static void SampleSaveCapture(const char *p, uint32_t size) { //创建一个后缀为jpg的文件,作为capture文件,并把传入的内存p保存其中。 } Recorder *SampleCreateRecorder() { //1、规定各种音视频编码参数 //2、Recorder *recorder = new Recorder(); //3、用这些音视频参数设置Recorder //4、return recorder; } //处理capture的回调类 class SampleFrameStateCallback : public FrameStateCallback { void OnFrameFinished(Camera &camera, FrameConfig &fc, FrameResult &result) override { if (fc.GetFrameConfigType() == FRAME_CONFIG_CAPTURE) { //从fc里面获取surface,再从surface里面得到编码后的buffer, //再调用SampleSaveCapture()把buffer存成jpg文件。 } } }; //该类可以处理相机状态变化的回调;同时该类还可以执行record、preview、capture操作。 //该类是本例子的重点 class SampleCameraStateMng : public CameraStateCallback { public: SampleCameraStateMng(EventHandler &eventHdlr) : eventHdlr_(eventHdlr) {} void OnCreated(Camera &c) override { //设置cam_,也就是摄像头实例,在其里面带入了config, //而config里面带入了fsCb_和eventHdlr,也就是帧状态改变回调和消息句柄 } int PrepareRecorder() { recorder_ = SampleCreateRecorder(); //创建音视频编码器 recordFd_ = SampleGetRecordFd(); //创建mp4文件句柄 } //执行record void StartRecord() { int ret = PrepareRecorder(); ret = recorder_->SetOutputFile(recordFd_); ret = recorder_->Prepare(); ret = recorder_->Start(); //建立输入输出通路 //创建并设置FrameConfig *fc,主要是把surface设置进去 ret = cam_->TriggerLoopingCapture(*fc); //触发record } void StartPreview() { //基本类似于StartRecord() } void Capture() { //类似于StartPreview(),不过只做一次 } }; int main() { CameraKit *camKit = CameraKit::GetInstance(); //返回CameraKit实例 list<string> camList = camKit->GetCameraIds(); //返回空闲的camId string camId; //找到第一个包含了1080p分辨率的ability,并让camId = 该摄像头的camId。 EventHandler eventHdlr; // Create a thread to handle callback events SampleCameraStateMng CamStateMng(eventHdlr); //在该类中实现用户的业务逻辑 camKit->CreateCamera(camId, CamStateMng, eventHdlr); //创建摄像头,本例子的核心 char input; while (cin >> input) { switch (input) { case '1': CamStateMng.Capture(); //执行capture break; case '2': CamStateMng.StartRecord(); //执行record break; case '3': CamStateMng.StartPreview(); //执行preview break; case 's': CamStateMng.Stop(); break; case 'q': CamStateMng.Stop(); goto EXIT; default: SampleHelp(); break; } } EXIT: cout << "Camera sample end." << endl; return 0; }
从以上对代码的阅读可知,本例子大体流程是将所要实现的功能在CamStateMng对象里面实现,然后通过函数camKit->CreateCamera(camId, CamStateMng, eventHdlr)来创建摄像头实例,在创建过程中用CamStateMng对象和其它2个参数对这个摄像头实例做一系列的设置。然后我们访问CamStateMng对象,就可以通过它提供的各种方法,对它背后的摄像头实例执行所需的各种操作。
所以我们首先来了解为了执行函数camKit->CreateCamera而所需的各种变量是怎么创建和设置的,也就是camKit、camId、CamStateMng、eventHdlr这4个。其中,最关键的是CamStateMng。
2.1. camKit
CameraKit *camKit = CameraKit::GetInstance();
CameraKit::GetInstance()本身很简单。阅读代码后可知camKit是全局唯一的,它的作用是初始化和维持整个摄像头架构。另外它实际上是对CameraManager *cameraManager_的封装。cameraManager_的功能是管理空闲摄像头池,可以认为是创建和操纵摄像头的真正入口,开发者实际上是通过它来创建和访问摄像头,然后就可以对摄像头做各种设置和操纵。当然开发者只需关心camKit而无需关心cameraManager_。
2.2. camId
listcamList = camKit->GetCameraIds(); string camId; for (auto &cam : camList) { cout << "camera name:" << cam << endl; const CameraAbility *ability = camKit->GetCameraAbility(cam); /* find camera which fits user's ability */ listsizeList = ability->GetSupportedSizes(0); if (find(sizeList.begin(), sizeList.end(), CAM_PIC_1080P) != sizeList.end()) { camId = cam; break; } } if (camId.empty()) { cout << "No available camera.(1080p wanted)" << endl; return 0; }
意思是camKit返回空闲的摄像头id列表,并逐一获取每个id所对应的ability,然后确定该ability是否包含了1080p的能力,如果是的话,就返回该空闲id当作camId;否则如果没有任何ability满足要求的话,程序退出。
获取ability是通过const CameraAbility *ability = camKit->GetCameraAbility(cam);来实现的,如前所述,实际的访问是通过cameraManager_来完成的。该空闲摄像头池的数据结构为map cameraMapCache_,string是空闲摄像头的ID号,CameraImpl则是可以操作该摄像头的类,ability就在该类里面。
2.3. CamStateMng 该对象所在类为CameraStateCallback,它的作用有两个:① 用回调来处理一些相机相关的状态变化消息,比如OnCreated()、OnReleased()、OnConfigured();② 实现一些功能供用户操作摄像头,也就是StartRecord()、StartPreview()、Capture()、Stop()。
OnCreated()的实现是先创建一个CameraConfig类的config对象,然后再把SampleFrameStateCallback类的fsCb_对象和EventHandler类的eventHdlr_对象都放在config里面。再把config放在CamStateMng的成员变量cam_里面。其中fsCb_负责用回调的方式把capture的输出保存为jpg文件,eventHdlr则是一个可以发送消息的句柄。
StartRecord()的功能是创建一个音视频编码器,并将码流输出文件的句柄recordFd_也设置在编码器里,然后启动该编码器,并从这个编码器里面获取surface,存放在FrameConfig *fc里面,再用cam_->TriggerLoopingCapture(*fc)这句话去触发capture。如前所述,cam_是在CamStateMng类的OnCreated(Camera &c)函数中被设置的。要注意的是码流输出文件句柄被设置在编码器中,因此每次捕获码流和输出都是该编码器自动完成的。
StartPreview()的功能类似于StartRecord(),但是并不创建音视频输出器,也不获取输出文件句柄,因此可以推断他的输出既不编码也不会存文件,只是在内存输出。
Capture()则是一次性的保存为jpg文件,这就需要用到在OnCreated()中设置的回调对象SampleFrameStateCallback fsCb_,具体的语句是config->SetFrameStateCallback(&fsCb_, &eventHdlr_),理论上fsCb_可以以回调方式处理eventHdlr_发来的各种帧状态变化消息,但是fsCb_实际上只实现了对于OnFrameFinished消息的FRAME_CONFIG_CAPTURE类型的处理,也就是只处理capture消息。
2.4. eventHdlr 该对象的设置很简单,直接定义该变量即可使用。
EventHandler eventHdlr; // Create a thread to handle callback events SampleCameraStateMng CamStateMng(eventHdlr);
2.5. 此时所需的变量都已经成功设置,是有效的变量,然后我们执行创建摄像头函数camKit->CreateCamera(camId, CamStateMng, eventHdlr)。如上文所述,此时本例子的所有功能都已经实现,因此CreateCamera()的作用仅仅是把他们有序的整合在同一个camera对象中,使得camera对象可以正确的执行。下面对这部分代码做梳理。
void CameraKit::CreateCamera(const string &cameraId, CameraStateCallback &callback, EventHandler &handler) { cameraManager_->CreateCamera(cameraId, callback, handler); }
如前所述,CameraKit类只是个包装,实际的功能都由cameraManager_来实现,具体实现如下。
void CreateCamera(const string &cameraId, CameraStateCallback &callback, EventHandler &handler) override { auto p = cameraMapCache_.find(cameraId); p->second->RegistCb(callback, handler); cameraService_->CreateCamera(cameraId); }
其中,cameraMapCache_是空闲摄像头池,它的定义为map cameraMapCache_;,因此p->second是一个CameraImpl类的对象。
void CameraImpl::RegistCb(CameraStateCallback &callback, EventHandler &handler) { handler_ = &handler; stateCb_ = &callback; }
所以p->second->RegistCb(callback, handler);的作用就是把callback和handler存放在CameraImpl类的变量stateCb_和handler_中。
void CameraService::CreateCamera(string cameraId) { cameraServiceCb_->OnCameraStatusChange(cameraId, CameraServiceCallback::CAMERA_STATUS_CREATED, device_); }
其中,device_是在CameraService::InitCameraDevices()中创建的,跟踪代码可以发现是在CameraKit::CameraKit()这个构造器中被创建的,所以可以认为是在整个架构的初始化中创建的,当调用OnCameraStatusChange时已经是有效的。
OnCameraStatusChange的实现如下:
void OnCameraStatusChange(string &cameraId, CameraStauts status, CameraDevice *device = nullptr) override { auto p = cameraMapCache_.find(cameraId); switch (status) { //。。。 case CAMERA_STATUS_CREATED: if (p != cameraMapCache_.end()) { p->second->OnCreate(device); } break; } }
如前所述,p是CameraManagerImpl类管理的空闲内存池里面的某个摄像头,因此p->second->OnCreate(device)的具体实现如下。
void CameraImpl::OnCreate(CameraDevice *device) { device_ = device; handler_->Post([this] { this->stateCb_->OnCreated(*this); }); }
所以它的作用第一个是给代表这个摄像头的CameraImpl类的device_赋值。由于device是唯一的,所以这也暗示了鸿蒙目前只能处理一个摄像头。第二个是发OnCreated消息给stateCb_。如前所述,stateCb_就是CamStateMng,它的OnCreated()函数已经在前面解读过,不再赘述。不过要值得注意的是OnCreated()的参数是*this,也就是CamStateMng的cam_被赋值成该CameraImpl的对象。
此时,所有的设置都已完成,调用CamStateMng对象的各个方法就可以正确运行了。
3、camera的各模块在camera的整个架构中的相互关系
我们可以看到,大体的架构是camKit或者cameraManager_在维护一个空闲摄像头池。每个空闲摄像头被一个二元组map 所代表,string是摄像头的ID,CameraImpl是摄像头的数据结构。而该CameraImpl处于核心地位,设置和操作摄像头所需的数据结构很多都在这个类里面,同时别的类也把这个类的对象作为成员变量,以方便访问。相互之间的关系图如下。
图5显示了camera架构是如何通过camKit访问到CameraImpl的。接下来我们看一下CameraImpl模块又是如何可以访问到整个架构的其他各个模块的数据,以及和其他各个模块的交互,见图6。
图6有2种箭头线,实线表示指针关系,箭尾所在的变量表示要被赋值的指针,箭头所在的变量表示内存地址;虚线箭头表示参数传递关系,箭尾所在的变量表示要被传递的参数,箭头所在表示被创建出来的对象所在的类。
可以看到相互之间的赋值关系是错综复杂的,不过还可以看出基本方式是先在别的类里面创建和初始化完毕他们的成员变量,然后再把这些成员变量直接和间接的赋值到CameraImpl类的成员变量中,这是本架构最重要的类,它代表了每个摄像头实体。次一级重要的是SampleCameraStateMng类,它代表了这个摄像头实体应该如何执行用户操作和响应消息。另外还有一个是CameraDevice类,他负责设置和执行和摄像头关联的编码器,本文暂不涉及。
4、鸿蒙camera子系统架构的特点 通过以上的解读和梳理,我们可以可看出鸿蒙camera子系统架构有如下特点:
4.1. 用户接口简单,只需要用户基于纯虚类CameraStateCallback派生一个新类CamStateMng,在里面实现用户自己的操作和回调消息的处理。然后再用这个新类创建摄像头,就可以实现对摄像头的操作。而且这个摄像头实例用户基本上无需关心,只需要访问CamStateMng类就可以实现所需操作。
4.2. 架构内部每个功能模块功能单一、模块分散、耦合度低。比如CameraManagerImpl类只负责维护空闲摄像头池:为每个摄像头创建实例、设置消息句柄和处理消息的回调类;CameraService类只负责将与摄像头音视频编码相关的device_和ability_与摄像头实例整合在一起;CameraConfigImpl只负责将发送消息的句柄和帧状态回调设置到device_里,用于捕获每帧的编码数据。。。这样做的好处是要维护的功能局限在每个细小的模块中,相互之间的影响较小,方便各自维护。
4.3. 采用c++来实现模块,封装和可扩展性良好。
4.4. 最后所有需要的模块都集中到摄像头实例类CameraImpl,这样如果需要做相对底层的操作的话,用户只要获取到摄像头实例,就可以访问到所有模块,因此给用户带来了极大的灵活性。
4.5. 目前开放的Camera API 只有基本的功能,没有考虑双摄,三摄协同工作的情况,PQ调试能力也需要完善,希望鸿蒙发展越来越好。