【Android】录制视频

三三要成为安卓糕手

零:总体代码

1:UI设计

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".CameraVideoRecordActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/preview_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv_record"
        android:layout_width="66dp"
        android:layout_height="66dp"
        android:src="@mipmap/icon_record"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.95" />

    <ImageView
        android:id="@+id/iv_switch"
        android:layout_width="44dp"
        android:layout_height="44dp"
        android:layout_margin="30dp"
        android:src="@mipmap/icon_switch_camera"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

2:Activity代码

java 复制代码
public class CameraVideoRecordActivity extends AppCompatActivity {
    private static final String TAG = "CameraVideoRecordActivity";

    private PreviewView previewView;
    private ImageView ivRecord;
    private ImageView ivSwitch;

    private boolean isRecording;
    private VideoCapture<Recorder> videoCapture;
    private ExecutorService executorService;
    private Recording recording;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera_video_record);


        previewView = findViewById(R.id.preview_view);
        ivRecord = findViewById(R.id.iv_record);
        ivSwitch = findViewById(R.id.iv_switch);


        ivRecord.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (isRecording) {
                    stopRecording();
                } else {
                    startRecording();
                }
            }
        });

        startCamera();
        //创建一个子线程,用于处理录制回调事件,避免阻塞主线程的进行
        executorService = Executors.newSingleThreadExecutor();
        requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO},100);
    }

    private void startCamera() {
        ListenableFuture<ProcessCameraProvider> providerListenableFuture = ProcessCameraProvider.getInstance(this);
        providerListenableFuture.addListener(new Runnable() {
            @Override
            public void run() {
                try {
                    ProcessCameraProvider cameraProvide = providerListenableFuture.get();

                    //后置摄像头
                    CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;

                    //创建预览实例,并把预览实例与previewView绑定
                    Preview preview = new Preview.Builder().build();
                    preview.setSurfaceProvider(previewView.getSurfaceProvider());

                    //视频录制的实例
                    Recorder recorder = new Recorder.Builder()
                            .setQualitySelector(QualitySelector.from(Quality.HD))
                            .build();
                    videoCapture = VideoCapture.withOutput(recorder);

                    //相机绑定生命周期
                    cameraProvide.unbindAll();
                    cameraProvide.bindToLifecycle(CameraVideoRecordActivity.this,
                            cameraSelector, preview,videoCapture);


                } catch (ExecutionException e) {
                    throw new RuntimeException(e);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
        }, ContextCompat.getMainExecutor(this));
    }


    /**
     * 开始录制
     */
    private void startRecording() {
        if(isRecording){
            Toast.makeText(this,"正在录制中",Toast.LENGTH_SHORT).show();
            return;
        }
        //在contentValues中指定文件名和类型
        ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME,
                "video_" + System.currentTimeMillis() + ".mp4");
        contentValues.put(MediaStore.MediaColumns.MIME_TYPE,"video/mp4");

        //指定文件输出位置
        MediaStoreOutputOptions.Builder builder =
                new MediaStoreOutputOptions.Builder(getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
                .setContentValues(contentValues);
        MediaStoreOutputOptions build = builder.build();


        //检查录音权限申请
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "清先获取录音的权限", Toast.LENGTH_SHORT).show();
            return;
        }
        PendingRecording recording = videoCapture.getOutput().prepareRecording(this, build)
                .withAudioEnabled();

        //启动录制
        this.recording = recording.start(executorService, new Consumer<VideoRecordEvent>() {
            @Override
            public void accept(VideoRecordEvent videoRecordEvent) {

                if (videoRecordEvent instanceof VideoRecordEvent.Start) {
                    Log.i(TAG, "accept: 开始录制");
                    isRecording = true;
                    ivRecord.setImageResource(R.mipmap.icon_record);
                } else if(videoRecordEvent instanceof VideoRecordEvent.Finalize){
                    Log.i(TAG, "accept: 录制结束");
                    isRecording = false;
                    ivRecord.setImageResource(R.mipmap.icon_stop_record);
                }
            }
        });
    }

    private void stopRecording() {
        if(recording != null && isRecording){
            recording.stop();//停止
            recording = null;//释放资源
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        executorService.shutdown();
    }
}

一:startCamera启动相机

1:创建视频录制实例

这里是跟拍照功能那有一些不一样的地方;这里使用构建设计模式,设置默认录制视频的清晰度

java 复制代码
                    //视频录制的实例
                    Recorder recorder = new Recorder.Builder()
                            .setQualitySelector(QualitySelector.from(Quality.HD))
                            .build();
                    videoCapture = VideoCapture.withOutput(recorder);


二:开始录制功能代码分析

1:视频文件的输出配置

这段代码的主要目的是创建一个视频文件的输出配置,告诉系统视频文件应该存储在哪里、以什么信息保存(文件名、格式等),以便后续视频录制完成后能正确写入到系统中。

