前言
前文提到过,已开发的家庭合影美颜相机应用是同时基于鸿蒙和安卓设备的,我们将对其包含的4个功能模块即视频编解码、视频渲染、通讯协议和美颜滤镜进行拆分讲解。
前几期内容中,我们对视频编解码和视频渲染模块的实现原理进行了解析。本期将继续为大家讲解通讯协议并简要概述安卓美颜滤镜的实现原理。
背景
RTP是用于Internet上针对流媒体传输的一种基础协议,在一对一或一对多的传输情况下工作,其目的是提供时间信息和实现流同步。它可以建立在底层的面向连接和非连接的传输协议上,一般使用UDP协议进行传输。从一个同步源发出的RTP分组序列称为流,一个RTP会话可能包含多个RTP流。
应用效果展示
1.家庭合影美颜相机应用效果回顾
先来带大家一起回顾下上期内容讲解的家庭合影美颜相机应用。
此应用能够将鸿蒙大屏拍摄的视频数据实时传输到安卓手机上;并在安卓端为其添加滤镜,再将处理后的视频数据传回到鸿蒙大屏进行渲染显示,从而实现鸿蒙大屏美颜拍照的功能,应用运行后的动态场景效果可以参考图1。
图中下方竖屏显示的是安卓手机,上方横屏显示的是鸿蒙手机(由于实验环境缺少搭载鸿蒙系统的大屏设备,因此我们使用鸿蒙手机替代大屏设备模拟实验场景),其显示的是视频解码后渲染的效果。
图1 家庭合影美颜相机应用运行效果图
2.RTP传输Demo效果
为了更清晰地讲解通讯协议,我们将家庭合影美颜相机应用中数据传输部分拆分出来,形成了一个RTP传输Demo,并进行了功能整理和优化,将原本的视频传输改为了图像传输,视频是由多帧图像构成,传输数据类型的改变不会影响RTP传输原理和步骤。RTP传输Demo的运行效果图如图2所示,上图为发送端效果,下图为接收端效果。
成功安装并打开应用后,在发送端点击蓝色按钮,发送开发者选中的特定区域的图片数据;在接收端点击粉色按钮,接收发送端刚发送的图片数据,并在按钮下方显示。
图2 RTP传输Demo运行效果图(上发送端,下接收端)
RTP传输原理及步骤解析
接下来为大家重点解析RTP传输的实现原理和步骤。
RTP传输Demo的原理流程可参考图3。在鸿蒙发送端(服务端),设置需要传输的图像数据,通过无线网络,使用RTP协议和Socket点对点的数据通信方式,发送到鸿蒙接收端(客户端)。
在鸿蒙接收端(客户端),接收到发送端发来的图像数据后,进行图像绘制。接下来将针对RTP传输Demo的实现步骤进行解析。
图3 RTP传输原理流程图
服务端数据发送
在服务端,将待发送的图像置于resources->base->media文件夹下,如图4所示。然后对待发送的图像数据进行格式转换。通过无线网络,使用RTP协议和Socket点对点的数据通信方式,将图像数据传输至鸿蒙接收端。
图4 图片在项目结构中的位置
服务端的数据发送流程包含以下三个步骤:
步骤1. 通过资源ID获取位图对象;
步骤2. 将位图指定区域像素进行格式转换;
步骤3. 数据传输;
(1)通过资源ID获取位图对象
通过getResource()方法,以资源IDdrawableID对象作为入参,获取资源输入流drawableInputStream;实例化图像设置类ImageSource.SourceOptions对象,并设置图像源格式为png;创建图像源,参数为资源输入流和图像源ImageSource类对象;实例化图像参数类DecodingOptions的对象,为其初始化图像尺寸、区域并设置位图格式;根据像参数类对象decodingOptions,通过图像源ImageSource类对象创建位图对象;返回位图对象。
- //通过资源ID获取位图对象
- private PixelMap getPixelMap(int drawableId) {
- InputStream drawableInputStream = null;
- try {
- //以资源ID作为入参,获取资源输入流
- drawableInputStream=this.getResourceManager().getResource(drawableId);
- //实例化图像源ImageSource类对象
- ImageSource.SourceOptions sourceOptions = new ImageSource.SourceOptions();
- sourceOptions.formatHint = "image/png";//设置图像源格式
- //创建图像源,参数为资源输入流和图像源ImageSource类对象
- ImageSource imageSource = ImageSource.create(drawableInputStream, sourceOptions);
- //实例化图像源解码操作DecodingOptions类对象
- ImageSource.DecodingOptions decodingOptions = new ImageSource.DecodingOptions();
- decodingOptions.desiredSize = new Size(0, 0);//设置图像尺寸
- decodingOptions.desiredRegion = new Rect(0, 0, 0, 0);
- decodingOptions.desiredPixelFormat = PixelFormat.ARGB_8888;//设置位图格式
- PixelMap pixelMap = imageSource.createPixelmap(decodingOptions);//根据解码操作类对象,创建位图
- return pixelMap;//返回位图
- }
- ...
- }
(2)将位图指定区域像素进行格式转换
在得到位图对象后,实例化矩形Rect矩形类对象,用于为开发者选中特定的图像区域(该区域应不大于resources->base->media路径下图像的大小);通过位图对象pixelMap调用readPixels()方法将指定区域像素转换为int[]类型数据;调用intToBytes()方法再将int[]类型数据格式转换为byte类型数据。
- // 读取指定区域像素
- Rect region = new Rect(0, 0, 30, 30);//实例化举行类对象,规定指定区域
- pixelMap.readPixels(pixelArray,0,30,region);//将指定区域像素转换为int[]类型数据
- pic = intToBytes(pixelArray);//将int[]类型数据昂虎子你换位byte类型数据
(3)数据传输
实例化RTP发送类对象RtpSenderWrapper,将IP地址设置为接收端手机IP地址,端口号设置为5005;调用sendAvcPacket()方法发送图像数据。
由于对RTP传输的数据类型做了简化,因此图像RTP传输会相对容易,而如果是原应用中的视频RTP传输,则需要逐帧对视频数据进行格式转换,并将从摄像头获取的YUV类型的原始视频数据压缩为h264类型的视频数据,以方便Socket进行传输。
- mRtpSenderWrapper = new RtpSenderWrapper("192.168.31.12", 5005, false);
- mRtpSenderWrapper.sendAvcPacket(pic, 0, pic.length, 0);//发送数据
客户端接收数据
在发送端通过RTP协议成功发送数据后,接收端就可以正常开始接收了。发送端接收数据的流程主要分为以下5个步骤:
步骤1. 创建数据接收线程;
步骤2. 接收数据;
步骤3. 在线程间进行数据传递;
步骤4. 处理位图数据得到pixelMapHolder;
步骤5. 绘制图像。
(1)创建数据接收线程
创建子线程作为数据接收线程。
- new Thread(new Runnable())//新开一个数据接收线程
(2)接收数据
在子线程接收线程中,实例化数据包DatagramPacket;通过Socket类对象调用receive()方法,接收发送端的数据到数据包DatagramPacket中;通过数据包DatagramPacket调用getData()方法获取数据包中的RTP数据。
- datagramPacket = new DatagramPacket(data,data.length);//实例化数据包
- socket.receive(datagramPacket);//接收数据到数据包中
- rtpData = datagramPacket.getData();获取数据包中的RTP数据
(3)在线程间进行数据传递
待子线程拿到RTP发送数据后,需要将RTP数据从子线程传递到主线程。这就涉及到线程间的数据传递。在此应用中,我们使用了Java类的SynchronousQueue并发队列来实现子线程和主线程间的数据传递。先实例化一个byte[]类型的并发队列SynchronousQueue类对象;将h264类型的数据放入并发队列中;再从队列中获取数据。
- SynchronousQueue<byte[]> queue = new SynchronousQueue<byte[]>();//实例化byte[]类型的并发队列
- queue.put(h264Data);//将h264类型的数据放入并发队列中
- rgbData = queue.take();//从队列中获取数据
(4)处理解码后的位图数据得到PixelMapHolder
主线程从队列中拿到图像RGB数据后即可进行图像绘制。PixelMap是接收得到的位图数据,PixelMapHolder 使用 PixelMap 生成渲染后端所需的数据,并提供数据作为 Canvas 中方法的输入参数。因此为了后续能够对位图进行渲染,需要在图像数据从子线程传递到主线程后,将图像数据pixelmap转换为pixelMapHolder类对象,即在实例化pixelMapHolder类对象时,将pixelmap位图数据作为入参传入实例化方法中。
- public void putPixelMap(PixelMap pixelMap){
- if (pixelMap != null) {//判断接收到的位图数据是否为空
- rectSrc = new RectFloat(0, 0, pixelMap.getImageInfo().size.width, pixelMap.getImageInfo().size.height);
- pixelMapHolder = new PixelMapHolder(pixelMap);//实例化PixelMapHolder类对象
- }else{
- pixelMapHolder = null;//若接收到的位图为空,则全部置为空
- setPixelMap(null);
- }
- }
(5)绘制图像
实例化一个矩形Rect类对象,设置图像信息并规定指定的区域如宽和高;添加一个同步绘制任务,先判断pixelMapHolder是否为空,若为空则直接返回,不为空则开始绘制任务;在绘制任务中,调用drawPixelMapHolderRoundRectShape()方法将PixelMapHolder类对象绘制到实例化得到的矩形Rect类对象中,并设置其为圆角效果;其位置由rectDst指定;绘制完成后释放pixelMapHolder,将其置为空。
- private void onDraw(){
- this.addDrawTask((view, canvas) -> { //添加绘制任务
- if (pixelMapHolder == null){//判断pixelMapHolder是否为空
- return;
- }
- synchronized (pixelMapHolder) {//在同步任务中绘制图像
- canvas.drawPixelMapHolderRoundRectShape(pixelMapHolder, rectSrc, rectDst, radius, radius);//绘制图像为圆角效果
- pixelMapHolder = null;//绘制完成后将pixelMapHolder释放
- }
- });
- }
安卓端美颜滤镜效果实现
美颜滤镜的部分我们参考了GitHub上的开源项目(https://github.com/google/grafika、https://github.com/cats-oss/android-gpuimage、https://github.com/wuhaoyu1990/MagicCamera),使用GPU着色器实现添加滤镜和切换滤镜的效果。由于不涉及鸿蒙的能力,此部分不作为重点讲述,只简要概括下其实现流程,可分为如下5个步骤:
(1)设置不同的滤镜
使用着色器语言,设置所需的多种代码。
图5 美颜相机使用的滤镜
(2)opengl绘制;
- import android.opengl.GLES20;
- ...
- // add the vertex shader to program
- GLES20.glAttachShader(mProgram, vertexShader);
- // add the fragment shader to program
- GLES20.glAttachShader(mProgram, fragmentShader);
- // creates OpenGL ES program executables
- GLES20.glLinkProgram(mProgram);
(3)添加滤镜;
- private List<FilterFactory.FilterType>filters = new ArrayList<>();
- ...
- filters.add(FilterFactory.FilterType.Original);
- filters.add(FilterFactory.FilterType.Sunrise);
- ...
(4)开启或关闭美颜滤镜;
- mCameraView.enableBeauty(true);
(5)设置美颜程度;
- mCameraView.setBeautyLevel(0.5f);
(6)设置切换滤镜和切换镜头,再设置相机拍摄和拍摄完成后的回调即可。
- mCameraView.updateFilter(filters.get(pos));//切花滤镜
- mCameraView.switchCamera();//切换镜头