鸿蒙和安卓设备之间如何交互?

把酒临风 2021-09-09 13:48:45 4168

面对鸿蒙这一全新的生态,广大消费者在积极尝鲜的同时,家中不可避免会出现安卓设备和鸿蒙设备并存的现象,短期内可能不会形成全鸿蒙的生态环境。因此,在未来的一段时间内,鸿蒙设备和安卓设备共存的现象会比较普遍。

那么为了给用户带来更加流畅的全场景体验,鸿蒙和安卓设备之间的交互就显得格外重要。

家庭合影美颜相机

家庭合影美颜相机应用是同时基于鸿蒙和安卓设备的应用,可以实现鸿蒙大屏借助安卓手机的能力进行美颜拍照的功能,其中安卓端使用了 GitHub 上的开源项目。

具体来说,此应用能够将鸿蒙大屏拍摄的视频数据实时传输到安卓手机上;并在安卓端为其添加滤镜,再将处理后的视频数据传回到鸿蒙大屏进行渲染显示,从而达到鸿蒙大屏进行美颜拍照的功能。

效果可以参考下图 1:

图 1:家庭合影美颜相机应用的效果示意图

应用运行后的动态场景效果可以参考图 2,图中上方横屏显示的是鸿蒙手机,下方竖屏显示的是安卓手机。

图 2:应用运行后的效果

此处需要说明的是,由于实验环境缺少搭载鸿蒙系统的大屏设备,因此我们使用鸿蒙手机替代大屏设备模拟实验场景。

应用成功运行后的效果如下:

在鸿蒙大屏设备上开启摄像头访问权限,点击主菜单界面的“点击发送大屏数据”按钮,即可将大屏拍摄到的视频数据通过 RTP 协议发送到安卓手机端。

在安卓手机端点击主菜单界面的“GLCAMERAVIEW”按钮,即可接收上述鸿蒙大屏传来的视频数据,并将视频数据显示在手机屏幕上。

安卓端在接收到视频后,会将数据实时渲染到手机屏幕上,用户可以选择给视频添加各种风格的滤镜。

安卓端会通过 RTP 协议将添加滤镜后的视频数据传输到鸿蒙端进行显示。

上述已经介绍过,此应用是同时基于鸿蒙和安卓设备的,因此在讲解此应用时不仅会包含鸿蒙相关知识,同时也会涉及到一些安卓方面的知识。

此应用包含 4 个功能模块,可参考图 3,分别是:视频编解码、通讯协议、美颜滤镜和视频渲染。

图 3:家庭合影美颜相机功能模块图

其中每个模块都会涉及到不同的技术点,如视频编解码会涉及视频流格式和编解码器参数设置;通讯协议会涉及 UDP、RTP 协议等。后续我们的文章将会按不同模块进行讲解和发布,敬请期待!

视频编解码应用案例解析

本期文章将介绍视频编解码模块,视频编解码是视频处理的基础,鸿蒙平台为我们提供了强大的视频处理能力。

为了更具体地讲解该模块功能,我们将家庭合影美颜相机应用中涉及实现视频编解码的代码独立拆分出来形成了一个视频编解码 Demo,将在后续进行效果展示和实现原理讲解。

相关代码已经开源,欢迎各位下载使用并提出宝贵意见:
https://gitee.com/isrc_ohos/cameraharmony

下面以拆分出来的视频编解码模块 Demo 为例,先向大家讲解鸿蒙视频编解码的具体实现原理,再对鸿蒙和安卓两者视频编解码的原理差异进行分析。

①运行效果和代码结构

视频编解码 Demo 的运行效果如图 4 所示:


图 4:视频编解码 Demo 运行效果图

开始运行后,会获取摄像头的权限,然后会在界面中间的矩形区域显示摄像头拍摄到的画面,此时用户可以点击界面上的“开始编解码”按钮,即可在原始视频的下方的矩形区域中看到编解码后视频的渲染效果。

接着介绍一下视频编解码 Demo 的代码结构,如图 5 所示:

图 5:视频编解码 Demo 代码结构

其中 MainAbilitySlice 类用于页面布局和实例化编解码器;我们还构建了 VDEecoder 类和 VDDecoder 类。

前者用于视频编码并对编码过程进行监听,将编码后的数据送去解码,后者用于视频解码,对解码过程进行监听,输出解码后的数据。

②实现流程解析

下面讲解此 Demo 实现视频编解码效果的具体实现流程,共分为 7 个步骤:
步骤 1:创建整体显示布局。

