| New file |
| | |
| | | package com.anyun.h264; |
| | | |
| | | import android.util.Log; |
| | | |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.IOException; |
| | | import java.util.Arrays; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | |
| | | /** |
| | | * H264文件传输器 |
| | | * 从H264文件读取数据,按照JT/T 1076-2016协议通过TCP/UDP传输 |
| | | * |
| | | * 使用示例: |
| | | * <pre> |
| | | * // 创建传输器 |
| | | * H264FileTransmitter transmitter = new H264FileTransmitter(); |
| | | * |
| | | * // 设置服务器地址和协议类型 |
| | | * transmitter.setServerAddress("192.168.1.100", 8888); |
| | | * transmitter.setProtocolType(JT1076ProtocolHelper.PROTOCOL_TYPE_TCP); // 或 PROTOCOL_TYPE_UDP |
| | | * |
| | | * // 设置协议参数 |
| | | * transmitter.setProtocolParams("013120122580", (byte)1); |
| | | * |
| | | * // 设置帧率(用于计算时间戳间隔) |
| | | * transmitter.setFrameRate(25); |
| | | * |
| | | * // 初始化Socket |
| | | * if (transmitter.initialize()) { |
| | | * // 开始传输文件 |
| | | * transmitter.transmitFile("/path/to/video.h264"); |
| | | * } |
| | | * |
| | | * // 停止传输 |
| | | * transmitter.stop(); |
| | | * </pre> |
| | | */ |
| | | public class H264FileTransmitter { |
| | | private static final String TAG = "H264FileTransmitter"; |
| | | |
| | | // H264 NAL起始码 |
| | | private static final byte[] START_CODE_3 = {0x00, 0x00, 0x01}; |
| | | private static final byte[] START_CODE_4 = {0x00, 0x00, 0x00, 0x01}; |
| | | |
| | | // JT/T 1076-2016 协议工具类 |
| | | private JT1076ProtocolHelper protocolHelper; |
| | | |
| | | // 传输控制 |
| | | private AtomicBoolean isRunning = new AtomicBoolean(false); |
| | | private Thread transmitThread; |
| | | |
| | | // 参数配置 |
| | | private int frameRate = 25; // 帧率,用于计算时间戳间隔 |
| | | private long frameInterval = 1000 / 25; // 帧间隔(毫秒) |
| | | |
| | | // 时间戳管理 |
| | | private long lastIFrameTime = 0; // 上一个I帧时间 |
| | | private long lastFrameTime = 0; // 上一帧时间 |
| | | private long baseTimestamp = 0; // 基准时间戳 |
| | | |
| | | /** |
| | | * 传输进度回调接口 |
| | | */ |
| | | public interface OnTransmitProgressCallback { |
| | | /** |
| | | * 传输进度回调 |
| | | * @param currentFrame 当前帧序号(从1开始) |
| | | * @param totalFrames 总帧数(如果未知则为-1) |
| | | */ |
| | | void onProgress(int currentFrame, int totalFrames); |
| | | |
| | | /** |
| | | * 传输完成回调 |
| | | */ |
| | | void onComplete(); |
| | | |
| | | /** |
| | | * 传输错误回调 |
| | | * @param error 错误信息 |
| | | */ |
| | | void onError(String error); |
| | | } |
| | | private OnTransmitProgressCallback progressCallback; |
| | | |
| | | public H264FileTransmitter() { |
| | | this.protocolHelper = new JT1076ProtocolHelper(); |
| | | // 默认使用TCP协议 |
| | | protocolHelper.setProtocolType(JT1076ProtocolHelper.PROTOCOL_TYPE_TCP); |
| | | } |
| | | |
| | | /** |
| | | * 设置服务器地址 |
| | | */ |
| | | public void setServerAddress(String ip, int port) { |
| | | protocolHelper.setServerAddress(ip, port); |
| | | } |
| | | |
| | | /** |
| | | * 设置传输协议类型(UDP或TCP) |
| | | * @param protocolType PROTOCOL_TYPE_UDP 或 PROTOCOL_TYPE_TCP |
| | | */ |
| | | public void setProtocolType(int protocolType) { |
| | | protocolHelper.setProtocolType(protocolType); |
| | | } |
| | | |
| | | /** |
| | | * 设置SIM卡号和逻辑通道号 |
| | | */ |
| | | public void setProtocolParams(String simCardNumber, byte logicalChannelNumber) { |
| | | protocolHelper.setProtocolParams(simCardNumber, logicalChannelNumber); |
| | | } |
| | | |
| | | /** |
| | | * 设置帧率(用于计算时间戳间隔) |
| | | * @param frameRate 帧率(fps) |
| | | */ |
| | | public void setFrameRate(int frameRate) { |
| | | this.frameRate = frameRate > 0 ? frameRate : 25; |
| | | this.frameInterval = 1000 / this.frameRate; |
| | | Log.d(TAG, "Frame rate set to: " + this.frameRate + " fps, interval: " + this.frameInterval + " ms"); |
| | | } |
| | | |
| | | /** |
| | | * 设置传输进度回调 |
| | | */ |
| | | public void setOnTransmitProgressCallback(OnTransmitProgressCallback callback) { |
| | | this.progressCallback = callback; |
| | | } |
| | | |
| | | /** |
| | | * 初始化Socket连接 |
| | | * @return 是否成功 |
| | | */ |
| | | public boolean initialize() { |
| | | if (isRunning.get()) { |
| | | Log.w(TAG, "Transmitter is already running"); |
| | | return false; |
| | | } |
| | | |
| | | if (!protocolHelper.initializeSocket()) { |
| | | Log.e(TAG, "Failed to initialize socket"); |
| | | return false; |
| | | } |
| | | |
| | | // 重置序号 |
| | | protocolHelper.resetSequenceNumber(); |
| | | |
| | | // 初始化时间戳 |
| | | baseTimestamp = System.currentTimeMillis(); |
| | | lastIFrameTime = 0; |
| | | lastFrameTime = 0; |
| | | |
| | | Log.d(TAG, "Socket initialized successfully"); |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * 开始传输H264文件 |
| | | * @param filePath H264文件路径 |
| | | */ |
| | | public void transmitFile(String filePath) { |
| | | if (isRunning.get()) { |
| | | Log.w(TAG, "Transmitter is already running"); |
| | | return; |
| | | } |
| | | |
| | | File file = new File(filePath); |
| | | if (!file.exists() || !file.isFile()) { |
| | | Log.e(TAG, "File does not exist: " + filePath); |
| | | if (progressCallback != null) { |
| | | progressCallback.onError("File does not exist: " + filePath); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | isRunning.set(true); |
| | | |
| | | // 启动传输线程 |
| | | transmitThread = new Thread(new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | transmitFileInternal(file); |
| | | } |
| | | }); |
| | | transmitThread.start(); |
| | | |
| | | Log.d(TAG, "Started transmitting file: " + filePath); |
| | | } |
| | | |
| | | /** |
| | | * 传输文件的内部实现 |
| | | */ |
| | | private void transmitFileInternal(File file) { |
| | | FileInputStream fis = null; |
| | | int frameCount = 0; |
| | | |
| | | try { |
| | | fis = new FileInputStream(file); |
| | | |
| | | // 读取整个文件到内存 |
| | | // 注意:对于大文件,可以改为流式读取,但为简化实现,这里使用一次性读取 |
| | | long fileSize = file.length(); |
| | | if (fileSize > Integer.MAX_VALUE) { |
| | | throw new IOException("File too large: " + fileSize + " bytes"); |
| | | } |
| | | |
| | | byte[] fileData = new byte[(int) fileSize]; |
| | | int bytesRead = 0; |
| | | int totalRead = 0; |
| | | while (totalRead < fileData.length && (bytesRead = fis.read(fileData, totalRead, fileData.length - totalRead)) > 0) { |
| | | totalRead += bytesRead; |
| | | } |
| | | |
| | | if (totalRead != fileData.length) { |
| | | Log.w(TAG, "File read incomplete, expected: " + fileData.length + ", actual: " + totalRead); |
| | | } |
| | | |
| | | Log.d(TAG, "File read complete, size: " + fileData.length + " bytes"); |
| | | |
| | | // 按帧解析并传输(一个帧包含从一个起始码到下一个起始码之间的所有数据,包括起始码) |
| | | int offset = 0; |
| | | while (offset < fileData.length && isRunning.get()) { |
| | | // 查找下一个帧的起始位置 |
| | | int nextFrameStart = findNextFrameStart(fileData, offset); |
| | | if (nextFrameStart < 0) { |
| | | // 没有找到下一个起始码,当前offset到文件末尾是一个完整的帧 |
| | | if (offset < fileData.length) { |
| | | byte[] frameData = Arrays.copyOfRange(fileData, offset, fileData.length); |
| | | transmitFrame(frameData, frameCount); |
| | | frameCount++; |
| | | |
| | | // 通知进度 |
| | | if (progressCallback != null) { |
| | | progressCallback.onProgress(frameCount, -1); |
| | | } |
| | | } |
| | | break; |
| | | } |
| | | |
| | | // 提取当前帧数据(包含起始码) |
| | | if (nextFrameStart > offset) { |
| | | byte[] frameData = Arrays.copyOfRange(fileData, offset, nextFrameStart); |
| | | transmitFrame(frameData, frameCount); |
| | | frameCount++; |
| | | |
| | | // 通知进度 |
| | | if (progressCallback != null) { |
| | | progressCallback.onProgress(frameCount, -1); |
| | | } |
| | | } |
| | | |
| | | // 移动到下一个帧的起始位置 |
| | | offset = nextFrameStart; |
| | | } |
| | | |
| | | Log.d(TAG, "Transmission complete, total frames: " + frameCount); |
| | | |
| | | if (progressCallback != null) { |
| | | progressCallback.onComplete(); |
| | | } |
| | | |
| | | } catch (IOException e) { |
| | | Log.e(TAG, "Error transmitting file", e); |
| | | if (progressCallback != null) { |
| | | progressCallback.onError("IO Error: " + e.getMessage()); |
| | | } |
| | | } catch (Exception e) { |
| | | Log.e(TAG, "Unexpected error during transmission", e); |
| | | if (progressCallback != null) { |
| | | progressCallback.onError("Error: " + e.getMessage()); |
| | | } |
| | | } finally { |
| | | if (fis != null) { |
| | | try { |
| | | fis.close(); |
| | | } catch (IOException e) { |
| | | Log.e(TAG, "Error closing file", e); |
| | | } |
| | | } |
| | | isRunning.set(false); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 查找下一个帧的起始位置(下一个起始码的位置) |
| | | * @param data 文件数据 |
| | | * @param currentFrameStart 当前帧的起始位置(起始码位置) |
| | | * @return 下一个帧的起始码位置,如果未找到返回-1 |
| | | */ |
| | | private int findNextFrameStart(byte[] data, int currentFrameStart) { |
| | | if (currentFrameStart >= data.length) { |
| | | return -1; |
| | | } |
| | | |
| | | // 跳过当前帧的起始码 |
| | | int offset = currentFrameStart; |
| | | if (isStartCodeAt(data, offset)) { |
| | | offset += getStartCodeLength(data, offset); |
| | | } |
| | | |
| | | // 查找下一个起始码 |
| | | for (int i = offset; i < data.length - 3; i++) { |
| | | if (isStartCodeAt(data, i)) { |
| | | return i; |
| | | } |
| | | } |
| | | |
| | | return -1; |
| | | } |
| | | |
| | | /** |
| | | * 检查指定位置是否为起始码 |
| | | */ |
| | | private boolean isStartCodeAt(byte[] data, int offset) { |
| | | if (offset + 3 > data.length) { |
| | | return false; |
| | | } |
| | | |
| | | // 检查4字节起始码 |
| | | if (offset + 4 <= data.length) { |
| | | if (data[offset] == 0x00 && data[offset + 1] == 0x00 && |
| | | data[offset + 2] == 0x00 && data[offset + 3] == 0x01) { |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | // 检查3字节起始码(确保前面不是0x00) |
| | | if (offset == 0 || data[offset - 1] != 0x00) { |
| | | if (data[offset] == 0x00 && data[offset + 1] == 0x00 && |
| | | data[offset + 2] == 0x01) { |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 获取起始码长度 |
| | | */ |
| | | private int getStartCodeLength(byte[] data, int offset) { |
| | | if (offset + 4 <= data.length && |
| | | data[offset] == 0x00 && data[offset + 1] == 0x00 && |
| | | data[offset + 2] == 0x00 && data[offset + 3] == 0x01) { |
| | | return 4; |
| | | } |
| | | return 3; |
| | | } |
| | | |
| | | /** |
| | | * 判断帧数据是否为I帧(IDR) |
| | | * @param frameData 帧数据(包含起始码) |
| | | * @return 是否为I帧 |
| | | */ |
| | | private boolean isIFrame(byte[] frameData) { |
| | | if (frameData == null || frameData.length < 5) { |
| | | return false; |
| | | } |
| | | |
| | | // 查找帧中所有的NAL单元,检查是否包含IDR帧 |
| | | int offset = 0; |
| | | boolean hasSpsPps = false; |
| | | |
| | | while (offset < frameData.length - 3) { |
| | | // 检查当前位置是否为起始码 |
| | | if (isStartCodeAt(frameData, offset)) { |
| | | // 跳过起始码 |
| | | int startCodeLen = getStartCodeLength(frameData, offset); |
| | | int nalStart = offset + startCodeLen; |
| | | |
| | | if (nalStart < frameData.length) { |
| | | // 获取NAL类型(第一个字节的低5位) |
| | | int nalType = frameData[nalStart] & 0x1F; |
| | | |
| | | // NAL类型5 = IDR (Instantaneous Decoder Refresh) 关键帧 |
| | | if (nalType == 5) { |
| | | return true; // 找到IDR帧,确定是I帧 |
| | | } |
| | | |
| | | // NAL类型7 = SPS, 类型8 = PPS |
| | | if (nalType == 7 || nalType == 8) { |
| | | hasSpsPps = true; |
| | | } |
| | | } |
| | | |
| | | // 移动到下一个可能的位置继续查找 |
| | | offset = nalStart + 1; |
| | | } else { |
| | | offset++; |
| | | } |
| | | } |
| | | |
| | | // 如果没有找到IDR,但包含SPS/PPS,也认为是关键帧相关的数据 |
| | | // (有些编码器可能将SPS/PPS单独作为一个"帧"发送) |
| | | return hasSpsPps; |
| | | } |
| | | |
| | | /** |
| | | * 传输一个完整的帧数据(类似H264Encoder的方式) |
| | | * @param frameData 帧数据(包含起始码,与H264Encoder输出的格式一致) |
| | | * @param frameIndex 帧序号 |
| | | */ |
| | | private void transmitFrame(byte[] frameData, int frameIndex) { |
| | | if (frameData == null || frameData.length == 0) { |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | // 判断帧类型 |
| | | boolean isKeyFrame = isIFrame(frameData); |
| | | |
| | | // 计算时间戳 |
| | | long timestamp = baseTimestamp + (frameIndex * frameInterval); |
| | | |
| | | // 计算时间间隔 |
| | | long lastIFrameInterval = (lastIFrameTime > 0) ? (timestamp - lastIFrameTime) : 0; |
| | | long lastFrameInterval = (lastFrameTime > 0) ? (timestamp - lastFrameTime) : frameInterval; |
| | | |
| | | if (isKeyFrame) { |
| | | lastIFrameTime = timestamp; |
| | | } |
| | | lastFrameTime = timestamp; |
| | | |
| | | // 判断帧类型(用于协议) |
| | | int dataType = isKeyFrame ? JT1076ProtocolHelper.DATA_TYPE_I_FRAME : |
| | | JT1076ProtocolHelper.DATA_TYPE_P_FRAME; |
| | | |
| | | // 分包发送(如果数据超过最大包大小) |
| | | int offset = 0; |
| | | int totalPackets = (int) Math.ceil((double) frameData.length / JT1076ProtocolHelper.MAX_PACKET_SIZE); |
| | | |
| | | for (int i = 0; i < totalPackets; i++) { |
| | | int packetDataSize = Math.min(JT1076ProtocolHelper.MAX_PACKET_SIZE, frameData.length - offset); |
| | | byte[] packetData = Arrays.copyOfRange(frameData, 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; |
| | | } |
| | | |
| | | // 控制发送速率(模拟帧率) |
| | | if (frameInterval > 0) { |
| | | try { |
| | | Thread.sleep(frameInterval); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | Log.d(TAG, "Transmission interrupted"); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | } catch (Exception e) { |
| | | Log.e(TAG, "Error transmitting frame", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 停止传输 |
| | | */ |
| | | public void stop() { |
| | | if (!isRunning.get()) { |
| | | return; |
| | | } |
| | | |
| | | isRunning.set(false); |
| | | |
| | | // 等待传输线程结束 |
| | | if (transmitThread != null) { |
| | | try { |
| | | transmitThread.join(2000); |
| | | } catch (InterruptedException e) { |
| | | Log.e(TAG, "Wait transmit thread error", e); |
| | | } |
| | | } |
| | | |
| | | // 关闭Socket |
| | | if (protocolHelper != null) { |
| | | protocolHelper.closeSocket(); |
| | | } |
| | | |
| | | Log.d(TAG, "H264 file transmitter stopped"); |
| | | } |
| | | |
| | | /** |
| | | * 释放资源 |
| | | */ |
| | | public void release() { |
| | | stop(); |
| | | } |
| | | } |
| | | |