小白也能开发相机?Sample来教你 精华
上期我们给大家介绍了HarmonyOS Sample,收到了不少小伙伴的反馈,想学习一下HarmonyOS相机开发,现在,他来了!
相机开发概览
相机是智能设备最重要的功能之一,它能捕捉美好的瞬间、记录关键的时刻,被广泛应用于日常生活中。本文将详细地讲解一个带有拍摄照片和录制视频功能的相机开发过程。
::: hljs-center
:::
目前,HarmonyOS相机模块支持相机业务的开发,开发者可以通过已开放的接口实现相机硬件的访问、操作和新功能开发。如下图所示,是HarmonyOS为相机应用开发者提供的3个包的内容,包括方法、枚举、以及常量/变量,方便开发者更容易地实现相机功能:
::: hljs-center
:::
相机的开发流程如图所示:
::: hljs-center
:::
相机权限申请
在使用相机之前,需要申请相机的相关权限,比如获取设备的相机权限、麦克风权限、存储权限等。保证应用拥有相机硬件及其他功能权限。
视频链接
开发者需在config.json中配置相关的权限,如下所示:
"reqPermissions": [
{
"name": "ohos.permission.CAMERA"
},
{
"name": "ohos.permission.WRITE_USER_STORAGE"
},
{
"name": "ohos.permission.READ_USER_STORAGE"
},
{
"name": "ohos.permission.MICROPHONE"
},
{
"name": "ohos.permission.LOCATION"
}
]
获取相关权限的具体代码如下所示:
private void requestPermissions() {
String[] permissions = {
//允许应用程序创建或删除文件,或将数据写入设备存储中的文件。
SystemPermission.WRITE_USER_STORAGE,
//允许应用程序从设备存储中读取文件。
SystemPermission.READ_USER_STORAGE,
//允许应用程序使用相机。
SystemPermission.CAMERA,
//允许应用程序访问麦克风。
SystemPermission.MICROPHONE,
//允许应用程序获取设备位置。
SystemPermission.LOCATION
};
List<String> permissionFiltered = Arrays.stream(permissions)
.filter(permission -> verifySelfPermission(permission) != IBundleManager.PERMISSION_GRANTED)
.collect(Collectors.toList());
requestPermissionsFromUser(permissionFiltered.toArray(new String[permissionFiltered.size()]), 0);
初始化相机界面
获取到设备权限后,需要初始化相机界面。通过getWindow()获取当前Ability对应的窗口,再通过addCallback()添加界面操作的回调。代码如下所示:
private void initSurface() {
//获取当前Ability对应的窗口,设置是否启用透明度。isEnable -指定是否启用透明度。True表示启用透明度,False表示禁用。
getWindow().setTransparent(true);
DirectionalLayout.LayoutConfig params = new DirectionalLayout.LayoutConfig(
ComponentContainer.LayoutConfig.MATCH_PARENT, ComponentContainer.LayoutConfig.MATCH_PARENT);
surfaceProvider = new SurfaceProvider(this);
surfaceProvider.setLayoutConfig(params);
/**
* 设置是否将此SurfaceProvider的Surface放置在AGP容器组件的顶层。
* 参数说明:
* toTop -指定是否将相机界面固定在顶部。值true表示将Surface放在AGP容器组件的顶层,而值false表示相反。*/
surfaceProvider.pinToZTop(false);
//通过addCallback()添加界面操作的回调
surfaceProvider.getSurfaceOps().get().addCallback(new SurfaceCallBack());
surfaceContainer.addComponent(surfaceProvider);
}
通过postTask()来执行相机任务:
private class SurfaceCallBack implements SurfaceOps.Callback {
@Override
public void surfaceCreated(SurfaceOps callbackSurfaceOps) {
if (callbackSurfaceOps != null) {
//setFixedSize:设置界面的固定大小。参数说明:宽度-指示界面的宽度。高度-指示界面的高度。
callbackSurfaceOps.setFixedSize(SCREEN_HEIGHT,SCREEN_WIDTH);
}
//设置延时200ms的postTask,用于相机界面的显示
eventHandler.postTask(new Runnable() {
@Override
public void run() {
openCamera();
}
},200);
}
相机设备创建
1 创建相机对象
相机界面准备好后,我们需要创建相机设备。在实现一个相机应用之前必须先创建一个独立的相机设备,然后才能继续相机的其他操作。CameraKit类是相机的入口API类,如下所示:
::: hljs-center
:::
相机设备创建的代码如下:
private void openCamera() {
//ImageReceiver:连接到图像输出设备的图像接收器,并提供一个缓冲队列来接收图像数据
//create:根据指定的图像宽度、高度、格式和缓冲队列容量创建ImageReceiver实例
imageReceiver = ImageReceiver.create(SCREEN_WIDTH, SCREEN_HEIGHT, ImageFormat.JPEG, IMAGE_RCV_CAPACITY);
//设置接收到新图像数据时将调用的图像侦听器。
imageReceiver.setImageArrivalListener(this::saveImage);
//获取CameraKit实例
CameraKit cameraKit = CameraKit.getInstance(getApplicationContext());
//获取当前使用的设备支持的逻辑相机列表
String[] cameraList = cameraKit.getCameraIds();
String cameraId = "";
//获取当前逻辑相机的信息
for (String logicalCameraId : cameraList) {
int faceType = cameraKit.getCameraInfo(logicalCameraId).getFacingType();
switch (faceType){
case CameraInfo.FacingType.CAMERA_FACING_FRONT:
if (isFrontCamera) {
cameraId = logicalCameraId;
}
break;
case CameraInfo.FacingType.CAMERA_FACING_BACK:
if (!isFrontCamera) {
cameraId = logicalCameraId;
}
break;
case CameraInfo.FacingType.CAMERA_FACING_OTHERS:
default:
break;
}
}
if (cameraId != null && !cameraId.isEmpty()) {
CameraStateCallbackImpl cameraStateCallback = new CameraStateCallbackImpl();
/**createCamera()用于创建相机对象
* 该方法的第一个参数代表要打开的摄像头ID;
* 第二个参数用于监听摄像头的状态;
* 第三个参数代表执行callback的Handler,如果程序希望直接在当前线程中执行callback,则可将handler参数设为null。*/
cameraKit.createCamera(cameraId, cameraStateCallback, eventHandler);
}
}
创建相机设备成功后,在CameraStateCallback中会触发onCreated(Camera camera)回调,并且带回Camera对象,用于执行相机设备的操作。
@Override
public void onCreated(Camera camera) {
previewSurface = surfaceProvider.getSurfaceOps().get().getSurface();
if (previewSurface == null) {
HiLog.error(LABEL_LOG, "%{public}s", "Create camera filed, preview surface is null");
return;
}
//获取相机Config.Builder实例。
CameraConfig.Builder cameraConfigBuilder = camera.getCameraConfigBuilder();
//addSurface:添加界面作为相机流的输出。
cameraConfigBuilder.addSurface(previewSurface);
cameraConfigBuilder.addSurface(imageReceiver.getRecevingSurface());
camera.configure(cameraConfigBuilder.build());
cameraDevice = camera;
updateComponentVisible(true);
}
2 配置预览
在使用相机的过程中,用户一般都是先看见预览画面才执行拍照或者其他功能,所以对于一个普通的相机应用,预览是必不可少的。在上述CameraStateCallback中,会调用configure()方法实现预览配置,通过triggerLoopingCapture()方法实现循环帧捕获,从而达到预览的目的。具体代码如下:
@Override
public void onConfigured(Camera camera) {
FrameConfig.Builder framePreviewConfigBuilder = camera.getFrameConfigBuilder(FRAME_CONFIG_PREVIEW);
framePreviewConfigBuilder.addSurface(previewSurface);
//triggerLoopingCapture:开始循环帧捕获。循环帧捕获通常用于预览或录制
camera.triggerLoopingCapture(framePreviewConfigBuilder.build());
}
相机功能实现
相机的基本功能主要分为拍摄照片和录制视频,目前HarmonyOS为开发者提供了如下相机拍照功能实现的Camera操作类,开发者可以通过这些方法,实现各种相机应用的开发:
::: hljs-center
:::
如下图所示是相机的使用过程,接下来的相机功能实现,也会根据此流程图来实现。
::: hljs-center
:::
1 选择功能
通过初始化相机界面组件,设置点击事件侦听器来实现相机功能选择。代码如下所示:
private void initComponents() {
Component takePhoto = findComponentById(ResourceTable.Id_take_photo);
Component videoRecord = findComponentById(ResourceTable.Id_video_record);
//设置点击事件侦听器,用于拍摄照片
takePhoto.setClickedListener((component) -> startAbility(TakePhotoAbility.class.getName()));
//设置点击事件侦听器,用于录制视频
videoRecord.setClickedListener((component) -> startAbility(VideoRecordAbility.class.getName()));
}
2 切换摄像头
开始拍摄照片或录制视频时,由于相机默认打开后置摄像头,需根据场景切换前置摄像头或后置摄像头。如果检测到相机正在工作中,将执行release()方法释放当前相机设备。代码如下:
Image takePhotoImage = (Image) findComponentById(ResourceTable.Id_tack_picture_btn);
//setClickedListener:为组件中的单击事件注册侦听器。
takePhotoImage.setClickedListener(this::takeSingleCapture);
private void takeSingleCapture(Component component) {
if (cameraDevice == null || imageReceiver == null) {
return;
}
//用于配置帧捕获、图像处理和图像输出的接口
FrameConfig.Builder framePictureConfigBuilder = cameraDevice.getFrameConfigBuilder(FRAME_CONFIG_PICTURE);
framePictureConfigBuilder.addSurface(imageReceiver.getRecevingSurface());
FrameConfig pictureFrameConfig = framePictureConfigBuilder.build();
//triggerSingleCapture():开始单帧捕获。这种方法通常用于拍照。
cameraDevice.triggerSingleCapture(pictureFrameConfig);
}
3 拍摄照片
拍照功能属于相机应用的最重要功能之一,而且照片质量对用户至关重要。相机模块基于相机复杂的逻辑,从应用接口层到器件驱动层都已经默认的做好了最适合用户的配置,这些默认配置尽可能地保证用户拍出的每张照片的质量。
实现单帧拍照
单帧拍照,其实就是单帧捕获的过程。通过设置点击事件侦听器setClickedListener(),来触发takeSingleCapture()方法,实现单帧捕获。具体代码如下:
Image takePhotoImage = (Image) findComponentById(ResourceTable.Id_tack_picture_btn);
//setClickedListener:为组件中的单击事件注册侦听器。
takePhotoImage.setClickedListener(this::takeSingleCapture);
private void takeSingleCapture(Component component) {
if (cameraDevice == null || imageReceiver == null) {
return;
}
//用于配置帧捕获、图像处理和图像输出的接口
FrameConfig.Builder framePictureConfigBuilder = cameraDevice.getFrameConfigBuilder(FRAME_CONFIG_PICTURE);
framePictureConfigBuilder.addSurface(imageReceiver.getRecevingSurface());
FrameConfig pictureFrameConfig = framePictureConfigBuilder.build();
//triggerSingleCapture():开始单帧捕获。这种方法通常用于拍照。
cameraDevice.triggerSingleCapture(pictureFrameConfig);
}
实现连拍
连拍功能方便用户一次拍照获取多张照片,用于捕捉精彩瞬间。
同单帧拍照的实现流程一致,但连拍需要使用triggerMultiCapture(frameConfigs)方法用于多帧捕获。
private void takeMultiCapture(Component component) {
FrameConfig.Builder framePictureConfigBuilder = cameraDevice.getFrameConfigBuilder(FRAME_CONFIG_PICTURE);
framePictureConfigBuilder.addSurface(imageReceiver.getRecevingSurface());
List<FrameConfig> frameConfigs = new ArrayList<>();
FrameConfig firstFrameConfig = framePictureConfigBuilder.build();
frameConfigs.add(firstFrameConfig);
FrameConfig secondFrameConfig = framePictureConfigBuilder.build();
frameConfigs.add(secondFrameConfig);
/**triggerMultiCapture(frameConfigs):启动多帧捕获。
cameraDevice.triggerMultiCapture(frameConfigs);
}
存储照片
拍摄后的照片通过saveImage()实现照片存储。具体代码如下:
/**
* 存储照片*/
private void saveImage(ImageReceiver receiver) {
//getFilesDir():获取应用程序在设备内部存储上的文件存储目录。
File saveFile = new File(getFilesDir(), "IMG_" + System.currentTimeMillis() + ".jpg");
ohos.media.image.Image image = receiver.readNextImage();
//定义图像格式,提供获取图像格式信息的接口。
ohos.media.image.Image.Component component = image.getComponent(ImageFormat.ComponentType.JPEG);
byte[] bytes = new byte[component.remaining()];
component.read(bytes);
try (FileOutputStream output = new FileOutputStream(saveFile)) {
output.write(bytes);
output.flush();
showTips(this, "Take photo succeed");
} catch (IOException e) {
HiLog.error(LABEL_LOG, "%{public}s", "saveImage IOException");
}
}
4 录制视频
配置音视频模块
录制视频除了要进行预览配置,还需要进行音视频模块的配置。
比如视频编码格式配置:
setRecorderVideoEncoder(Recorder.VideoEncoder.H264)
音频编码格式配置:
setRecorderAudioEncoder(Recorder.AudioEncoder.AAC)
以及视频的编码码率、帧捕获率、帧率配置等。代码如下所示:
private void initMediaRecorder() {
mediaRecorder = new Recorder();
VideoProperty.Builder videoPropertyBuilder = new VideoProperty.Builder();
videoPropertyBuilder.setRecorderBitRate(10000000);//setRecorderBitRate:设置视频编码码率。
videoPropertyBuilder.setRecorderDegrees(90); //setRecorderDegrees:设置视频旋转角度。
videoPropertyBuilder.setRecorderFps(30);//setRecorderFps:设置视频帧捕获速率。
videoPropertyBuilder.setRecorderHeight(Math.min(1440, 720));//设置视频高度。
videoPropertyBuilder.setRecorderWidth(Math.max(1440, 720));//设置视频宽度。
videoPropertyBuilder.setRecorderVideoEncoder(Recorder.VideoEncoder.H264);//setRecorderVideoEncoder:设置视频编码格式。
videoPropertyBuilder.setRecorderRate(30);//setRecorderRate:设置视频帧率。
Source source = new Source();
//表示使用麦克风作为音频源。
source.setRecorderAudioSource(Recorder.AudioSource.MIC);
//表示使用相机界面作为视频源。
source.setRecorderVideoSource(Recorder.VideoSource.SURFACE);
mediaRecorder.setSource(source);
//setOutputFormat:设置输出文件格式。
mediaRecorder.setOutputFormat(Recorder.OutputFormat.MPEG_4);
//getFilesDir():获取文件存储目录。
File file = new File(getFilesDir(), "VID_" + System.currentTimeMillis() + ".mp4");
StorageProperty.Builder storagePropertyBuilder = new StorageProperty.Builder();
storagePropertyBuilder.setRecorderFile(file);
mediaRecorder.setStorageProperty(storagePropertyBuilder.build());
AudioProperty.Builder audioPropertyBuilder = new AudioProperty.Builder();
//setRecorderAudioEncoder:设置音频编码格式。
audioPropertyBuilder.setRecorderAudioEncoder(Recorder.AudioEncoder.AAC);
mediaRecorder.setAudioProperty(audioPropertyBuilder.build());
mediaRecorder.setVideoProperty(videoPropertyBuilder.build());
mediaRecorder.prepare();
}
开始录制
通过设置长按点击事件侦听器setLongClickedListener()来触发startRecord(),实现开始录制。代码如下:
//为组件中的长单击事件注册侦听器(单击并按住组件)。所有注册的观察员都将收到调度到组件的长单击事件的通知。
videoRecord.setLongClickedListener(component -> {
startRecord();
isRecording = true;
videoRecord.setPixelMap(ResourceTable.Media_ic_camera_video_press);
});
private void startRecord() {
if (cameraDevice == null) {
HiLog.error(LABEL_LOG, "%{public}s", "startRecord failed, parameters is illegal");
return;
}
synchronized (lock) {
initMediaRecorder();
recorderSurface = mediaRecorder.getVideoSurface();
cameraConfigBuilder = cameraDevice.getCameraConfigBuilder();
try {
cameraConfigBuilder.addSurface(previewSurface);
if (recorderSurface != null) {
//添加界面作为相机流的输出。
cameraConfigBuilder.addSurface(recorderSurface);
}
cameraDevice.configure(cameraConfigBuilder.build());
} catch (IllegalStateException | IllegalArgumentException e) {
HiLog.error(LABEL_LOG, "%{public}s", "startRecord IllegalStateException ");
}
}
new ToastDialog(this).setText("Recording").show();
}
停止录制
通过设置触摸事件侦听器setTouchEventListener()来触发stopRecord(),实现停止录制。
//在组件中注册触摸事件的侦听器。所有注册的观察员都将收到调度到组件的触摸事件的通知。
videoRecord.setTouchEventListener((component, touchEvent) -> {
if (touchEvent != null && touchEvent.getAction() == TouchEvent.PRIMARY_POINT_UP && isRecording) {
stopRecord();
isRecording = false;
videoRecord.setPixelMap(ResourceTable.Media_ic_camera_video_ready);
}
return true;
});
private void stopRecord() {
synchronized (lock) {
try {
eventHandler.postTask(() -> mediaRecorder.stop());//stop():停止录制。
if (cameraDevice == null || cameraDevice.getCameraConfigBuilder() == null) {
HiLog.error(LABEL_LOG, "%{public}s", "StopRecord cameraDevice or getCameraConfigBuilder is null");
return;
}
cameraConfigBuilder = cameraDevice.getCameraConfigBuilder();
cameraConfigBuilder.addSurface(previewSurface);
cameraConfigBuilder.removeSurface(recorderSurface);
cameraDevice.configure(cameraConfigBuilder.build());
} catch (IllegalStateException | IllegalArgumentException exception) {
HiLog.error(LABEL_LOG, "%{public}s", "stopRecord occur exception");
}
}
new ToastDialog(this).setText("video saved").show();
}
视频存储和照片存储类似,本文就不做赘述,更多相机的开发请参照HarmonyOS相机开发指导:
相机设备释放
使用完相机后,必须通过release()来关闭相机和释放资源,否则可能导致其他相机应用无法启动。一旦相机被释放,它所提供的操作就不能再被调用,否则会导致不可预期的结果,或是会引发状态异常。相机设备释放的示例代码如下:
private void releaseCamera() {
if (cameraDevice != null) {
//release():释放摄像机。
//释放摄像机后,摄像机的所有操作都不可用。任何操作都会导致意外结果或异常。
cameraDevice.release();
}
if (imageReceiver != null) {
imageReceiver.release();
}
}
至此,就完成了一个具有拍摄照片和录制视频功能的相机开发,开发者也可以通过合适的接口或者接口组合实现闪光灯控制、曝光时间控制、手动对焦和自动对焦控制、变焦控制、人脸识别以及更多的功能。
如果能直接Intent 相机,那该多好。
哎,我也遇到这个问题,不能intent 相机。自己开发太麻烦了。又不需要那么多功能。