【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:效果


相关推荐
Black蜡笔小新4 小时前
视频融合平台EasyCVR国标GB28181视频诊断功能详解与实践
音视频
啦工作呢5 小时前
ES6 promise-try-catch-模块化开发
android·okhttp
xiangxiongfly9156 小时前
Android 自定义View之BubbleImageView
android·气泡·bubbleimageview·气泡imageview
vivo高启强6 小时前
R8 如何优化我们的代码(2) -- 空值数据流分析
android
2501_916013746 小时前
iOS 26 系统电耗分析实战指南 如何检测电池掉电、液体玻璃导致的能耗变化
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_915921436 小时前
iOS 原生开发全流程解析,iOS 应用开发步骤、Xcode 开发环境配置、ipa 文件打包上传与 App Store 上架实战经验
android·macos·ios·小程序·uni-app·iphone·xcode
低调小一6 小时前
双端 FPS 全景解析:Android 与 iOS 的渲染机制、监控与优化
android·ios·kotlin·swift·fps
如此风景6 小时前
Compose UI中padding操作符顺序对布局的影响
android
山烛7 小时前
OpenCV :基于 Lucas-Kanade 算法的视频光流估计实现
人工智能·opencv·计算机视觉·音视频·图像识别·特征提取·光流估计