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

那么为了给用户带来更加流畅的全场景体验,鸿蒙和安卓设备之间的交互就显得格外重要。
家庭合影美颜相机
家庭合影美颜相机应用是同时基于鸿蒙和安卓设备的应用,可以实现鸿蒙大屏借助安卓手机的能力进行美颜拍照的功能,其中安卓端使用了 GitHub 上的开源项目。
具体来说,此应用能够将鸿蒙大屏拍摄的视频数据实时传输到安卓手机上;并在安卓端为其添加滤镜,再将处理后的视频数据传回到鸿蒙大屏进行渲染显示,从而达到鸿蒙大屏进行美颜拍照的功能。

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

图 2:应用运行后的效果
此处需要说明的是,由于实验环境缺少搭载鸿蒙系统的大屏设备,因此我们使用鸿蒙手机替代大屏设备模拟实验场景。
应用成功运行后的效果如下:
在鸿蒙大屏设备上开启摄像头访问权限,点击主菜单界面的“点击发送大屏数据”按钮,即可将大屏拍摄到的视频数据通过 RTP 协议发送到安卓手机端。
在安卓手机端点击主菜单界面的“GLCAMERAVIEW”按钮,即可接收上述鸿蒙大屏传来的视频数据,并将视频数据显示在手机屏幕上。
安卓端在接收到视频后,会将数据实时渲染到手机屏幕上,用户可以选择给视频添加各种风格的滤镜。
安卓端会通过 RTP 协议将添加滤镜后的视频数据传输到鸿蒙端进行显示。
此应用包含 4 个功能模块,可参考图 3,分别是:视频编解码、通讯协议、美颜滤镜和视频渲染。

视频编解码应用案例解析
相关代码已经开源,欢迎各位下载使用并提出宝贵意见:
https://gitee.com/isrc_ohos/cameraharmony
①运行效果和代码结构
视频编解码 Demo 的运行效果如图 4 所示:


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

②实现流程解析
步骤 1:创建整体显示布局。
步骤 2:实例化编码类 VDEncoder 的对象并初始化编码器。
步骤 3:获取相机数据并将其加入编码队列。
步骤 4:初始化解码器。
步骤 5:设置 Button 监听事件,执行编码操作。
步骤 6:监听编码器,获取编码后的数据并送去解码。
步骤 7:执行解码操作。
实例化编码 VDEncoder 类对象,使用带有参数 framerate 的构造函数,其中 framerate 代表帧速率,此处设为 15。
VDEncoder vdEncoder = new VDEncoder(15);// 创建编码类对象
并初始化自定义的单例线程池用于编码线程,由于摄像头获取到的数据会被按顺序放入视频队列 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();// 初始化自定义单例线程池
}
在正式开始编码之前,需要通过相机的图像监听事件 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;
}
}
};
通过 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;
}
}
为整体显示布局中用于控制是否开始编解码的 Button 按钮设置 onCilick() 点击事件,调用 VDEncoder 类对象的 start() 方法控制开始编码。判断如果编码正在进行,则显示当前编码状态。
button.setClickedListener(component -> {// 按钮被点击
mLog.log("button", "start");
vdEncoder.start();// 开始编码
if(vdEncoder.isRuning){// 如果编码正在进行,显示当前编码状态
text.setText("成功进行编解码,并显示在下方");
}
});
接着将数据通过 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);//对缓冲区数据进行处理
}
}
});
}
再通过当前解码类对象 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()方法,将编码完成的视频数据送去解码
}...
};
在完成解码之后,通过事件监听类获得输出数据,并按需对解码后的视频数据进行画面渲染显示等相关操作。
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 的区别如下:
先来对比观察一下鸿蒙和安卓编解码实现原理的代码:
//鸿蒙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 理解:

项目贡献人:朱伟、李珂、郑森文、陈美汝
一周年庆典 盖楼送礼品

扫码留言,多重好礼等你来拿!!!华为手环 6、社区周年庆惊喜礼盒、2000ml 健康随行杯、抱枕等。
👇点击关注鸿蒙技术社区👇
了解鸿蒙一手资讯

点“阅读原文”参加盖楼活动




