marclee44 / me

1 stars 0 forks source link

Android摄像头预览及录像——CameraX+PreviewView+VideoCapture #22

Open marclee44 opened 2 years ago

marclee44 commented 2 years ago

简介

CameraX是一个Jetpack支持库,旨在简化相机应用的开发工作。它提供一致且易用的API接口,适用于大多数Android设备,并可向后兼容至Android 5.0(API 级别 21)。 虽然CameraX利用了camera2的功能,但采取了一种具有生命周期感知能力且基于用例的更简单方式。它还解决了设备兼容性问题,因此无需在代码库中添加设备专属代码。这些功能减少了将相机功能添加到应用时需要编写的代码量。

基本使用

开发者使用CameraX,借助名为“用例”的抽象概念与设备的相机进行交互。目前提供的用例如下:

不同用例可以组合使用,也可以同时处于活跃状态。例如,应用中可以加入预览用例,以便让用户查看进入相机视野的画面;加入图片分析用例,以确定照片里的人物是否在微笑;还可以加入图片拍摄用例,以在人物微笑时拍摄照片。

依赖项

当前CameraX的版本依然事将以下内容添加到使用的每个模块的build.gradle文件中:

dependencies {
    def camerax_version = "1.1.0-alpha06"
    // 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-alpha26"
}

权限

AndroidManifest.xml中添加如下权限

    <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"/>

在使用摄像头的Activity/Fragment中,需要进行权限验证

    private void checkPermissions() {
        ActivityResultLauncher<String[]> mRequestPermission = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(),
                result -> {
                    if (result.values().stream().allMatch(p -> p)) {
                        init();
                    } else {
                        ToastUtils.showShort("有权限被拒绝,无法使用本功能。请在设置中打开所需权限。");
                    }
                });

        if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED &&
                ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED &&
                ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
                ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
            init();
        } else {
            mRequestPermission.launch(new String[]{
                    Manifest.permission.CAMERA,
                    Manifest.permission.RECORD_AUDIO,
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
            });
        }
    }

主要代码

UI层最简单,只要放一个PreviewView即可。当然,开始录像/停止录像/切换摄像头按钮不可少

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

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <Button
            android:id="@+id/btn_video_recode_start"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="录制" />

        <Button
            android:id="@+id/btn_video_recode_stop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="停止" />

        <Button
            android:id="@+id/btn_switch_camera"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="切换摄像头" />
    </LinearLayout>

控制分几部分说

    private PreviewView mPreviewView;
    private VideoCapture mVideoCapture;
    private ProcessCameraProvider mCameraProvider;
    private ListenableFuture<ProcessCameraProvider> mCameraFuture;
    private CameraSelector mCameraSelector;
    private Preview mPreview;
    private Button mStartBtn;
    private Button mStopBtn;
    private Button mSwitchBtn;
    private void initEvent() {
        mStartBtn = findViewById(R.id.btn_video_recode_start);
        mStopBtn = findViewById(R.id.btn_video_recode_stop);
        mSwitchBtn = findViewById(R.id.btn_switch_camera);

        mStartBtn.setOnClickListener(v -> startRecorder());
        mStopBtn.setOnClickListener(v -> stopRecorder());
        mSwitchBtn.setOnClickListener(v -> switchCamera());
    }

    @SuppressLint("RestrictedApi")
    private void initCamera() {
        mCameraFuture = ProcessCameraProvider.getInstance(requireContext());
        mCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
        mPreview = new Preview.Builder()
                .build();
        mPreview.setSurfaceProvider(mPreviewView.getSurfaceProvider());
        mVideoCapture = new VideoCapture.Builder()
                .build();
        try {
            mCameraProvider = mCameraFuture.get();
        } catch (ExecutionException | InterruptedException e) {
            Logger.e("初始化摄像头失败,%s", e.toString());
        }
    }

多个用例可以同时运行。虽然可以将多个用例依序绑定到一个生命周期,但最好通过对CameraProcessProvider.bindToLifecycle()的一次调用来绑定所有用例。

    private void startPreview() {
        mCameraFuture.addListener(() -> {
            try {
                mCameraProvider.unbindAll();
                mCameraProvider.bindToLifecycle(getViewLifecycleOwner(), mCameraSelector, mPreview, mVideoCapture);
            } catch (Exception e) {
                Logger.e("预览摄像头失败,%s", e.toString());
            }
        }, ContextCompat.getMainExecutor(requireContext()));
    }

在预览状态(未在录像中),可以切换前后摄像头。此时,需要重新绑定所有用例。

    private void switchCamera() {
        mCameraSelector = mCameraSelector == CameraSelector.DEFAULT_BACK_CAMERA ? CameraSelector.DEFAULT_FRONT_CAMERA : CameraSelector.DEFAULT_BACK_CAMERA;
        startPreview();
    }

停止录像/录像异常的回调,在开始录像时进行设置

    @SuppressLint("RestrictedApi")
    private void startRecorder() {
        String dirPath = requireContext().getExternalFilesDir("").getAbsolutePath();
        File dirFile = new File(dirPath);
        if (!dirFile.exists()) {
            boolean mkdir = dirFile.mkdir();
        }
        File file = new File(dirFile, System.currentTimeMillis() + ".mp4");
        if (!file.exists()) {
            try {
                boolean newFile = file.createNewFile();
            } catch (IOException e) {
                Logger.e("创建视频文件失败,%s", e.toString());
            }
        }

        if (ActivityCompat.checkSelfPermission(this.requireContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            return;
        }

        mVideoCapture.startRecording(new VideoCapture.OutputFileOptions.Builder(file).build(), ContextCompat.getMainExecutor(requireContext()),
                new VideoCapture.OnVideoSavedCallback() {
                    @Override
                    public void onVideoSaved(@NonNull VideoCapture.OutputFileResults outputFileResults) {
                        Uri uri = outputFileResults.getSavedUri();、
                        ToastUtils.showShort(uri.getPath()+"录制完成。");
                    }

                    @Override
                    public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) {
                        Logger.e("录制出错。code:%s,%s", videoCaptureError, message);
                    }
                }
        );
    }
    @SuppressLint("RestrictedApi")
    private void stopRecorder() {
        if (mVideoCapture != null) {
            mVideoCapture.stopRecording();
        }
    }