鸿蒙开源全场景应用开发——视频渲染

世事洞明皆学问,人情练达即文章。这篇文章主要讲述鸿蒙开源全场景应用开发——视频渲染相关的知识,希望能为你提供帮助。
背景上期内容提到过,已开发的家庭合影美颜相机应用是同时基于鸿蒙和安卓设备的,我们将对其4个功能模块即视频编解码、视频渲染、通讯协议和美颜滤镜进行拆分讲解。上一期内容中,我们对视频编解码模块的实现原理进行了解析。本期将继续为大家讲解视频渲染模块,并解析鸿蒙视频渲染相关类之间的关系。相关代码已经开源到Gitee(https://gitee.com/isrc_ohos/cameraharmony ),欢迎各位下载使用并提出宝贵意见!
家庭合影美颜相机应用效果回顾先来带家一起回顾下上期内容讲解的家庭合影美颜相机应用。此应用能够将鸿蒙大屏拍摄的视频数据实时传输到安卓手机上;并在安卓端为其添加滤镜,再将处理后的视频数据传回到鸿蒙大屏进行渲染显示,从而实现鸿蒙大屏美颜拍照的功能,其流程可以参考图1,其数据流向图可以参考图2:
::: hljs-center

鸿蒙开源全场景应用开发——视频渲染

文章图片

:::
::: hljs-center
图1 家庭合影美颜相机应用的效果示意图
:::
::: hljs-center
鸿蒙开源全场景应用开发——视频渲染

文章图片

:::
::: hljs-center
图2美颜相机应用视频数据流向图
:::
应用运行后的动态场景效果可以参考图3,图中下方竖屏显示的是安卓手机,上方横屏显示的是鸿蒙手机(由于实验环境缺少搭载鸿蒙系统的大屏设备,因此我们使用鸿蒙手机替代大屏设备模拟实验场景 ),其显示的是视频解码后渲染的效果。
::: hljs-center
鸿蒙开源全场景应用开发——视频渲染

文章图片

:::
::: hljs-center
图3 应用运行效果图
:::
SurfaceProvider视频渲染解析在鸿蒙中,SurfaceProvider是专门用于绘制图像视图的组件,作为基本组件之一,它通常被用于需要快速绘制图像的地方,如播放视频的情况。下面为大家讲解在完成视频编解码处理后,通过鸿蒙SurfaceProvider完成视频渲染显示的具体实现原理。共分为如下6个步骤:
步骤1. 声明SurfaceProvider类对象。
步骤2. 设置SurfaceProvider属性并添加在页面整体布局中。
步骤3. 解码类VDDecoder继承 SurfaceOps.Callback接口类。
步骤4. 获取SurfaceOps并设置回调。
步骤5. 重写SurfaceCreated()方法,获取当前Surface。
步骤6. 渲染视频数据。
(1)声明SurfaceProvider类对象
在进行视频渲染之前,需要声明用于渲染视频的SurfaceProvider类对象。
private SurfaceProvider surfaceview; // SurfaceProvider用于显示解码后的视频

【鸿蒙开源全场景应用开发——视频渲染】(2)设置SurfaceProvider属性并添加在页面整体布局中
实例化SurfaceProvider类对象并设置相关属性。先使用setWidth()和setHeight()方法设置大小;pinToZTop()方法使surfaceview置于屏幕布局最顶层显示。由于可能会出现待渲染视频数据本身是横屏而屏幕为竖屏显示,或待渲染视频数据本身是竖屏而屏幕为横屏显示等不匹配的情况,因此需要使用setRotation()方法调整屏幕参数,使得屏幕显示方向与视频数据方向相符,其中,屏幕参数0-180度为横屏显示,90-270度为竖屏显示,本应用中原始视频数据是横屏的所以此处需要将屏幕参数设置为180度。接着最主要的是,需要通过getSurfaceOps().get().addCallback()方法设置回调,这样可以通过回调将SurfaceProvider和设备相机相关联。
surfaceview1 = new SurfaceProvider(this); // 实例化类对象 surfaceview1.setWidth(400); // 设置 SurfaceProvider 大小surfaceview1.setHeight(300); surfaceview1.getSurfaceOps().get().addCallback(callback); // 设置回调 surfaceview1.pinToZTop(true); surfaceview1.setRotation(180); // 设置屏幕旋转角度

通过Layout的addComponent()方法将SurfaceProvider添加到整体布局中。
myLayout.addComponent(surfaceview); // 添加到布局中

(3)解码类VDDecoder继承SurfaceOps.Callback类
SurfaceOps.Callback提供了SurfaceProvider被创建、销毁或者改变时的回调通知。由于进行视频渲染的阶段是在完成视频编解码处理之后,因此解码类VDDecoder需要继承SurfaceOps.Callback类,即为SurfaceOps提供一个回调接口。其中需要全局声明Surface和SurfaceOps类对象并重写SurfaceCreated()、SurfaceDestroyed()和SurfaceDestroyed()方法。
public class VDDecoder implements SurfaceOps.Callback { private SurfaceOps holder; // 全局声明SurfaceOps和SurfaceOps类对象 private Surface mSurface; ... @Override// 重写 SurfaceProvider被创建时的回调 public void surfaceCreated(SurfaceOps holder) { ... } @Override// 重写SurfaceProvider被改变时的回调 public void surfaceChanged(SurfaceOps holder, int format, int width, int height) { ... } @Override// 重写SurfaceProvider被销毁时的回调 public void surfaceDestroyed(SurfaceOps holder) { ... } }

(4)获取SurfaceOps并设置回调
在实例化解码类对象时,将用于渲染编解码后视频的surfaceview作为参数传入。
vdDecoder = new VDDecoder(surfaceview); // 创建解码类对象,并使用surfaceview显示解码后的视频

在解码类VDDecoder构造函数中设置SurfaceProvider,调用SurfaceProvider类的getSurfaceOps().get()方法获取surfaceview的SurfaceOps;通过SurfaceOps类对象holder调用addCallback()方法设置回调;再调用setKeepScreenOn()方法,将参数设为true,来实现使屏幕一直显示不会自动关闭的效果。
public VDDecoder(SurfaceProvider playerView) { // 设置 SurfaceProvider,即使用 surfaceview播放解码后的视频 this.holder = surfaceview.getSurfaceOps().get(); holder.addCallback(this); // 设置回调 // 设置该组件让屏幕不会自动关闭 holder.setKeepScreenOn(true); ... }

(5)重写SurfaceCreated()方法,获取当前Surface
surfaceCreated()和surfaceDestroyed()是渲染处理的边界,分别代表SurfaceProvider的创建和销毁,正式的渲染操作必须在SurfaceProvider被创建后才能进行。重写surfaceCreated()方法创建SurfaceProvider,将创建状态isSurfaceCreated变量设置为true,表示已创建;通过SurfaceOps类对象holder调用getSurface()方法获得当前Surface到类对象mSurface中,以便后续将视频数据通过mSurface渲染到界面上。
@Override// 重写 SurfaceProvider被创建时的回调 public void surfaceCreated(SurfaceOps holder) { isSurfaceCreated = true; // 设置创建状态为已创建 mSurface = holder.getSurface(); // 获得当前Surface ... }

(6)渲染视频数据
在编解码类的监听事件decoderlistener中,获取编解码后的数据准备渲染。由于得到的相机图像数据是逆时针旋转90度的,此时如果直接进行渲染,显示的也会是逆时针旋转的效果,因此为了得到正常的显示画面,需要对图像参数进行调整,调用rotateNV21()方法对视频画面进行顺时针旋转90度,并将旋转后的数据存放在byte数组rotate_bytes中。
通过Surface类对象mSurface调用showRawImage()方法对旋转后的视频数据进行渲染。此方法第一个参数表示待渲染数据的byte数组;第二个表示待渲染数据的格式,由于此Demo中编解码的是摄像头直接获取的数据,所以格式是NV21即YUV420_SP;第三和第四个参数分别表示渲染画面的宽和高。
private Codec.ICodecListener decoderlistener = new Codec.ICodecListener() { // 用于监听解码器,获取解码完成后的数据 @Override public void onReadBuffer(ByteBuffer byteBuffer, BufferInfo bufferInfo, int i) { ... // 将解码后的 NV21(YUV420SP) 数据 bytes 顺时针旋转 90 度,并通过 Surface 显示 rotateNV21(bytes, rotate_bytes, 640, 480, 90); // 旋转后的数据用 rotate_bytes 存放 // 渲染旋转后的数据 rotate_bytes 通过mSurface显示出来,第二个参数是待渲染的数据格式即YUV420SP mSurface.showRawImage(rotate_bytes, Surface.PixelFormat.PIXEL_FORMAT_YCRCB_420_SP,640, 480); } ... };

之后运行并点击“开始编解码”按钮即可得到上述图1中展示的将编解码后的视频数据渲染在surfaceview中的效果。
Surface、SurfaceOps与SurfaceProvider的关系经过上述讲解,相信大家已经能够在鸿蒙中正确使用SurfaceProvider来进行视频渲染了。熟悉安卓的读者可能已经发现,鸿蒙SurfaceProvider用法和安卓Surface用法有异曲同工之妙。为了方便理解,可以将鸿蒙中的SurfaceProvider、Surface和SurfaceOps分别与安卓中的SurfaceView、Surface、和SurfaceHolder对照查看,其原理类似。下面将为大家解析在鸿蒙中这三个视频渲染类之间的关系。
::: hljs-center
鸿蒙开源全场景应用开发——视频渲染

文章图片

:::
::: hljs-center
图4SurfaceProvider、Surface、SurfaceOps关系示意图
:::
1.Surface与SurfaceProvider关系
Surface与SurfaceProvider之间的关系如图2所示。在鸿蒙中,每个窗口会对应一个SurfaceProvider,每个Surface会对应一块屏幕缓冲区,而SurfaceProvider的作用是处理屏幕缓冲区中的视频数据,并使用该数据在屏幕上绘图。也就是说,Surfac负责对视频数据进行管理;eSurfaceProvider负责对视频数据进行展示,Surface需要通过SurfaceProvider才能展示其中的内容并控制视图的位置和尺寸。
2.SurfaceOps与SurfaceProvider关系
SurfaceOps是一个接口,其作用类似于一个关于Surface的监听器,能够访问SurfaceProvider对应的Surface并调用Surface中的相关方法。并通过三个回调方法,及时捕捉Surface的状态如创建、销毁或者改变。
获取SurfaceOps的方式是:调用SurfaceProvider类中getSurfaceOps()方法,得到元素类型为SurfaceOps的Optional容器,再通过get()方法从容器中取出SurfaceOps类对象并返回。在成功调用并得到返回值之后,就可以通过返回的SurfaceOps类对象调用addCallback()方法为Surface设置回调:
void addCallback(SurfaceOps.Callback var1); // 设置SurfaceOps回调

图2中显示,在Surface与SurfaceProvider之间还存在一个SurfaceOps.Callback类,SurfaceOps的回调就是通过内部子接口SurfaceOps.Callback实现的,其有三个回调方法:
  • surfaceCreated():当SurfaceProvider发生结构性的变化如格式或大小改变时,调用此方法。
  • surfaceChanged():当SurfaceProvide被创建时,调用此方法。
  • surfaceDestroyed():当SurfaceProvider在要被销毁时,立即调用此方法。
    public interface Callback {// 内部子接口CallBack void surfaceCreated(SurfaceOps var1); // SurfaceProvider被创建时 void surfaceChanged(SurfaceOps var1, int var2, int var3, int var4); // SurfaceProvider被改变时 void surfaceDestroyed(SurfaceOps var1); // SurfaceProvider被销毁时 }

    上面提到过SurfaceOps是一个接口,因此在实际使用之前,需要先重写上述三个回调方法,才能正常感知到SurfaceProvider的创建、改变或销毁。
    项目贡献人李珂 朱伟 郑森文 陈美汝
想了解更多关于鸿蒙的内容,请访问:
51CTO和华为官方战略合作共建的鸿蒙技术社区
https://harmonyos.51cto.com/#bkwz
鸿蒙开源全场景应用开发——视频渲染

文章图片


    推荐阅读