当前位置: 首页 > 科技观察

鸿蒙开源全场景应用开发-视频编解码

时间:2023-03-21 01:52:40 科技观察

更多内容请访问:鸿蒙科技社区https://harmonyos.51cto.com,与华为官方后台共同打造面对鸿蒙新生态,然而,在消费者积极尝试新产品的同时,安卓设备和鸿蒙设备在家中并存是不可避免的。短期内可能无法形成完整的鸿蒙生态环境。因此,在未来一段时间内,鸿蒙设备与安卓设备并存的情况会更加普遍。所以为了给用户带来更流畅的全场景体验,鸿蒙与安卓设备的交互显得尤为重要。家庭照片美颜相机家庭照片美颜相机应用基于鸿蒙和Android设备。可以借助安卓手机实现鸿蒙大屏拍照功能。Android端使用GitHub上的开源项目。具体来说,这款应用可以将鸿蒙大屏拍摄的视频数据实时传输到安卓手机上;在Android端为其添加滤镜,然后将处理后的视频数据传回鸿蒙大屏进行渲染显示,从而实现鸿蒙大屏美颜拍照功能,效果可参考如下图1:图1全家福美颜相机应用效果图应用运行后的动态场景效果可以参考图2。鸿蒙手机,下方竖屏为安卓手机。这里需要说明的是,由于实验环境缺少搭载鸿蒙系统的大屏设备,所以我们使用鸿蒙手机代替大屏设备来模拟实验场景。图2应用运行后效果应用运行成功后效果如下:在鸿蒙大屏设备上开启摄像头访问权限,点击主菜单“点击发送大屏数据”按钮接口,可以将大屏截取的视频数据通过RTP协议发送到安卓手机端。在安卓手机上点击主菜单界面的“GLCAMERAVIEW”按钮,即可从上述鸿蒙大屏接收视频数据,并将视频数据显示在手机屏幕上。Android端接收到视频后,会实时将数据渲染到手机屏幕上,用户可以选择为视频添加各种风格的滤镜;Android端将添加滤镜后的视频数据通过RTP协议传输到鸿蒙端。展示。上面说了,这个应用是基于鸿蒙和安卓设备的,所以在讲解这个应用的时候,除了鸿蒙相关的知识,还会包含一些安卓的知识。本应用包含4个功能模块,见图3,分别是:视频编解码、通信协议、美颜滤镜和视频渲染。每个模块都会涉及到不同的技术点,比如视频编解码会涉及到视频码流格式和编解码参数设置;通信协议会涉及UDP、RTP协议等。我们后续的文章会根据不同的模块进行讲解和发布,敬请期待!图3家庭美颜相机功能模块图视频编解码应用案例分析本文将介绍视频编解码模块,视频编解码就是视频处理鸿蒙平台为我们提供了强大的视频处理能力。为了更具体的说明这个模块的功能,我们将家庭合影美颜相机应用中视频编解码实现所涉及的代码独立拆分成一个视频编解码demo,将展示效果和后续说明实现原理。以拆分视频编解码模块Demo为例,首先说明鸿蒙视频编解码的具体实现原理,然后分析鸿蒙与Android在视频编解码原理上的区别。一、运行效果及代码结构视频编解码器Demo的运行效果如图4所示,开始运行后会获取摄像头的权限,然后在矩形区域显示摄像头拍摄的图片界面中间。这时,用户可以点击界面上的“开始解码”按钮,视频就会显示在原视频下方的矩形区域中。中可以看到编解码视频的渲染效果。图4视频编解码Demo运行效果图接下来介绍视频编解码Demo的代码结构,如图5所示。其中MainAbilitySlice类用于页面布局和实例化编解码;我们还构建了VDEecoder类和VDDecoder类,前者用于视频编码,监控编码过程,并将编码后的数据送去解码,后者用于视频解码,监控解码过程,输出解码数据。图5视频编解码Demo代码结构2.实现过程分析下面说明本demo实现视频编解码效果的具体实现过程,分为7个步骤:步骤1.创建整体显示布局。Step2.实例化编码类VDEncoder的对象,初始化编码器。Step3.获取相机数据并将其添加到编码队列中。步骤4.初始化解码器。Step5.设置Button监听事件,进行编码操作。Step6.监听编码器,得到编码后的数据并发送给解码器。Step7.执行解码操作。(1)创建整体显示布局在MainAbilitySlice中,定义控制解码器的Button按钮控件,显示解码器状态的Text文本控件,以及分别显示摄像头拍摄的视频和解码器视频的两个SurfaceProvider屏幕渲染控件,并设置上述控件的相关属性,如图4所示。(2)实例化编码类VDEncoder的对象,初始化编码器。实例化编码VDEncoder类对象,使用带参数framerate的构造函数,其中framerate表示帧率,这里设置为15。VDEncodervdEncoder=newVDEncoder(15);//创建一个编码类对象在构造函数中,需要进行编码器的初始化操作,比如设置图像大小、码率、颜色格式、帧率、关键帧间隔时间等编码器格式,码率模式等。需要注意的是码率、帧率等参数要选择合适,否则很可能编码解码后视频无法显示或显示效果不正常。设置好每个属性参数后,通过set()方法将上述格式属性配置到编码器对象中;然后设置监视器获取编码后的输出数据;使用start()方法控制编码器开始执行;并初始化自定义单例线程池,用于编码线程。由于摄像头获取到的数据会依次放入视频队列YUVQueue中,因此需要使用线程来提高处理效率。publicVDEncoder(intframerate){Formatfmt=newFormat();//创建编码器格式fmt.putStringValue("mime","video/avc");fmt.putIntValue("width",640);//视频图像宽度fmt.putIntValue("height",480);//视频图像高度fmt.putIntValue("bitrate",392000);//码率fmt.putIntValue("color-format",21);//颜色格式fmt.putIntValue("frame-rate",framerate);//帧率fmt.putIntValue("i-frame-interval",1);//关键帧间隔时间fmt.putIntValue("bitrate-mode",1);//位码率模式mCodec.setCodecFormat(fmt);//设置编码器格式mCodec.registerCodecListener(encoderlistener);//设置监听mCodec.start();//编码器开始执行singleThreadExecutor=newSingleThreadExecutor();//初始化自定义单例线程pool}(3)获取摄像头数据,加入编码队列。在正式开始编码前,需要监听摄像头的图像事件ImageReceiver.IImageArrivalListener,获取实时返回的原生视频数据,存储到ByteBuffer类对象中。然后将它们一个一个读入字节数组,存入YUV_DATA。privatefinalImageReceiver.IImageArrivalListenerimagerArivalListener=newImageReceiver.IImageArrivalListener(){@OverridepublicvoidonImageArrival(ImageReceiverimageReceiver){//摄像头开始运行时,用于实时监听并返回原始视频数据mLog.log("imagearival","arrival");ImagemImage=图像接收器。readNextImage();//用于读取视频图像if(mImage!=null){ByteBuffermBuffer;byte[]YUV_DATA=newbyte[VIDEO_HEIGHT*VIDEO_WIDTH*3/2];//存储从摄像头获取的原始YUV视频数据...//从摄像头获取实时视频数据,并将Image读取的视频流数据存入mBuffermBuffer=mImage.getComponent(ImageFormat.ComponentType.YUV_Y).getBuffer();//逐一读取从视频流的mBuffer中将其作为字节数组存入YUV_DATAfor(i=0;i{//按钮被点击mLog.log("button","start");vdEncoder.start();//开始编码if(vdEncoder.isRuning){//如果编码是进行中,显示当前编码状态text.setText("编解码成功,下方显示");}});在具体执行编码操作的线程中,首先会调用Codec类的getAvailableBuffer()方法获取编码在指定索引处可用的bufferByteBuffer,其中参数timeout表示用于填充的buffer索引有效数据;然后创建缓冲区信息BufferInfo,注意ByteBuffer和BufferInfo要成对使用,调用setInfo()方法设置偏移量、数据长度、时间戳、缓冲区类型等相关信息;然后通过put()方法将数据放入缓冲区ByteBuffer;并通过Codec类的WriteBuffer()方法处理传入的ByteBuffer和BufferInfo。privatevoidstartEncoderThread(){singleThreadExecutor.execute(newRunnable(){@Overridepublicvoidrun(){byte[]data;while(isRuning){try{data=YUVQueue.take();//获取原始摄像头的原始视频数据queue}catch(InterruptedExceptione){e.printStackTrace();break;}//通过Codec类将数据编码为Buffer和BufferUnfo形式ByteBufferbuffer=mCodec.getAvailableBuffer(-1);BufferInfobufferInfo=newBufferInfo();//ByteBuffer成对使用buffer.put(data);//将数据放入缓冲区bufferInfo.setInfo(0,data.length,System.currentTimeMillis(),0);//设置数据相关信息mCodec.writeBuffer(buffer,bufferInfo);//处理缓冲区数据}}});}(6)监听编码器,获取编码后的数据发送给解码器设置编码器监听事件,监听编码器行为。重写onReadBuffer()方法获取编码后的输出缓冲区ByteBuffer和缓冲区信息BufferInfo,通过ByteBuffer类对象调用get()方法获取输出数据,并存储到字节数组数据中;然后通过当前解码类对象vdDecoder()方法调用toDecoder,将编码后的视频数据送去解码。privateCodec.ICodecListenerencoderlistener=newCodec.ICodecListener(){//用于监听编码器,获取编码后的数据@OverridepublicvoidonReadBuffer(ByteBufferbyteBuffer,BufferInfobufferInfo,inti){byte[]data=newbyte[bufferInfo.size];byteBuffer.get(data);//从encoder的byteBuffer中获取数据mLog.log("pushdata","encodeddata:"+data.length);vdDecoder.toDecoder(data);//通过解码类的toDecoder()方法,将编码后的视频数据送去解码}...};(7)进行解码操作解码操作是通过decoder()方法进行的,其原理与上面讲解的编码原理相同。使用Codec类的getAvailableBuffer()方法获取指定索引处的可用缓冲区ByteBuffer;创建缓冲区信息BufferInfo,调用setInfo()方法设置相关信息并将数据放入缓冲区ByteBuffer,WriteBuffer()方法处理传入的ByteBuffer和BufferInfo。解码完成后,通过事件监听类获取输出数据,根据需要对解码后的视频数据进行画面渲染、显示等相关操作。privatevoiddecoder(byte[]video){//解码器具体执行过程ByteBuffermBuffer=mCodec.getAvailableBuffer(-1);BufferInfoinfo=newBufferInfo();//与ByteBuffer配对info.setInfo(0,video.length,0,0);//设置数据相关信息mBuffer.put(video);//将数据放入缓冲区mCodec.writeBuffer(mBuffer,info);//处理缓冲区数据}鸿蒙编解码器Codec与Android编解码器MediaCodec解码器的区别MediaCodec类是Android多媒体基础框架的一部分,通过访问底层媒体编解码器,即编解码器组件,实现音视频编解码功能。MediaCodec一共支持4种数据类型,分别是原始音视频数据和压缩音视频数据。鸿蒙平台的Codec编解码类也支持上述四种数据类型。与Android平台的MediaCodec类相比,两者的区别主要体现在使用方式上,即获取输出数据的方式和Indexbuffer索引的使用。我们先对比观察一下鸿蒙和Androidcodec的代码实现原理://鸿蒙Codeccodec:privatevoiddecoder(byte[]video){//通过Codec类ByteBuffermBuffer=mCodec.getAvailableBuffer解码Buffer和BufferUnfo形式的数据(-1);BufferInfoinfo=newBufferInfo();info.setInfo(0,video.length,0,0);mBuffer.put(video);//将数据放入mCodec.writeBuffer(mBuffer,info);}//鸿蒙监听类privateCodec.ICodecListenerdecoderlistener=newCodec.ICodecListener(){//用于监听编码器,获取解码后的数据@OverridepublicvoidonReadBuffer(ByteBufferbyteBuffer,BufferInfobufferInfo,inti){byte[]bytes=newbyte[bufferInfo.size];//自定义array用于存储输出数据byteBuffer.get(bytes);//从缓冲区的byteBuffer中获取数据}};//AndroidMediaCodeccodec:ByteBuffer[]inputBuffers=mediaCodec.getInputBuffers();ByteBuffer[]outputBuffers=mediaCodec.getOutputBuffers();//将处理数据放入inputBufferIndex=mediaCodec.dequeueInputBuffer(-1);ByteBufferinputBuffer=mediaCodec.getInputBuffer(inputBufferIndex);//获取编码器传入数据queueInputBuffer(inputBufferIndex,0,inputBuffer.limit(),0,0);//通知编码器,放入数据//处理完成的数据inoutputBufferIndex=mediaCodec.dequeueOutputBuffer(timeoutUs);while(outputBufferIndex>=0){outputBuffers=mediaCodec.getOutputBuffer(outputBufferIndex);//得到编码后的数据//outputBuffer编码器处理后的数据mediaCodec.releaseOutputBuffer(outputBufferIndex,false);//告诉编码器数据处理完成outputBufferIndex=mediaCodec.dequeueOutputBuffer(bufferInfo,1000);//可能一次放入数据处理会输出多条数据}(1)获取输出数据的方式先简单说明一下Android中codec的原理,在请求(或接收)时可以结合图6理解)一个空的输入缓冲区(inputbuffers),首先将要处理的数据填充到这个缓冲区中,然后送入编解码器进行处理;然后编解码器将处理后的数据填充到空的输出缓冲区(outputbuffers)中;以便请求(或接收)输出缓冲区中的数据,并在数据获取完成后释放缓冲区。在请求输出缓冲区数据的过程中,通过while循环验证输出缓冲区的索引(outputBufferIndex)是否大于等于0。当满足以上条件时,表示可以读取输出缓冲区中的数据,否则一直等待输出缓冲区数据。图6Android编解码器示意图(来源于网络,侵权必删)。在鸿蒙中,同样需要使用输入缓冲区(mBuffer)和输出缓冲区(byteBuffer)来加载数据,但是在请求输出缓冲区数据的过程中,鸿蒙采用了codec监听的方式。使用ICodecListener类监视编解码器的数据输出。当可以从输出缓冲区获取输出数据时,可以在重写方法onReadBuffer(ByteBufferbyteBuffer,BufferInfobufferInfo,inti)中获取数据,并在数据获取完成后释放缓冲区。(2)Indexbuffer索引的使用Android和鸿蒙的另一个区别是Android也使用了一套对应的dequeueInputBuffer()和queueInputBuffer()方法来处理输入数据流,并在处理输入数据时标记buffer索引。.其中,dequeueInputBuffer()用于返回输入缓冲区的索引;queueInputBuffer()用于通知编码器数据已经放入指定的输入缓冲区,以便正确释放输入缓冲区。同样,在处理输出数据时,会用到一组实现原理相同的方法dequeueOutputBuffer()和queueOutputBuffer(),这里不再赘述。鸿蒙终端没有采用上述缓冲区索引的概念,因此视频编解码的过程更加流畅和精简。更多信息请访问:Harmonyos.51cto.com,与华为官方合作打造的鸿蒙技术社区