步骤 2:实例化编码类 VDEncoder 的对象并初始化编码器。

步骤 3:获取相机数据并将其加入编码队列。

步骤 4:初始化解码器。

步骤 5:设置 Button 监听事件,执行编码操作。

步骤 6:监听编码器,获取编码后的数据并送去解码。

步骤 7:执行解码操作。

(1)创建整体显示布局

在 MainAbilitySlice 中,定义用于控制编解码的 Button 按钮控件、用于显示编解码状态的 Text 文本控件、两个分别用于显示摄像头拍摄的视频和编解码后视频的 SurfaceProvider 画面渲染控件,并设置上述控件的相关属性,如图 4 效果所示。

(2)实例化编码类 VDEncoder 的对象并初始化编码器

实例化编码 VDEncoder 类对象,使用带有参数 framerate 的构造函数,其中 framerate 代表帧速率,此处设为 15。

VDEncoder vdEncoder = new VDEncoder(15);// 创建编码类对象

在构造函数中,需要进行编码器初始化操作,如设置编码器格式如图像大小、比特率、颜色格式、帧率、关键帧间隔时间、比特率模式等。

需要注意的是,要选择合适的比特率、帧率等参数,不然极有可能出现编解码后视频显示不出来或显示效果异常的情况。

在设置完各属性参数后,通过 set() 方法将上述格式属性配置到编码器对象中;再设置监听用于获取编码输出数据;使用 start() 方法控制编码器开始执行。

并初始化自定义的单例线程池用于编码线程,由于摄像头获取到的数据会被按顺序放入视频队列 YUVQueue 中,因此需要使用线程来提高处理效率。

public VDEncoder(int framerate){
Format fmt = new Format();// 创建编码器格式
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 = new SingleThreadExecutor();// 初始化自定义单例线程池

}

(3)获取相机数据并将其加入编码队列

在正式开始编码之前,需要通过相机的图像监听事件 ImageReceiver.IImageArrivalListener,获取实时返回的原生视频数据并将其存放在 ByteBuffer 类对象中,再逐个读取成 byte 数组的形式,存储在 YUV_DATA 中。

private final ImageReceiver.IImageArrivalListener imagerArivalListener = new ImageReceiver.IImageArrivalListener() {
@Override
public void onImageArrival(ImageReceiver imageReceiver) {// 当相机开始运行后,用于监听,实时返回视频原始数据
mLog.log("imagearival", "arrival");
Image mImage = imageReceiver.readNextImage();// 用于读取视频画面
if(mImage != null){
ByteBuffer mBuffer;
byte[] YUV_DATA = new byte[VIDEO_HEIGHT VIDEO_WIDTH 3 / 2];// 存放从相机获取的原始 YUV 视频数据
...
// 从相机获取实时拍摄的视频数据,并将 Image 读取到的视频流数据存放在 mBuffer
mBuffer = mImage.getComponent(ImageFormat.ComponentType.YUV_Y).getBuffer();
// 从视频流mBuffer逐个读取成 byte 数组的形式,并存储在 YUV_DATA 中
for(i=0;i< VIDEO_WIDTH * VIDEO_HEIGHT;i++){
YUV_DATA[i] = mBuffer.get(i);
}
...
vdEncoder.addFrame(YUV_DATA);// 将视频数据 YUV_DATA 加入到队列等待编解码
mImage.release();// 获取完视频数据之后及时释放
return;
}
}
};

(4)初始化解码器

通过 VDEncoder 类对象调用 prepareDecoder() 方法,初始化解码器,并将用于显示编解码后视频的 SurfaceProvider 对象作为入参传入方法中。

vdEncoder.prepareDecoder(surfaceview2);

在 VDEncoder 类的 prepareDecoder() 方法中,先实例化解码 VDDecoder 类对象,并使用 SurfaceProvider 类对象 surfaceview 显示编解码后的视频,再调用 start() 方法控制开始解码。

public void prepareDecoder(SurfaceProvider surfaceview){
vdDecoder = new VDDecoder(surfaceview);// 创建解码类对象,并使用surfaceview显示解码后的视频
vdDecoder.start();// 开始解码
}

视频解码的初始化方法 beginCodec() 与编码初始化实现原理类似,也需要对各种格式进行配置,此处不再进行赘述,唯一不同之处是帧率和关键帧间隔时间的设置,具体含义可参考下面代码中的注释信息。

