鸿蒙开源全场景应用开发——视频渲染 原创 精华

朱伟ISRC
发布于 2021-9-18 16:40
浏览
9收藏

背景

上期内容提到过,已开发的家庭合影美颜相机应用是同时基于鸿蒙和安卓设备的,我们将对其4个功能模块即视频编解码、视频渲染、通讯协议和美颜滤镜进行拆分讲解。上一期内容中,我们对视频编解码模块的实现原理进行了解析。本期将继续为大家讲解视频渲染模块,并解析鸿蒙视频渲染相关类之间的关系。相关代码已经开源到Gitee(https://gitee.com/isrc_ohos/cameraharmony/tree/BeautyCamera/),欢迎各位下载使用并提出宝贵意见!

家庭合影美颜相机应用效果回顾

先来带家一起回顾下上期内容讲解的家庭合影美颜相机应用。此应用能够将鸿蒙大屏拍摄的视频数据实时传输到安卓手机上;并在安卓端为其添加滤镜,再将处理后的视频数据传回到鸿蒙大屏进行渲染显示,从而实现鸿蒙大屏美颜拍照的功能,其流程可以参考图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
图4 SurfaceProvider、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的创建、改变或销毁。

项目贡献人

李珂 朱伟 郑森文 陈美汝

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2021-9-27 19:26:39修改
14
收藏 9
回复
举报
7条回复
按时间正序
/
按时间倒序
鱼儿会飞啦啦啦
鱼儿会飞啦啦啦

又来一键三连了

回复
2021-9-18 17:19:01
Like_kk
Like_kk

满满的干货~

回复
2021-9-18 17:24:39
朱伟ISRC
朱伟ISRC 回复了 鱼儿会飞啦啦啦
又来一键三连了

爱你~

回复
2021-9-18 17:45:14
朱伟ISRC
朱伟ISRC 回复了 Like_kk
满满的干货~

谢谢支持!

1
回复
2021-9-18 17:45:24
大白兔的耳朵
大白兔的耳朵

支持支持,

1
回复
2021-9-20 10:04:51
Junfeng0613
Junfeng0613

有点东西啊

回复
2021-9-23 09:15:28
朱伟ISRC
朱伟ISRC 回复了 Junfeng0613
有点东西啊

谢谢支持!

回复
2021-9-23 09:54:32
回复
    相关推荐