(1)MediaStoreOutputOptions.Builder构建器
java 复制代码
MediaStoreOutputOptions.Builder builder =
    new MediaStoreOutputOptions.Builder(getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
    .setContentValues(contentValues);
  • 构造参数 1:getContentResolver()

    获取内容解析器(ContentResolver),用于访问 Android 系统的媒体库(如照片、视频库)。

  • 构造参数 2:MediaStore.Video.Media.EXTERNAL_CONTENT_URI

    指定视频文件的存储位置 ------外部存储的系统视频媒体库(即系统默认的 "视频" 文件夹),之前用过的

  • .setContentValues(contentValues)

    将之前创建的contentValues(包含文件名、MIME 类型等信息)设置到构建器中,这些信息会被用于创建视频文件。

(2)MediaStoreOutputOptions

  • MediaStoreOutputOptions:CameraX 库中用于配置媒体文件(视频 / 图片)输出位置和属性的类,确保文件能正确写入系统媒体库。
  • 为什么用这种方式存储?
    直接通过MediaStore存储视频,会让文件自动出现在系统相册 / 视频库中,无需手动刷新媒体库;同时符合 Android 10 + 的存储权限规范(无需申请WRITE_EXTERNAL_STORAGE权限)。

2:录音权限检查

在启动带音频的视频录制前,检查应用是否已获得录音权限RECORD_AUDIO),这是 Android 权限管理的强制要求(属于 "危险权限",必须明确申请)。

java 复制代码
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
    Toast.makeText(this, "请先获取录音的权限", Toast.LENGTH_SHORT).show();
    return;
}

3:创建 "待启动" 的录制对象。

  • videoCapture.getOutput()
    videoCapture 是 CameraX 库中负责视频捕获的核心对象,在startCamera方法中已经创建好了,并且提取了成员变量,getOutput() 用于获取其输出控制器,后续通过它配置录制参数。

  • .prepareRecording(this, build)

    准备录制会话,参数说明:

    • build:前面创建的MediaStoreOutputOptions对象(包含视频存储路径、文件名等配置)
      这一步会根据配置初始化录制环境,确定视频文件的存储位置和格式。
  • .withAudioEnabled()

    启用音频录制功能(默认可能只录视频不含声音),使最终生成的视频包含音频轨道;所以这里需要检查录音权限

  • PendingRecording recording

    接收返回的PendingRecording对象,它表示 "已准备好但尚未启动" 的录制任务,后续通过它的start()方法真正开始录制。

4:开始录制

通过调用start()方法正式启动视频录制,并设置一个回调监听器,实时接收录制过程中的各种状态事件(如开始、结束等),进而更新应用的状态标记和 UI 显示。

(1)启动录制并绑定回调

  • start(...):用于正式启动录制,需要传入两个参数:

    • executorService:一个线程池(ExecutorService),指定回调事件的处理线程(避免在主线程处理耗时操作)
    • Consumer<VideoRecordEvent>接口实现,用于接收录制过程中的各种事件(回调函数)。
  • this.recording:将启动后的录制对象保存到成员变量中,方便后续控制资源释放(如调用stop()停止录制)。

(2)回调接口Consumer<VideoRecordEvent>

  • VideoRecordEvent:CameraX 定义的录制事件基类,包含多种子类,分别表示不同的录制状态(如开始、暂停、结束等)。
java 复制代码
        //回调中的videoRecordEvent会有下面几种状态:
        //VideoRecordEvent.Start:录制开始。
        //VideoRecordEvent.Pause:录制暂停。
        //VideoRecordEvent.Resume:录制恢复。
        //VideoRecordEvent.Finalize:录制完成(停止或失败)。
        //VideoRecordEvent.Status:录制状态更新(持续获取统计信息)。
  • accept()方法:当录制状态发生变化时,系统会自动调用该方法,并传入对应的事件对象recordEvent

(3)关键技术点

线程切换executorService指定了回调的执行线程,通常会在回调中通过runOnUiThread()切换到主线程更新 UI,setImageResource()属于 UI 操作,需确保在主线程执行

异步回调机制: 录制过程是异步的(在后台执行),通过回调通知 UI 线程状态变化,避免阻塞主线程。

状态管理isRecording变量用于全局跟踪录制状态,防止重复操作(如多次点击开始录制)。

(4)可能的扩展

  • 可以增加对其他事件的处理,如VideoRecordEvent.Pause(暂停)、VideoRecordEvent.Resume(恢复)等。
  • 可在Finalize事件中添加视频保存成功 / 失败的判断(通过recordEvent.getError()检查是否有错误)。
  • 可以在回调中实时更新录制时长(通过recordEvent.getRecordingStats().getRecordedDurationMs()获取已录制时长)。

5:效果


相关推荐
阿巴斯甜12 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker12 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952713 小时前
Andorid Google 登录接入文档
android
黄林晴14 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android