【Android】录制视频
三三要成为安卓糕手
零:总体代码
1:UI设计
<?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.PreviewViewandroid: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" /><ImageViewandroid: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" /><ImageViewandroid: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代码
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;@Overrideprotected 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() {@Overridepublic 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() {@Overridepublic 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>() {@Overridepublic 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;//释放资源}}@Overrideprotected void onDestroy() {super.onDestroy();executorService.shutdown();}
}
一:startCamera启动相机
1:创建视频录制实例
这里是跟拍照功能那有一些不一样的地方;这里使用构建设计模式,设置默认录制视频的清晰度
//视频录制的实例Recorder recorder = new Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.HD)).build();videoCapture = VideoCapture.withOutput(recorder);
二:开始录制功能代码分析
1:视频文件的输出配置
这段代码的主要目的是创建一个视频文件的输出配置,告诉系统视频文件应该存储在哪里、以什么信息保存(文件名、格式等),以便后续视频录制完成后能正确写入到系统中。
(1)MediaStoreOutputOptions.Builder
构建器
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 权限管理的强制要求(属于 “危险权限”,必须明确申请)。
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 定义的录制事件基类,包含多种子类,分别表示不同的录制状态(如开始、暂停、结束等)。
//回调中的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:效果