3. Camera2录像
任务说明
本案例实现基于Camera2 API的视频录制功能:打开相机、配置MediaRecorder、录制视频并保存到应用私有目录。完成目标后,录制的视频文件可通过系统相册或文件管理器查看。
实现方案
使用Camera2的CameraCaptureSession配合MediaRecorder完成录制。核心流程为:打开相机→配置MediaRecorder→创建录制会话→开始录制→停止并释放资源。UI层使用TextureView作为相机预览载体。
代码实现
// Camera2RecordActivity.java
public class Camera2RecordActivity extends AppCompatActivity {
private static final int REQUEST_CAMERA_PERMISSION = 200;
private TextureView textureView;
private CameraManager cameraManager;
private String cameraId;
private CameraDevice cameraDevice;
private CameraCaptureSession captureSession;
private CaptureRequest.Builder previewRequestBuilder;
private MediaRecorder mediaRecorder;
private String videoFilePath;
private boolean isRecording = false;
private Size videoSize = new Size(1280, 720);
private HandlerThread backgroundThread;
private Handler backgroundHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera2_record);
textureView = findViewById(R.id.texture_view);
cameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);
findViewById(R.id.btn_record).setOnClickListener(v -> {
if (isRecording) stopRecording();
else startRecording();
});
}
@Override
protected void onResume() {
super.onResume();
startBackgroundThread();
if (textureView.isAvailable()) openCamera();
else textureView.setSurfaceTextureListener(surfaceTextureListener);
}
@Override
protected void onPause() {
closeCamera();
stopBackgroundThread();
super.onPause();
}
private TextureView.SurfaceTextureListener surfaceTextureListener = new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
openCamera();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return false; }
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
};
private void openCamera() {
try {
if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO}, REQUEST_CAMERA_PERMISSION);
return;
}
cameraId = cameraManager.getCameraIdList()[0];
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map == null) return;
cameraManager.openCamera(cameraId, stateCallback, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
cameraDevice = camera;
startPreview();
}
@Override
public void onDisconnected(CameraDevice camera) {
cameraDevice.close();
cameraDevice = null;
}
@Override
public void onError(CameraDevice camera, int error) {
cameraDevice.close();
cameraDevice = null;
}
};
private void startPreview() {
try {
SurfaceTexture texture = textureView.getSurfaceTexture();
texture.setDefaultBufferSize(videoSize.getWidth(), videoSize.getHeight());
Surface previewSurface = new Surface(texture);
previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
previewRequestBuilder.addTarget(previewSurface);
cameraDevice.createCaptureSession(Arrays.asList(previewSurface), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
captureSession = session;
previewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
try {
captureSession.setRepeatingRequest(previewRequestBuilder.build(), null, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {}
}, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void startRecording() {
if (cameraDevice == null || textureView == null) return;
try {
setupMediaRecorder();
SurfaceTexture texture = textureView.getSurfaceTexture();
texture.setDefaultBufferSize(videoSize.getWidth(), videoSize.getHeight());
Surface previewSurface = new Surface(texture);
Surface recordSurface = mediaRecorder.getSurface();
previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
previewRequestBuilder.addTarget(previewSurface);
previewRequestBuilder.addTarget(recordSurface);
cameraDevice.createCaptureSession(Arrays.asList(previewSurface, recordSurface), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
captureSession = session;
previewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
try {
captureSession.setRepeatingRequest(previewRequestBuilder.build(), null, backgroundHandler);
mediaRecorder.start();
isRecording = true;
runOnUiThread(() -> ((Button) findViewById(R.id.btn_record)).setText("停止录制"));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {}
}, backgroundHandler);
} catch (Exception e) {
e.printStackTrace();
}
}
private void setupMediaRecorder() throws IOException {
mediaRecorder = new MediaRecorder();
videoFilePath = getExternalFilesDir(null).getAbsolutePath() + "/video_" + System.currentTimeMillis() + ".mp4";
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mediaRecorder.setOutputFile(videoFilePath);
mediaRecorder.setVideoEncodingBitRate(10000000);
mediaRecorder.setVideoFrameRate(30);
mediaRecorder.setVideoSize(videoSize.getWidth(), videoSize.getHeight());
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.prepare();
}
private void stopRecording() {
if (mediaRecorder != null) {
mediaRecorder.stop();
mediaRecorder.reset();
mediaRecorder.release();
mediaRecorder = null;
isRecording = false;
((Button) findViewById(R.id.btn_record)).setText("开始录制");
Toast.makeText(this, "视频已保存: " + videoFilePath, Toast.LENGTH_LONG).show();
}
if (captureSession != null) {
captureSession.close();
captureSession = null;
}
startPreview();
}
private void closeCamera() {
if (captureSession != null) {
captureSession.close();
captureSession = null;
}
if (cameraDevice != null) {
cameraDevice.close();
cameraDevice = null;
}
if (mediaRecorder != null) {
mediaRecorder.release();
mediaRecorder = null;
}
}
private void startBackgroundThread() {
backgroundThread = new HandlerThread("CameraBackground");
backgroundThread.start();
backgroundHandler = new Handler(backgroundThread.getLooper());
}
private void stopBackgroundThread() {
if (backgroundThread != null) {
backgroundThread.quitSafely();
try {
backgroundThread.join();
backgroundThread = null;
backgroundHandler = null;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// activity_camera2_record.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextureView
android:id="@+id/texture_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/btn_record"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="开始录制"
android:padding="16dp" />
</LinearLayout>
// AndroidManifest.xml 中注册权限与Activity
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<activity android:name=".Camera2RecordActivity" />
落地与验证
运行方式:将代码放入Android工程,在Android 6.0+设备上运行(需支持Camera2 API)。首次启动会请求相机和录音权限,授予后点击“开始录制”按钮,预览画面开始录制;再次点击按钮停止录制。
验证方法:录制10秒后停止,观察Toast提示的保存路径。使用文件管理器或adb命令adb pull /sdcard/Android/data/<包名>/files/video_*.mp4导出视频,用播放器确认画面与声音正常。若录制过程中预览流畅、停止后文件可正常播放,且文件大小随时间增长,则判定通过。
预期输出:点击“开始录制”后按钮文字变为“停止录制”,停止后Toast显示视频保存路径,该路径下生成一个mp4文件,文件大小与录制时长正相关。