private synchronized void beginCodec() {//初始化解码器各参数
System.out.println("isSurfaceCreated = " + Boolean.toString(isSurfaceCreated));
if (isSurfaceCreated) {
isSurfaceCreated = false;
Format fmt = new Format();// 创建解码器格式
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", 30);// 帧率
fmt.putIntValue("i-frame-interval", -1);// 关键帧间隔时间
fmt.putIntValue("bitrate-mode", 1);// 比特率模式

    mCodec.setCodecFormat(fmt);// 设置解码器格式
    mCodec.registerCodecListener(decoderlistener);// 设置监听
    mCodec.start();// 解码器开始执行
    isMediaCodecInit = true;
}

}

(5)设置 Button 监听事件,执行编码操作

为整体显示布局中用于控制是否开始编解码的 Button 按钮设置 onCilick() 点击事件,调用 VDEncoder 类对象的 start() 方法控制开始编码。判断如果编码正在进行,则显示当前编码状态。

button.setClickedListener(component -> {// 按钮被点击
mLog.log("button", "start");
vdEncoder.start();// 开始编码
if(vdEncoder.isRuning){// 如果编码正在进行,显示当前编码状态
text.setText("成功进行编解码,并显示在下方");
}
});

在具体执行编码操作的线程中,会先调用 Codec 类的 getAvailableBuffer() 方法在指定索引处获取编码时的可用缓冲区 ByteBuffer,其中参数 timeout 表示用于填充有效数据的缓冲区索引。

再创建缓冲区信息 BufferInfo,注意 ByteBuffer 和 BufferInfo 要成对使用,调用 setInfo() 方法设置相关信息如偏移量、数据长度、时间戳和缓冲区类型。

接着将数据通过 put() 方法放入缓冲区 ByteBuffer 中;并通过 Codec 类的 WriteBuffer() 方法将传入的 ByteBuffer 和 BufferInfo 进行处理。

private void startEncoderThread() {
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
byte[] data;
while (isRuning) {
try {
data = YUVQueue.take();// 从队列中获取原相机得到的原生视频数据
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
// 将数据以 Buffer 和 BufferUnfo 的形式通过 Codec 类进行编码
ByteBuffer buffer = mCodec.getAvailableBuffer(-1);
BufferInfo bufferInfo = new BufferInfo();//与ByteBuffer成对使用
buffer.put(data);//将数据放入缓冲区
bufferInfo.setInfo(0, data.length, System.currentTimeMillis(), 0);//设置数据相关信息
mCodec.writeBuffer(buffer, bufferInfo);//对缓冲区数据进行处理
}
}
});
}

(6)监听编码器,获取编码后的数据并送去解码

设置编码器监听事件,监听编码器行为。重写 onReadBuffer() 方法获取编码后的输出缓冲区 ByteBuffer 和缓冲区信息 BufferInfo,通过 ByteBuffer 类对象调用 get() 方法获得输出数据,并存放在 byte 数组 data 中。

再通过当前解码类对象 vdDecoder 调用 toDecoder() 方法,即可将完成编码后的视频数据送去解码。

private Codec.ICodecListener encoderlistener = new Codec.ICodecListener() {
// 用于监听编码器,获取编码完成后的数据
@Override
public void onReadBuffer(ByteBuffer byteBuffer, BufferInfo bufferInfo, int i) {
byte[] data = new byte[bufferInfo.size];
byteBuffer.get(data);// 从编码器的 byteBuffer 中获取数据
mLog.log("pushdata", "encoded data:" + data.length);
vdDecoder.toDecoder(data);// 通过解码类的 toDecoder()方法,将编码完成的视频数据送去解码
}...
};

(7)执行解码操作

通过 decoder() 方法执行解码操作,其原理和上述讲解过的编码原理相同。使用 Codec 类的 getAvailableBuffer() 方法在指定索引处获取解码时的可用缓冲区 ByteBuffer。

创建缓冲区信息 BufferInfo,调用 setInfo() 方法设置相关信息并将数据放入缓冲区 ByteBuffer、WriteBuffer() 方法处理传入的 ByteBuffer 和 BufferInfo。

在完成解码之后,通过事件监听类获得输出数据,并按需对解码后的视频数据进行画面渲染显示等相关操作。

private void decoder(byte[] video) {//解码器具体执行流程
ByteBuffer mBuffer = mCodec.getAvailableBuffer(-1);
BufferInfo info = new BufferInfo();//与ByteBuffer成对使用
info.setInfo(0, video.length, 0, 0);//设置数据相关信息
mBuffer.put(video);//将数据放入缓冲区
mCodec.writeBuffer(mBuffer, info);//对缓冲区数据进行处理
}

