MediaRecorder 介绍和使用,结合Camera2录制视频实例(含完整项目)
在中通过设定的宽高去修改view的宽高。@Override} else {} else {可以将相机的一些基础操作和实现都封装起来,并在CameraSurfaceView中完成相关操作。完整文件地址:// 封装相机的各种操作// 设置屏幕常亮@Override@Override// 将View的大小修改为和相机预览分辨率相同的比例@Override。
一、简介
MediaRecorder 是 Android 提供的一个用于音视频录制的高级类,旨在简化音频和视频的录制过程。它封装了底层的音视频编码器(通常是 MediaCodec)和其他相关组件。
如果你不需要对音视频进行更底层的控制,而只是想要方便地进行录制操作,那么可以选择使用 MediaRecorder。否则可以考虑使用 MediaCodec + MediaMutex。
官网文档链接:MediaRecorder
二、常用设置和方法
MediaRecorder 提供了一些默认的配置,但也提供了一些可供调整的参数,以便你更好地适应你的应用需求。
1. 视频设置:
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); // 设置视频来源
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // 设置视频编码格式
mediaRecorder.setVideoSize(width, height); // 设置视频分辨率大小
mediaRecorder.setVideoFrameRate(30); // 设置视频帧率
mediaRecorder.setCaptureRate(30); // 设置视频捕获率
int bitRate = width * height * 8;
mediaRecorder.setVideoEncodingBitRate(bitRate); // 设置比特率
- 现在一般都是用Camera2的接口了,这里就指定视频源为 MediaRecorder.VideoSource.SURFACE。(MediaRecorder.VideoSource.CAMERA 是旧的Camera使用的)
- H264 是常用的视频编码格式。
- setVideoFrameRate() 方法系统会尽量匹配你设置的帧率,但实际录制的帧率可能会受到硬件和系统限制。
- 视频比特率 1440*1080分辨率为例上述代码设置的频比特率大约为 11.8 Mbps。
视频比特率
与视频质量和文件大小之间存在紧密的关系。在给定的视频分辨率下,增加比特率可以提高视频质量,但也会增加文件大小;减小比特率则会导致视频质量下降,但文件大小减小。以下是一些建议的视频比特率范围,用于一般应用场景:
- 低质量视频(360p 分辨率):比特率范围:200 kbps - 500 kbps。
- 标准质量视频(480p 到 720p 分辨率):比特率范围:500 kbps - 2.5 Mbps。
- 高质量视频(720p 到 1080p 分辨率):比特率范围:2.5 Mbps - 8 Mbps。
- 全高清视频(1080p 到 1440p 分辨率):比特率范围:8 Mbps - 16 Mbps。
- 超高清视频(4K 分辨率及以上):比特率范围:16 Mbps - 50 Mbps 或更高。
2. 音频设置:
如果不需要录制音频,这里也可以不用设置。
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 设置音频来源从麦克风采集
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); // 设置音频编码格式
mediaRecorder.setAudioEncodingBitRate(96000); // 设置音频编码比特率(一般音乐和语音录制)
mediaRecorder.setAudioSamplingRate(44100); // 设置音频采样率(CD音质)
音频编码比特率
一般而言,音频编码比特率的典型范围是 16 kbps 到 320 kbps。以下是一些常见的音频比特率设置:
- 16 kbps:较低质量,适用于语音通话等要求较低的场景。
- 64 kbps - 128 kbps:一般质量,适用于一般音乐和语音录制。
- 192 kbps - 320 kbps:较高质量,适用于对音频质量要求较高的音乐录制等场景。
音频采样率
音频采样率表示在一秒钟内对声音信号进行采样的次数,通常以赫兹(Hz)为单位。以下是一些常见的音频采样率:
- 8 kHz: 适用于电话通话等语音通信,因为人类语音的主要频率范围通常在 300 Hz 到 3.4 kHz 之间。
- 16 kHz: 常用于语音识别和语音合成等应用,可以更好地捕捉语音的细节。
- 44.1 kHz: CD音质,适用于音乐录制和播放,因为这是 CD 音频的标准采样率。
- 48 kHz: 常用于广播、视频制作和音频/视频同步应用,是许多数字音频设备的标准采样率。
- 96 kHz 或更高: 在专业音频制作领域,高采样率用于捕捉更宽的频率范围,但对于大多数应用,通常不需要使用这么高的采样率。
3. 输出路径、格式等其他设置
mediaRecorder.setMaxDuration(maxDurationMillis); // 设置最大录制时长
mediaRecorder.setMaxFileSize(maxFileSizeBytes); // 设置最大文件大小
File outputFile = new File(GALLERY_PATH, "output.mp4");
mediaRecorder.setOutputFile(outputFile); // 设置输出路径
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 设置输出格式
最大录制时长和最大文件大小不设置也可以。输出格式设置为常用的 mp4 格式。
4. 开始和停止录制
请注意,之前的设置需要在 prepare()
之前进行,以确保 MediaRecorder 正确初始化。
mediaRecorder.prepare();
mediaRecorder.start();
// ...
mediaRecorder.stop();
mediaRecorder.reset();
mediaRecorder.release();
5. 代码流程
MediaRecorder 中很多方法都是有一定先后的执行顺序的,错误的执行顺序会导致抛出 IllegalStateException 异常,使用的时候需要额外注意。
下面是一个简单的代码流程示例:
MediaRecorder mediaRecorder = new MediaRecorder();
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); // 设置视频来源
mediaRecorder.setVideoEncodingBitRate(mPreviewSize.getWidth() * mPreviewSize.getHeight() * 8); // 设置比特率
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 设置音频来源
mediaRecorder.setAudioEncodingBitRate(96000); // 设置音频编码比特率(一般音乐和语音录制)
mediaRecorder.setAudioSamplingRate(44100); // 设置音频采样率(CD音质)
mediaRecorder.setCaptureRate(30); // 捕获率
mediaRecorder.setOrientationHint(0); // 设置录制视频时的预期方向,取值为 0、90、180 或 270
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 设置输出格式
mediaRecorder.setVideoSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); // 设置视频宽高
mediaRecorder.setVideoFrameRate(30); // 设置帧数
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // 设置视频编码格式
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); // 设置音频编码格式
mediaRecorder.setInputSurface(mRecordSurface);
mediaRecorder.setOutputFile(outputFile);
mediaRecorder.prepare();
mediaRecorder.start(); // 开始录制
// 录制...
mediaRecorder.stop(); // 停止录制
mediaRecorder.reset();
mediaRecorder.release(); // 释放
三、应用实例
想要使用 Camera2 + MediaRecorder 做一个相机录制视频的功能,可以按照如下几步完成。
注意: 避免篇幅过长,示例代码会进行一定缩减,完整的代码会在链接中贴出。
1. 实现相机显示View
我们选择使用 SurfaceView 来显示相机的预览画面,它相比 TextureView 性能更优,且比 GLSurfaceView 简单易用。另外我们需要修改 SurfaceView 的宽高,使其和相机预览的分辨率比例一致,这样画面才不会被拉伸。
a. 自定义一个可以设置宽高比例的SurfaceView
在 onMeasure()
中通过设定的宽高去修改view的宽高。
public class AutoFitSurfaceView extends SurfaceView {
private int mRatioWidth = 0;
private int mRatioHeight = 0;
public AutoFitSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setAspectRatio(int width, int height) {
if (width < 0 || height < 0) {
throw new IllegalArgumentException("Size cannot be negative.");
}
mRatioWidth = width;
mRatioHeight = height;
post(() -> requestLayout());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (0 == mRatioWidth || 0 == mRatioHeight) {
setMeasuredDimension(width, height);
} else {
if (width < height * mRatioWidth / mRatioHeight) {
setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
} else {
setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
}
}
}
}
b. 自定义一个能操作相机的CameraSurfaceView
可以将相机的一些基础操作和实现都封装起来,并在CameraSurfaceView中完成相关操作。
public class CameraSurfaceView extends AutoFitSurfaceView {
private CameraModule mCameraModule; // 封装相机的各种操作
private Size mPreviewSize;
public CameraSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
getHolder().addCallback(mSurfaceHolderCallback);
setKeepScreenOn(true); // 设置屏幕常亮
}
public void setCameraModule(CameraModule cameraModule) {
mCameraModule = cameraModule;
mPreviewSize = mCameraModule.getPreviewSize();
}
private SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
holder.setFixedSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// 将View的大小修改为和相机预览分辨率相同的比例
setAspectRatio(mPreviewSize.getHeight(), mPreviewSize.getWidth());
mCameraModule.setPreviewSurface(holder.getSurface());
mCameraModule.openCamera();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mCameraModule.releaseCamera();
}
};
}
2. 相机封装类
将打开、配置、关闭相机,以及开始、停止录制等和相机息息相关的操作封装起来,方便日后维护和扩展,即 CameraModule 类。
public class CameraModule {
private Activity mActivity;
private CameraManager mCameraManager; // 相机管理者
private CameraCharacteristics mCameraCharacteristics; // 相机属性
private CameraDevice mCameraDevice; // 相机对象
private CameraCaptureSession mCameraSession; // 相机事务
private Surface mPreviewSurface; // 预览的Surface
private CaptureRequest.Builder mRequestBuilder;
private Handler mCameraHandler;
private HandlerThread mCameraThread;
private int mDisplayRotation; // 用于设置视频方向
private CameraConfig mCameraConfig;
private Size mPreviewSize;
/* 录制相关*/
private Surface mRecordSurface;
private MediaRecorder mMediaRecorder;
private boolean mIsRecording; // 是否正在录制
private CameraDevice.StateCallback mCameraOpenCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
mCameraDevice = camera;
createVideoSession(); // 相机打开后开始创建session
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {}
@Override
public void onError(@NonNull CameraDevice camera, int error) {}
};
public void openCamera() {
String cameraId = mCameraConfig.getCameraId();
startBackgroundThread();
try {
mCameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId);
initDisplayRotation(mCameraCharacteristics);
mCameraManager.openCamera(cameraId, mCameraOpenCallback, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void startBackgroundThread() {
mCameraThread = new HandlerThread("CameraBackground");
mCameraThread.start();
mCameraHandler = new Handler(mCameraThread.getLooper());
}
private void initDisplayRotation(CameraCharacteristics cameraCharacteristics) {
int displayRotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
switch (displayRotation) {
case Surface.ROTATION_0:
displayRotation = 90;
break;
case Surface.ROTATION_90:
displayRotation = 0;
break;
case Surface.ROTATION_180:
displayRotation = 270;
break;
case Surface.ROTATION_270:
displayRotation = 180;
break;
}
int sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
mDisplayRotation = (displayRotation + sensorOrientation + 270) % 360;
}
private void createVideoSession() {
try {
mRecordSurface = MediaCodec.createPersistentInputSurface();
mMediaRecorder = createRecorder();
ArrayList<Surface> sessionSurfaces = new ArrayList<>();
sessionSurfaces.add(mPreviewSurface);
sessionSurfaces.add(mRecordSurface);
createPreviewRequest();
mCameraDevice.createCaptureSession(sessionSurfaces, mSessionCreateCallback, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void createPreviewRequest() {
CaptureRequest.Builder builder;
try {
builder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
} catch (CameraAccessException e) {
e.printStackTrace();
return;
}
builder.addTarget(mPreviewSurface);
builder.addTarget(mRecordSurface);
builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
builder.set(CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO); // 设置聚焦
builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO); // 设置白平衡
builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); // 设置曝光
mRequestBuilder = builder;
}
private CameraCaptureSession.StateCallback mSessionCreateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCameraSession = session;
startPreview(); // 开始预览
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {}
};
public void startPreview() {
try {
CaptureRequest captureRequest = mRequestBuilder.build();
mCameraSession.setRepeatingRequest(captureRequest, null, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private MediaRecorder createRecorder() {
MediaRecorder mediaRecorder = new MediaRecorder();
try {
File tmpFile = configRecorder(mediaRecorder);
if (tmpFile != null) tmpFile.delete();
} catch (IOException e) {
e.printStackTrace();
}
return mediaRecorder;
}
private File configRecorder(@NonNull MediaRecorder mediaRecorder) throws IOException {
mediaRecorder.reset();
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); // 设置视频来源
mediaRecorder.setVideoEncodingBitRate(mPreviewSize.getWidth() * mPreviewSize.getHeight() * 8); // 设置比特率
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 设置音频来源
mediaRecorder.setAudioEncodingBitRate(96000); // 设置音频编码比特率(一般音乐和语音录制)
mediaRecorder.setAudioSamplingRate(44100); // 设置音频采样率(CD音质)
mediaRecorder.setCaptureRate(30); // 捕获率
mediaRecorder.setOrientationHint(mDisplayRotation); // 设置录制视频时的预期方向,取值为 0、90、180 或 270
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 设置输出格式
mediaRecorder.setVideoSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); // 设置视频宽高
mediaRecorder.setVideoFrameRate(30); // 设置帧数
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // 设置视频编码格式
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); // 设置音频编码格式
mediaRecorder.setInputSurface(mRecordSurface);
File outputFile = getOutputFile();
mediaRecorder.setOutputFile(outputFile);
mediaRecorder.prepare();
mIsRecording = false;
return outputFile;
}
private File getOutputFile() {
File saveDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "CameraRecorder");
saveDirectory.mkdirs();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
String fileName = simpleDateFormat.format(new Date(System.currentTimeMillis())) + ".mp4";
File outputFile = new File(saveDirectory, fileName);
return outputFile;
}
public void startRecorder() {
try {
configRecorder(mMediaRecorder);
mMediaRecorder.start();
mIsRecording = true;
} catch (IOException e) {
Log.e(TAG, "startRecorder failed! " + e.getMessage());
}
}
public void stopRecorder() {
if (mMediaRecorder != null && mIsRecording) {
mMediaRecorder.stop();
mIsRecording = false;
}
}
private void releaseRecorder() {
stopRecorder();
mMediaRecorder.reset();
mMediaRecorder.release();
mMediaRecorder = null;
}
public void releaseCamera() {
releaseRecorder(); // 如果正在录制,则先停止录制
stopPreview();
closeCameraSession();
closeCameraDevice();
stopBackgroundThread();
}
public void stopPreview() {
try {
mCameraSession.stopRepeating();
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void closeCameraSession() {
if (mCameraSession != null) {
mCameraSession.close();
mCameraSession = null;
}
}
private void closeCameraDevice() {
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
}
private void stopBackgroundThread() {
if (mCameraThread != null) {
mCameraThread.quitSafely();
try {
mCameraThread.join();
mCameraThread = null;
mCameraHandler = null;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
通过该类,我们就可以比较方便的去开始和停止录制了,重点关注 MediaRecorder 类相关代码即可。
相机开发步骤不是本文重点,包含拍照等功能也移除掉了,感兴趣的也可以参考其他文章去修改。如:
自定义Camera系列之:SurfaceView + Camera2
3. 布局文件
基本包含一个 CameraSurfaceView 和一个录制按钮,一个停止录制按钮,其他功能可以再根据需要扩展。
<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:layout_width="match_parent"
android:layout_height="match_parent">
<com.afei.camerarecorder.ui.CameraSurfaceView
android:id="@+id/camera_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/start_recorder_iv"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginBottom="10dp"
android:src="@mipmap/shutter_btn_video"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="@+id/stop_recorder_iv"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginBottom="10dp"
android:src="@mipmap/shutter_btn_video_stop"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
4. CameraActivity调用代码
逻辑比较简单,监听相应的点击事件,去进行开始录制和停止录制的操作即可。
public class CameraActivity extends AppCompatActivity implements View.OnClickListener {
private ActivityCameraBinding mBinding;
private CameraModule mCameraModule;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = ActivityCameraBinding.inflate(getLayoutInflater());
setContentView(mBinding.getRoot());
init();
}
private void init() {
// 这是一个提前创建好的配置文件,包含摄像头id,分辨率大小等信息
CameraConfig config = CameraConfig.sCameraConfig;
mCameraModule = new CameraModule(this, config);
mBinding.cameraView.setCameraModule(mCameraModule);
mBinding.startRecorderIv.setOnClickListener(this::onClick);
mBinding.stopRecorderIv.setOnClickListener(this::onClick);
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.start_recorder_iv) {
startRecorder();
} else if (id == R.id.stop_recorder_iv) {
stopRecorder();
} else {
Log.w(TAG, "unknown view id = " + id);
}
}
private void startRecorder() {
mCameraModule.startRecorder();
mBinding.startRecorderIv.setVisibility(View.GONE);
mBinding.stopRecorderIv.setVisibility(View.VISIBLE);
}
private void stopRecorder() {
mCameraModule.stopRecorder();
mBinding.startRecorderIv.setVisibility(View.VISIBLE);
mBinding.stopRecorderIv.setVisibility(View.GONE);
}
}
5. 项目地址
完整且可运行的代码地址如下:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)