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();
// 重新启动音频录制
已修复时间戳计算问题。主要修改如下:修复内容:
之前:使用 (frameIndex * 1000000) / FRAME_RATE 计算,假设固定帧率
现在:使用 (System.nanoTime() - recordingStartTimeNs) / 1000,基于实际经过时间
使用 (totalSamples * 1000000) / SAMPLE_RATE 计算
totalSamples 是累计采样数,确保时间戳连续
使用 recordingStartTimeNs 记录开始时间(纳秒)
在创建新文件时重置,确保每个文件的时间戳从 0 开始
视频和音频都从同一个基准时间开始
每个新文件的时间戳都从 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)
目录:<TF 根>/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 或没有文件。