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 = 25; private int bitrate = 2000000; // 2Mbps private int iFrameInterval = 1; // 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网络传输 // 编码回调 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) { 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(); 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) { // 检查是否为关键帧 boolean isKeyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0; // 复制编码数据 byte[] encodedData = new byte[bufferInfo.size]; outputBuffer.position(bufferInfo.offset); outputBuffer.get(encodedData, 0, bufferInfo.size); // 写入文件 writeToFile(encodedData, isKeyFrame); // 发送编码数据 sendEncodedData(encodedData, timestamp, isKeyFrame); // 回调 if (callback != null) { callback.onFrameEncoded(encodedData, isKeyFrame); } } encoder.releaseOutputBuffer(outputBufferIndex, false); outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0); } } catch (Exception e) { Timber.e(e,"Encode frame error"); } } /** * 将编码数据写入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(); 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; } }