鸿蒙和安卓编解码器的区别

鸿蒙编解码器 Codec 和安卓编解码器 MediaCodec 的区别如下:

MediaCodec 类作为安卓多媒体基础框架的一部分,通过访问底层的媒体编解码器,即编解码器组件,来实现对于音视频的编解码功能。

MediaCodec 共支持 4 种数据类型,分别是原始的音、视频数据,和压缩后的音、视频数据。

鸿蒙平台的 Codec 编解码类同样支持上述提到的4种数据类型,相比较安卓平台的 MediaCodec 类,二者区别主要体现在使用方式上,即输出数据的获取方式和 Index 缓冲区索引的使用。

先来对比观察一下鸿蒙和安卓编解码实现原理的代码:

//鸿蒙Codec编解码:
private void decoder(byte[] video) {//将数据以 Buffer 和 BufferUnfo 的形式通过 Codec 类进行解码
ByteBuffer mBuffer = mCodec.getAvailableBuffer(-1);
BufferInfo info = new BufferInfo();
info.setInfo(0, video.length, 0, 0);
mBuffer.put(video);//数据放入
mCodec.writeBuffer(mBuffer, info);
}
//鸿蒙监听类
private Codec.ICodecListener decoderlistener = new Codec.ICodecListener() {
// 用于监听编码器,获取解码完成后的数据
@Override
public void onReadBuffer(ByteBuffer byteBuffer, BufferInfo bufferInfo, int i) {
byte[] bytes = new byte[bufferInfo.size];//自定义数组用来存放输出数据
byteBuffer.get(bytes);// 从缓冲区的 byteBuffer 中获取数据
}
};

//安卓MediaCodec编解码:
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
//放入处理数据
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);//获取编码器传入数据ByteBuffer
inputBuffer.clear();//清除以前数据
inputBuffer.put(PCMbuffer);//PCMbuffer需要编码器处理数据
mediaCodec.queueInputBuffer(inputBufferIndex, 0, inputBuffer.limit(), 0, 0);//通知编码器,数据放入

//处理完成数据
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(timeoutUs);
while (outputBufferIndex >= 0) {
outputBuffers = mediaCodec.getOutputBuffer(outputBufferIndex );//获取编码数据
//outputBuffer 编码器处理完成的数据
mediaCodec.releaseOutputBuffer(outputBufferIndex , false);//告诉编码器数据处理完成
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 1000);//可能一次放入的数据处理会输出多个数据
}

①获取输出数据的方式

先简单解释一下安卓中编解码器的原理,可结合图 6 理解:

当请求(或接收)一个空的输入缓冲区(input buffers)时,先将待处理的数据填充到这个缓冲区,并将其发送到编解码器进行处理;然后编解码器将处理完成后的数据填充到空的输出缓冲区(output buffers)。

这样就可以请求(或接收)输出缓冲区中的数据,在数据获取完成之后再将缓冲区释放掉。

在请求输出缓冲区数据的过程中,通过 while 循环验证输出缓冲区的索引(outputBufferIndex)是否大于等于 0,当满足上述条件时,表示可以读取输出缓冲区的数据,否则一直等待输出缓冲区的数据。

在鸿蒙中,也需要使用输入缓存区(mBuffer)和输出缓存区(byteBuffer)来装载数据,但是在请求输出缓冲区数据的过程中,鸿蒙采用了编解码监听的方式。

通过 ICodecListener 类来监听编解码器的数据输出,当可以从输出缓存区获取输出数据时,可在重写方法 onReadBuffer(ByteBuffer byteBuffer, BufferInfo bufferInfo, int i)中获取数据,在数据获取完成之后再将缓冲区释放掉。

②Index 缓冲区索引的使用

安卓与鸿蒙端的另一区别是,安卓在处理输入数据时还使用了一组对应的 dequeueInputBuffer() 和 queueInputBuffer() 方法用来处理输入数据流,标记缓冲区索引。

其中,dequeueInputBuffer() 用于返回输入缓冲区的索引;queueInputBuffer() 用于告知编码器数据已经被放入指定的输入缓冲区中,这样才可以正确释放输入缓冲区。

同理,在处理输出数据时也会使用一组实现原理相同的方法 dequeueOutputBuffer() 和 queueOutputBuffer(),此处不进行赘述。

鸿蒙端并未采用上述缓冲区索引的概念,因此视频编解码的过程更加流畅精简。

来源:鸿蒙技术社区

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
红包 92 收藏 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
把酒临风
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区