package com.anyun.h264;
|
|
import android.media.MediaCodec;
|
import android.media.MediaCodecInfo;
|
import android.media.MediaFormat;
|
import com.anyun.libusbcamera.UsbCamera;
|
import com.anyun.libusbcamera.WatermarkParam;
|
import com.anyun.h264.model.WatermarkInfo;
|
|
import java.io.File;
|
import java.io.FileOutputStream;
|
import java.io.IOException;
|
import java.nio.ByteBuffer;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import timber.log.Timber;
|
|
/**
|
* H264视频编码器
|
* 使用UsbCamera获取视频数据,进行H264编码,并通过UDP按JT/T 1076-2016协议上传
|
*
|
* 使用示例:
|
* <pre>
|
* // 创建编码器
|
* H264Encoder encoder = new H264Encoder();
|
*
|
* // 设置编码参数
|
* encoder.setEncoderParams(640, 480, 25, 2000000);
|
*
|
* // 设置输出文件(可选,用于保存H264编码数据,可用VLC播放验证)
|
* // 使用应用外部存储目录(推荐)
|
* File outputFile = new File(context.getExternalFilesDir(null), "test.h264");
|
* encoder.setOutputFile(outputFile.getAbsolutePath());
|
* encoder.setEnableFileOutput(true); // 启用文件输出,设置为false则不写入文件
|
*
|
* // 设置UDP服务器地址(可选)
|
* encoder.setServerAddress("192.168.1.100", 8888);
|
* encoder.setProtocolParams("123456789012", (byte)1);
|
* encoder.setEnableNetworkTransmission(true); // 启用TCP/UDP网络传输,false表示禁用
|
*
|
* // 初始化并启动
|
* int[] cameraIdRange = {0, 0};
|
* int[] resolution = {640, 480};
|
* if (encoder.initialize(cameraIdRange, "camera", resolution, false)) {
|
* encoder.start();
|
* }
|
*
|
* // 停止编码
|
* encoder.stop();
|
* </pre>
|
*
|
* 生成的.h264文件可以用VLC播放器直接播放验证。
|
*/
|
public class H264Encoder {
|
private static final String TAG = "H264Encoder";
|
|
private UsbCamera usbCamera;
|
private MediaCodec encoder;
|
private Thread encodeThread;
|
private AtomicBoolean isRunning = new AtomicBoolean(false);
|
|
// 编码参数
|
private int width = 640;
|
private int height = 480;
|
private int frameRate = 15;
|
private int bitrate = 2000000; // 2Mbps
|
private int iFrameInterval = 2; // I帧间隔(秒)
|
|
// JT/T 1076-2016 协议工具类
|
private JT1076ProtocolHelper protocolHelper;
|
private long lastIFrameTime = 0; // 上一个I帧时间
|
private long lastFrameTime = 0; // 上一帧时间
|
|
// 文件输出
|
private FileOutputStream fileOutputStream;
|
private String outputFilePath;
|
private boolean enableFileOutput = false; // 是否启用文件输出
|
private boolean spsPpsWritten = false; // 标记SPS/PPS是否已写入
|
|
// 网络传输控制
|
private boolean enableNetworkTransmission = true; // 是否启用TCP/UDP网络传输
|
|
// SPS/PPS 缓存(用于网络传输)
|
private byte[] spsBuffer = null; // SPS 缓存
|
private byte[] ppsBuffer = null; // PPS 缓存
|
|
// 编码回调
|
public interface OnFrameEncodedCallback {
|
void onFrameEncoded(byte[] data, boolean isKeyFrame);
|
}
|
private OnFrameEncodedCallback callback;
|
|
public H264Encoder() {
|
this.usbCamera = new UsbCamera();
|
this.protocolHelper = new JT1076ProtocolHelper();
|
protocolHelper.setProtocolType(JT1076ProtocolHelper.PROTOCOL_TYPE_TCP);//设置为tcp传输
|
}
|
|
/**
|
* 设置编码参数
|
*/
|
public void setEncoderParams(int width, int height, int frameRate, int bitrate) {
|
this.width = width;
|
this.height = height;
|
this.frameRate = frameRate;
|
this.bitrate = bitrate;
|
}
|
|
/**
|
* 设置UDP服务器地址
|
*/
|
public void setServerAddress(String ip, int port) {
|
protocolHelper.setServerAddress(ip, port);
|
}
|
|
/**
|
* 设置SIM卡号和逻辑通道号
|
*/
|
public void setProtocolParams(String simCardNumber, byte logicalChannelNumber) {
|
protocolHelper.setProtocolParams(simCardNumber, logicalChannelNumber);
|
}
|
|
/**
|
* 设置编码回调
|
*/
|
public void setOnFrameEncodedCallback(OnFrameEncodedCallback callback) {
|
this.callback = callback;
|
}
|
|
/**
|
* 设置输出文件路径(用于保存H264编码数据)
|
* @param filePath 文件路径,例如:"/sdcard/test.h264" 或使用Context.getExternalFilesDir()
|
*/
|
public void setOutputFile(String filePath) {
|
this.outputFilePath = filePath;
|
}
|
|
/**
|
* 设置是否启用文件输出
|
* @param enable true表示启用文件输出,false表示禁用
|
*/
|
public void setEnableFileOutput(boolean enable) {
|
Timber.d("开启h264文件输出");
|
this.enableFileOutput = enable;
|
}
|
|
/**
|
* 设置是否启用TCP/UDP网络传输
|
* @param enable true表示启用网络传输,false表示禁用
|
*/
|
public void setEnableNetworkTransmission(boolean enable) {
|
this.enableNetworkTransmission = enable;
|
Timber.d("Network transmission " + (enable ? "enabled" : "disabled"));
|
}
|
|
/**
|
* 设置水印信息
|
* @param watermarkInfo 水印信息对象
|
*/
|
public void setWatermarkInfo(WatermarkInfo watermarkInfo) {
|
if (watermarkInfo == null) {
|
Timber.w("WatermarkInfo is null, disabling watermark");
|
usbCamera.enableWatermark(false, null);
|
return;
|
}
|
|
try {
|
// 构建水印文本列表(分行显示,每行一个信息项)
|
ArrayList<WatermarkParam> watermarkParams = new ArrayList<>();
|
|
// 从左上角开始,每行间隔25像素
|
int yOffset = 30;
|
int xOffset = 10;
|
|
// 车牌号
|
if (watermarkInfo.getPlateNumber() != null && !watermarkInfo.getPlateNumber().isEmpty()) {
|
watermarkParams.add(new WatermarkParam(xOffset, yOffset,
|
"车牌:" + watermarkInfo.getPlateNumber()));
|
yOffset += 25;
|
}
|
|
// 学员姓名
|
if (watermarkInfo.getStudent() != null && !watermarkInfo.getStudent().isEmpty()) {
|
watermarkParams.add(new WatermarkParam(xOffset, yOffset,
|
"学员:" + watermarkInfo.getStudent()));
|
yOffset += 25;
|
}
|
|
// 教练姓名
|
if (watermarkInfo.getCoach() != null && !watermarkInfo.getCoach().isEmpty()) {
|
watermarkParams.add(new WatermarkParam(xOffset, yOffset,
|
"教练:" + watermarkInfo.getCoach()));
|
yOffset += 25;
|
}
|
|
// 位置信息(纬度,经度)
|
if (watermarkInfo.getLongitude() != null && watermarkInfo.getLatitude() != null) {
|
watermarkParams.add(new WatermarkParam(xOffset, yOffset,
|
String.format("位置:%.6f,%.6f", watermarkInfo.getLatitude(), watermarkInfo.getLongitude())));
|
yOffset += 25;
|
}
|
|
// 驾校名称
|
if (watermarkInfo.getDrivingSchool() != null && !watermarkInfo.getDrivingSchool().isEmpty()) {
|
watermarkParams.add(new WatermarkParam(xOffset, yOffset,
|
"驾校:" + watermarkInfo.getDrivingSchool()));
|
yOffset += 25;
|
}
|
|
// 车速
|
if (watermarkInfo.getSpeed() != null) {
|
watermarkParams.add(new WatermarkParam(xOffset, yOffset,
|
String.format("车速:%.1fkm/h", watermarkInfo.getSpeed())));
|
}
|
|
if (!watermarkParams.isEmpty()) {
|
// 启用水印,使用默认字体路径(如果系统有字体文件)
|
// 颜色:0-REVERSE(反色),1-BLACK,2-WHITE,3-RED,4-GREEN,5-BLUE
|
// 字体大小、倍数可以根据需要调整
|
String fontPath = "/system/fonts/DroidSans.ttf"; // 默认字体路径,如果不存在可以传null
|
usbCamera.enableWatermark(true, fontPath);
|
|
// 设置水印:颜色(2=白色),字体大小(24),倍数(1),文本列表
|
usbCamera.setWatermark(2, 24, 1, watermarkParams);
|
|
Timber.d("Watermark set successfully: %s", watermarkInfo);
|
} else {
|
Timber.w("No watermark text to display, disabling watermark");
|
usbCamera.enableWatermark(false, null);
|
}
|
} catch (Exception e) {
|
Timber.e(e, "Failed to set watermark");
|
usbCamera.enableWatermark(false, null);
|
}
|
}
|
|
/**
|
* 初始化摄像头和编码器
|
*/
|
public boolean initialize(int[] cameraIdRange, String cameraName, int[] resolution, boolean ayCamera) {
|
try {
|
// 1. setenv
|
usbCamera.setenv();
|
|
// 2. prepareCamera (最多尝试3次:初始1次 + 重试2次)
|
int[] actualResolution = new int[2];
|
actualResolution[0] = 640;
|
actualResolution[1] = 480;
|
System.arraycopy(resolution, 0, actualResolution, 0, 2);
|
|
int result = -1;
|
int maxRetries = 3; // 总共尝试3次
|
for (int attempt = 0; attempt < maxRetries; attempt++) {
|
result = usbCamera.prepareCamera(cameraIdRange, cameraName, actualResolution, ayCamera);
|
if (result == 0) {
|
// 成功,跳出循环
|
if (attempt > 0) {
|
Timber.d("prepareCamera succeeded on attempt " + (attempt + 1));
|
}
|
break;
|
} else {
|
// 失败,记录日志
|
Timber.w( "prepareCamera failed on attempt " + (attempt + 1) + ": " + result);
|
if (attempt < maxRetries - 1) {
|
Timber.d( "Retrying prepareCamera...");
|
}
|
}
|
}
|
|
if (result != 0) {
|
Timber.e("prepareCamera failed after " + maxRetries + " attempts: " + result);
|
return false;
|
}
|
|
// 更新实际分辨率
|
width = actualResolution[0];
|
height = actualResolution[1];
|
Timber.d("Camera initialized with resolution: " + width + "x" + height);
|
|
// 3. 初始化H264编码器
|
initEncoder();
|
|
// 4. 初始化Socket(UDP或TCP,根据协议类型自动选择)
|
// 只有在启用网络传输时才初始化Socket
|
if (enableNetworkTransmission) {
|
if (!protocolHelper.initializeSocket()) {
|
return false;
|
}
|
} else {
|
Timber.d("Network transmission disabled, skipping socket initialization");
|
}
|
|
// 5. 初始化文件输出(仅创建文件,SPS/PPS在第一次输出时写入)
|
if (enableFileOutput && outputFilePath != null && !outputFilePath.isEmpty()) {
|
if (!initFileOutput()) {
|
Timber.w("File output initialization failed, continuing without file output");
|
}
|
}
|
|
return true;
|
} catch (Exception e) {
|
Timber.e(e,"Initialize failed");
|
return false;
|
}
|
}
|
|
/**
|
* 初始化H264编码器
|
*/
|
private void initEncoder() throws IOException {
|
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
|
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
|
|
encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
encoder.start();
|
|
// 清理 SPS/PPS 缓存
|
spsBuffer = null;
|
ppsBuffer = null;
|
|
Timber.d( "H264 encoder initialized");
|
}
|
|
/**
|
* 初始化文件输出
|
* @return 是否成功
|
*/
|
private boolean initFileOutput() {
|
try {
|
File file = new File(outputFilePath);
|
File parentDir = file.getParentFile();
|
if (parentDir != null && !parentDir.exists()) {
|
boolean created = parentDir.mkdirs();
|
if (!created && !parentDir.exists()) {
|
Timber.e("Failed to create parent directory: " + parentDir.getAbsolutePath());
|
return false;
|
}
|
}
|
|
fileOutputStream = new FileOutputStream(file);
|
spsPpsWritten = false;
|
|
Timber.d("File output initialized: " + outputFilePath);
|
return true;
|
} catch (Exception e) {
|
Timber.e(e,"Initialize file output failed");
|
if (fileOutputStream != null) {
|
try {
|
fileOutputStream.close();
|
} catch (IOException ie) {
|
Timber.e(ie, "Close file output stream failed");
|
}
|
fileOutputStream = null;
|
}
|
return false;
|
}
|
}
|
|
/**
|
* 写入SPS/PPS到文件(从CSD或关键帧数据中提取)
|
*/
|
private void writeSpsPpsToFile() {
|
if (!enableFileOutput || fileOutputStream == null || spsPpsWritten) {
|
return;
|
}
|
|
try {
|
// 尝试从编码器输出格式中获取CSD
|
MediaFormat format = encoder.getOutputFormat();
|
ByteBuffer spsBuffer = format.getByteBuffer("csd-0"); // SPS
|
ByteBuffer ppsBuffer = format.getByteBuffer("csd-1"); // PPS
|
|
if (spsBuffer != null && ppsBuffer != null) {
|
// CSD格式通常是AVCC格式,需要转换为Annex-B
|
byte[] sps = new byte[spsBuffer.remaining()];
|
byte[] pps = new byte[ppsBuffer.remaining()];
|
spsBuffer.get(sps);
|
ppsBuffer.get(pps);
|
|
// 写入SPS和PPS到文件(Annex-B格式)
|
byte[] nalStartCode = {0x00, 0x00, 0x00, 0x01};
|
|
// CSD数据通常是AVCC格式:前4字节是大端格式的长度,后面是NAL数据
|
// 检查并跳过长度前缀
|
int spsOffset = 0;
|
int ppsOffset = 0;
|
int spsLength = sps.length;
|
int ppsLength = pps.length;
|
|
// 检查是否为AVCC格式(前4字节是长度,通常不会是0x00000001)
|
if (sps.length > 4 && (sps[0] != 0x00 || sps[1] != 0x00 || sps[2] != 0x00 || sps[3] != 0x01)) {
|
// AVCC格式:前4字节是长度(大端)
|
spsOffset = 4;
|
spsLength = sps.length - 4;
|
}
|
if (pps.length > 4 && (pps[0] != 0x00 || pps[1] != 0x00 || pps[2] != 0x00 || pps[3] != 0x01)) {
|
// AVCC格式:前4字节是长度(大端)
|
ppsOffset = 4;
|
ppsLength = pps.length - 4;
|
}
|
|
// 写入SPS
|
fileOutputStream.write(nalStartCode);
|
fileOutputStream.write(sps, spsOffset, spsLength);
|
|
// 写入PPS
|
fileOutputStream.write(nalStartCode);
|
fileOutputStream.write(pps, ppsOffset, ppsLength);
|
fileOutputStream.flush();
|
|
spsPpsWritten = true;
|
Timber.d("SPS/PPS written to file, SPS size: " + spsLength + ", PPS size: " + ppsLength);
|
} else {
|
Timber.w("SPS/PPS not found in CSD, will extract from first key frame");
|
}
|
} catch (Exception e) {
|
Timber.e(e,"Write SPS/PPS to file error");
|
}
|
}
|
|
/**
|
* 开始编码
|
*/
|
public void start() {
|
if (isRunning.get()) {
|
Timber.w("Encoder is already running");
|
return;
|
}
|
|
isRunning.set(true);
|
|
// 启动编码线程
|
encodeThread = new Thread(new Runnable() {
|
@Override
|
public void run() {
|
encodeLoop();
|
}
|
});
|
encodeThread.start();
|
|
Timber.d("H264 encoder started");
|
}
|
|
/**
|
* 编码循环
|
*/
|
private void encodeLoop() {
|
// YUV420P格式: Y平面 + U平面 + V平面
|
// Y平面大小 = width * height
|
// U平面大小 = width * height / 4
|
// V平面大小 = width * height / 4
|
// 总大小 = width * height * 3 / 2
|
byte[] yuvBuffer = new byte[width * height * 3 / 2];
|
|
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
|
while (isRunning.get()) {
|
try {
|
// processCamera - 读取一帧
|
int processResult = usbCamera.processCamera();
|
if (processResult != 0) {
|
Timber.w("processCamera returned: " + processResult);
|
Thread.sleep(10);
|
continue;
|
}
|
|
// 获取RGBA数据 (type=1 表示推流,输出YUV420P格式)
|
usbCamera.rgba(0, yuvBuffer);
|
|
// 将YUV420P数据送入编码器
|
long timestamp = System.currentTimeMillis();
|
encodeFrame(nv12ToNV21(yuvBuffer,width,height), timestamp, bufferInfo);
|
|
} catch (Exception e) {
|
Timber.e(e, "Encode loop error");
|
try {
|
Thread.sleep(100);
|
} catch (InterruptedException ie) {
|
break;
|
}
|
}
|
}
|
|
Timber.d( "Encode loop exited");
|
}
|
|
/**
|
* 编码一帧数据
|
*/
|
private void encodeFrame(byte[] yuvData, long timestamp, MediaCodec.BufferInfo bufferInfo) {
|
try {
|
// 获取输入缓冲区
|
int inputBufferIndex = encoder.dequeueInputBuffer(10000);
|
if (inputBufferIndex >= 0) {
|
ByteBuffer inputBuffer = encoder.getInputBuffer(inputBufferIndex);
|
if (inputBuffer != null) {
|
inputBuffer.clear();
|
inputBuffer.put(yuvData);
|
encoder.queueInputBuffer(inputBufferIndex, 0, yuvData.length, timestamp * 1000, 0);
|
}
|
}
|
|
// 获取输出数据
|
int outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0);
|
while (outputBufferIndex >= 0) {
|
ByteBuffer outputBuffer = encoder.getOutputBuffer(outputBufferIndex);
|
if (outputBuffer != null && bufferInfo.size > 0) {
|
// 复制编码数据
|
byte[] nalUnit = new byte[bufferInfo.size];
|
outputBuffer.position(bufferInfo.offset);
|
outputBuffer.get(nalUnit, 0, bufferInfo.size);
|
|
// 解析并处理 NAL 单元
|
processNalUnit(nalUnit, timestamp);
|
|
// 写入文件(保持原有逻辑)
|
boolean isKeyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
|
writeToFile(nalUnit, isKeyFrame);
|
|
// 回调
|
if (callback != null) {
|
callback.onFrameEncoded(nalUnit, isKeyFrame);
|
}
|
}
|
|
encoder.releaseOutputBuffer(outputBufferIndex, false);
|
outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0);
|
}
|
|
} catch (Exception e) {
|
Timber.e(e,"Encode frame error");
|
}
|
}
|
|
/**
|
* 处理 NAL 单元,识别类型并分别处理
|
*/
|
private void processNalUnit(byte[] data, long timestamp) {
|
if (data == null || data.length < 5) {
|
return;
|
}
|
|
// MediaCodec 输出的数据是 Annex-B 格式,可能包含多个 NAL 单元
|
// 每个 NAL 单元以 0x00000001 或 0x000001 开头
|
int offset = 0;
|
while (offset < data.length) {
|
// 查找起始码
|
int startCodePos = findStartCode(data, offset);
|
if (startCodePos == -1) {
|
break; // 没有找到起始码,结束
|
}
|
|
// 跳过起始码,找到 NAL 单元数据
|
int nalStart = startCodePos;
|
int startCodeLength = (startCodePos + 2 < data.length &&
|
data[startCodePos] == 0x00 &&
|
data[startCodePos + 1] == 0x00 &&
|
data[startCodePos + 2] == 0x01) ? 3 : 4;
|
int nalDataStart = nalStart + startCodeLength;
|
|
// 查找下一个起始码,确定当前 NAL 单元的结束位置
|
int nextStartCodePos = findStartCode(data, nalDataStart);
|
int nalDataEnd = (nextStartCodePos == -1) ? data.length : nextStartCodePos;
|
int nalDataLength = nalDataEnd - nalDataStart;
|
|
if (nalDataLength > 0 && nalDataStart < data.length) {
|
// 提取 NAL 单元数据(包含起始码)
|
byte[] nalUnit = new byte[nalDataLength + startCodeLength];
|
System.arraycopy(data, nalStart, nalUnit, 0, startCodeLength);
|
System.arraycopy(data, nalDataStart, nalUnit, startCodeLength, nalDataLength);
|
|
// 获取 NAL 类型(起始码后的第一个字节的低5位)
|
int nalType = (nalUnit[startCodeLength] & 0x1F);
|
|
// 根据 NAL 类型处理
|
switch (nalType) {
|
case 7: // SPS
|
spsBuffer = nalUnit.clone();
|
Timber.d("SPS cached, size: %d", spsBuffer.length);
|
break;
|
|
case 8: // PPS
|
ppsBuffer = nalUnit.clone();
|
Timber.d("PPS cached, size: %d", ppsBuffer.length);
|
break;
|
|
case 5: // IDR 帧 (关键帧)
|
// 关键帧必须与 SPS、PPS 一起发送
|
if (spsBuffer != null && ppsBuffer != null) {
|
// 将 SPS, PPS, IDR 帧数据组合成一个完整的"帧数据单元"
|
byte[] frameData = combineFrameData(spsBuffer, ppsBuffer, nalUnit);
|
// 发送组合后的数据
|
sendEncodedData(frameData, timestamp, true);
|
} else {
|
Timber.w("Received IDR frame, but SPS or PPS not ready, dropping");
|
// 如果没有 SPS/PPS,仍然发送 IDR 帧(可能包含在数据中)
|
sendEncodedData(nalUnit, timestamp, true);
|
}
|
break;
|
|
case 1: // 非 IDR 帧 (P 帧)
|
// 直接发送 P 帧
|
sendEncodedData(nalUnit, timestamp, false);
|
break;
|
|
default:
|
// 其他 NALU 类型(如 SEI 等),根据协议决定是否处理
|
// 这里可以选择发送或忽略
|
Timber.d("NAL unit type %d, size: %d", nalType, nalUnit.length);
|
break;
|
}
|
}
|
|
// 移动到下一个可能的起始位置
|
offset = nalDataEnd;
|
}
|
}
|
|
/**
|
* 查找起始码位置(0x00000001 或 0x000001)
|
* @param data 数据数组
|
* @param startPos 开始搜索的位置
|
* @return 起始码位置,如果未找到返回 -1
|
*/
|
private int findStartCode(byte[] data, int startPos) {
|
for (int i = startPos; i < data.length - 2; i++) {
|
if (data[i] == 0x00 && data[i + 1] == 0x00) {
|
if (i + 2 < data.length && data[i + 2] == 0x01) {
|
return i; // 找到 0x000001
|
}
|
if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) {
|
return i; // 找到 0x00000001
|
}
|
}
|
}
|
return -1;
|
}
|
|
/**
|
* 组合 SPS、PPS 和 IDR 帧数据
|
* @param sps SPS 数据(包含起始码)
|
* @param pps PPS 数据(包含起始码)
|
* @param idr IDR 帧数据(包含起始码)
|
* @return 组合后的数据
|
*/
|
private byte[] combineFrameData(byte[] sps, byte[] pps, byte[] idr) {
|
int totalLength = sps.length + pps.length + idr.length;
|
byte[] combined = new byte[totalLength];
|
int offset = 0;
|
|
// 复制 SPS
|
System.arraycopy(sps, 0, combined, offset, sps.length);
|
offset += sps.length;
|
|
// 复制 PPS
|
System.arraycopy(pps, 0, combined, offset, pps.length);
|
offset += pps.length;
|
|
// 复制 IDR 帧
|
System.arraycopy(idr, 0, combined, offset, idr.length);
|
|
Timber.d("Combined SPS/PPS/IDR frame, total size: %d (SPS: %d, PPS: %d, IDR: %d)",
|
totalLength, sps.length, pps.length, idr.length);
|
|
return combined;
|
}
|
|
/**
|
* 将编码数据写入H264文件
|
*/
|
private void writeToFile(byte[] data, boolean isKeyFrame) {
|
if (!enableFileOutput || fileOutputStream == null) {
|
return;
|
}
|
|
try {
|
// 如果是第一个关键帧,确保SPS/PPS已写入
|
if (isKeyFrame && !spsPpsWritten) {
|
writeSpsPpsToFile();
|
|
// 如果从CSD获取失败,尝试从关键帧数据中提取
|
// MediaCodec输出的关键帧通常已经包含SPS/PPS,但为了确保文件完整性,
|
// 我们已经从CSD写入,这里直接写入关键帧数据即可
|
if (!spsPpsWritten) {
|
Timber.d("SPS/PPS will be included in key frame data");
|
}
|
}
|
|
// MediaCodec输出的H264数据已经是Annex-B格式(包含0x00000001分隔符)
|
// 直接写入文件即可
|
fileOutputStream.write(data);
|
fileOutputStream.flush();
|
} catch (IOException e) {
|
Timber.e(e, "Write to file error");
|
}
|
}
|
|
/**
|
* 发送编码后的数据(按JT/T 1076-2016协议打包)
|
*/
|
private void sendEncodedData(byte[] data, long timestamp, boolean isKeyFrame) {
|
// 如果未启用网络传输,直接返回
|
if (!enableNetworkTransmission) {
|
return;
|
}
|
|
try {
|
// 计算时间间隔
|
long currentTime = System.currentTimeMillis();
|
long lastIFrameInterval = (lastIFrameTime > 0) ? (currentTime - lastIFrameTime) : 0;
|
long lastFrameInterval = (lastFrameTime > 0) ? (currentTime - lastFrameTime) : 0;
|
|
if (isKeyFrame) {
|
lastIFrameTime = currentTime;
|
}
|
lastFrameTime = currentTime;
|
|
// 判断帧类型
|
int dataType = isKeyFrame ? JT1076ProtocolHelper.DATA_TYPE_I_FRAME :
|
JT1076ProtocolHelper.DATA_TYPE_P_FRAME;
|
|
// 分包发送(如果数据超过最大包大小)
|
int offset = 0;
|
int totalPackets = (int) Math.ceil((double) data.length / JT1076ProtocolHelper.MAX_PACKET_SIZE);
|
|
for (int i = 0; i < totalPackets; i++) {
|
int packetDataSize = Math.min(JT1076ProtocolHelper.MAX_PACKET_SIZE, data.length - offset);
|
byte[] packetData = Arrays.copyOfRange(data, offset, offset + packetDataSize);
|
|
// 确定分包标记
|
int packetMark;
|
if (totalPackets == 1) {
|
packetMark = JT1076ProtocolHelper.PACKET_MARK_ATOMIC;
|
} else if (i == 0) {
|
packetMark = JT1076ProtocolHelper.PACKET_MARK_FIRST;
|
} else if (i == totalPackets - 1) {
|
packetMark = JT1076ProtocolHelper.PACKET_MARK_LAST;
|
} else {
|
packetMark = JT1076ProtocolHelper.PACKET_MARK_MIDDLE;
|
}
|
|
// 创建RTP包
|
byte[] rtpPacket = protocolHelper.createVideoRtpPacket(
|
packetData, timestamp, dataType, packetMark,
|
lastIFrameInterval, lastFrameInterval);
|
|
// 发送RTP包(UDP或TCP,根据协议类型自动选择)
|
protocolHelper.sendPacket(rtpPacket);
|
|
offset += packetDataSize;
|
}
|
|
} catch (Exception e) {
|
Timber.e(e,"Send encoded data error");
|
}
|
}
|
|
/**
|
* 停止编码
|
*/
|
public void stop() {
|
if (!isRunning.get()) {
|
return;
|
}
|
|
isRunning.set(false);
|
|
// 等待编码线程结束
|
if (encodeThread != null) {
|
try {
|
encodeThread.join(2000);
|
} catch (InterruptedException e) {
|
Timber.e(e, "Wait encode thread error");
|
}
|
}
|
|
// 停止摄像头
|
if (usbCamera != null) {
|
usbCamera.stopCamera();
|
}
|
|
// 释放编码器
|
if (encoder != null) {
|
try {
|
encoder.stop();
|
encoder.release();
|
encoder = null;
|
} catch (Exception e) {
|
Timber.e(e, "Release encoder error");
|
}
|
}
|
|
// 关闭Socket(UDP或TCP,根据协议类型自动选择)
|
// 只有在启用网络传输时才需要关闭Socket
|
if (enableNetworkTransmission && protocolHelper != null) {
|
protocolHelper.closeSocket();
|
}
|
|
// 关闭文件输出
|
closeFileOutput();
|
|
// 清理 SPS/PPS 缓存
|
spsBuffer = null;
|
ppsBuffer = null;
|
|
Timber.d("H264 encoder stopped");
|
}
|
|
/**
|
* 关闭文件输出
|
*/
|
private void closeFileOutput() {
|
if (fileOutputStream != null) {
|
try {
|
fileOutputStream.flush();
|
fileOutputStream.close();
|
Timber.d("File output closed: " + outputFilePath);
|
} catch (IOException e) {
|
Timber.e(e, "Close file output error");
|
} finally {
|
fileOutputStream = null;
|
spsPpsWritten = false;
|
}
|
}
|
}
|
|
/**
|
* 释放资源
|
*/
|
public void release() {
|
stop();
|
}
|
|
byte[] ret = null;
|
// YYYYYYYY UVUV(nv12)--> YYYYYYYY VUVU(nv21)
|
private byte[] nv12ToNV21(byte[] nv12, int width, int height) {
|
// Log.i(TAG,"nv12toNv21:"+width+"height:"+height);
|
if (ret == null){
|
ret = new byte[width * height * 3 /2];
|
}
|
int framesize = width * height;
|
int i = 0, j = 0;
|
// 拷贝Y分量
|
System.arraycopy(nv12, 0,ret , 0, framesize);
|
// 拷贝UV分量
|
for (j = framesize; j < nv12.length; j += 2) {
|
ret[j] = nv12[j+1];
|
ret[j+1] = nv12[j];
|
}
|
return ret;
|
}
|
}
|