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协议上传
*
* 使用示例:
*
* // 创建编码器
* 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();
*
*
* 生成的.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 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;
}
}