package com.safeluck.floatwindow.manager;
|
|
import android.content.Context;
|
import android.media.AudioFormat;
|
import android.media.AudioRecord;
|
import android.media.MediaCodec;
|
import android.media.MediaCodecInfo;
|
import android.media.MediaFormat;
|
import android.media.MediaMuxer;
|
import android.media.MediaRecorder;
|
import android.text.TextUtils;
|
import android.util.Log;
|
|
import com.anyun.libusbcamera.UsbCamera;
|
import com.anyun.libusbcamera.WatermarkParam;
|
import com.safeluck.floatwindow.MediaArgu;
|
import com.safeluck.floatwindow.ResponseVO;
|
import com.safeluck.floatwindow.util.GlobalData;
|
import com.safeluck.floatwindow.util.VideoFileUtils;
|
|
import timber.log.Timber;
|
|
import java.io.File;
|
import java.io.IOException;
|
import java.nio.ByteBuffer;
|
import java.text.SimpleDateFormat;
|
import java.util.ArrayList;
|
import java.util.Date;
|
import java.util.concurrent.Executors;
|
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.TimeUnit;
|
|
/**
|
* USB摄像头录像管理器
|
* 每分钟保存一个MP4文件
|
*/
|
public class UsbCameraRecordManager {
|
private static final String TAG = "UsbCameraRecordManager";
|
private static final int FRAME_RATE = 20;
|
private static final int I_FRAME_INTERVAL = 1; // 1秒一个I帧
|
private static final long RECORD_INTERVAL_MS = 60 * 1000; // 60秒
|
|
// 音频参数
|
private static final int SAMPLE_RATE = 44100; // 采样率
|
private static final int CHANNEL_COUNT = 1; // 单声道
|
private static final int AUDIO_BIT_RATE = 64000; // 音频码率
|
private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
|
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
|
|
private Context context;
|
private MediaArgu mediaArgu;
|
private RecordCallback callback;
|
|
// USB摄像头相关
|
private UsbCamera usbCamera;
|
private RecordThread recordThread;
|
private boolean isRunning = false;
|
private boolean cameraExists = false;
|
|
// 分辨率数组 [width, height]
|
private int[] resolutionArr = new int[]{640, 480};
|
|
// 是否开启摄像头加密
|
private boolean ay_encrypt = false;
|
|
// MediaCodec和MediaMuxer
|
private MediaCodec videoEncoder;
|
private MediaCodec audioEncoder;
|
private AudioRecord audioRecord;
|
private MediaMuxer mediaMuxer;
|
private int videoTrackIndex = -1;
|
private int audioTrackIndex = -1;
|
private boolean muxerStarted = false;
|
|
// 音频相关
|
private AudioThread audioThread;
|
private int audioBufferSize;
|
|
// 当前录像文件
|
private File currentVideoFile;
|
private long currentFileStartTime;
|
|
// 录制开始时间(纳秒),用于时间戳同步
|
private volatile long recordingStartTimeNs = 0;
|
|
/**
|
* 录像回调接口
|
*/
|
public interface RecordCallback {
|
void onResult(ResponseVO response);
|
}
|
|
public UsbCameraRecordManager(Context context) {
|
this.context = context;
|
}
|
|
/**
|
* 设置回调
|
*/
|
public void setCallback(RecordCallback callback) {
|
this.callback = callback;
|
}
|
|
/**
|
* 开始录像
|
*/
|
public void startRecord(MediaArgu media) {
|
if (media == null) {
|
notifyCallback(0, -1, "MediaArgu is null");
|
return;
|
}
|
|
this.mediaArgu = media;
|
|
// 设置分辨率
|
if (media.getM_screen() != null) {
|
resolutionArr[0] = media.getM_screen().getWidth();
|
resolutionArr[1] = media.getM_screen().getHeight();
|
Timber.d("设置分辨率: %dx%d", resolutionArr[0], resolutionArr[1]);
|
}
|
|
try {
|
// 检查并打开USB摄像头
|
if (!openUsbCamera()) {
|
cameraExists = false;
|
notifyCallback(0, -1, "USB摄像头打开失败");
|
return;
|
}
|
setWaterMask();
|
cameraExists = true;
|
Timber.d("USB摄像头打开成功");
|
|
// 启动录像线程
|
startRecordThread();
|
|
notifyCallback(0, 0, "录像已启动");
|
} catch (Exception e) {
|
Timber.e(e, "Failed to start record");
|
notifyCallback(0, -3, "启动录像失败: " + e.getMessage());
|
}
|
}
|
|
WatermarkParam watermarkParam;
|
ArrayList<WatermarkParam> watermarkParamList = new ArrayList<>();
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
int baseY = 20;
|
int fontSize= 24;
|
private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
private ScheduledFuture<?> watermarkFuture;
|
private void setWaterMask() {
|
|
// 防止重复 schedule(startRecord 可能被多次调用)
|
if (watermarkFuture != null && !watermarkFuture.isCancelled()) {
|
return;
|
}
|
if (scheduledExecutorService == null || scheduledExecutorService.isShutdown()) {
|
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
}
|
|
watermarkFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> {
|
|
|
|
if (!TextUtils.isEmpty(GlobalData.getInstance().getWaterMaskInfo())){
|
Log.i(TAG,"tieshuiin");
|
|
if (resolutionArr[0]==320&&resolutionArr[1]==240){
|
fontSize = 24;
|
baseY = 2;
|
}else if (resolutionArr[0]==640&&resolutionArr[1]==480){
|
fontSize = 32;
|
baseY = 4;
|
}else if (resolutionArr[0]==1280&&resolutionArr[1]==720){
|
fontSize = 48;
|
baseY = 6;
|
}else{
|
baseY = 2;
|
fontSize = 24;
|
}
|
String school = GlobalData.getInstance().parseWaterMaskInfo("school", "无", GlobalData.ShareType.STRING);
|
watermarkParam = new WatermarkParam(10,baseY,school);
|
watermarkParamList.clear();
|
watermarkParamList.add(watermarkParam);
|
|
String teacher = GlobalData.getInstance().parseWaterMaskInfo("teacher", "无", GlobalData.ShareType.STRING);
|
|
|
String stu = GlobalData.getInstance().parseWaterMaskInfo("student", "无", GlobalData.ShareType.STRING);
|
baseY = fontSize*11/10+baseY;
|
watermarkParam = new WatermarkParam(10,baseY,"教练:"+teacher+" 学员:"+stu);
|
watermarkParamList.add(watermarkParam);
|
|
double speed = GlobalData.getInstance().parseWaterMaskInfo("speed", 0.0, GlobalData.ShareType.DOUBLE);
|
|
|
String czh = GlobalData.getInstance().parseWaterMaskInfo("car_license", "无", GlobalData.ShareType.STRING) + GlobalData.getInstance().getCameraTag;
|
baseY = fontSize*11/10+baseY;
|
watermarkParam = new WatermarkParam(10,resolutionArr[1]-baseY,czh +" "+String.format("速度:%.1f",speed));
|
watermarkParamList.add(watermarkParam);
|
|
|
double latitude = GlobalData.getInstance().parseWaterMaskInfo("latitude", 29.51228918, GlobalData.ShareType.DOUBLE);
|
double longitude = GlobalData.getInstance().parseWaterMaskInfo("longitude", 106.45556208, GlobalData.ShareType.DOUBLE);
|
|
// new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
|
baseY = fontSize*11/10+baseY;
|
watermarkParam = new WatermarkParam(10,resolutionArr[1]-fontSize, String.format("%.6f %.6f", latitude, longitude)+" "+sdf.format(new Date()));
|
watermarkParamList.add(watermarkParam);
|
|
if (resolutionArr[0]==320&&resolutionArr[1]==240){
|
usbCamera.enableWatermark(true,"/system/ms_unicode_24.bin");
|
usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
|
}else if (resolutionArr[0]==640&&resolutionArr[1]==480){
|
usbCamera.enableWatermark(true,"/system/ms_unicode_32.bin");
|
usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
|
}else if (resolutionArr[0]==1280&&resolutionArr[1]==720){
|
usbCamera.enableWatermark(true,"/system/ms_unicode_48.bin");
|
usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
|
}else{
|
usbCamera.enableWatermark(true,"/system/ms_unicode_24.bin");
|
usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
|
}
|
}
|
|
},1,1, TimeUnit.SECONDS);
|
}
|
|
private void stopWaterMaskSchedule() {
|
Timber.i("%s_stopWaterMaskSchedule", TAG);
|
try {
|
if (watermarkFuture != null) {
|
watermarkFuture.cancel(true);
|
watermarkFuture = null;
|
}
|
} catch (Throwable t) {
|
Timber.w(t, "cancel watermarkFuture failed");
|
}
|
try {
|
if (scheduledExecutorService != null && !scheduledExecutorService.isShutdown()) {
|
scheduledExecutorService.shutdownNow();
|
}
|
} catch (Throwable t) {
|
Timber.w(t, "shutdown watermark scheduledExecutorService failed");
|
} finally {
|
scheduledExecutorService = null;
|
}
|
}
|
|
|
|
/**
|
* 停止录像
|
*/
|
public void stopRecord() {
|
Timber.d("stopRecord called");
|
|
stopWaterMaskSchedule();
|
|
// 停止音频线程
|
if (audioThread != null) {
|
audioThread.stopRecording();
|
try {
|
audioThread.join(1000);
|
} catch (InterruptedException e) {
|
Timber.e(e, "Error stopping audio thread");
|
}
|
audioThread = null;
|
}
|
|
stopRecordThread();
|
releaseResources();
|
if (usbCamera != null) {
|
usbCamera.stopCamera();
|
}
|
notifyCallback(0, 4, "录像已停止");
|
}
|
|
/**
|
* 打开USB摄像头
|
*/
|
private boolean openUsbCamera() {
|
try {
|
if (usbCamera == null) {
|
usbCamera = new UsbCamera();
|
}
|
|
// 打开摄像头之前先调用setenv
|
usbCamera.setenv();
|
|
// 使用prepareCamera方法,camera_id范围[0,9]
|
int[] cameraIds = {0, 2};
|
String cameraName = null; // 不指定特定名称
|
|
// 如果返回非0,代表打开失败,则先stopCamera再重试,最多3次
|
int ret = -1;
|
for (int i = 0; i < 3; i++) {
|
ret = usbCamera.prepareCamera(cameraIds, cameraName, resolutionArr, ay_encrypt);
|
Timber.d("USB录像摄像头第%d次打开结果: %d, 分辨率: %dx%d", i + 1, ret, resolutionArr[0], resolutionArr[1]);
|
if (ret == 0) {
|
break;
|
}
|
// 打开失败则先关闭再重试
|
usbCamera.stopCamera();
|
}
|
|
// 成功标准:prepareCamera 返回 0
|
return ret == 0;
|
} catch (Exception e) {
|
Timber.e(e, "打开USB摄像头异常");
|
return false;
|
}
|
}
|
|
/**
|
* 初始化编码器和Muxer
|
*/
|
private boolean initEncoderAndMuxer() {
|
try {
|
int width = resolutionArr[0];
|
int height = resolutionArr[1];
|
|
// 创建视频编码器
|
MediaFormat videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
|
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
|
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 3); // 码率
|
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
|
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL);
|
|
videoEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
videoEncoder.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
videoEncoder.start();
|
|
// 创建音频编码器
|
MediaFormat audioFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNEL_COUNT);
|
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
|
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, AUDIO_BIT_RATE);
|
|
audioEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
|
audioEncoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
audioEncoder.start();
|
|
// 初始化AudioRecord
|
audioBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT);
|
if (audioBufferSize <= 0) {
|
audioBufferSize = SAMPLE_RATE * 2; // 默认缓冲区大小
|
}
|
|
audioRecord = new AudioRecord(
|
MediaRecorder.AudioSource.MIC,
|
SAMPLE_RATE,
|
CHANNEL_CONFIG,
|
AUDIO_FORMAT,
|
audioBufferSize * 2
|
);
|
|
if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
|
Timber.e("AudioRecord初始化失败");
|
return false;
|
}
|
|
// 创建新的视频文件
|
currentVideoFile = VideoFileUtils.getVideoFile(context, mediaArgu.getTfCardFlag());
|
if (currentVideoFile == null) {
|
Timber.e("Failed to create video file");
|
return false;
|
}
|
|
mediaMuxer = new MediaMuxer(currentVideoFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
|
videoTrackIndex = -1;
|
audioTrackIndex = -1;
|
muxerStarted = false;
|
currentFileStartTime = System.currentTimeMillis();
|
|
Timber.d("编码器和Muxer初始化成功,文件: %s", currentVideoFile.getAbsolutePath());
|
return true;
|
} catch (Exception e) {
|
Timber.e(e, "初始化编码器和Muxer失败");
|
return false;
|
}
|
}
|
|
/**
|
* 释放资源
|
*/
|
private void releaseResources() {
|
|
if (audioRecord != null) {
|
try {
|
if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
|
audioRecord.stop();
|
}
|
audioRecord.release();
|
} catch (Exception e) {
|
Timber.e(e, "Error releasing audio record");
|
}
|
audioRecord = null;
|
}
|
|
if (audioEncoder != null) {
|
try {
|
audioEncoder.stop();
|
audioEncoder.release();
|
} catch (Exception e) {
|
Timber.e(e, "Error releasing audio encoder");
|
}
|
audioEncoder = null;
|
}
|
|
if (videoEncoder != null) {
|
try {
|
videoEncoder.stop();
|
videoEncoder.release();
|
} catch (Exception e) {
|
Timber.e(e, "Error releasing video encoder");
|
}
|
videoEncoder = null;
|
}
|
|
if (mediaMuxer != null) {
|
try {
|
if (muxerStarted) {
|
mediaMuxer.stop();
|
}
|
mediaMuxer.release();
|
} catch (Exception e) {
|
Timber.e(e, "Error releasing media muxer");
|
}
|
mediaMuxer = null;
|
}
|
|
muxerStarted = false;
|
videoTrackIndex = -1;
|
audioTrackIndex = -1;
|
}
|
|
/**
|
* 启动录像线程
|
*/
|
private void startRecordThread() {
|
if (recordThread == null || !isRunning) {
|
isRunning = true;
|
recordThread = new RecordThread();
|
recordThread.start();
|
Timber.d("Record thread started");
|
}
|
}
|
|
/**
|
* 停止录像线程
|
*/
|
private void stopRecordThread() {
|
isRunning = false;
|
if (recordThread != null) {
|
try {
|
recordThread.join(2000);
|
} catch (InterruptedException e) {
|
Timber.e(e, "Error stopping record thread");
|
}
|
recordThread = null;
|
}
|
Timber.d("Record thread stopped");
|
}
|
|
/**
|
* 录像线程
|
*/
|
private class RecordThread extends Thread {
|
@Override
|
public void run() {
|
super.run();
|
Timber.d("RecordThread started");
|
|
try {
|
int width = resolutionArr[0];
|
int height = resolutionArr[1];
|
|
// 计算YUV420缓冲区大小
|
int bufferSize = width * height * 3 / 2;
|
byte[] buffer = new byte[bufferSize];
|
|
// 初始化编码器和Muxer
|
if (!initEncoderAndMuxer()) {
|
notifyCallback(0, -1, "初始化编码器失败");
|
return;
|
}
|
|
// 启动音频录制
|
if (audioRecord != null) {
|
audioRecord.startRecording();
|
Timber.d("音频录制已启动");
|
}
|
|
// 注意:不要在这里主动检查音频编码器输出格式
|
// 因为 MediaCodec 的 getOutputFormat() 在编码器启动后可能返回 null
|
// 应该等待 INFO_OUTPUT_FORMAT_CHANGED 事件
|
|
// 启动音频编码线程
|
audioThread = new AudioThread();
|
audioThread.start();
|
|
Timber.d("开始录像,分辨率: %dx%d", width, height);
|
|
// 记录开始时间(纳秒,用于精确时间戳)
|
recordingStartTimeNs = System.nanoTime();
|
long frameCount = 0;
|
long lastFileChangeTime = System.currentTimeMillis();
|
|
// 循环处理摄像头数据
|
while (isRunning && cameraExists) {
|
// 检查是否需要创建新文件(每分钟)
|
long currentTime = System.currentTimeMillis();
|
if (currentTime - lastFileChangeTime >= RECORD_INTERVAL_MS) {
|
// 停止音频线程
|
if (audioThread != null) {
|
audioThread.stopRecording();
|
try {
|
audioThread.join(1000);
|
} catch (InterruptedException e) {
|
Timber.e(e, "Error stopping audio thread");
|
}
|
audioThread = null;
|
}
|
|
// 释放当前资源
|
releaseResources();
|
|
// 初始化新的编码器和Muxer
|
if (!initEncoderAndMuxer()) {
|
Timber.e("Failed to create new video file");
|
break;
|
}
|
|
// 重置开始时间
|
recordingStartTimeNs = System.nanoTime();
|
|
// 重新启动音频录制
|
if (audioRecord != null) {
|
audioRecord.startRecording();
|
}
|
audioThread = new AudioThread();
|
audioThread.start();
|
|
lastFileChangeTime = currentTime;
|
frameCount = 0;
|
|
// 通知文件创建
|
if (currentVideoFile != null) {
|
notifyCallback(2, 0, currentVideoFile.getName());
|
}
|
}
|
|
// 处理摄像头数据
|
int processResult = usbCamera.processCamera();
|
if (processResult == -1) {
|
Timber.w("processCamera返回-1,摄像头可能断开");
|
cameraExists = false;
|
notifyCallback(0, -1, "USB摄像头断开");
|
break;
|
}
|
|
// 获取YUV数据 (参数0表示录像)
|
usbCamera.rgba(2, buffer);
|
|
// 编码并写入文件
|
if (videoEncoder != null && mediaMuxer != null) {
|
// 计算实际经过的时间(微秒)
|
long elapsedTimeUs = (System.nanoTime() - recordingStartTimeNs) / 1000;
|
encodeFrame(buffer, elapsedTimeUs, width, height);
|
frameCount++;
|
}
|
|
// 控制帧率,约20fps
|
Thread.sleep(50);
|
}
|
|
} catch (Exception e) {
|
Timber.e(e, "Error in record thread");
|
cameraExists = false;
|
notifyCallback(0, -1, "录像线程异常: " + e.getMessage());
|
} finally {
|
// 停止音频线程
|
if (audioThread != null) {
|
audioThread.stopRecording();
|
try {
|
audioThread.join(1000);
|
} catch (InterruptedException e) {
|
Timber.e(e, "Error stopping audio thread");
|
}
|
audioThread = null;
|
}
|
|
releaseResources();
|
Timber.d("RecordThread ended");
|
}
|
}
|
|
/**
|
* 编码一帧数据
|
* @param yuvData YUV数据
|
* @param presentationTimeUs 时间戳(微秒),基于实际开始时间
|
* @param width 宽度
|
* @param height 高度
|
*/
|
private void encodeFrame(byte[] yuvData, long presentationTimeUs, int width, int height) {
|
try {
|
// 获取输入缓冲区
|
int inputBufferIndex = videoEncoder.dequeueInputBuffer(10000);
|
if (inputBufferIndex >= 0) {
|
ByteBuffer inputBuffer = videoEncoder.getInputBuffer(inputBufferIndex);
|
if (inputBuffer != null) {
|
inputBuffer.clear();
|
inputBuffer.put(yuvData);
|
|
// 使用实际经过的时间作为时间戳
|
videoEncoder.queueInputBuffer(inputBufferIndex, 0, yuvData.length,
|
presentationTimeUs, 0);
|
}
|
}
|
|
// 获取输出缓冲区
|
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
int outputBufferIndex = videoEncoder.dequeueOutputBuffer(bufferInfo, 0);
|
|
while (outputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) {
|
if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
// 输出格式改变,添加视频轨道
|
MediaFormat newFormat = videoEncoder.getOutputFormat();
|
synchronized (UsbCameraRecordManager.this) {
|
// 检查 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);
|
try {
|
mediaMuxer.writeSampleData(videoTrackIndex, outputBuffer, bufferInfo);
|
} catch (Exception e) {
|
Timber.e(e, "Error writing video sample data");
|
}
|
}
|
videoEncoder.releaseOutputBuffer(outputBufferIndex, false);
|
}
|
|
outputBufferIndex = videoEncoder.dequeueOutputBuffer(bufferInfo, 0);
|
}
|
} catch (Exception e) {
|
Timber.e(e, "Error encoding frame");
|
}
|
}
|
|
}
|
|
/**
|
* 检查并启动Muxer(当视频和音频轨道都准备好时)
|
*/
|
private synchronized void checkAndStartMuxer() {
|
if (!muxerStarted && videoTrackIndex >= 0 && audioTrackIndex >= 0) {
|
try {
|
mediaMuxer.start();
|
muxerStarted = true;
|
Timber.d("Muxer started, video track: %d, audio track: %d", videoTrackIndex, audioTrackIndex);
|
} catch (Exception e) {
|
Timber.e(e, "Failed to start muxer");
|
}
|
} else {
|
Timber.d("Muxer not started yet, video track: %d, audio track: %d", videoTrackIndex, audioTrackIndex);
|
}
|
}
|
|
/**
|
* 音频录制和编码线程
|
*/
|
private class AudioThread extends Thread {
|
private volatile boolean isRecording = true;
|
|
public void stopRecording() {
|
isRecording = false;
|
}
|
|
@Override
|
public void run() {
|
super.run();
|
Timber.d("AudioThread started");
|
|
try {
|
byte[] audioBuffer = new byte[audioBufferSize];
|
long totalSamplesRead = 0; // 总采样数
|
|
while (isRecording && audioRecord != null && audioEncoder != null) {
|
// 读取音频数据
|
int readSize = audioRecord.read(audioBuffer, 0, audioBuffer.length);
|
if (readSize < 0) {
|
Timber.w("AudioRecord read error: %d", readSize);
|
break;
|
}
|
|
// 编码音频数据
|
encodeAudio(audioBuffer, readSize, totalSamplesRead);
|
totalSamplesRead += readSize / 2; // 16位采样,每个采样2字节
|
}
|
|
} catch (Exception e) {
|
Timber.e(e, "Error in audio thread");
|
} finally {
|
Timber.d("AudioThread ended");
|
}
|
}
|
|
/**
|
* 编码音频数据
|
* @param audioData 音频数据
|
* @param size 数据大小(字节)
|
* @param totalSamples 总采样数(从开始到现在的累计采样数)
|
*/
|
private void encodeAudio(byte[] audioData, int size, long totalSamples) {
|
try {
|
// 获取输入缓冲区(即使 muxer 未启动也要编码,以便尽快获得输出格式)
|
int inputBufferIndex = audioEncoder.dequeueInputBuffer(10000);
|
if (inputBufferIndex >= 0) {
|
ByteBuffer inputBuffer = audioEncoder.getInputBuffer(inputBufferIndex);
|
if (inputBuffer != null) {
|
inputBuffer.clear();
|
inputBuffer.put(audioData, 0, size);
|
|
// 使用采样数计算时间戳(微秒)
|
// totalSamples 是采样数,SAMPLE_RATE 是每秒采样数
|
long presentationTimeUs = (totalSamples * 1000000) / SAMPLE_RATE;
|
audioEncoder.queueInputBuffer(inputBufferIndex, 0, size,
|
presentationTimeUs, 0);
|
}
|
}
|
|
// 获取输出缓冲区
|
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
int outputBufferIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0);
|
|
while (outputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) {
|
if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
// 输出格式改变,添加音频轨道
|
MediaFormat newFormat = audioEncoder.getOutputFormat();
|
synchronized (UsbCameraRecordManager.this) {
|
// 检查 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);
|
try {
|
mediaMuxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo);
|
} catch (Exception e) {
|
Timber.e(e, "Error writing audio sample data");
|
}
|
}
|
audioEncoder.releaseOutputBuffer(outputBufferIndex, false);
|
}
|
|
outputBufferIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0);
|
}
|
} catch (Exception e) {
|
Timber.e(e, "Error encoding audio");
|
}
|
}
|
}
|
|
/**
|
* 通知回调
|
*/
|
private void notifyCallback(int type, int errCode, String message) {
|
if (callback != null) {
|
ResponseVO response = new ResponseVO();
|
response.setType(type);
|
response.setErrCode(errCode);
|
response.setMessage(message);
|
callback.onResult(response);
|
}
|
}
|
}
|