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