编辑 | blame | 历史 | 原始文档
  • 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
        }
    }