* MediaMuxer 启动后不能再添加轨道 * 视频和音频的时间戳需要正确同步 现在代码应该: * 正确等待两个轨道都准备好 * 同步启动 MediaMuxer * 正确写入视频和音频数据 * 避免状态冲突导致的错误 mp4文件明显时间加快了,现实世界1秒钟,可能mp4文件的视频就是半秒,所有录一分钟的视频mp4文件,视频只播到30秒就不再播了 检查并修复时间戳计算问题,可能是帧率或时间戳计算不正确: 修复时间戳计算:改用实际时间而非帧索引,确保时间戳准确: 修复时间戳计算,使用实际时间而非帧索引: UsbCameraRecordManager.java long frameCount = 0; // 记录开始时间(纳秒,用于精确时间戳) long recordingStartTimeNs = System.nanoTime(); long frameCount = 0; UsbCameraRecordManager.java if (videoEncoder != null && mediaMuxer != null) { encodeFrame(buffer, frameCount, width, height); frameCount++; encodeFrame(buffer, frameCount, width, height); if (videoEncoder != null && mediaMuxer != null) { // 计算实际经过的时间(微秒) long elapsedTimeUs = (System.nanoTime() - recordingStartTimeNs) / 1000; encodeFrame(buffer, elapsedTimeUs, width, height); frameCount++; UsbCameraRecordManager.java * 编码一帧数据 */ * 编码一帧数据 * @param yuvData YUV数据 * @param presentationTimeUs 时间戳(微秒),基于实际开始时间 * @param width 宽度 * @param height 高度 */ 修复音频时间戳,使其与视频时间戳同步: UsbCameraRecordManager.java +3 /** // 录制开始时间(纳秒),用于时间戳同步 private volatile long recordingStartTimeNs = 0; /** UsbCameraRecordManager.java +1-1 // 记录开始时间(纳秒,用于精确时间戳) long recordingStartTimeNs = System.nanoTime(); long frameCount = 0; long recordingStartTimeNs = System.nanoTime(); // 记录开始时间(纳秒,用于精确时间戳) recordingStartTimeNs = System.nanoTime(); long frameCount = 0; UsbCameraRecordManager.java +3-3 byte[] audioBuffer = new byte[audioBufferSize]; long audioFrameCount = 0; // 编码音频数据 long audioFrameCount = 0; byte[] audioBuffer = new byte[audioBufferSize]; long totalSamplesRead = 0; // 总采样数 // 编码音频数据 UsbCameraRecordManager.java +7-2 * 编码音频数据 */ private void encodeAudio(byte[] audioData, int size, long frameCount) { private void encodeAudio(byte[] audioData, int size, long frameCount) { * 编码音频数据 * @param audioData 音频数据 * @param size 数据大小(字节) * @param totalSamples 总采样数(从开始到现在的累计采样数) */ 在创建新文件时重置开始时间: UsbCameraRecordManager.java +3 // 重新启动音频录制 // 重置开始时间 recordingStartTimeNs = System.nanoTime(); // 重新启动音频录制 已修复时间戳计算问题。主要修改如下:修复内容: * * * 1. 视频时间戳使用实际时间: * 之前:使用 (frameIndex * 1000000) / FRAME_RATE 计算,假设固定帧率 * 现在:使用 (System.nanoTime() - recordingStartTimeNs) / 1000,基于实际经过时间 1. 音频时间戳基于采样数: * 使用 (totalSamples * 1000000) / SAMPLE_RATE 计算 * totalSamples 是累计采样数,确保时间戳连续 1. 添加了录制开始时间记录: * 使用 recordingStartTimeNs 记录开始时间(纳秒) * 在创建新文件时重置,确保每个文件的时间戳从 0 开始 1. 确保时间戳同步: * 视频和音频都从同一个基准时间开始 * 每个新文件的时间戳都从 0 开始 修复原理: * 问题原因:使用帧索引计算时间戳,假设固定帧率。实际采集帧率可能不同,导致时间戳不准确 * 解决方案:使用实际经过的时间计算时间戳,确保时间戳与实际时间一致 现在: * 视频时间戳基于实际经过的时间 * 音频时间戳基于实际读取的采样数 * 两者都从 0 开始,确保同步 #### 使用 private var mediaAidlInterface: IMediaAidlInterface? = null private var serviceConnection: ServiceConnection? = null private var isServiceBound = false private val isServiceBoundState = mutableStateOf(false) serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { mediaAidlInterface = IMediaAidlInterface.Stub.asInterface(service) isServiceBound = true isServiceBoundState.value = true Timber.d("FloatingService connected") } override fun onServiceDisconnected(name: ComponentName?) { mediaAidlInterface = null isServiceBound = false isServiceBoundState.value = false Timber.d("FloatingService disconnected") onBindService = { if (!isServiceBoundState.value && serviceConnection != null) { val intent = Intent(this@MainActivity, FloatingService::class.java) bindService(intent, serviceConnection!!, Context.BIND_AUTO_CREATE) Timber.d("Binding FloatingService") } }, } } private fun startAndroidRecord() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot start Android record") return } try { val mediaArgu = MediaArgu().apply { isPush = false isUsedOutCamera = false // Android 内置摄像头 codeRate = 0 frameRate = 0 m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率 recordTime = 0 tfCardFlag = 0 // 内部存储 } mediaAidlInterface?.registerCallback(callback) mediaAidlInterface?.startMedia(mediaArgu) Timber.d("Started Android camera record") } catch (e: RemoteException) { Timber.e(e, "Error starting Android record") } } private fun stopAndroidRecord() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot stop Android record") return } try { mediaAidlInterface?.stopMedia() Timber.d("Stopped Android camera record") } catch (e: RemoteException) { Timber.e(e, "Error stopping Android record") } } private fun startUsbRecord() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot start USB record") return } try { val mediaArgu = MediaArgu().apply { isPush = false isUsedOutCamera = true // USB 摄像头 codeRate = 0 frameRate = 0 m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率 recordTime = 0 tfCardFlag = 0 // 内部存储 } mediaAidlInterface?.registerCallback(callback) mediaAidlInterface?.startMedia(mediaArgu) Timber.d("Started USB camera record") } catch (e: RemoteException) { Timber.e(e, "Error starting USB record") } } private fun stopUsbRecord() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot stop USB record") return } try { mediaAidlInterface?.stopMedia() Timber.d("Stopped USB camera record") } catch (e: RemoteException) { Timber.e(e, "Error stopping USB record") } } private fun startUsbPush() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot start USB push") return } try { val mediaArgu = MediaArgu().apply { isPush = true isUsedOutCamera = true // USB 摄像头 codeRate = 0 frameRate = 0 m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率 url = "rtmp://192.168.16.143/live/livestream" // TODO: 需要设置实际的推流地址 userName = "" pwd = "" } mediaAidlInterface?.registerCallback(callback) mediaAidlInterface?.startMedia(mediaArgu) Timber.d("Started USB camera push") } catch (e: RemoteException) { Timber.e(e, "Error starting USB push") } } private fun stopUsbPush() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot stop USB push") return } try { mediaAidlInterface?.stopMedia() Timber.d("Stopped USB camera push") } catch (e: RemoteException) { Timber.e(e, "Error stopping USB push") } } private fun unbindServiceInternal() { if (isServiceBound && serviceConnection != null) { try { mediaAidlInterface?.unregisterCallback(callback) } catch (e: RemoteException) { Timber.e(e, "Error unregistering callback") } try { unbindService(serviceConnection!!) Timber.d("Unbinding FloatingService") } catch (e: Exception) { Timber.e(e, "Error unbinding service") } isServiceBound = false isServiceBoundState.value = false mediaAidlInterface = null } } TF 卡录制(tfCardFlag == 1) 目录:/AnYun_VIDEO/yyMMdd/HHmmss_... .mp4 每次新建 1 分钟 MP4 前: 基于 cleanupH264Files(现为 MP4 版本): 按日期目录统计所有 MP4 总大小。 超过 5GB 或 TF 卡剩余空间 < 1GB 时,从最早日期目录开始整目录删除。 内部 Flash 录制(tfCardFlag == 0) 目录:/sdcard/AnYun_VIDEO/yyMMdd/HHmmss_... .mp4 每次新建 1 分钟 MP4 前: 基于 ensureInternalFlashSpaceForH264(现为 MP4 版本): 如果该分区剩余空间 < 800MB: 递归收集 AnYun_VIDEO 下所有 MP4。 按 lastModified 从早到晚依次删,直到 ≥ 800MB 或没有文件。