# cameraharmony **Repository Path**: isrc_ohos/cameraharmony ## Basic Information - **Project Name**: cameraharmony - **Description**: 一个利用鸿蒙Code编解码器实现视频编解码效果的Demo。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 8 - **Forks**: 2 - **Created**: 2021-09-03 - **Last Updated**: 2024-07-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # cameraharmony **视频编解码Demo。通过调取相机获取原生视频数据并实时显示在界面上,利用鸿蒙Codec编解码器,对视频数据进行编解码,再将编解码后的视频同样显示到界面上,实现了视频编解码的效果** ## 项目介绍 - 项目名称:“家庭合影”鸿蒙端视频编解码渲染Demo - 所属系列:Demo全场景应用 - 功能:支持调用手机相机实时显示视频画面,对视频进行编解码,并将编解码后的视频显示到界面 - 开发版本:sdk5,DevEco Studio2.2 beta1 - 项目发起作者:朱伟,李珂 - 邮箱:[isrc_hm@iscas.ac.cn](mailto:isrc_hm@iscas.ac.cn) ## 项目介绍 - 编程语言:Java ## 安装教程 1. 下载家庭合影demo代码。 2. 启动DevEco Studio,点击file->open,选择此项目打开。 ![](C:\Users\Admin\Desktop\图片1.png) 3. 待编译完成后即可运行。 在sdk5,DevEco Studio2.1 beta3下项目可直接运行 如无法运行,删除项目.gradle,.idea,build,gradle,build.gradle文件, 并依据自己的版本创建新项目,将新项目的对应文件复制到根目录下 ## 使用说明 1. MainAbilitySlice中定义布局。先声明布局,使用竖向的DirectionalLayout布局 ```java private DirectionalLayout myLayout = new DirectionalLayout(this); ``` ​ 在onStart方法中设置布局属性 ```java myLayout.setWidth(ComponentContainer.LayoutConfig.MATCH_PARENT);// 设置布局大小 myLayout.setHeight(ComponentContainer.LayoutConfig.MATCH_PARENT); LayoutConfig config = new LayoutConfig(ComponentContainer.LayoutConfig.MATCH_CONTENT, ComponentContainer.LayoutConfig.MATCH_CONTENT); myLayout.setLayoutConfig(config);// 设置布局属性 myLayout.setOrientation(Component.VERTICAL); myLayout.setPadding(32,32,32,32);// 设置布局内边距 ``` ​ 添加控件。Button按钮控件用于控制编解码,声明控件并设置属性之后,使用布局的addComponent方法将Button控件添加到当前布局中 ```java Button button = new Button(this);// 声明 Button 控件 button.setLayoutConfig(config);// 设置 Button 属性 button.setText("开始编解码."); button.setTextSize(50); button.setMarginLeft(350); button.setId(ID_BUTTON);// 设置 Button ID ShapeElement background = new ShapeElement(); background.setRgbColor(new RgbColor(0xFF51A8DD));// 设置控件背景颜色 background.setCornerRadius(25);// 设置控件圆角 button.setBackground(background);// 将背景set到Button中 button.setPadding(10, 10, 10, 10);// 设置布局内边距 myLayout.addComponent(button);// 将 Button 添加到布局中 ``` ​ Text控件用于文本显示,显示编解码状态,设置相应属性之后,将Button控件添加到当前布局中 ```java Text text = new Text(this);// 声明Text控件 text.setLayoutConfig(config); text.setMarginTop(30); text.setMarginLeft(200); text.setText("这里显示编解码状态..."); text.setTextSize(50);// 设置 Text 中字体大小 text.setId(ID_TEXT); myLayout.addComponent(text);// 将 Text 添加到布局中 ``` ​ 声明两个SurfaceProvider控件进行视频渲染处理 ```java private SurfaceProvider surfaceview1;// 两个SurfaceProvider分别用于显示编码前后的视频 private SurfaceProvider surfaceview2; ``` ​ surfaceview1用于显示手机相机直接获取到的视频,surfaceview2用于编解码后的视频,设置完相应属性后同样需要添加到当前布局中,下面以surfaceview1为例,surfaceview2同理 ```java surfaceview1 = new SurfaceProvider(this); surfaceview1.setWidth(400);// 设置 SurfaceProvider 大小 surfaceview1.setHeight(300); surfaceview1.getSurfaceOps().get().addCallback(callback);// 设置回调 surfaceview1.pinToZTop(true); surfaceview1.setRotation(180);// 设置画面旋转角度 surfaceview1.setMarginTop(30); surfaceview1.setMarginLeft(300); myLayout.addComponent(surfaceview1);// 添加到布局中 ``` 2. 通过CameraStateCallbackImpl类创建相机运行时的回调;并根据上述设置的运行时的回调,使用createCamera()方法创建相机设备 ```java CameraStateCallbackImpl cameraStateCallback = new CameraStateCallbackImpl(); if(cameraStateCallback ==null) { System.out.println("cameraStateCallback is null"); } EventHandler eventHandler = new EventHandler(EventRunner.create("CameraCb")); if(eventHandler ==null) { System.out.println("eventHandler is null"); } // 根据上述设置的运行时的回调,创建相机设备 cameraKit.createCamera(cameraIds[0], cameraStateCallback, eventHandler); ``` 3. 完成相机相关初始化和相应的相机配置,配置预览的Surface,即使用surfaceview1预览相机拍摄的视频 ```java mSnackBar = new SnackBar.Builder(ability,component1) ``` 4. 通过IImageArrivalListener类监听相机获取实时返回的YUV视频数据,并通过编码类的addFrame方法将数据先加入到队列中,等待编码 ```java 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 视频数据 int i; // 从相机获取实时拍摄的视频数据,并将 Image 读取到的视频流数据存放在 mBuffer 中 mBuffer = mImage.getComponent(ImageFormat.ComponentType.YUV_Y).getBuffer(); mLog.log("imagearival", "***********获取到的数据:"+mBuffer); System.out.println("***********获取到的数据:"+mBuffer); // 从视频流mBuffer逐个读取成 byte 数组的形式,并存储在 YUV_DATA 中 for(i=0;i< VIDEO_WIDTH * VIDEO_HEIGHT;i++){ YUV_DATA[i] = mBuffer.get(i); } mBuffer = mImage.getComponent(ImageFormat.ComponentType.YUV_V).getBuffer(); for(i=0;i< VIDEO_WIDTH * VIDEO_HEIGHT / 4;i++){ YUV_DATA[(VIDEO_WIDTH * VIDEO_HEIGHT) + i * 2] = mBuffer.get(i * 2); } mBuffer = mImage.getComponent(ImageFormat.ComponentType.YUV_U).getBuffer(); for(i=0;i< VIDEO_WIDTH * VIDEO_HEIGHT / 4;i++){ YUV_DATA[(VIDEO_WIDTH * VIDEO_HEIGHT) + i * 2 + 1] = mBuffer.get(i * 2); } // 将视频数据 YUV_DATA 加入到队列等待编解码 vdEncoder.addFrame(YUV_DATA); mImage.release();// 获取完视频数据之后及时释放 return; } } }; ``` 5. 通过VDEncoder构造函数初始化编码器,并通过当前编码类对象vdEncoder使用prepareDecoder()方法创建并初始化解码器VDDecoder,编解码器初始化的过程相同只是其中一些参数可能不同,以编码器为例 ```java 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();// 初始化自定义单例线程池 } ``` 6. 同时设置SurfaceProvider,将用于显示编解码后视频的Surfaceview2作为入参传过去 ```java public void prepareDecoder(SurfaceProvider surfaceview){ vdDecoder = new VDDecoder(surfaceview);// 创建解码类对象,并使用surfaceview显示解码后的视频 vdDecoder.start();// 开始解码 } ``` 7. 在控件button的click点击事件中,通过VDEncoder的start方法开始编码 ```java button.setClickedListener(component -> { // 按钮被点击 mLog.log("button", "start"); vdEncoder.start();// 开始编码 if(vdEncoder.isRuning){// 如果编码正在进行,显示当前编码状态 text.setText("成功进行编解码,并显示在下方"); } }); ``` 8. 编码类VDEncoder中的编码线程。首先从存放相机原始数据的队列中获取数据保存到byte数组data中,之后将数据以Buffer和BufferInfo的形式通过Codec类进行编码。注意,在Harmony中用Codec类进行编解码时,Buffer和BufferInfo要成对使用 ```java 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(); buffer.put(data); bufferInfo.setInfo(0, data.length, System.currentTimeMillis(), 0); mCodec.writeBuffer(buffer, bufferInfo); } } }); } ``` 9. 通过Codec的事件监听类监听编码器,获取编码完成后的数据;并通过当前解码类对象vdDecoder,使用toDecoder()方法,将编码完成的视频数据送去解码 ```java 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 方法,将编码完成的视频数据送去解码 } @Override public void onError(int i, int i1, int i2) { throw new RuntimeException(); } }; ``` 10. 判断如果解码器完成了初始化操作,就将拿到的编码后的数据,通过decoder()方法开始解码 ```java public void toDecoder(byte[] video){ if (isMediaCodecInit) { decoder(video);// 解码 mLog.log("videocallback", "length:" + video.length); } } ``` 11. 解码。将数据以Buffer和BufferInfo的形式通过Codec类进行解码 ```java private void decoder(byte[] video) { ByteBuffer mBuffer = mCodec.getAvailableBuffer(-1); BufferInfo info = new BufferInfo(); info.setInfo(0, video.length, 0, 0); mBuffer.put(video); mCodec.writeBuffer(mBuffer, info); } ``` 12. 通过Codec的事件监听类监听解码器,获取解码完成后的数据。由于直接渲染解码完后的数据得到的是逆时针旋转90度后的画面,所以在渲染之前需要先使用rotateNV21()方法进行一步旋转操作从而使图像正向显示 ```java private Codec.ICodecListener decoderlistener = new Codec.ICodecListener() { // 用于监听解码器,获取解码完成后的数据 @Override public void onReadBuffer(ByteBuffer byteBuffer, BufferInfo bufferInfo, int i) { byte[] bytes = new byte[bufferInfo.size]; byte[] rotate_bytes = new byte[bufferInfo.size]; byteBuffer.get(bytes);// 从解码器的 byteBuffer 中获取数据 mLog.log("bytes", "byte:" + bytes); // 将解码后的 NV21(YUV420SP) 数据 bytes 顺时针旋转 90 度,并通过 Surface 显示 rotateNV21(bytes, rotate_bytes, 640, 480, 90);// 旋转后的数据用 rotate_bytes 存放 ``` 13. 渲染。通过SurfaceProvider的getSurfaceOps方法获取Holder即SurfaceOps,设置回调等属性。在surfaceCreated方法中通过Holder即SurfaceOps的getSurface方法得到Surface ```java @Override public void surfaceCreated(SurfaceOps holder) { isSurfaceCreated = true; mSurface = holder.getSurface(); beginCodec();// 准备初始化解码器 mLog.log("surfacecreated", "surfaceCreated!!!!!!"); } ``` ​ 之后通过Surface,使用showRawImage()方法在surfaceview2控件中渲染出来 ```java mSurface.showRawImage(rotate_bytes, Surface.PixelFormat.PIXEL_FORMAT_YCRCB_420_SP, 640, 480); ``` ## 版权和许可信息 - cameraharmony经过[Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0)授权许可。