| | |
| | | private File currentVideoFile; |
| | | private long currentFileStartTime; |
| | | |
| | | // 录制开始时间(纳秒),用于时间戳同步 |
| | | private volatile long recordingStartTimeNs = 0; |
| | | |
| | | /** |
| | | * 录像回调接口 |
| | | */ |
| | |
| | | usbCamera.setenv(); |
| | | |
| | | // 使用prepareCamera方法,camera_id范围[0,9] |
| | | int[] cameraIds = {0, 9}; |
| | | int[] cameraIds = {0, 2}; |
| | | String cameraName = null; // 不指定特定名称 |
| | | |
| | | // 如果返回非0,代表打开失败,则先stopCamera再重试,最多3次 |
| | |
| | | Timber.d("音频录制已启动"); |
| | | } |
| | | |
| | | // 主动检查音频编码器输出格式(可能在处理数据前就有格式) |
| | | try { |
| | | MediaFormat audioFormat = audioEncoder.getOutputFormat(); |
| | | if (audioFormat != null) { |
| | | synchronized (UsbCameraRecordManager.this) { |
| | | audioTrackIndex = mediaMuxer.addTrack(audioFormat); |
| | | Timber.d("音频轨道已添加(主动检查): %d", audioTrackIndex); |
| | | checkAndStartMuxer(); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | Timber.d("音频编码器输出格式尚未准备好,将在处理数据时添加"); |
| | | } |
| | | // 注意:不要在这里主动检查音频编码器输出格式 |
| | | // 因为 MediaCodec 的 getOutputFormat() 在编码器启动后可能返回 null |
| | | // 应该等待 INFO_OUTPUT_FORMAT_CHANGED 事件 |
| | | |
| | | // 启动音频编码线程 |
| | | audioThread = new AudioThread(); |
| | |
| | | |
| | | Timber.d("开始录像,分辨率: %dx%d", width, height); |
| | | |
| | | // 记录开始时间(纳秒,用于精确时间戳) |
| | | recordingStartTimeNs = System.nanoTime(); |
| | | long frameCount = 0; |
| | | long lastFileChangeTime = System.currentTimeMillis(); |
| | | long audioTrackWaitStartTime = System.currentTimeMillis(); |
| | | final long AUDIO_TRACK_TIMEOUT_MS = 3000; // 3秒超时 |
| | | |
| | | // 循环处理摄像头数据 |
| | | while (isRunning && cameraExists) { |
| | |
| | | Timber.e("Failed to create new video file"); |
| | | break; |
| | | } |
| | | |
| | | // 重置开始时间 |
| | | recordingStartTimeNs = System.nanoTime(); |
| | | |
| | | // 重新启动音频录制 |
| | | if (audioRecord != null) { |
| | |
| | | } |
| | | |
| | | // 获取YUV数据 (参数0表示录像) |
| | | usbCamera.rgba(0, buffer); |
| | | usbCamera.rgba(2, buffer); |
| | | |
| | | // 编码并写入文件 |
| | | if (videoEncoder != null && mediaMuxer != null) { |
| | | encodeFrame(buffer, frameCount, width, height); |
| | | // 计算实际经过的时间(微秒) |
| | | long elapsedTimeUs = (System.nanoTime() - recordingStartTimeNs) / 1000; |
| | | encodeFrame(buffer, elapsedTimeUs, width, height); |
| | | frameCount++; |
| | | |
| | | // 如果音频轨道超时未准备好,尝试仅用视频轨道启动 |
| | | if (!muxerStarted && videoTrackIndex >= 0 && audioTrackIndex < 0) { |
| | | // 重用循环中已定义的 currentTime 变量 |
| | | if (currentTime - audioTrackWaitStartTime > AUDIO_TRACK_TIMEOUT_MS) { |
| | | Timber.w("音频轨道超时未准备好,仅使用视频轨道启动Muxer"); |
| | | synchronized (UsbCameraRecordManager.this) { |
| | | // 创建一个假的音频轨道索引,或者直接启动(但MediaMuxer要求至少一个轨道) |
| | | // 实际上MediaMuxer需要至少一个轨道,所以如果视频轨道准备好了就可以启动 |
| | | if (videoTrackIndex >= 0) { |
| | | try { |
| | | mediaMuxer.start(); |
| | | muxerStarted = true; |
| | | Timber.d("Muxer started with video track only: %d", videoTrackIndex); |
| | | } catch (Exception e) { |
| | | Timber.e(e, "Failed to start muxer with video only"); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 控制帧率,约20fps |
| | |
| | | |
| | | /** |
| | | * 编码一帧数据 |
| | | * @param yuvData YUV数据 |
| | | * @param presentationTimeUs 时间戳(微秒),基于实际开始时间 |
| | | * @param width 宽度 |
| | | * @param height 高度 |
| | | */ |
| | | private void encodeFrame(byte[] yuvData, long frameIndex, int width, int height) { |
| | | private void encodeFrame(byte[] yuvData, long presentationTimeUs, int width, int height) { |
| | | try { |
| | | // 获取输入缓冲区 |
| | | int inputBufferIndex = videoEncoder.dequeueInputBuffer(10000); |
| | |
| | | inputBuffer.clear(); |
| | | inputBuffer.put(yuvData); |
| | | |
| | | long presentationTimeUs = (frameIndex * 1000000) / FRAME_RATE; |
| | | // 使用实际经过的时间作为时间戳 |
| | | videoEncoder.queueInputBuffer(inputBufferIndex, 0, yuvData.length, |
| | | presentationTimeUs, 0); |
| | | } |
| | |
| | | // 输出格式改变,添加视频轨道 |
| | | MediaFormat newFormat = videoEncoder.getOutputFormat(); |
| | | synchronized (UsbCameraRecordManager.this) { |
| | | videoTrackIndex = mediaMuxer.addTrack(newFormat); |
| | | Timber.d("视频轨道已添加: %d", videoTrackIndex); |
| | | checkAndStartMuxer(); |
| | | // 检查 muxer 是否已启动,如果已启动则无法添加轨道 |
| | | if (muxerStarted) { |
| | | Timber.e("Muxer already started, cannot add video track"); |
| | | break; |
| | | } |
| | | if (videoTrackIndex < 0) { |
| | | videoTrackIndex = mediaMuxer.addTrack(newFormat); |
| | | Timber.d("视频轨道已添加: %d", videoTrackIndex); |
| | | checkAndStartMuxer(); |
| | | } |
| | | } |
| | | outputBufferIndex = videoEncoder.dequeueOutputBuffer(bufferInfo, 0); |
| | | continue; |
| | | } else if (outputBufferIndex >= 0) { |
| | | ByteBuffer outputBuffer = videoEncoder.getOutputBuffer(outputBufferIndex); |
| | | // 只有在 muxer 已启动且轨道索引有效时才写入数据 |
| | | if (outputBuffer != null && muxerStarted && videoTrackIndex >= 0) { |
| | | outputBuffer.position(bufferInfo.offset); |
| | | outputBuffer.limit(bufferInfo.offset + bufferInfo.size); |
| | | mediaMuxer.writeSampleData(videoTrackIndex, outputBuffer, bufferInfo); |
| | | try { |
| | | mediaMuxer.writeSampleData(videoTrackIndex, outputBuffer, bufferInfo); |
| | | } catch (Exception e) { |
| | | Timber.e(e, "Error writing video sample data"); |
| | | } |
| | | } |
| | | videoEncoder.releaseOutputBuffer(outputBufferIndex, false); |
| | | } |
| | |
| | | |
| | | try { |
| | | byte[] audioBuffer = new byte[audioBufferSize]; |
| | | long audioFrameCount = 0; |
| | | long totalSamplesRead = 0; // 总采样数 |
| | | |
| | | while (isRecording && audioRecord != null && audioEncoder != null) { |
| | | // 读取音频数据 |
| | |
| | | } |
| | | |
| | | // 编码音频数据 |
| | | encodeAudio(audioBuffer, readSize, audioFrameCount); |
| | | audioFrameCount += readSize / 2; // 16位采样,每个采样2字节 |
| | | encodeAudio(audioBuffer, readSize, totalSamplesRead); |
| | | totalSamplesRead += readSize / 2; // 16位采样,每个采样2字节 |
| | | } |
| | | |
| | | } catch (Exception e) { |
| | |
| | | |
| | | /** |
| | | * 编码音频数据 |
| | | * @param audioData 音频数据 |
| | | * @param size 数据大小(字节) |
| | | * @param totalSamples 总采样数(从开始到现在的累计采样数) |
| | | */ |
| | | private void encodeAudio(byte[] audioData, int size, long frameCount) { |
| | | private void encodeAudio(byte[] audioData, int size, long totalSamples) { |
| | | try { |
| | | // 获取输入缓冲区 |
| | | // 获取输入缓冲区(即使 muxer 未启动也要编码,以便尽快获得输出格式) |
| | | int inputBufferIndex = audioEncoder.dequeueInputBuffer(10000); |
| | | if (inputBufferIndex >= 0) { |
| | | ByteBuffer inputBuffer = audioEncoder.getInputBuffer(inputBufferIndex); |
| | |
| | | inputBuffer.clear(); |
| | | inputBuffer.put(audioData, 0, size); |
| | | |
| | | long presentationTimeUs = (frameCount * 1000000) / SAMPLE_RATE; |
| | | // 使用采样数计算时间戳(微秒) |
| | | // totalSamples 是采样数,SAMPLE_RATE 是每秒采样数 |
| | | long presentationTimeUs = (totalSamples * 1000000) / SAMPLE_RATE; |
| | | audioEncoder.queueInputBuffer(inputBufferIndex, 0, size, |
| | | presentationTimeUs, 0); |
| | | } |
| | |
| | | // 输出格式改变,添加音频轨道 |
| | | MediaFormat newFormat = audioEncoder.getOutputFormat(); |
| | | synchronized (UsbCameraRecordManager.this) { |
| | | audioTrackIndex = mediaMuxer.addTrack(newFormat); |
| | | Timber.d("音频轨道已添加: %d", audioTrackIndex); |
| | | checkAndStartMuxer(); |
| | | // 检查 muxer 是否已启动,如果已启动则无法添加轨道 |
| | | if (muxerStarted) { |
| | | Timber.e("Muxer already started, cannot add audio track"); |
| | | break; |
| | | } |
| | | if (audioTrackIndex < 0) { |
| | | audioTrackIndex = mediaMuxer.addTrack(newFormat); |
| | | Timber.d("音频轨道已添加: %d", audioTrackIndex); |
| | | checkAndStartMuxer(); |
| | | } |
| | | } |
| | | outputBufferIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0); |
| | | continue; |
| | | } else if (outputBufferIndex >= 0) { |
| | | ByteBuffer outputBuffer = audioEncoder.getOutputBuffer(outputBufferIndex); |
| | | // 只有在 muxer 已启动且轨道索引有效时才写入数据 |
| | | if (outputBuffer != null && muxerStarted && audioTrackIndex >= 0) { |
| | | outputBuffer.position(bufferInfo.offset); |
| | | outputBuffer.limit(bufferInfo.offset + bufferInfo.size); |
| | | mediaMuxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo); |
| | | try { |
| | | mediaMuxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo); |
| | | } catch (Exception e) { |
| | | Timber.e(e, "Error writing audio sample data"); |
| | | } |
| | | } |
| | | audioEncoder.releaseOutputBuffer(outputBufferIndex, false); |
| | | } |