| New file |
| | |
| | | * 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 |
| | | } |
| | | } |