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 com.safeluck.floatwindow.util.FileUtil; 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(),0); 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(); // 通知文件创建(当前这段 1 分钟文件) if (currentVideoFile != null) { notifyCallback(2, 0, currentVideoFile.getName()); } // 每次写入新文件前,检查并清理存储空间(AnYun_VIDEO 下的 mp4) ensureStorageSpaceForMp4(); // 开始新的录像(创建下一分钟的新文件) startRecording(); // 继续定时 startFileRotationTimer(); } } }, RECORD_INTERVAL_MS); } /** * 确保存储空间足够(针对 AnYun_VIDEO 下的 mp4) * TF 卡:使用 FileUtil.cleanupH264Files(内部已改为清理 mp4)按日期目录删除最早的视频 * 内部 Flash:使用 FileUtil.ensureInternalFlashSpaceForH264(内部已改为清理 mp4) */ private void ensureStorageSpaceForMp4() { if (context == null || mediaArgu == null) { return; } try { int tfFlag = mediaArgu.getTfCardFlag(); // 0-内部存储,1-TF 卡 // 先定位当前使用的日期目录,再取其父目录 AnYun_VIDEO 作为根目录 File dateDir = VideoFileUtils.getVideoDirectory(context, tfFlag); if (dateDir == null) { return; } File rootDir = dateDir.getParentFile(); // .../AnYun_VIDEO if (rootDir == null) { return; } String rootPath = rootDir.getAbsolutePath(); if (tfFlag == 1) { // TF 卡:限制总大小 + 剩余空间 FileUtil.cleanupH264Files(context, rootPath); } else { // 内部 Flash:确保剩余空间 ≥ 800MB FileUtil.ensureInternalFlashSpaceForH264(context); } } catch (Exception e) { Timber.e(e, "ensureStorageSpaceForMp4 error"); } } /** * 停止当前录像 */ 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); } } }