From 3d0d377e1e8cfa2c47f3c4f32b68e30797e38d2a Mon Sep 17 00:00:00 2001
From: Dana <Dana_Lee1016@126.com>
Date: 星期六, 29 十一月 2025 15:28:50 +0800
Subject: [PATCH] 1.h264 aac 这些
---
settings.gradle | 1
app/src/main/java/com/anyun/h264/MainActivity.kt | 158 +++++
app/src/main/AndroidManifest.xml | 8
app/src/main/java/com/anyun/h264/H264Encoder.java | 606 ++++++++++++++++++++++++
app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java | 303 ++++++++++++
gradle/wrapper/gradle-wrapper.properties | 4
app/src/main/java/com/anyun/h264/AACEncoder.java | 361 ++++++++++++++
7 files changed, 1,419 insertions(+), 22 deletions(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e013846..713fae5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ android:sharedUserId="android.uid.system"
xmlns:tools="http://schemas.android.com/tools">
+ <!-- 缃戠粶鏉冮檺锛堢敤浜嶶DP涓婁紶锛� -->
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+
+ <!-- USB鎽勫儚澶存潈闄� -->
+ <uses-feature android:name="android.hardware.usb.host" />
+
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
diff --git a/app/src/main/java/com/anyun/h264/AACEncoder.java b/app/src/main/java/com/anyun/h264/AACEncoder.java
new file mode 100644
index 0000000..0a07d82
--- /dev/null
+++ b/app/src/main/java/com/anyun/h264/AACEncoder.java
@@ -0,0 +1,361 @@
+package com.anyun.h264;
+
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaFormat;
+import android.util.Log;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * AAC闊抽缂栫爜鍣�
+ * 閲囬泦闊抽鏁版嵁锛岃繘琛孉AC缂栫爜锛屽苟閫氳繃UDP鎸塉T/T 1076-2016鍗忚涓婁紶
+ */
+public class AACEncoder {
+ private static final String TAG = "AACEncoder";
+
+ private MediaCodec encoder;
+ private AudioRecord audioRecord;
+ private Thread encodeThread;
+ private AtomicBoolean isRunning = new AtomicBoolean(false);
+
+ // 闊抽鍙傛暟
+ private int sampleRate = 16000; // 閲囨牱鐜�
+ private int channelCount = 1; // 澹伴亾鏁帮紙1=鍗曞0閬擄紝2=绔嬩綋澹帮級
+ private int bitrate = 64000; // 姣旂壒鐜�
+ private int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
+
+ // JT/T 1076-2016 鍗忚宸ュ叿绫�
+ private JT1076ProtocolHelper protocolHelper;
+ private long lastFrameTime = 0; // 涓婁竴甯ф椂闂�
+
+ // 缂栫爜鍥炶皟
+ public interface OnFrameEncodedCallback {
+ void onFrameEncoded(byte[] data);
+ }
+ private OnFrameEncodedCallback callback;
+
+ public AACEncoder() {
+ this.protocolHelper = new JT1076ProtocolHelper();
+ }
+
+ /**
+ * 璁剧疆闊抽鍙傛暟
+ */
+ public void setAudioParams(int sampleRate, int channelCount, int bitrate) {
+ this.sampleRate = sampleRate;
+ this.channelCount = channelCount;
+ 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;
+ }
+
+ /**
+ * 鍒濆鍖栭煶棰戝綍鍒跺拰缂栫爜鍣�
+ */
+ public boolean initialize() {
+ try {
+ // 1. 鍒濆鍖朅udioRecord
+ int channelConfig = channelCount == 1 ?
+ AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO;
+ int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
+
+ if (bufferSize == AudioRecord.ERROR_BAD_VALUE || bufferSize == AudioRecord.ERROR) {
+ Log.e(TAG, "Invalid audio parameters");
+ return false;
+ }
+
+ // 浣跨敤鏇村ぇ鐨勭紦鍐插尯浠ラ伩鍏嶆瑺杞�
+ bufferSize *= 4;
+
+ audioRecord = new AudioRecord(
+ android.media.MediaRecorder.AudioSource.MIC,
+ sampleRate,
+ channelConfig,
+ audioFormat,
+ bufferSize
+ );
+
+ if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
+ Log.e(TAG, "AudioRecord initialization failed");
+ return false;
+ }
+
+ Log.d(TAG, "AudioRecord initialized: sampleRate=" + sampleRate +
+ ", channels=" + channelCount + ", bufferSize=" + bufferSize);
+
+ // 2. 鍒濆鍖朅AC缂栫爜鍣�
+ initEncoder();
+
+ // 3. 鍒濆鍖朥DP Socket
+ if (!protocolHelper.initializeUdpSocket()) {
+ return false;
+ }
+
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Initialize failed", e);
+ return false;
+ }
+ }
+
+ /**
+ * 鍒濆鍖朅AC缂栫爜鍣�
+ */
+ private void initEncoder() throws IOException {
+ MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
+ sampleRate, channelCount);
+ format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
+ format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
+ format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 4096);
+
+ encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
+ encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+ encoder.start();
+
+ Log.d(TAG, "AAC encoder initialized");
+ }
+
+ /**
+ * 寮�濮嬬紪鐮�
+ */
+ public void start() {
+ if (isRunning.get()) {
+ Log.w(TAG, "Encoder is already running");
+ return;
+ }
+
+ if (audioRecord == null || encoder == null) {
+ Log.e(TAG, "Encoder not initialized");
+ return;
+ }
+
+ isRunning.set(true);
+
+ // 寮�濮嬪綍闊�
+ audioRecord.startRecording();
+
+ // 鍚姩缂栫爜绾跨▼
+ encodeThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ encodeLoop();
+ }
+ });
+ encodeThread.start();
+
+ Log.d(TAG, "AAC encoder started");
+ }
+
+ /**
+ * 缂栫爜寰幆
+ */
+ private void encodeLoop() {
+ // 璇诲彇缂撳啿鍖哄ぇ灏忥紙1024涓噰鏍风偣锛�
+ int inputBufferSize = sampleRate * channelCount * 2 / 25; // 40ms鐨勯煶棰戞暟鎹�
+ byte[] pcmBuffer = new byte[inputBufferSize];
+
+ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+
+ while (isRunning.get()) {
+ try {
+ // 浠嶢udioRecord璇诲彇PCM鏁版嵁
+ int bytesRead = audioRecord.read(pcmBuffer, 0, pcmBuffer.length);
+
+ if (bytesRead < 0) {
+ Log.w(TAG, "AudioRecord read error: " + bytesRead);
+ Thread.sleep(10);
+ continue;
+ }
+
+ // 灏哖CM鏁版嵁閫佸叆缂栫爜鍣�
+ long timestamp = System.currentTimeMillis();
+ encodeFrame(pcmBuffer, bytesRead, timestamp, bufferInfo);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Encode loop error", e);
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException ie) {
+ break;
+ }
+ }
+ }
+
+ Log.d(TAG, "Encode loop exited");
+ }
+
+ /**
+ * 缂栫爜涓�甯ф暟鎹�
+ */
+ private void encodeFrame(byte[] pcmData, int dataSize, 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(pcmData, 0, dataSize);
+ encoder.queueInputBuffer(inputBufferIndex, 0, dataSize, timestamp * 1000, 0);
+ }
+ }
+
+ // 鑾峰彇杈撳嚭鏁版嵁
+ int outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0);
+ while (outputBufferIndex >= 0) {
+ ByteBuffer outputBuffer = encoder.getOutputBuffer(outputBufferIndex);
+ if (outputBuffer != null && bufferInfo.size > 0) {
+ // 澶嶅埗缂栫爜鏁版嵁
+ byte[] encodedData = new byte[bufferInfo.size];
+ outputBuffer.position(bufferInfo.offset);
+ outputBuffer.get(encodedData, 0, bufferInfo.size);
+
+ // 鍙戦�佺紪鐮佹暟鎹�
+ sendEncodedData(encodedData, timestamp);
+
+ // 鍥炶皟
+ if (callback != null) {
+ callback.onFrameEncoded(encodedData);
+ }
+ }
+
+ encoder.releaseOutputBuffer(outputBufferIndex, false);
+ outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Encode frame error", e);
+ }
+ }
+
+ /**
+ * 鍙戦�佺紪鐮佸悗鐨勬暟鎹紙鎸塉T/T 1076-2016鍗忚鎵撳寘锛�
+ */
+ private void sendEncodedData(byte[] data, long timestamp) {
+ try {
+ // 璁$畻鏃堕棿闂撮殧
+ long currentTime = System.currentTimeMillis();
+ long lastFrameInterval = (lastFrameTime > 0) ? (currentTime - lastFrameTime) : 0;
+ lastFrameTime = currentTime;
+
+ // 鍒嗗寘鍙戦�侊紙濡傛灉鏁版嵁瓒呰繃鏈�澶у寘澶у皬锛�
+ 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鍖咃紙闊抽涓嶉渶瑕丩ast I Frame Interval鍜孡ast Frame Interval瀛楁锛�
+ byte[] rtpPacket = protocolHelper.createAudioRtpPacket(
+ packetData, timestamp, JT1076ProtocolHelper.DATA_TYPE_AUDIO, packetMark);
+
+ // 鍙戦�乁DP鍖�
+ protocolHelper.sendUdpPacket(rtpPacket);
+
+ offset += packetDataSize;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Send encoded data error", e);
+ }
+ }
+
+ /**
+ * 鍋滄缂栫爜
+ */
+ public void stop() {
+ if (!isRunning.get()) {
+ return;
+ }
+
+ isRunning.set(false);
+
+ // 鍋滄褰曢煶
+ if (audioRecord != null) {
+ try {
+ audioRecord.stop();
+ } catch (Exception e) {
+ Log.e(TAG, "Stop AudioRecord error", e);
+ }
+ }
+
+ // 绛夊緟缂栫爜绾跨▼缁撴潫
+ if (encodeThread != null) {
+ try {
+ encodeThread.join(2000);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Wait encode thread error", e);
+ }
+ }
+
+ // 閲婃斁缂栫爜鍣�
+ if (encoder != null) {
+ try {
+ encoder.stop();
+ encoder.release();
+ encoder = null;
+ } catch (Exception e) {
+ Log.e(TAG, "Release encoder error", e);
+ }
+ }
+
+ // 閲婃斁AudioRecord
+ if (audioRecord != null) {
+ audioRecord.release();
+ audioRecord = null;
+ }
+
+ // 鍏抽棴UDP Socket
+ if (protocolHelper != null) {
+ protocolHelper.closeUdpSocket();
+ }
+
+ Log.d(TAG, "AAC encoder stopped");
+ }
+
+ /**
+ * 閲婃斁璧勬簮
+ */
+ public void release() {
+ stop();
+ }
+}
+
diff --git a/app/src/main/java/com/anyun/h264/H264Encoder.java b/app/src/main/java/com/anyun/h264/H264Encoder.java
new file mode 100644
index 0000000..4009033
--- /dev/null
+++ b/app/src/main/java/com/anyun/h264/H264Encoder.java
@@ -0,0 +1,606 @@
+package com.anyun.h264;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaFormat;
+import android.util.Log;
+import com.anyun.libusbcamera.UsbCamera;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * H264瑙嗛缂栫爜鍣�
+ * 浣跨敤UsbCamera鑾峰彇瑙嗛鏁版嵁锛岃繘琛孒264缂栫爜锛屽苟閫氳繃UDP鎸塉T/T 1076-2016鍗忚涓婁紶
+ *
+ * 浣跨敤绀轰緥锛�
+ * <pre>
+ * // 鍒涘缓缂栫爜鍣�
+ * H264Encoder encoder = new H264Encoder();
+ *
+ * // 璁剧疆缂栫爜鍙傛暟
+ * encoder.setEncoderParams(640, 480, 25, 2000000);
+ *
+ * // 璁剧疆杈撳嚭鏂囦欢锛堝彲閫夛紝鐢ㄤ簬淇濆瓨H264缂栫爜鏁版嵁锛屽彲鐢╒LC鎾斁楠岃瘉锛�
+ * // 浣跨敤搴旂敤澶栭儴瀛樺偍鐩綍锛堟帹鑽愶級
+ * 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);
+ *
+ * // 鍒濆鍖栧苟鍚姩
+ * int[] cameraIdRange = {0, 0};
+ * int[] resolution = {640, 480};
+ * if (encoder.initialize(cameraIdRange, "camera", resolution, false)) {
+ * encoder.start();
+ * }
+ *
+ * // 鍋滄缂栫爜
+ * encoder.stop();
+ * </pre>
+ *
+ * 鐢熸垚鐨�.h264鏂囦欢鍙互鐢╒LC鎾斁鍣ㄧ洿鎺ユ挱鏀鹃獙璇併��
+ */
+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; // 涓婁竴涓狪甯ф椂闂�
+ private long lastFrameTime = 0; // 涓婁竴甯ф椂闂�
+
+ // 鏂囦欢杈撳嚭
+ private FileOutputStream fileOutputStream;
+ private String outputFilePath;
+ private boolean enableFileOutput = false; // 鏄惁鍚敤鏂囦欢杈撳嚭
+ private boolean spsPpsWritten = false; // 鏍囪SPS/PPS鏄惁宸插啓鍏�
+
+ // 缂栫爜鍥炶皟
+ public interface OnFrameEncodedCallback {
+ void onFrameEncoded(byte[] data, boolean isKeyFrame);
+ }
+ private OnFrameEncodedCallback callback;
+
+ public H264Encoder() {
+ this.usbCamera = new UsbCamera();
+ this.protocolHelper = new JT1076ProtocolHelper();
+ }
+
+ /**
+ * 璁剧疆缂栫爜鍙傛暟
+ */
+ 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;
+ }
+
+ /**
+ * 璁剧疆杈撳嚭鏂囦欢璺緞锛堢敤浜庝繚瀛楬264缂栫爜鏁版嵁锛�
+ * @param filePath 鏂囦欢璺緞锛屼緥濡傦細"/sdcard/test.h264" 鎴栦娇鐢–ontext.getExternalFilesDir()
+ */
+ public void setOutputFile(String filePath) {
+ this.outputFilePath = filePath;
+ }
+
+ /**
+ * 璁剧疆鏄惁鍚敤鏂囦欢杈撳嚭
+ * @param enable true琛ㄧず鍚敤鏂囦欢杈撳嚭锛宖alse琛ㄧず绂佺敤
+ */
+ public void setEnableFileOutput(boolean enable) {
+ this.enableFileOutput = enable;
+ }
+
+ /**
+ * 鍒濆鍖栨憚鍍忓ご鍜岀紪鐮佸櫒
+ */
+ 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) {
+ Log.d(TAG, "prepareCamera succeeded on attempt " + (attempt + 1));
+ }
+ break;
+ } else {
+ // 澶辫触锛岃褰曟棩蹇�
+ Log.w(TAG, "prepareCamera failed on attempt " + (attempt + 1) + ": " + result);
+ if (attempt < maxRetries - 1) {
+ Log.d(TAG, "Retrying prepareCamera...");
+ }
+ }
+ }
+
+ if (result != 0) {
+ Log.e(TAG, "prepareCamera failed after " + maxRetries + " attempts: " + result);
+ return false;
+ }
+
+ // 鏇存柊瀹為檯鍒嗚鲸鐜�
+ width = actualResolution[0];
+ height = actualResolution[1];
+ Log.d(TAG, "Camera initialized with resolution: " + width + "x" + height);
+
+ // 3. 鍒濆鍖朒264缂栫爜鍣�
+ initEncoder();
+
+ // 4. 鍒濆鍖朥DP Socket
+ if (!protocolHelper.initializeUdpSocket()) {
+ return false;
+ }
+
+ // 5. 鍒濆鍖栨枃浠惰緭鍑猴紙浠呭垱寤烘枃浠讹紝SPS/PPS鍦ㄧ涓�娆¤緭鍑烘椂鍐欏叆锛�
+ if (enableFileOutput && outputFilePath != null && !outputFilePath.isEmpty()) {
+ if (!initFileOutput()) {
+ Log.w(TAG, "File output initialization failed, continuing without file output");
+ }
+ }
+
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Initialize failed", e);
+ return false;
+ }
+ }
+
+ /**
+ * 鍒濆鍖朒264缂栫爜鍣�
+ */
+ 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();
+
+ Log.d(TAG, "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()) {
+ Log.e(TAG, "Failed to create parent directory: " + parentDir.getAbsolutePath());
+ return false;
+ }
+ }
+
+ fileOutputStream = new FileOutputStream(file);
+ spsPpsWritten = false;
+
+ Log.d(TAG, "File output initialized: " + outputFilePath);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Initialize file output failed", e);
+ if (fileOutputStream != null) {
+ try {
+ fileOutputStream.close();
+ } catch (IOException ie) {
+ Log.e(TAG, "Close file output stream failed", ie);
+ }
+ fileOutputStream = null;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * 鍐欏叆SPS/PPS鍒版枃浠讹紙浠嶤SD鎴栧叧閿抚鏁版嵁涓彁鍙栵級
+ */
+ private void writeSpsPpsToFile() {
+ if (!enableFileOutput || fileOutputStream == null || spsPpsWritten) {
+ return;
+ }
+
+ try {
+ // 灏濊瘯浠庣紪鐮佸櫒杈撳嚭鏍煎紡涓幏鍙朇SD
+ MediaFormat format = encoder.getOutputFormat();
+ ByteBuffer spsBuffer = format.getByteBuffer("csd-0"); // SPS
+ ByteBuffer ppsBuffer = format.getByteBuffer("csd-1"); // PPS
+
+ if (spsBuffer != null && ppsBuffer != null) {
+ // CSD鏍煎紡閫氬父鏄疉VCC鏍煎紡锛岄渶瑕佽浆鎹负Annex-B
+ byte[] sps = new byte[spsBuffer.remaining()];
+ byte[] pps = new byte[ppsBuffer.remaining()];
+ spsBuffer.get(sps);
+ ppsBuffer.get(pps);
+
+ // 鍐欏叆SPS鍜孭PS鍒版枃浠讹紙Annex-B鏍煎紡锛�
+ byte[] nalStartCode = {0x00, 0x00, 0x00, 0x01};
+
+ // CSD鏁版嵁閫氬父鏄疉VCC鏍煎紡锛氬墠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;
+ Log.d(TAG, "SPS/PPS written to file, SPS size: " + spsLength + ", PPS size: " + ppsLength);
+ } else {
+ Log.w(TAG, "SPS/PPS not found in CSD, will extract from first key frame");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Write SPS/PPS to file error", e);
+ }
+ }
+
+ /**
+ * 寮�濮嬬紪鐮�
+ */
+ public void start() {
+ if (isRunning.get()) {
+ Log.w(TAG, "Encoder is already running");
+ return;
+ }
+
+ isRunning.set(true);
+
+ // 鍚姩缂栫爜绾跨▼
+ encodeThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ encodeLoop();
+ }
+ });
+ encodeThread.start();
+
+ Log.d(TAG, "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) {
+ Log.w(TAG, "processCamera returned: " + processResult);
+ Thread.sleep(10);
+ continue;
+ }
+
+ // 鑾峰彇RGBA鏁版嵁 (type=1 琛ㄧず鎺ㄦ祦锛岃緭鍑篩UV420P鏍煎紡)
+ usbCamera.rgba(1, yuvBuffer);
+
+ // 灏哬UV420P鏁版嵁閫佸叆缂栫爜鍣�
+ long timestamp = System.currentTimeMillis();
+ encodeFrame(yuvBuffer, timestamp, bufferInfo);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Encode loop error", e);
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ie) {
+ break;
+ }
+ }
+ }
+
+ Log.d(TAG, "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);
+ Log.i(TAG,"1111");
+ if (outputBuffer != null && bufferInfo.size > 0) {
+ // 妫�鏌ユ槸鍚︿负鍏抽敭甯�
+ boolean isKeyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
+ Log.i(TAG,"2222");
+ // 澶嶅埗缂栫爜鏁版嵁
+ 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) {
+ Log.e(TAG, "Encode frame error", e);
+ }
+ }
+
+ /**
+ * 灏嗙紪鐮佹暟鎹啓鍏264鏂囦欢
+ */
+ private void writeToFile(byte[] data, boolean isKeyFrame) {
+ if (!enableFileOutput || fileOutputStream == null) {
+ return;
+ }
+
+ try {
+ // 濡傛灉鏄涓�涓叧閿抚锛岀‘淇漇PS/PPS宸插啓鍏�
+ if (isKeyFrame && !spsPpsWritten) {
+ writeSpsPpsToFile();
+
+ // 濡傛灉浠嶤SD鑾峰彇澶辫触锛屽皾璇曚粠鍏抽敭甯ф暟鎹腑鎻愬彇
+ // MediaCodec杈撳嚭鐨勫叧閿抚閫氬父宸茬粡鍖呭惈SPS/PPS锛屼絾涓轰簡纭繚鏂囦欢瀹屾暣鎬э紝
+ // 鎴戜滑宸茬粡浠嶤SD鍐欏叆锛岃繖閲岀洿鎺ュ啓鍏ュ叧閿抚鏁版嵁鍗冲彲
+ if (!spsPpsWritten) {
+ Log.d(TAG, "SPS/PPS will be included in key frame data");
+ }
+ }
+
+ // MediaCodec杈撳嚭鐨凥264鏁版嵁宸茬粡鏄疉nnex-B鏍煎紡锛堝寘鍚�0x00000001鍒嗛殧绗︼級
+ // 鐩存帴鍐欏叆鏂囦欢鍗冲彲
+ fileOutputStream.write(data);
+ fileOutputStream.flush();
+ } catch (IOException e) {
+ Log.e(TAG, "Write to file error", e);
+ }
+ }
+
+ /**
+ * 鍙戦�佺紪鐮佸悗鐨勬暟鎹紙鎸塉T/T 1076-2016鍗忚鎵撳寘锛�
+ */
+ private void sendEncodedData(byte[] data, long timestamp, boolean isKeyFrame) {
+ 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);
+
+ // 鍙戦�乁DP鍖�
+ protocolHelper.sendUdpPacket(rtpPacket);
+
+ offset += packetDataSize;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Send encoded data error", e);
+ }
+ }
+
+ /**
+ * 鍋滄缂栫爜
+ */
+ public void stop() {
+ if (!isRunning.get()) {
+ return;
+ }
+
+ isRunning.set(false);
+
+ // 绛夊緟缂栫爜绾跨▼缁撴潫
+ if (encodeThread != null) {
+ try {
+ encodeThread.join(2000);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Wait encode thread error", e);
+ }
+ }
+
+ // 鍋滄鎽勫儚澶�
+ if (usbCamera != null) {
+ usbCamera.stopCamera();
+ }
+
+ // 閲婃斁缂栫爜鍣�
+ if (encoder != null) {
+ try {
+ encoder.stop();
+ encoder.release();
+ encoder = null;
+ } catch (Exception e) {
+ Log.e(TAG, "Release encoder error", e);
+ }
+ }
+
+ // 鍏抽棴UDP Socket
+ if (protocolHelper != null) {
+ protocolHelper.closeUdpSocket();
+ }
+
+ // 鍏抽棴鏂囦欢杈撳嚭
+ closeFileOutput();
+
+ Log.d(TAG, "H264 encoder stopped");
+ }
+
+ /**
+ * 鍏抽棴鏂囦欢杈撳嚭
+ */
+ private void closeFileOutput() {
+ if (fileOutputStream != null) {
+ try {
+ fileOutputStream.flush();
+ fileOutputStream.close();
+ Log.d(TAG, "File output closed: " + outputFilePath);
+ } catch (IOException e) {
+ Log.e(TAG, "Close file output error", e);
+ } 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;
+ }
+}
+
diff --git a/app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java b/app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java
new file mode 100644
index 0000000..0613a1c
--- /dev/null
+++ b/app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java
@@ -0,0 +1,303 @@
+package com.anyun.h264;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+/**
+ * JT/T 1076-2016 鍗忚宸ュ叿绫�
+ * 鎻愪緵UDP鍙戦�併�丼IM鍗″彿BCD杞崲銆丷TP鍖呭垱寤虹瓑鍏叡鍔熻兘
+ */
+public class JT1076ProtocolHelper {
+ private static final String TAG = "JT1076ProtocolHelper";
+
+ // JT/T 1076-2016 RTP鍗忚甯搁噺
+ public static final byte[] FRAME_HEADER = {0x30, 0x31, 0x63, 0x64}; // 甯уご鏍囪瘑
+ public static final int MAX_PACKET_SIZE = 950; // 鏁版嵁浣撴渶澶ч暱搴�
+
+ // 瑙嗛鏁版嵁绫诲瀷
+ public static final int DATA_TYPE_I_FRAME = 0x00; // I甯�
+ public static final int DATA_TYPE_P_FRAME = 0x10; // P甯�
+ public static final int DATA_TYPE_B_FRAME = 0x20; // B甯�
+
+ // 闊抽鏁版嵁绫诲瀷
+ public static final int DATA_TYPE_AUDIO = 0x30; // 闊抽甯�
+
+ // 鍒嗗寘澶勭悊鏍囪
+ public static final int PACKET_MARK_ATOMIC = 0x00; // 鍘熷瓙鍖�
+ public static final int PACKET_MARK_FIRST = 0x01; // 棣栧寘
+ public static final int PACKET_MARK_LAST = 0x02; // 鏈寘
+ 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_AUDIO = 97; // 闊抽璐熻浇绫诲瀷
+
+ // UDP鍙傛暟
+ private String serverIp;
+ private int serverPort;
+ private DatagramSocket udpSocket;
+ private InetAddress serverAddress;
+
+ // RTP鍗忚鍙傛暟
+ private String simCardNumber = "123456789012"; // 12浣峉IM鍗″彿
+ private byte logicalChannelNumber = 1; // 閫昏緫閫氶亾鍙�
+ private short sequenceNumber = 0; // 鍖呭簭鍙凤紙鑷姩閫掑锛�
+
+ /**
+ * 璁剧疆UDP鏈嶅姟鍣ㄥ湴鍧�
+ */
+ public void setServerAddress(String ip, int port) {
+ this.serverIp = ip;
+ this.serverPort = port;
+ }
+
+ /**
+ * 璁剧疆SIM鍗″彿鍜岄�昏緫閫氶亾鍙�
+ */
+ public void setProtocolParams(String simCardNumber, byte logicalChannelNumber) {
+ this.simCardNumber = simCardNumber;
+ this.logicalChannelNumber = logicalChannelNumber;
+ }
+
+ /**
+ * 鍒濆鍖朥DP Socket
+ */
+ public boolean initializeUdpSocket() {
+ try {
+ if (serverIp == null || serverIp.isEmpty()) {
+ Log.e(TAG, "Server IP not set");
+ return false;
+ }
+
+ udpSocket = new DatagramSocket();
+ serverAddress = InetAddress.getByName(serverIp);
+ Log.d(TAG, "UDP socket initialized, target: " + serverIp + ":" + serverPort);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Initialize UDP socket failed", e);
+ return false;
+ }
+ }
+
+ /**
+ * 鍏抽棴UDP Socket
+ */
+ public void closeUdpSocket() {
+ if (udpSocket != null) {
+ try {
+ udpSocket.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Close UDP socket error", e);
+ }
+ udpSocket = null;
+ }
+ serverAddress = null;
+ }
+
+ /**
+ * 鍙戦�乁DP鍖�
+ */
+ public void sendUdpPacket(byte[] packet) {
+ try {
+ if (udpSocket != null && serverAddress != null) {
+ DatagramPacket datagramPacket = new DatagramPacket(
+ packet, packet.length, serverAddress, serverPort);
+ udpSocket.send(datagramPacket);
+ } else {
+ Log.w(TAG, "UDP socket not initialized");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Send UDP packet error", e);
+ }
+ }
+
+ /**
+ * 灏哠IM鍗″彿杞崲涓築CD鏍煎紡锛�6瀛楄妭锛�
+ */
+ public byte[] convertSimToBCD(String simNumber) {
+ byte[] bcd = new byte[6];
+ // 纭繚SIM鍗″彿涓�12浣嶆暟瀛楋紝涓嶈冻鍓嶉潰琛�0
+ StringBuilder sim = new StringBuilder();
+ if (simNumber != null) {
+ // 鍙繚鐣欐暟瀛楀瓧绗�
+ String digits = simNumber.replaceAll("[^0-9]", "");
+ sim.append(digits);
+ }
+ // 琛ラ綈鎴栨埅鏂埌12浣�
+ while (sim.length() < 12) {
+ sim.insert(0, '0');
+ }
+ if (sim.length() > 12) {
+ sim.setLength(12);
+ }
+
+ // 杞崲涓築CD鏍煎紡
+ String simStr = sim.toString();
+ for (int i = 0; i < 6; i++) {
+ int high = Character.digit(simStr.charAt(i * 2), 10);
+ int low = Character.digit(simStr.charAt(i * 2 + 1), 10);
+ if (high < 0) high = 0;
+ if (low < 0) low = 0;
+ bcd[i] = (byte) ((high << 4) | low);
+ }
+ return bcd;
+ }
+
+ /**
+ * 鑾峰彇涓嬩竴涓寘搴忓彿锛堣嚜鍔ㄩ�掑锛�
+ */
+ public short getNextSequenceNumber() {
+ return sequenceNumber++;
+ }
+
+ /**
+ * 閲嶇疆鍖呭簭鍙�
+ */
+ public void resetSequenceNumber() {
+ sequenceNumber = 0;
+ }
+
+ /**
+ * 鍒涘缓瑙嗛RTP鍖咃紙JT/T 1076-2016鍗忚鏍煎紡锛�
+ * @param dataBody 鏁版嵁浣�
+ * @param timestamp 鏃堕棿鎴筹紙姣锛�
+ * @param dataType 鏁版嵁绫诲瀷锛圛甯�/P甯�/B甯э級
+ * @param packetMark 鍒嗗寘澶勭悊鏍囪
+ * @param lastIFrameInterval 璺濅笂涓�涓狪甯х殑鏃堕棿闂撮殧锛堟绉掞級
+ * @param lastFrameInterval 璺濅笂涓�甯х殑鏃堕棿闂撮殧锛堟绉掞級
+ * @param payloadType RTP璐熻浇绫诲瀷锛堥粯璁�96锛�
+ * @return RTP鍖呮暟鎹�
+ */
+ public byte[] createVideoRtpPacket(byte[] dataBody, long timestamp, int dataType,
+ int packetMark, long lastIFrameInterval,
+ long lastFrameInterval, int payloadType) {
+ // 璁$畻鍖呭ぇ灏忥細甯уご(4) + RTP澶�(2) + 搴忓彿(2) + SIM鍗″彿(6) + 閫昏緫閫氶亾(1) +
+ // 鏁版嵁绫诲瀷(1) + 鏃堕棿鎴�(8) + Last I Frame Interval(2) + Last Frame Interval(2) +
+ // 鏁版嵁浣撻暱搴�(2) + 鏁版嵁浣�
+ int packetSize = 4 + 2 + 2 + 6 + 1 + 1 + 8 + 2 + 2 + 2 + dataBody.length;
+ ByteBuffer buffer = ByteBuffer.allocate(packetSize);
+ buffer.order(java.nio.ByteOrder.BIG_ENDIAN);
+
+ // 1. 甯уご鏍囪瘑 (4 bytes)
+ buffer.put(FRAME_HEADER);
+
+ // 2. RTP澶撮儴 (2 bytes)
+ // V=2 (2 bits), P=0 (1 bit), X=0 (1 bit), CC=1 (4 bits), M=0/1 (1 bit), PT (7 bits)
+ byte rtpHeaderByte1 = (byte) 0x81; // V=2, P=0, X=0, CC=1
+ byte rtpHeaderByte2 = (byte) (payloadType & 0x7F); // M=0, PT
+ buffer.put(rtpHeaderByte1);
+ buffer.put(rtpHeaderByte2);
+
+ // 3. 鍖呭簭鍙� (2 bytes)
+ buffer.putShort(getNextSequenceNumber());
+
+ // 4. SIM鍗″彿 (6 bytes BCD鏍煎紡)
+ byte[] simBytes = convertSimToBCD(simCardNumber);
+ buffer.put(simBytes, 0, 6);
+
+ // 5. 閫昏緫閫氶亾鍙� (1 byte)
+ buffer.put(logicalChannelNumber);
+
+ // 6. 鏁版嵁绫诲瀷 + 鍒嗗寘澶勭悊鏍囪 (1 byte)
+ byte dataTypeAndMark = (byte) ((dataType & 0xF0) | (packetMark & 0x0F));
+ buffer.put(dataTypeAndMark);
+
+ // 7. 鏃堕棿鎴� (8 bytes, 姣)
+ buffer.putLong(timestamp);
+
+ // 8. Last I Frame Interval (2 bytes, 姣)
+ buffer.putShort((short) lastIFrameInterval);
+
+ // 9. Last Frame Interval (2 bytes, 姣)
+ buffer.putShort((short) lastFrameInterval);
+
+ // 10. 鏁版嵁浣撻暱搴� (2 bytes)
+ buffer.putShort((short) dataBody.length);
+
+ // 11. 鏁版嵁浣�
+ buffer.put(dataBody);
+
+ return buffer.array();
+ }
+
+ /**
+ * 鍒涘缓瑙嗛RTP鍖咃紙浣跨敤榛樿瑙嗛璐熻浇绫诲瀷96锛�
+ */
+ public byte[] createVideoRtpPacket(byte[] dataBody, long timestamp, int dataType,
+ int packetMark, long lastIFrameInterval,
+ long lastFrameInterval) {
+ return createVideoRtpPacket(dataBody, timestamp, dataType, packetMark,
+ lastIFrameInterval, lastFrameInterval, RTP_PAYLOAD_TYPE_VIDEO);
+ }
+
+ /**
+ * 鍒涘缓闊抽RTP鍖咃紙JT/T 1076-2016鍗忚鏍煎紡锛�
+ * 娉ㄦ剰锛氶煶棰戝抚涓嶅寘鍚獿ast I Frame Interval鍜孡ast Frame Interval瀛楁
+ * @param dataBody 鏁版嵁浣�
+ * @param timestamp 鏃堕棿鎴筹紙姣锛�
+ * @param dataType 鏁版嵁绫诲瀷锛堥煶棰戝抚锛�
+ * @param packetMark 鍒嗗寘澶勭悊鏍囪
+ * @param payloadType RTP璐熻浇绫诲瀷锛堥粯璁�97锛�
+ * @return RTP鍖呮暟鎹�
+ */
+ public byte[] createAudioRtpPacket(byte[] dataBody, long timestamp, int dataType,
+ int packetMark, int payloadType) {
+ // 璁$畻鍖呭ぇ灏忥紙闊抽锛氫笉鍖呭惈Last I Frame Interval鍜孡ast Frame Interval锛�
+ // 甯уご(4) + RTP澶�(2) + 搴忓彿(2) + SIM鍗″彿(6) + 閫昏緫閫氶亾(1) +
+ // 鏁版嵁绫诲瀷(1) + 鏃堕棿鎴�(8) + 鏁版嵁浣撻暱搴�(2) + 鏁版嵁浣�
+ int packetSize = 4 + 2 + 2 + 6 + 1 + 1 + 8 + 2 + dataBody.length;
+ ByteBuffer buffer = ByteBuffer.allocate(packetSize);
+ buffer.order(java.nio.ByteOrder.BIG_ENDIAN);
+
+ // 1. 甯уご鏍囪瘑 (4 bytes)
+ buffer.put(FRAME_HEADER);
+
+ // 2. RTP澶撮儴 (2 bytes)
+ // V=2 (2 bits), P=0 (1 bit), X=0 (1 bit), CC=1 (4 bits), M=0/1 (1 bit), PT (7 bits)
+ byte rtpHeaderByte1 = (byte) 0x81; // V=2, P=0, X=0, CC=1
+ byte rtpHeaderByte2 = (byte) (payloadType & 0x7F); // M=0, PT
+ buffer.put(rtpHeaderByte1);
+ buffer.put(rtpHeaderByte2);
+
+ // 3. 鍖呭簭鍙� (2 bytes)
+ buffer.putShort(getNextSequenceNumber());
+
+ // 4. SIM鍗″彿 (6 bytes BCD鏍煎紡)
+ byte[] simBytes = convertSimToBCD(simCardNumber);
+ buffer.put(simBytes, 0, 6);
+
+ // 5. 閫昏緫閫氶亾鍙� (1 byte)
+ buffer.put(logicalChannelNumber);
+
+ // 6. 鏁版嵁绫诲瀷 + 鍒嗗寘澶勭悊鏍囪 (1 byte)
+ byte dataTypeAndMark = (byte) ((dataType & 0xF0) | (packetMark & 0x0F));
+ buffer.put(dataTypeAndMark);
+
+ // 7. 鏃堕棿鎴� (8 bytes, 姣)
+ buffer.putLong(timestamp);
+
+ // 娉ㄦ剰锛氶煶棰戝抚涓嶅寘鍚獿ast I Frame Interval鍜孡ast Frame Interval瀛楁
+
+ // 8. 鏁版嵁浣撻暱搴� (2 bytes)
+ buffer.putShort((short) dataBody.length);
+
+ // 9. 鏁版嵁浣�
+ buffer.put(dataBody);
+
+ return buffer.array();
+ }
+
+ /**
+ * 鍒涘缓闊抽RTP鍖咃紙浣跨敤榛樿闊抽璐熻浇绫诲瀷97锛�
+ */
+ public byte[] createAudioRtpPacket(byte[] dataBody, long timestamp, int dataType, int packetMark) {
+ return createAudioRtpPacket(dataBody, timestamp, dataType, packetMark, RTP_PAYLOAD_TYPE_AUDIO);
+ }
+}
+
+
diff --git a/app/src/main/java/com/anyun/h264/MainActivity.kt b/app/src/main/java/com/anyun/h264/MainActivity.kt
index 398a53e..5de8803 100644
--- a/app/src/main/java/com/anyun/h264/MainActivity.kt
+++ b/app/src/main/java/com/anyun/h264/MainActivity.kt
@@ -1,47 +1,165 @@
package com.anyun.h264
import android.os.Bundle
+import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import com.anyun.h264.ui.theme.MyApplicationTheme
+import java.io.File
class MainActivity : ComponentActivity() {
+ private var h264Encoder: H264Encoder? = null
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
+
setContent {
+ var isRunning by remember { mutableStateOf(false) }
+
MyApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding)
+ MainScreen(
+ modifier = Modifier.padding(innerPadding),
+ isRunning = isRunning,
+ onStartH264Click = {
+ val success = startH264Encoder()
+ if (success) {
+ isRunning = true
+ }
+ },
+ onStopH264Click = {
+ stopH264Encoder()
+ isRunning = false
+ }
)
}
}
}
}
+
+ override fun onDestroy() {
+ super.onDestroy()
+ stopH264Encoder()
+ }
+
+ private fun startH264Encoder(): Boolean {
+ if (h264Encoder != null) {
+ Log.w("MainActivity", "H264Encoder is already running")
+ return false
+ }
+
+ try {
+ // 鍒涘缓缂栫爜鍣�
+ h264Encoder = H264Encoder()
+
+ // 璁剧疆缂栫爜鍙傛暟
+ h264Encoder?.setEncoderParams(640, 480, 25, 2000000)
+
+ // 璁剧疆杈撳嚭鏂囦欢锛堝彲閫夛級
+ val outputFile = File(getExternalFilesDir(null), "test.h264")
+ h264Encoder?.setOutputFile(outputFile.absolutePath)
+ h264Encoder?.setEnableFileOutput(true) // 鍚敤鏂囦欢杈撳嚭
+
+ // 璁剧疆UDP鏈嶅姟鍣ㄥ湴鍧�锛堝彲閫夛級
+ h264Encoder?.setServerAddress("192.168.1.100", 8888)
+ h264Encoder?.setProtocolParams("123456789012", 1)
+
+ // 鍒濆鍖栧苟鍚姩
+ val cameraIdRange = intArrayOf(1, 2)
+ val resolution = intArrayOf(640, 480)
+
+ if (h264Encoder?.initialize(cameraIdRange, null, resolution, false) == true) {
+ h264Encoder?.start()
+ Log.d("MainActivity", "H264Encoder started successfully")
+ Log.d("MainActivity", "Output file: ${outputFile.absolutePath}")
+ return true
+ } else {
+ Log.e("MainActivity", "Failed to initialize H264Encoder")
+ h264Encoder = null
+ return false
+ }
+ } catch (e: Exception) {
+ Log.e("MainActivity", "Failed to start H264Encoder", e)
+ h264Encoder = null
+ return false
+ }
+ }
+
+ private fun stopH264Encoder() {
+ h264Encoder?.let { encoder ->
+ try {
+ encoder.stop()
+ Log.d("MainActivity", "H264Encoder stopped")
+ } catch (e: Exception) {
+ Log.e("MainActivity", "Failed to stop H264Encoder", e)
+ }
+ h264Encoder = null
+ }
+ }
}
@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Hello $name!",
+fun MainScreen(
+ modifier: Modifier = Modifier,
+ isRunning: Boolean,
+ onStartH264Click: () -> Unit,
+ onStopH264Click: () -> Unit
+) {
+ Column(
modifier = modifier
- )
-}
-
-@Preview(showBackground = true)
-@Composable
-fun GreetingPreview() {
- MyApplicationTheme {
- Greeting("Android")
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = "H264 缂栫爜鍣�",
+ style = MaterialTheme.typography.headlineMedium
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ Button(
+ onClick = onStartH264Click,
+ enabled = !isRunning,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp)
+ ) {
+ Text("鍚姩 H264")
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(
+ onClick = onStopH264Click,
+ enabled = isRunning,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("鍋滄 H264")
+ }
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ if (isRunning) {
+ Text(
+ text = "缂栫爜鍣ㄨ繍琛屼腑...",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
}
}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a7a8780..0c1d388 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Sat Nov 29 11:22:04 CST 2025
+#Sat Nov 29 14:15:17 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+distributionUrl=file:///D:/config/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle b/settings.gradle
index 2e6846e..d949239 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -15,6 +15,7 @@
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
+
mavenCentral()
}
}
--
Gitblit v1.8.0