| | |
| | | int framerate = config != null ? config.framerate : DEFAULT_FRAME_RATE; |
| | | h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE); |
| | | |
| | | long timeFile = System.currentTimeMillis(); |
| | | long timeFile = System.currentTimeMillis()/1000*1000;//Date是秒,所以为了跟下发的Date starttime一致,此处除以1000 秒 |
| | | SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss"); |
| | | String str = bcdFormat.format(timeFile); |
| | | Timber.i("文件名:%s", str); |
| | |
| | | int height = DEFAULT_HEIGHT; |
| | | int framerate = DEFAULT_FRAME_RATE; |
| | | h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE); |
| | | |
| | | // 禁用文件输出 |
| | | h264Encoder.setEnableFileOutput(false); |
| | | |
| | | long timeFile = System.currentTimeMillis()/1000*1000; |
| | | SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss"); |
| | | String str = bcdFormat.format(timeFile); |
| | | Timber.i("startNetworkEncode 文件名:%s", str); |
| | | // 设置输出文件 |
| | | String fileName = "h264_" + timeFile+ ".h264"; |
| | | File outputFile = new File(outputFileDirectory, fileName); |
| | | h264Encoder.setOutputFile(outputFile.getAbsolutePath()); |
| | | h264Encoder.setEnableFileOutput(true); // 启用文件输出 |
| | | |
| | | |
| | | // 启用网络传输并设置服务器地址 |
| | | h264Encoder.setEnableNetworkTransmission(true); |
| | |
| | | // 编码参数 |
| | | private int width = 640; |
| | | private int height = 480; |
| | | private int frameRate = 25; |
| | | private int frameRate = 15; |
| | | private int bitrate = 2000000; // 2Mbps |
| | | private int iFrameInterval = 1; // I帧间隔(秒) |
| | | private int iFrameInterval = 2; // I帧间隔(秒) |
| | | |
| | | // JT/T 1076-2016 协议工具类 |
| | | private JT1076ProtocolHelper protocolHelper; |
| | |
| | | |
| | | // 网络传输控制 |
| | | private boolean enableNetworkTransmission = true; // 是否启用TCP/UDP网络传输 |
| | | |
| | | // SPS/PPS 缓存(用于网络传输) |
| | | private byte[] spsBuffer = null; // SPS 缓存 |
| | | private byte[] ppsBuffer = null; // PPS 缓存 |
| | | |
| | | // 编码回调 |
| | | public interface OnFrameEncodedCallback { |
| | |
| | | * @param enable true表示启用文件输出,false表示禁用 |
| | | */ |
| | | public void setEnableFileOutput(boolean enable) { |
| | | Timber.d("开启h264文件输出"); |
| | | this.enableFileOutput = enable; |
| | | } |
| | | |
| | |
| | | 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"); |
| | | } |
| | |
| | | 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]; |
| | | byte[] nalUnit = new byte[bufferInfo.size]; |
| | | outputBuffer.position(bufferInfo.offset); |
| | | outputBuffer.get(encodedData, 0, bufferInfo.size); |
| | | outputBuffer.get(nalUnit, 0, bufferInfo.size); |
| | | |
| | | // 写入文件 |
| | | writeToFile(encodedData, isKeyFrame); |
| | | // 解析并处理 NAL 单元 |
| | | processNalUnit(nalUnit, timestamp); |
| | | |
| | | // 发送编码数据 |
| | | sendEncodedData(encodedData, timestamp, isKeyFrame); |
| | | // 写入文件(保持原有逻辑) |
| | | boolean isKeyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0; |
| | | writeToFile(nalUnit, isKeyFrame); |
| | | |
| | | // 回调 |
| | | if (callback != null) { |
| | | callback.onFrameEncoded(encodedData, isKeyFrame); |
| | | callback.onFrameEncoded(nalUnit, isKeyFrame); |
| | | } |
| | | } |
| | | |
| | |
| | | } 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | // 关闭文件输出 |
| | | closeFileOutput(); |
| | | |
| | | // 清理 SPS/PPS 缓存 |
| | | spsBuffer = null; |
| | | ppsBuffer = null; |
| | | |
| | | Timber.d("H264 encoder stopped"); |
| | | } |
| | | |
| | |
| | | private int frameRate = 25; // 帧率,用于计算时间戳间隔 |
| | | private long frameInterval = 1000 / 25; // 帧间隔(毫秒) |
| | | |
| | | // SPS/PPS缓存 |
| | | private byte[] spsBuffer; |
| | | private byte[] ppsBuffer; |
| | | |
| | | // 时间戳管理 |
| | | private long lastIFrameTime = 0; // 上一个I帧时间 |
| | | private long lastFrameTime = 0; // 上一帧时间 |
| | |
| | | baseTimestamp = System.currentTimeMillis(); |
| | | lastIFrameTime = 0; |
| | | lastFrameTime = 0; |
| | | spsBuffer = null; |
| | | ppsBuffer = null; |
| | | |
| | | Timber.d("Socket initialized successfully"); |
| | | return true; |
| | |
| | | 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; |
| | | } |
| | | processNalUnits(frameData, timestamp, lastIFrameInterval, lastFrameInterval); |
| | | |
| | | // 控制发送速率(模拟帧率) |
| | | if (frameInterval > 0) { |
| | |
| | | protocolHelper.closeSocket(); |
| | | } |
| | | |
| | | spsBuffer = null; |
| | | ppsBuffer = null; |
| | | |
| | | Timber.d("H264 file transmitter stopped"); |
| | | } |
| | | |
| | | /** |
| | | * 解析帧中的NAL单元并根据类型处理 |
| | | */ |
| | | private void processNalUnits(byte[] frameData, long timestamp, |
| | | long lastIFrameInterval, long lastFrameInterval) { |
| | | if (frameData == null || frameData.length == 0) { |
| | | return; |
| | | } |
| | | |
| | | boolean nalProcessed = false; |
| | | int offset = 0; |
| | | while (offset < frameData.length) { |
| | | int startCodePos = findStartCode(frameData, offset); |
| | | if (startCodePos < 0) { |
| | | break; |
| | | } |
| | | |
| | | int startCodeLen = getStartCodeLength(frameData, startCodePos); |
| | | int nalDataStart = startCodePos + startCodeLen; |
| | | if (nalDataStart >= frameData.length) { |
| | | break; |
| | | } |
| | | |
| | | int nextStart = findStartCode(frameData, nalDataStart); |
| | | int nalEnd = (nextStart == -1) ? frameData.length : nextStart; |
| | | int nalLength = nalEnd - startCodePos; |
| | | if (nalLength <= startCodeLen) { |
| | | break; |
| | | } |
| | | |
| | | byte[] nalUnit = Arrays.copyOfRange(frameData, startCodePos, nalEnd); |
| | | int nalType = nalUnit[startCodeLen] & 0x1F; |
| | | |
| | | handleNalUnit(nalUnit, nalType, timestamp, lastIFrameInterval, lastFrameInterval); |
| | | nalProcessed = true; |
| | | offset = nalEnd; |
| | | } |
| | | |
| | | if (!nalProcessed) { |
| | | // 没有解析出NAL单元时,直接按P帧发送 |
| | | sendFramePayload(frameData, timestamp, false, lastIFrameInterval, lastFrameInterval); |
| | | } |
| | | } |
| | | |
| | | private void handleNalUnit(byte[] nalUnit, int nalType, long timestamp, |
| | | long lastIFrameInterval, long lastFrameInterval) { |
| | | switch (nalType) { |
| | | case 7: // SPS |
| | | spsBuffer = nalUnit.clone(); |
| | | Timber.d("Cached SPS, size: %d", spsBuffer.length); |
| | | break; |
| | | case 8: // PPS |
| | | ppsBuffer = nalUnit.clone(); |
| | | Timber.d("Cached PPS, size: %d", ppsBuffer.length); |
| | | break; |
| | | case 5: // IDR |
| | | if (spsBuffer != null && ppsBuffer != null) { |
| | | byte[] combined = combineFrameData(spsBuffer, ppsBuffer, nalUnit); |
| | | sendFramePayload(combined, timestamp, true, lastIFrameInterval, lastFrameInterval); |
| | | } else { |
| | | Timber.w("IDR frame without SPS/PPS cache, sending raw IDR"); |
| | | sendFramePayload(nalUnit, timestamp, true, lastIFrameInterval, lastFrameInterval); |
| | | } |
| | | break; |
| | | case 1: // P frame |
| | | sendFramePayload(nalUnit, timestamp, false, lastIFrameInterval, lastFrameInterval); |
| | | break; |
| | | default: |
| | | Timber.d("Forwarding NAL type %d, size: %d", nalType, nalUnit.length); |
| | | sendFramePayload(nalUnit, timestamp, false, lastIFrameInterval, lastFrameInterval); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | private void sendFramePayload(byte[] payload, long timestamp, boolean isKeyFrame, |
| | | long lastIFrameInterval, long lastFrameInterval) { |
| | | if (payload == null || payload.length == 0) { |
| | | return; |
| | | } |
| | | |
| | | int dataType = isKeyFrame ? JT1076ProtocolHelper.DATA_TYPE_I_FRAME : |
| | | JT1076ProtocolHelper.DATA_TYPE_P_FRAME; |
| | | |
| | | int offset = 0; |
| | | int totalPackets = (int) Math.ceil((double) payload.length / JT1076ProtocolHelper.MAX_PACKET_SIZE); |
| | | for (int i = 0; i < totalPackets; i++) { |
| | | int packetDataSize = Math.min(JT1076ProtocolHelper.MAX_PACKET_SIZE, payload.length - offset); |
| | | byte[] packetData = Arrays.copyOfRange(payload, 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; |
| | | } |
| | | |
| | | byte[] rtpPacket = protocolHelper.createVideoRtpPacket( |
| | | packetData, timestamp, dataType, packetMark, |
| | | lastIFrameInterval, lastFrameInterval); |
| | | protocolHelper.sendPacket(rtpPacket); |
| | | offset += packetDataSize; |
| | | } |
| | | } |
| | | |
| | | private int findStartCode(byte[] data, int startPos) { |
| | | if (data == null || startPos >= data.length - 3) { |
| | | return -1; |
| | | } |
| | | for (int i = startPos; i < data.length - 3; i++) { |
| | | if (isStartCodeAt(data, i)) { |
| | | return i; |
| | | } |
| | | } |
| | | // 处理剩余不足4字节但可能是3字节起始码的情况 |
| | | for (int i = Math.max(startPos, data.length - 3); i < data.length - 2; i++) { |
| | | if (isStartCodeAt(data, i)) { |
| | | return i; |
| | | } |
| | | } |
| | | return -1; |
| | | } |
| | | |
| | | private byte[] combineFrameData(byte[] sps, byte[] pps, byte[] idr) { |
| | | int totalLength = (sps != null ? sps.length : 0) + |
| | | (pps != null ? pps.length : 0) + |
| | | (idr != null ? idr.length : 0); |
| | | byte[] combined = new byte[totalLength]; |
| | | int offset = 0; |
| | | if (sps != null) { |
| | | System.arraycopy(sps, 0, combined, offset, sps.length); |
| | | offset += sps.length; |
| | | } |
| | | if (pps != null) { |
| | | System.arraycopy(pps, 0, combined, offset, pps.length); |
| | | offset += pps.length; |
| | | } |
| | | if (idr != null) { |
| | | System.arraycopy(idr, 0, combined, offset, idr.length); |
| | | } |
| | | // Timber.d("Combined SPS/PPS/IDR payload, total: %d", totalLength); |
| | | return combined; |
| | | } |
| | | |
| | | /** |
| | | * 释放资源 |
| | | */ |
| | | public void release() { |
| | |
| | | public static final int PACKET_MARK_MIDDLE = 0x03; // 中间包 |
| | | |
| | | // RTP负载类型 |
| | | public static final int RTP_PAYLOAD_TYPE_VIDEO = 96; // 视频负载类型 |
| | | public static final int RTP_PAYLOAD_TYPE_VIDEO = 98; // 视频负载类型 |
| | | public static final int RTP_PAYLOAD_TYPE_AUDIO = 97; // 音频负载类型 |
| | | |
| | | // 传输协议类型 |
| | |
| | | public void setServerAddress(String ip, int port) { |
| | | this.serverIp = ip; |
| | | this.serverPort = port; |
| | | Timber.w("设置IP="+ip+"_port:"+port); |
| | | // 如果TCP客户端已存在,更新地址 |
| | | if (tcpClient != null) { |
| | | tcpClient.setServerAddress(ip, port); |
| | |
| | | try { |
| | | // 将字节数组包装为ByteBuf |
| | | ByteBuf buffer = Unpooled.wrappedBuffer(packet); |
| | | String str = BytesUtils.bytesToHexString( BytesUtils.subArray(buffer.array(),0,30)); |
| | | Timber.i( "Send TCP packet:"+ str); |
| | | /* byte[] dataBytes = buffer.array(); |
| | | int len = 0; |
| | | if (dataBytes.length<100){ |
| | | len = dataBytes.length; |
| | | }else { |
| | | len = 100; |
| | | } |
| | | String str = BytesUtils.bytesToHexString( BytesUtils.subArray(dataBytes,0,len)); |
| | | Timber.i( "Send TCP packet:"+ str);*/ |
| | | // 异步写入 |
| | | ChannelFuture future = channel.writeAndFlush(buffer); |
| | | |
| | |
| | | package com.anyun.h264 |
| | | |
| | | import android.os.Bundle |
| | | import timber.log.Timber |
| | | import android.util.Log |
| | | import androidx.activity.ComponentActivity |
| | | import androidx.activity.compose.setContent |
| | | import androidx.activity.enableEdgeToEdge |
| | |
| | | import androidx.compose.ui.Modifier |
| | | import androidx.compose.ui.unit.dp |
| | | import com.anyun.h264.ui.theme.MyApplicationTheme |
| | | import timber.log.Timber |
| | | import java.io.File |
| | | |
| | | class MainActivity : ComponentActivity() { |
| | | private var h264Encoder: H264Encoder? = null |
| | | |
| | | private var transmitter: H264FileTransmitter? = null |
| | | companion object{ |
| | | const val TAG ="MainActivity" |
| | | } |
| | | override fun onCreate(savedInstanceState: Bundle?) { |
| | | super.onCreate(savedInstanceState) |
| | | enableEdgeToEdge() |
| | | |
| | | |
| | | setContent { |
| | | var isRunning by remember { mutableStateOf(false) } |
| | | |
| | | |
| | | MyApplicationTheme { |
| | | Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> |
| | | MainScreen( |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | override fun onDestroy() { |
| | | super.onDestroy() |
| | | stopH264Encoder() |
| | | } |
| | | |
| | | |
| | | private fun startFileTransmitter():Boolean { |
| | | |
| | | if (transmitter != null) { |
| | | Timber.w("H264Encoder is already running") |
| | | return false |
| | | } |
| | | |
| | | |
| | | |
| | | try { |
| | | transmitter = H264FileTransmitter() |
| | | Log.i(TAG,"startFileTransmitter") |
| | | transmitter?.setServerAddress("192.168.16.138", 1078) |
| | | transmitter?.setProtocolType(JT1076ProtocolHelper.PROTOCOL_TYPE_TCP) // 或 PROTOCOL_TYPE_UDP |
| | | |
| | | |
| | | // 设置协议参数 |
| | | transmitter?.setProtocolParams("013120122580", 1.toByte()) |
| | | |
| | | |
| | | // 设置帧率(用于计算时间戳间隔) |
| | | transmitter?.setFrameRate(25) |
| | | |
| | | |
| | | // 初始化Socket |
| | | if (transmitter?.initialize()==true) { |
| | | // 开始传输文件 |
| | | transmitter?.transmitFile("/storage/emulated/0/Android/data/com.anyun.h264/files/h264_1764574451071.h264") |
| | | return true |
| | | }else{ |
| | | return false |
| | | } |
| | | } catch (e: Exception) { |
| | | Timber.e(e, "Failed to start H264Encoder") |
| | | transmitter = null |
| | | return false |
| | | } |
| | | } |
| | | |
| | | private fun startH264Encoder(): Boolean { |
| | | if (h264Encoder != null) { |
| | | Timber.w("H264Encoder is already running") |
| | | return false |
| | | } |
| | | |
| | | |
| | | try { |
| | | // 创建编码器 |
| | | h264Encoder = H264Encoder() |
| | | |
| | | |
| | | // 设置编码参数 |
| | | h264Encoder?.setEncoderParams(640, 480, 25, 2000000) |
| | | |
| | | |
| | | // 设置输出文件(可选) |
| | | val outputFile = File(getExternalFilesDir(null), "test.h264") |
| | | val outputFile = File(getExternalFilesDir(null), "test2.h264") |
| | | h264Encoder?.setOutputFile(outputFile.absolutePath) |
| | | h264Encoder?.setEnableFileOutput(true) // 启用文件输出 |
| | | |
| | | h264Encoder?.setEnableFileOutput(false) // 启用文件输出 |
| | | |
| | | // 设置UDP服务器地址(可选) |
| | | // h264Encoder?.setServerAddress("58.48.93.67", 11935) |
| | | h264Encoder?.setEnableNetworkTransmission(false) |
| | | h264Encoder?.setServerAddress("192.168.16.12", 11935) |
| | | h264Encoder?.setProtocolParams("013120122580", 1) |
| | | |
| | | h264Encoder?.setEnableNetworkTransmission(true) |
| | | h264Encoder?.setServerAddress("192.168.16.138", 1078) |
| | | h264Encoder?.setProtocolParams("013120122580", 1) |
| | | |
| | | // 初始化并启动 |
| | | val cameraIdRange = intArrayOf(1, 2) |
| | | val resolution = intArrayOf(640, 480) |
| | | |
| | | |
| | | if (h264Encoder?.initialize(cameraIdRange, null, resolution, false) == true) { |
| | | h264Encoder?.start() |
| | | Timber.d("H264Encoder started successfully") |
| | |
| | | return false |
| | | } |
| | | } |
| | | |
| | | |
| | | private fun stopH264Encoder() { |
| | | h264Encoder?.let { encoder -> |
| | | try { |
| | |
| | | text = "H264 编码器", |
| | | style = MaterialTheme.typography.headlineMedium |
| | | ) |
| | | |
| | | |
| | | Spacer(modifier = Modifier.height(32.dp)) |
| | | |
| | | |
| | | Button( |
| | | onClick = onStartH264Click, |
| | | enabled = !isRunning, |
| | |
| | | ) { |
| | | Text("启动 H264") |
| | | } |
| | | |
| | | |
| | | Spacer(modifier = Modifier.height(16.dp)) |
| | | |
| | | |
| | | Button( |
| | | onClick = onStopH264Click, |
| | | enabled = isRunning, |
| | |
| | | ) { |
| | | Text("停止 H264") |
| | | } |
| | | |
| | | |
| | | Spacer(modifier = Modifier.height(32.dp)) |
| | | |
| | | |
| | | if (isRunning) { |
| | | Text( |
| | | text = "编码器运行中...", |