package com.safeluck.floatwindow.manager;
|
|
import android.Manifest;
|
import android.content.Context;
|
import android.content.pm.PackageManager;
|
import android.hardware.camera2.CameraAccessException;
|
import android.hardware.camera2.CameraCaptureSession;
|
import android.hardware.camera2.CameraDevice;
|
import android.hardware.camera2.CameraManager;
|
import android.hardware.camera2.CaptureRequest;
|
import android.media.MediaCodec;
|
import android.media.MediaCodecInfo;
|
import android.media.MediaFormat;
|
import android.media.MediaMuxer;
|
import android.media.MediaRecorder;
|
import android.os.Handler;
|
import android.os.HandlerThread;
|
import android.view.Surface;
|
|
import androidx.core.app.ActivityCompat;
|
|
import com.safeluck.floatwindow.MediaArgu;
|
import com.safeluck.floatwindow.ResponseVO;
|
import com.safeluck.floatwindow.util.VideoFileUtils;
|
|
import timber.log.Timber;
|
|
import java.io.File;
|
import java.io.IOException;
|
import java.util.Arrays;
|
|
/**
|
* Android相机录像管理器
|
* 使用MediaRecorder进行录像,每分钟保存一个MP4文件
|
*/
|
public class AndroidCameraRecordManager {
|
private static final String TAG = "AndroidCameraRecordManager";
|
private static final long RECORD_INTERVAL_MS = 60 * 1000; // 60秒
|
|
private Context context;
|
private MediaArgu mediaArgu;
|
private RecordCallback callback;
|
|
// 相机相关
|
private CameraManager cameraManager;
|
private CameraDevice cameraDevice;
|
private CameraCaptureSession captureSession;
|
private HandlerThread backgroundThread;
|
private Handler backgroundHandler;
|
|
// MediaRecorder
|
private MediaRecorder mediaRecorder;
|
private boolean isRecording = false;
|
|
// 当前录像文件
|
private File currentVideoFile;
|
private long currentFileStartTime;
|
|
// 分辨率
|
private int width = 640;
|
private int height = 480;
|
|
/**
|
* 录像回调接口
|
*/
|
public interface RecordCallback {
|
void onResult(ResponseVO response);
|
}
|
|
public AndroidCameraRecordManager(Context context) {
|
this.context = context;
|
cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
|
}
|
|
/**
|
* 设置回调
|
*/
|
public void setCallback(RecordCallback callback) {
|
this.callback = callback;
|
}
|
|
/**
|
* 开始录像
|
*/
|
public void startRecord(MediaArgu media) {
|
if (media == null) {
|
notifyCallback(0, -1, "AndroidCameraRecordManager MediaArgu is null");
|
return;
|
}
|
|
this.mediaArgu = media;
|
|
// 设置分辨率
|
if (media.getM_screen() != null) {
|
width = media.getM_screen().getWidth();
|
height = media.getM_screen().getHeight();
|
Timber.d("设置分辨率: %dx%d", width, height);
|
}
|
|
try {
|
startBackgroundThread();
|
openCamera();
|
notifyCallback(0, 0, "录像已启动");
|
} catch (Exception e) {
|
Timber.e(e, "Failed to start record");
|
notifyCallback(0, -3, "启动录像失败: " + e.getMessage());
|
}
|
}
|
|
/**
|
* 停止录像
|
*/
|
public void stopRecord() {
|
Timber.d("stopRecord called");
|
stopCurrentRecording();
|
closeCamera();
|
stopBackgroundThread();
|
notifyCallback(0, 4, "录像已停止");
|
}
|
|
/**
|
* 打开相机
|
*/
|
private void openCamera() {
|
try {
|
String[] cameraIdList = cameraManager.getCameraIdList();
|
if (cameraIdList.length == 0) {
|
notifyCallback(0, -1, "没有可用的相机");
|
return;
|
}
|
|
// 使用后置相机
|
String cameraId = cameraIdList[0];
|
for (String id : cameraIdList) {
|
// 可以在这里选择前置或后置相机
|
cameraId = id;
|
break;
|
}
|
|
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
|
return;
|
}
|
cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
|
@Override
|
public void onOpened(CameraDevice camera) {
|
cameraDevice = camera;
|
startRecording();
|
}
|
|
@Override
|
public void onDisconnected(CameraDevice camera) {
|
camera.close();
|
cameraDevice = null;
|
notifyCallback(0, -1, "相机断开连接");
|
}
|
|
@Override
|
public void onError(CameraDevice camera, int error) {
|
camera.close();
|
cameraDevice = null;
|
notifyCallback(0, -1, "相机错误: " + error);
|
}
|
}, backgroundHandler);
|
} catch (CameraAccessException e) {
|
Timber.e(e, "打开相机失败");
|
notifyCallback(0, -1, "打开相机失败: " + e.getMessage());
|
}
|
}
|
|
/**
|
* 开始录像
|
*/
|
private void startRecording() {
|
if (cameraDevice == null) {
|
Timber.e("Camera device is null");
|
return;
|
}
|
|
try {
|
// 创建新的视频文件
|
currentVideoFile = VideoFileUtils.getVideoFile(context, mediaArgu.getTfCardFlag());
|
if (currentVideoFile == null) {
|
Timber.e("Failed to create video file");
|
return;
|
}
|
|
// 设置MediaRecorder
|
// 注意:MediaRecorder的设置顺序非常重要,必须严格按照以下顺序:
|
mediaRecorder = new MediaRecorder();
|
|
// 1. 设置数据源(必须在setOutputFormat之前)
|
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
|
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
|
|
// 2. 设置输出格式(必须在setOutputFile和编码器之前)
|
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
|
|
// 3. 设置输出文件(必须在编码器之前)
|
mediaRecorder.setOutputFile(currentVideoFile.getAbsolutePath());
|
|
// 4. 设置编码器(必须在setOutputFormat之后)
|
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
|
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
|
|
// 5. 设置编码参数(必须在编码器之后)
|
mediaRecorder.setAudioEncodingBitRate(64000); // 64kbps
|
mediaRecorder.setAudioSamplingRate(44100); // 44.1kHz
|
mediaRecorder.setVideoEncodingBitRate(width * height * 3);
|
mediaRecorder.setVideoFrameRate(20);
|
mediaRecorder.setVideoSize(width, height);
|
|
// 6. 准备MediaRecorder
|
mediaRecorder.prepare();
|
|
// 创建Surface
|
Surface surface = mediaRecorder.getSurface();
|
|
// 创建CaptureRequest
|
CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
|
builder.addTarget(surface);
|
builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
|
|
// 创建CaptureSession
|
cameraDevice.createCaptureSession(Arrays.asList(surface),
|
new CameraCaptureSession.StateCallback() {
|
@Override
|
public void onConfigured(CameraCaptureSession session) {
|
captureSession = session;
|
try {
|
session.setRepeatingRequest(builder.build(), null, backgroundHandler);
|
mediaRecorder.start();
|
isRecording = true;
|
currentFileStartTime = System.currentTimeMillis();
|
|
Timber.d("开始录像,文件: %s", currentVideoFile.getAbsolutePath());
|
|
// 启动定时器,每分钟切换文件
|
startFileRotationTimer();
|
} catch (CameraAccessException e) {
|
Timber.e(e, "开始录像失败");
|
}
|
}
|
|
@Override
|
public void onConfigureFailed(CameraCaptureSession session) {
|
Timber.e("配置CaptureSession失败");
|
notifyCallback(0, -1, "配置相机失败");
|
}
|
}, backgroundHandler);
|
|
} catch (IOException | CameraAccessException e) {
|
Timber.e(e, "准备MediaRecorder失败");
|
notifyCallback(0, -1, "准备录像器失败: " + e.getMessage());
|
}
|
}
|
|
/**
|
* 启动文件轮换定时器
|
*/
|
private void startFileRotationTimer() {
|
backgroundHandler.postDelayed(new Runnable() {
|
@Override
|
public void run() {
|
if (isRecording && cameraDevice != null) {
|
// 停止当前录像
|
stopCurrentRecording();
|
|
// 通知文件创建
|
if (currentVideoFile != null) {
|
notifyCallback(2, 0, currentVideoFile.getName());
|
}
|
|
// 开始新的录像
|
startRecording();
|
|
// 继续定时
|
startFileRotationTimer();
|
}
|
}
|
}, RECORD_INTERVAL_MS);
|
}
|
|
/**
|
* 停止当前录像
|
*/
|
private void stopCurrentRecording() {
|
if (mediaRecorder != null && isRecording) {
|
try {
|
mediaRecorder.stop();
|
mediaRecorder.reset();
|
mediaRecorder.release();
|
mediaRecorder = null;
|
isRecording = false;
|
Timber.d("当前录像已停止");
|
} catch (Exception e) {
|
Timber.e(e, "停止录像失败");
|
}
|
}
|
|
if (captureSession != null) {
|
try {
|
captureSession.stopRepeating();
|
captureSession.close();
|
captureSession = null;
|
} catch (Exception e) {
|
Timber.e(e, "关闭CaptureSession失败");
|
}
|
}
|
}
|
|
/**
|
* 关闭相机
|
*/
|
private void closeCamera() {
|
stopCurrentRecording();
|
|
if (captureSession != null) {
|
captureSession.close();
|
captureSession = null;
|
}
|
|
if (cameraDevice != null) {
|
cameraDevice.close();
|
cameraDevice = 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();
|
} catch (InterruptedException e) {
|
Timber.e(e, "停止后台线程失败");
|
}
|
backgroundThread = null;
|
backgroundHandler = null;
|
}
|
}
|
|
/**
|
* 通知回调
|
*/
|
private void notifyCallback(int type, int errCode, String message) {
|
if (callback != null) {
|
ResponseVO response = new ResponseVO();
|
response.setType(type);
|
response.setErrCode(errCode);
|
response.setMessage(message);
|
callback.onResult(response);
|
}
|
}
|
}
|