一、前言

从Camera1到Camera2,再到现在的Jetpack中的CameraX,Google一直在致力于让Android应用开发者更加方便的去使用摄像头相关的API,更加简单快速的实现摄像头相关的应用功能;下面就来学习一下使用CameraX实现拍照和录制视频功能;

二、CameraX概述

CameraX是Android Jetpack中的组件,旨在简化相机相关的应用开发工作,但是要注意的是CameraX向下最低兼容到Android 5.0(API 21);CameraX利用了Camera2的功能,但采取了一种具有生命周期感知能力且基于用例(UseCase)的更简单的方式,它还解决了设备兼容性问题。

三、CameraX使用

添加依赖:

在Module的build.gradle中添加CameraX相关的依赖

dependencies {
    .....
    def camerax_version = "1.0.0-beta07"
    // CameraX core library using camera2 implementation
    implementation "androidx.camera:camera-camera2:$camerax_version"
    // CameraX Lifecycle Library
    implementation "androidx.camera:camera-lifecycle:$camerax_version"
    // CameraX View class
    implementation "androidx.camera:camera-view:1.0.0-alpha14"
}

同时CameraX需要一些属于java8的方法,因此我们需要相应地设置编译选项。在build.gradle文件中android块的末尾,buildTypes之后,添加以下内容:

android {
  ...
 compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
  }

添加相关权限

添加相机权限,这里因为我们还要实现照片拍摄,视频录制所以除了相机权限,还需要录音权限,以及存储的读写权限;

<uses-feature android:name="android.hardware.camera.any" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

相机预览

相机预览我们肯定是要有一个显示摄像头画面的View,在使用Camera1或Camera2时我们一般都是使用SurfaceView来显示摄像头画面,CameraX中提供PreviewView
作为显示摄像头画面的View

所以我么在布局中添加一个PreviewView

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.camera.view.PreviewView
        android:id="@+id/preview_activity_video_recode"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

并且使用Preview相关的API实现摄像头画面的预览

@SuppressLint("RestrictedApi")
    private void preview() {
        ListenableFuture<ProcessCameraProvider> listenableFuture = ProcessCameraProvider.getInstance(mActivity);
        try {
            mProcessCameraProvider = listenableFuture.get();
            mProcessCameraProvider = listenableFuture.get();

            mPreview = new Preview.Builder()
                    .setTargetResolution(new Size(640, 480))
                    .build();
            mPreview.setSurfaceProvider(mPreviewView.createSurfaceProvider());
            ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
                    .build();
            imageAnalysis
                    .setAnalyzer(ContextCompat.getMainExecutor(mActivity),
                            new ImageAnalysis.Analyzer() {
                                @Override
                                public void analyze(@NonNull ImageProxy image) {
                                    Log.e(TAG, "analyze: " + image);
                                    image.close();
                                }
                            }
                    );
            mProcessCameraProvider.bindToLifecycle((LifecycleOwner) mActivity, CameraSelector.DEFAULT_BACK_CAMERA,
                    mPreview,
                    imageAnalysis
            );
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

我们这里在实现摄像头画面预览的同时,还添加了一ImageAnalysis 图像分析用例,ImageAnalysis设置一个ImageAnalysis.Analyzer分析器,这个用例是用来实现每一帧画面的数据分析的,每一帧画面都会回调到analyze(ImageProxy image)方法
我们可以从IamgeProxy中拿到每一帧画面的图像数据;

这里要注意在回调方法中使用完IamgeProxy后,一定要调用IamgeProxy.close()方法关闭,否则analyze(ImageProxy image)这个方法只会回调一次,这是个坑一定要注意关闭;

拍摄照片

使用IamgeCapture相关的API实现照片拍摄功能

private void takePhoto() {
        ImageCapture imageCapture = new ImageCapture.Builder()
                //优化捕获速度,可能降低图片质量
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                //设置宽高比
                .setTargetResolution(new Size(640, 480))
                //设置初始的旋转角度
                .build();
        String dirPath = getExternalFilesDir("").getAbsolutePath()
                + File.separator + "TestRecode";
        File dirFile = new File(dirPath);
        if (!dirFile.exists()) {
            boolean mkdir = dirFile.mkdir();
            Log.e(TAG, "takePhoto: mkdir:" + mkdir);
        }
        File file = new File(dirFile, System.currentTimeMillis() + ".jpg");
        if (!file.exists()) {
            try {
                boolean newFile = file.createNewFile();
                Log.e(TAG, "takePhoto: newFile:" + newFile);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(file).build();


        mProcessCameraProvider.bindToLifecycle((LifecycleOwner) mActivity, CameraSelector.DEFAULT_BACK_CAMERA,
                imageCapture
        );

        imageCapture.takePicture(outputFileOptions, ContextCompat.getMainExecutor(mActivity), new ImageCapture.OnImageSavedCallback() {
            @Override
            public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
                Log.e(TAG, "onImageSaved: " + outputFileResults);
                Uri savedUri = outputFileResults.getSavedUri();
                Log.e(TAG, "onImageSaved: " + savedUri);
                if (savedUri == null) {
                    savedUri = Uri.fromFile(file);
                }
                Toast.makeText(mActivity, "拍摄成功", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onError(@NonNull ImageCaptureException exception) {
                Log.e(TAG, "onError: " + exception);
            }
        });
    }

录制视频

使用VideoCapture相关的API实现视频录制的功能

@SuppressLint("RestrictedApi")
    private void startRecorder() {

        mVideoCapture = new VideoCapture.Builder()
                .build();

        String dirPath = getExternalFilesDir("").getAbsolutePath()
                + File.separator + "TestRecode";
        File dirFile = new File(dirPath);
        if (!dirFile.exists()) {
            boolean mkdir = dirFile.mkdir();
            Log.e(TAG, "startRecorder: mkdir:" + mkdir);
        }
        File file = new File(dirFile, System.currentTimeMillis() + ".mp4");
        if (!file.exists()) {
            try {
                boolean newFile = file.createNewFile();
                Log.e(TAG, "startRecorder: newFile:" + newFile);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        mProcessCameraProvider.bindToLifecycle(
                this, CameraSelector.DEFAULT_BACK_CAMERA, mPreview,
                mVideoCapture);

        mVideoCapture.startRecording(file, ContextCompat.getMainExecutor(mActivity),
                new VideoCapture.OnVideoSavedCallback() {
                    @Override
                    public void onVideoSaved(@NonNull File file) {
                        Log.e(TAG, "onVideoSaved: " + file);
                        Toast.makeText(mActivity, "录制结束", Toast.LENGTH_SHORT).show();
                    }

                    @Override
                    public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) {
                        Log.e(TAG, "onError: " + videoCaptureError + "," + message);
                        Toast.makeText(mActivity, "录制出错:code:" + videoCaptureError + "," + message, Toast.LENGTH_SHORT).show();
                        mIsRecording = false;
                    }
                }
        );
    }

    @SuppressLint("RestrictedApi")
    private void stopRecorder() {
        if (mVideoCapture != null) {
            mVideoCapture.stopRecording();
        }
    }

一开始的时候回调到onError中,videoCaptureError是2,message是MediaMuxer creation failed!,百度也没有找到解决方法,后来发现是视频文件File没有创建成功,所以要注意检查视频文件File有没有创建成功;

当然别忘记了在程序中先要动态申请相关权限;

以上就是使用CameraX实现摄像头预览、拍照、视频录制功能;

Demo源码地址:https://github.com/maqing-programmer/CameraXDemo