7个文件已修改
22个文件已添加
3054 ■■■■■ 已修改文件
README_H264_CHECK.md 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/build.gradle 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/AndroidManifest.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/aidl/com/anyun/h264/IH264EncodeService.aidl 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/aidl/com/anyun/h264/model/ResourceInfo.aidl 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/AACEncoder.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264EncodeService.java 605 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264Encoder.java 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264FileTransmitter.java 511 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/JT1076TcpClient.java 260 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/MainActivity.kt 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/model/ResourceInfo.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/service/H264EncodeServiceClient.java 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/util/BytesUtils.java 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
check_h264.py 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
gradle/libs.versions.toml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
gradlew 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
gradlew.bat 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
key/Verify.txt 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
key/bjkey.jks 补丁 | 查看 | 原始文档 | blame | 历史
key/key.jks 补丁 | 查看 | 原始文档 | blame | 历史
key/key.jks.ori 补丁 | 查看 | 原始文档 | blame | 历史
key/keyrk3288.jks 补丁 | 查看 | 原始文档 | blame | 历史
key/keysc200.jks 补丁 | 查看 | 原始文档 | blame | 历史
key/keysc626.jks 补丁 | 查看 | 原始文档 | blame | 历史
keystore.properties 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test.h264 补丁 | 查看 | 原始文档 | blame | 历史
如何检查test.h264文件.md 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
README_H264_CHECK.md
New file
@@ -0,0 +1,120 @@
# H264文件检查工具使用说明
## å¿«é€Ÿæ£€æŸ¥
### æ–¹æ³•1: ç›´æŽ¥ä»Žè®¾å¤‡è·¯å¾„检查(如果文件在本地)
```bash
python check_h264.py test.h264
```
### æ–¹æ³•2: ä»ŽAndroid设备下载文件后检查
1. **连接Android设备并启用USB调试**
2. **查找文件路径**
   Android设备上的文件路径通常是:
   ```
   /storage/emulated/0/Android/data/com.anyun.h264/files/test.h264
   ```
   æˆ–者使用adb查找:
   ```bash
   adb shell "find /sdcard -name test.h264 2>/dev/null"
   ```
3. **下载文件到本地**
   ```bash
   adb pull /storage/emulated/0/Android/data/com.anyun.h264/files/test.h264 ./test.h264
   ```
4. **运行检查工具**
   ```bash
   python check_h264.py test.h264
   ```
## æ£€æŸ¥å†…容
工具会检查以下内容:
1. âœ… **文件是否存在**
2. âœ… **文件大小**(应该大于100字节)
3. âœ… **NALU单元数量**(至少应该有SPS/PPS + 1个关键帧)
4. âœ… **是否包含SPS**(序列参数集,必需)
5. âœ… **是否包含PPS**(图像参数集,必需)
6. âœ… **是否包含IDR关键帧**(必需)
7. âœ… **是否有多个帧**
## é¢„期结果
一个可以播放的H264文件应该包含:
- âœ… è‡³å°‘1个SPS (类型7)
- âœ… è‡³å°‘1个PPS (类型8)
- âœ… è‡³å°‘1个IDR关键帧 (类型5)
- âœ… å¤šä¸ªNALU单元(建议>10个)
## å¸¸è§é—®é¢˜
### Q: æ–‡ä»¶åªæœ‰1帧怎么办?
**A:** è¿™æ˜¯ä¹‹å‰çš„问题。现在的代码已经修复:
- âœ… æ­£ç¡®å¤„理SPS/PPS配置
- âœ… åœ¨å…³é”®å¸§æ—¶åˆå¹¶SPS/PPS
- âœ… æ”¹è¿›ç¼–码循环,确保处理所有输出
- âœ… æ·»åŠ æ¸…ç©ºç¼–ç å™¨åŠŸèƒ½
**建议:**
1. é‡æ–°ç¼–译并运行应用
2. å½•制至少3-5秒视频
3. åœæ­¢ç¼–码后检查文件
### Q: æ–‡ä»¶æ— æ³•播放怎么办?
**可能原因:**
1. âŒ ç¼ºå°‘SPS/PPS - æ£€æŸ¥æ˜¯å¦è¾“出了配置数据
2. âŒ æ–‡ä»¶æ ¼å¼é”™è¯¯ - æ£€æŸ¥æ˜¯å¦æ˜¯Annex-B格式
3. âŒ åªæœ‰1帧 - æ£€æŸ¥ç¼–码循环是否正常工作
**解决方案:**
1. æŸ¥çœ‹Logcat日志,搜索 "H264Encoder"
2. æ£€æŸ¥æ˜¯å¦æœ‰ "SPS/PPS included" æ—¥å¿—
3. æ£€æŸ¥æ˜¯å¦æœ‰ "Frame encoded" æ—¥å¿—
4. ä½¿ç”¨æ­¤å·¥å…·æ£€æŸ¥æ–‡ä»¶ç»“æž„
### Q: å¦‚何播放H264文件?
可以使用以下播放器:
- **VLC Media Player**(推荐)
- **ffplay** (FFmpeg自带)
- **MPC-HC**
- **PotPlayer**
直接双击 `.h264` æ–‡ä»¶æˆ–拖放到播放器窗口即可。
## è°ƒè¯•建议
如果文件无法播放,请检查:
1. **查看Logcat日志**
   ```bash
   adb logcat -s H264Encoder:D MainActivity:D
   ```
2. **检查关键日志**
   - "SPS/PPS included in key frame data" - è¯´æ˜ŽSPS/PPS已合并
   - "Frame encoded: ..." - è¯´æ˜Žæœ‰å¸§è¾“出
   - "Encoder output EOS" - è¯´æ˜Žæ­£å¸¸ç»“束
3. **验证文件结构**
   ```bash
   python check_h264.py test.h264
   ```
4. **使用hexdump查看文件头**
   ```bash
   hexdump -C test.h264 | head -20
   ```
   åº”该看到:`00 00 00 01` æˆ– `00 00 01`(Annex-B起始码)
app/build.gradle
@@ -19,6 +19,9 @@
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildFeatures{
        aidl true
    }
    signingConfigs{
@@ -64,6 +67,7 @@
    implementation libs.androidx.ui.graphics
    implementation libs.androidx.ui.tooling.preview
    implementation libs.androidx.material3
    implementation libs.netty.all
    testImplementation libs.junit
    androidTestImplementation libs.androidx.junit
    androidTestImplementation libs.androidx.espresso.core
app/src/main/AndroidManifest.xml
@@ -31,6 +31,16 @@
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!-- H264编码服务 -->
        <service
            android:name=".H264EncodeService"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.anyun.h264.H264EncodeService" />
            </intent-filter>
        </service>
    </application>
</manifest>
app/src/main/aidl/com/anyun/h264/IH264EncodeService.aidl
New file
@@ -0,0 +1,37 @@
package com.anyun.h264;
import com.anyun.h264.model.ResourceInfo;
import java.util.List;
/**
 * H264编码服务AIDL接口
 */
interface IH264EncodeService {
    /**
     * æŽ§åˆ¶H264编码和文件传输
     * @param action æ“ä½œç±»åž‹ï¼š
     *               0-开启h264文件写入,
     *               1-停止h264编码并停止写入文件,
     *               2-开启网络推送h264(不写入文件),
     *               3-停止h264编码并停止网络推送,
     *               4-开始传输H264文件(从文件读取并网络推送),
     *               5-停止H264文件传输
     * @param jsonConfig JSON格式的配置参数
     *                  action 0/2: åŒ…含:ip(服务器IP)、port(服务器端口)、width(视频宽度)、height(视频高度)、framerate(帧率)、simPhone(SIM卡号)
     *                          ç¤ºä¾‹ï¼š{"ip":"192.168.1.100","port":8888,"width":640,"height":480,"framerate":25,"simPhone":"013120122580"}
     *                  action 4: åŒ…含:ip(服务器IP)、port(服务器端口)、framerate(帧率)、simPhone(SIM卡号)、filePath(H264文件路径)、protocolType(协议类型,可选,1-UDP,2-TCP,默认TCP)
     *                          ç¤ºä¾‹ï¼š{"ip":"192.168.1.100","port":8888,"framerate":25,"simPhone":"013120122580","filePath":"/sdcard/video.h264","protocolType":2}
     *                  action 1/3/5: æ­¤å‚数可为空或null(停止操作不需要配置)
     * @return 0-成功,1-失败
     */
    int controlEncode(int action, String jsonConfig);
    /**
     * èŽ·å–èµ„æºåˆ—è¡¨
     * @param startTime å¼€å§‹æ—¶é—´ï¼ˆæ ¼å¼ï¼šYYMMDDHHmmss,BCD编码的6字节字符串)
     * @param endTime ç»“束时间(格式:YYMMDDHHmmss,BCD编码的6字节字符串)
     * @return èµ„源列表(根据JT/T 1076-2016表23定义)
     */
    List<ResourceInfo> getResourceList(String startTime, String endTime);
}
app/src/main/aidl/com/anyun/h264/model/ResourceInfo.aidl
New file
@@ -0,0 +1,7 @@
package com.anyun.h264.model;
/**
 * éŸ³è§†é¢‘资源信息(根据JT/T 1076-2016表23定义)
 */
parcelable ResourceInfo;
app/src/main/java/com/anyun/h264/AACEncoder.java
@@ -111,8 +111,8 @@
            // 2. åˆå§‹åŒ–AAC编码器
            initEncoder();
            
            // 3. åˆå§‹åŒ–UDP Socket
            if (!protocolHelper.initializeUdpSocket()) {
            // 3. åˆå§‹åŒ–Socket(UDP或TCP,根据协议类型自动选择)
            if (!protocolHelper.initializeSocket()) {
                return false;
            }
            
@@ -287,8 +287,8 @@
                byte[] rtpPacket = protocolHelper.createAudioRtpPacket(
                    packetData, timestamp, JT1076ProtocolHelper.DATA_TYPE_AUDIO, packetMark);
                
                // å‘送UDP包
                protocolHelper.sendUdpPacket(rtpPacket);
                // å‘送RTP包(UDP或TCP,根据协议类型自动选择)
                protocolHelper.sendPacket(rtpPacket);
                
                offset += packetDataSize;
            }
@@ -343,9 +343,9 @@
            audioRecord = null;
        }
        
        // å…³é—­UDP Socket
        // å…³é—­Socket(UDP或TCP,根据协议类型自动选择)
        if (protocolHelper != null) {
            protocolHelper.closeUdpSocket();
            protocolHelper.closeSocket();
        }
        
        Log.d(TAG, "AAC encoder stopped");
app/src/main/java/com/anyun/h264/H264EncodeService.java
New file
@@ -0,0 +1,605 @@
package com.anyun.h264;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.anyun.h264.model.ResourceInfo;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
 * H264编码服务
 * æä¾›AIDL接口供客户端调用,用于控制H264编码和查询资源列表
 */
public class H264EncodeService extends Service {
    private static final String TAG = "H264EncodeService";
    private H264Encoder h264Encoder;
    private H264FileTransmitter h264FileTransmitter; // H264文件传输器
    private String outputFileDirectory; // H264文件输出目录
    // é»˜è®¤ç¼–码参数
    private static final int DEFAULT_WIDTH = 640;
    private static final int DEFAULT_HEIGHT = 480;
    private static final int DEFAULT_FRAME_RATE = 25;
    private static final int DEFAULT_BITRATE = 2000000; // 2Mbps
    // é»˜è®¤æ‘„像头参数
    private static final int[] DEFAULT_CAMERA_ID_RANGE = {1, 2};
    private static final int[] DEFAULT_RESOLUTION = {640, 480};
    // AIDL接口实现
    private final IH264EncodeService.Stub binder = new IH264EncodeService.Stub() {
        @Override
        public int controlEncode(int action, String jsonConfig) throws RemoteException {
            return H264EncodeService.this.controlEncode(action, jsonConfig);
        }
        @Override
        public List<ResourceInfo> getResourceList(String startTime, String endTime) throws RemoteException {
            return H264EncodeService.this.getResourceList(startTime, endTime);
        }
    };
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "H264EncodeService created");
        // åˆå§‹åŒ–输出文件目录(使用应用外部存储目录)
        outputFileDirectory = getExternalFilesDir(null).getAbsolutePath();
        Log.d(TAG, "Output file directory: " + outputFileDirectory);
    }
    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "Service bound");
        return binder;
    }
    @Override
    public boolean onUnbind(Intent intent) {
        Log.d(TAG, "Service unbound");
        // ä¸è‡ªåŠ¨åœæ­¢ç¼–ç å™¨ï¼Œè®©å®ƒåœ¨æœåŠ¡ä¸­ä¿æŒè¿è¡Œ
        return super.onUnbind(intent);
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "Service destroyed");
        // åœæ­¢å¹¶é‡Šæ”¾ç¼–码器和文件传输器
        stopEncoder();
        stopFileTransmitter();
    }
    /**
     * ç¼–码配置类
     */
    private static class EncodeConfig {
        String ip;
        int port;
        int width;
        int height;
        int framerate;
        String simPhone;
        // ä»ŽJSON解析配置
        static EncodeConfig fromJson(String jsonConfig) throws JSONException {
            EncodeConfig config = new EncodeConfig();
            if (jsonConfig == null || jsonConfig.trim().isEmpty()) {
                // ä½¿ç”¨é»˜è®¤å€¼
                config.width = DEFAULT_WIDTH;
                config.height = DEFAULT_HEIGHT;
                config.framerate = DEFAULT_FRAME_RATE;
                config.ip = null;
                config.port = 0;
                config.simPhone = null;
                return config;
            }
            JSONObject json = new JSONObject(jsonConfig);
            config.width = json.optInt("width", DEFAULT_WIDTH);
            config.height = json.optInt("height", DEFAULT_HEIGHT);
            config.framerate = json.optInt("framerate", DEFAULT_FRAME_RATE);
            config.ip = json.optString("ip", null);
            config.port = json.optInt("port", 0);
            config.simPhone = json.optString("simPhone", null);
            return config;
        }
    }
    /**
     * æ–‡ä»¶ä¼ è¾“配置类
     */
    private static class FileTransmitConfig {
        String ip;
        int port;
        int framerate;
        String simPhone;
        String filePath; // H264文件路径
        int protocolType; // åè®®ç±»åž‹ï¼š1-UDP,2-TCP
        // ä»ŽJSON解析配置
        static FileTransmitConfig fromJson(String jsonConfig) throws JSONException {
            FileTransmitConfig config = new FileTransmitConfig();
            if (jsonConfig == null || jsonConfig.trim().isEmpty()) {
                throw new JSONException("File transmit config cannot be empty");
            }
            JSONObject json = new JSONObject(jsonConfig);
            config.ip = json.optString("ip", null);
            config.port = json.optInt("port", 0);
            config.framerate = json.optInt("framerate", DEFAULT_FRAME_RATE);
            config.simPhone = json.optString("simPhone", "013120122580");
            config.filePath = json.optString("filePath", null);
            // åè®®ç±»åž‹ï¼šé»˜è®¤TCP(2),1-UDP,2-TCP
            config.protocolType = json.optInt("protocolType", JT1076ProtocolHelper.PROTOCOL_TYPE_TCP);
            return config;
        }
    }
    /**
     * æŽ§åˆ¶H264编码和文件传输
     * @param action æ“ä½œç±»åž‹ï¼š
     *               0-开启h264文件写入,
     *               1-停止h264编码并停止写入文件,
     *               2-开启网络推送h264(不写入文件),
     *               3-停止h264编码并停止网络推送,
     *               4-开始传输H264文件(从文件读取并网络推送),
     *               5-停止H264文件传输
     * @param jsonConfig JSON格式的配置参数
     *                   action 0/2: åŒ…含:ip、port、width、height、framerate、simPhone
     *                   action 4: åŒ…含:ip、port、framerate、simPhone、filePath、protocolType(可选,1-UDP,2-TCP,默认TCP)
     *                   action 1/3/5: æ­¤å‚数可为空或null
     * @return 0-成功,1-失败
     */
    private synchronized int controlEncode(int action, String jsonConfig) {
        Log.d(TAG, "controlEncode called with action: " + action + ", jsonConfig: " + jsonConfig);
        try {
            switch (action) {
                case 0: // å¼€å¯h264文件写入
                    try {
                        EncodeConfig config0 = EncodeConfig.fromJson(jsonConfig);
                        return startFileEncode(config0);
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to parse JSON config: " + jsonConfig, e);
                        return 1;
                    }
                case 1: // åœæ­¢h264编码并停止写入文件
                    return stopEncoder();
                case 2: // å¼€å¯ç½‘络推送h264(不写入文件)
                    try {
                        EncodeConfig config2 = EncodeConfig.fromJson(jsonConfig);
                        return startNetworkEncode(config2);
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to parse JSON config: " + jsonConfig, e);
                        return 1;
                    }
                case 3: // åœæ­¢h264编码并停止网络推送
                    return stopEncoder();
                case 4: // å¼€å§‹ä¼ è¾“H264文件
                    try {
                        FileTransmitConfig config4 = FileTransmitConfig.fromJson(jsonConfig);
                        return startFileTransmit(config4);
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to parse JSON config: " + jsonConfig, e);
                        return 1;
                    }
                case 5: // åœæ­¢H264文件传输
                    return stopFileTransmitter();
                default:
                    Log.e(TAG, "Unknown action: " + action);
                    return 1; // å¤±è´¥
            }
        } catch (Exception e) {
            Log.e(TAG, "Error in controlEncode", e);
            return 1; // å¤±è´¥
        }
    }
    /**
     * å¯åŠ¨æ–‡ä»¶ç¼–ç æ¨¡å¼ï¼ˆåªå†™å…¥æ–‡ä»¶ï¼Œä¸è¿›è¡Œç½‘ç»œæŽ¨é€ï¼‰
     */
    private int startFileEncode(EncodeConfig config) {
        Log.d(TAG, "Starting file encode mode");
        // å¦‚果编码器已经在运行,先停止
        if (h264Encoder != null) {
            Log.w(TAG, "Encoder is already running, stopping it first");
            stopEncoder();
        }
        try {
            // åˆ›å»ºç¼–码器
            h264Encoder = new H264Encoder();
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            int width = config != null ? config.width : DEFAULT_WIDTH;
            int height = config != null ? config.height : DEFAULT_HEIGHT;
            int framerate = config != null ? config.framerate : DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            long timeFile = System.currentTimeMillis();
            SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss");
            String str = bcdFormat.format(timeFile);
            Log.i(TAG,"文件名:"+str);
            // è®¾ç½®è¾“出文件
            String fileName = "h264_" + timeFile+ ".h264";
            File outputFile = new File(outputFileDirectory, fileName);
            h264Encoder.setOutputFile(outputFile.getAbsolutePath());
            h264Encoder.setEnableFileOutput(true); // å¯ç”¨æ–‡ä»¶è¾“出
            // ç¦ç”¨ç½‘络传输
            h264Encoder.setEnableNetworkTransmission(false);
            // åˆå§‹åŒ–并启动(使用配置中的分辨率)
            int[] resolution = {width, height};
            if (h264Encoder.initialize(DEFAULT_CAMERA_ID_RANGE, null, resolution, false)) {
                h264Encoder.start();
                Log.d(TAG, "File encode started successfully, output file: " + outputFile.getAbsolutePath() +
                        ", resolution: " + width + "x" + height + ", framerate: " + framerate);
                return 0; // æˆåŠŸ
            } else {
                Log.e(TAG, "Failed to initialize encoder");
                h264Encoder = null;
                return 1; // å¤±è´¥
            }
        } catch (Exception e) {
            Log.e(TAG, "Failed to start file encode", e);
            h264Encoder = null;
            return 1; // å¤±è´¥
        }
    }
    /**
     * å¯åŠ¨ç½‘ç»œæŽ¨é€æ¨¡å¼ï¼ˆåªè¿›è¡Œç½‘ç»œæŽ¨é€ï¼Œä¸å†™å…¥æ–‡ä»¶ï¼‰
     */
    private int startNetworkEncode(EncodeConfig config) {
        Log.d(TAG, "Starting network encode mode");
        // å¦‚果编码器已经在运行,先停止
        if (h264Encoder != null) {
            Log.w(TAG, "Encoder is already running, stopping it first");
            stopEncoder();
        }
        // æ£€æŸ¥å¿…需的配置参数
        if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) {
            Log.e(TAG, "Network encode requires valid ip and port in config");
            return 1; // å¤±è´¥
        }
        try {
            // åˆ›å»ºç¼–码器
            h264Encoder = new H264Encoder();
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            int width = config != null ? config.width : DEFAULT_WIDTH;
            int height = config != null ? config.height : DEFAULT_HEIGHT;
            int framerate = config != null ? config.framerate : DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            // ç¦ç”¨æ–‡ä»¶è¾“出
            h264Encoder.setEnableFileOutput(false);
            // å¯ç”¨ç½‘络传输并设置服务器地址
            h264Encoder.setEnableNetworkTransmission(true);
            h264Encoder.setServerAddress(config.ip, config.port);
            // è®¾ç½®åè®®å‚数(使用配置中的simPhone,如果未提供则使用默认值)
            String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty()
                    ? config.simPhone : "013120122580";
            h264Encoder.setProtocolParams(simPhone, (byte)1);
            // åˆå§‹åŒ–并启动(使用配置中的分辨率)
            int[] resolution = {width, height};
            if (h264Encoder.initialize(DEFAULT_CAMERA_ID_RANGE, null, resolution, false)) {
                h264Encoder.start();
                Log.d(TAG, "Network encode started successfully, server: " + config.ip + ":" + config.port +
                        ", resolution: " + width + "x" + height + ", framerate: " + framerate);
                return 0; // æˆåŠŸ
            } else {
                Log.e(TAG, "Failed to initialize encoder");
                h264Encoder = null;
                return 1; // å¤±è´¥
            }
        } catch (Exception e) {
            Log.e(TAG, "Failed to start network encode", e);
            h264Encoder = null;
            return 1; // å¤±è´¥
        }
    }
    /**
     * åœæ­¢ç¼–码器
     */
    private int stopEncoder() {
        Log.d(TAG, "Stopping encoder");
        if (h264Encoder != null) {
            try {
                h264Encoder.stop();
                h264Encoder.release();
                h264Encoder = null;
                Log.d(TAG, "Encoder stopped successfully");
                return 0; // æˆåŠŸ
            } catch (Exception e) {
                Log.e(TAG, "Error stopping encoder", e);
                h264Encoder = null;
                return 1; // å¤±è´¥
            }
        } else {
            Log.w(TAG, "Encoder is not running");
            return 0; // æˆåŠŸï¼ˆæ²¡æœ‰è¿è¡Œçš„ç¼–ç å™¨ï¼Œè§†ä¸ºæˆåŠŸï¼‰
        }
    }
    /**
     * å¯åŠ¨æ–‡ä»¶ä¼ è¾“æ¨¡å¼ï¼ˆä»ŽH264文件读取并网络推送)
     */
    private int startFileTransmit(FileTransmitConfig config) {
        Log.d(TAG, "Starting file transmit mode");
        // å¦‚果文件传输器已经在运行,先停止
        if (h264FileTransmitter != null) {
            Log.w(TAG, "File transmitter is already running, stopping it first");
            stopFileTransmitter();
        }
        // æ£€æŸ¥å¿…需的配置参数
        if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) {
            Log.e(TAG, "File transmit requires valid ip and port in config");
            return 1; // å¤±è´¥
        }
        if (config.filePath == null || config.filePath.trim().isEmpty()) {
            Log.e(TAG, "File transmit requires valid filePath in config");
            return 1; // å¤±è´¥
        }
        try {
            // æ£€æŸ¥æ–‡ä»¶æ˜¯å¦å­˜åœ¨
            File file = new File(config.filePath);
            if (!file.exists() || !file.isFile()) {
                Log.e(TAG, "File does not exist: " + config.filePath);
                return 1; // å¤±è´¥
            }
            // åˆ›å»ºæ–‡ä»¶ä¼ è¾“器
            h264FileTransmitter = new H264FileTransmitter();
            // è®¾ç½®æœåŠ¡å™¨åœ°å€
            h264FileTransmitter.setServerAddress(config.ip, config.port);
            // è®¾ç½®åè®®ç±»åž‹
            h264FileTransmitter.setProtocolType(config.protocolType);
            // è®¾ç½®åè®®å‚数(SIM卡号和逻辑通道号)
            String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty()
                    ? config.simPhone : "013120122580";
            h264FileTransmitter.setProtocolParams(simPhone, (byte)1);
            // è®¾ç½®å¸§çŽ‡ï¼ˆç”¨äºŽè®¡ç®—æ—¶é—´æˆ³é—´éš”ï¼‰
            int framerate = config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE;
            h264FileTransmitter.setFrameRate(framerate);
            // è®¾ç½®è¿›åº¦å›žè°ƒï¼ˆå¯é€‰ï¼Œç”¨äºŽæ—¥å¿—输出)
            h264FileTransmitter.setOnTransmitProgressCallback(new H264FileTransmitter.OnTransmitProgressCallback() {
                @Override
                public void onProgress(int currentFrame, int totalFrames) {
                    Log.d(TAG, "File transmit progress: frame " + currentFrame +
                            (totalFrames > 0 ? " of " + totalFrames : ""));
                }
                @Override
                public void onComplete() {
                    Log.d(TAG, "File transmit completed");
                }
                @Override
                public void onError(String error) {
                    Log.e(TAG, "File transmit error: " + error);
                }
            });
            // åˆå§‹åŒ–Socket连接
            if (!h264FileTransmitter.initialize()) {
                Log.e(TAG, "Failed to initialize file transmitter socket");
                h264FileTransmitter = null;
                return 1; // å¤±è´¥
            }
            // å¼€å§‹ä¼ è¾“文件
            h264FileTransmitter.transmitFile(config.filePath);
            Log.d(TAG, "File transmit started successfully, file: " + config.filePath +
                    ", server: " + config.ip + ":" + config.port +
                    ", protocol: " + (config.protocolType == JT1076ProtocolHelper.PROTOCOL_TYPE_UDP ? "UDP" : "TCP") +
                    ", framerate: " + framerate);
            return 0; // æˆåŠŸ
        } catch (Exception e) {
            Log.e(TAG, "Failed to start file transmit", e);
            if (h264FileTransmitter != null) {
                try {
                    h264FileTransmitter.stop();
                } catch (Exception ex) {
                    Log.e(TAG, "Error stopping file transmitter after failure", ex);
                }
                h264FileTransmitter = null;
            }
            return 1; // å¤±è´¥
        }
    }
    /**
     * åœæ­¢æ–‡ä»¶ä¼ è¾“器
     */
    private int stopFileTransmitter() {
        Log.d(TAG, "Stopping file transmitter");
        if (h264FileTransmitter != null) {
            try {
                h264FileTransmitter.stop();
                h264FileTransmitter = null;
                Log.d(TAG, "File transmitter stopped successfully");
                return 0; // æˆåŠŸ
            } catch (Exception e) {
                Log.e(TAG, "Error stopping file transmitter", e);
                h264FileTransmitter = null;
                return 1; // å¤±è´¥
            }
        } else {
            Log.w(TAG, "File transmitter is not running");
            return 0; // æˆåŠŸï¼ˆæ²¡æœ‰è¿è¡Œçš„æ–‡ä»¶ä¼ è¾“å™¨ï¼Œè§†ä¸ºæˆåŠŸï¼‰
        }
    }
    /**
     * èŽ·å–èµ„æºåˆ—è¡¨ï¼ˆæ ¹æ®JT/T 1076-2016表23定义)
     * @param startTime å¼€å§‹æ—¶é—´ï¼ˆæ ¼å¼ï¼šYYMMDDHHmmss)
     * @param endTime ç»“束时间(格式:YYMMDDHHmmss)
     * @return èµ„源列表
     */
    private List<ResourceInfo> getResourceList(String startTime, String endTime) {
        Log.d(TAG, "getResourceList called, startTime: " + startTime + ", endTime: " + endTime);
        List<ResourceInfo> resourceList = new ArrayList<>();
        try {
            // æ‰«æè¾“出目录中的H264文件
            File dir = new File(outputFileDirectory);
            if (!dir.exists() || !dir.isDirectory()) {
                Log.w(TAG, "Output directory does not exist: " + outputFileDirectory);
                return resourceList;
            }
            File[] files = dir.listFiles((dir1, name) -> name.toLowerCase().endsWith(".h264"));
            if (files == null || files.length == 0) {
                Log.d(TAG, "No H264 files found in directory");
                return resourceList;
            }
            // è§£æžæ—¶é—´èŒƒå›´
            Date startDate = parseTime(startTime);
            Date endDate = parseTime(endTime);
            if (startDate == null || endDate == null) {
                Log.e(TAG, "Invalid time format, startTime: " + startTime + ", endTime: " + endTime);
                return resourceList;
            }
            // éåŽ†æ–‡ä»¶ï¼ŒæŸ¥æ‰¾åœ¨æ—¶é—´èŒƒå›´å†…çš„æ–‡ä»¶
            for (File file : files) {
                ResourceInfo resourceInfo = createResourceInfoFromFile(file, startDate, endDate);
                if (resourceInfo != null) {
                    resourceList.add(resourceInfo);
                }
            }
            Log.d(TAG, "Found " + resourceList.size() + " resources in time range");
            return resourceList;
        } catch (Exception e) {
            Log.e(TAG, "Error getting resource list", e);
            return resourceList;
        }
    }
    /**
     * ä»Žæ–‡ä»¶åˆ›å»ºèµ„源信息(如果文件在时间范围内)
     */
    private ResourceInfo createResourceInfoFromFile(File file, Date startDate, Date endDate) {
        try {
            // ä»Žæ–‡ä»¶åæˆ–文件修改时间获取文件时间
            // è¿™é‡Œå‡è®¾æ–‡ä»¶ååŒ…含时间戳,或者使用文件修改时间
            Date fileDate = new Date(file.lastModified());
            // æ£€æŸ¥æ–‡ä»¶æ—¶é—´æ˜¯å¦åœ¨æŒ‡å®šèŒƒå›´å†…
            if (fileDate.before(startDate) || fileDate.after(endDate)) {
                return null; // ä¸åœ¨æ—¶é—´èŒƒå›´å†…
            }
            // åˆ›å»ºèµ„源信息对象
            ResourceInfo resourceInfo = new ResourceInfo();
            // é€»è¾‘通道号(默认值,实际应从配置获取)
            resourceInfo.setLogicalChannelNumber((byte) 1);
            // å¼€å§‹æ—¶é—´å’Œç»“束时间(使用文件修改时间)
            SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss", Locale.CHINA);
            resourceInfo.setStartTime(bcdFormat.format(fileDate));
            // ç»“束时间可以使用文件修改时间加上一个默认时长(例如1分钟)
            long fileDuration = 60000; // é»˜è®¤1分钟,实际应该根据文件内容计算
            Date endTime = new Date(fileDate.getTime() + fileDuration);
            resourceInfo.setEndTime(bcdFormat.format(endTime));
            // æŠ¥è­¦æ ‡å¿—(默认值,实际应从文件元数据获取)
            resourceInfo.setAlarmFlag(0L);
            // éŸ³è§†é¢‘资源类型:2-视频
            resourceInfo.setResourceType((byte) 2);
            // ç æµç±»åž‹ï¼š1-主码流
            resourceInfo.setStreamType((byte) 1);
            // å­˜å‚¨å™¨ç±»åž‹ï¼š1-主存储器
            resourceInfo.setStorageType((byte) 1);
            // æ–‡ä»¶å¤§å°
            resourceInfo.setFileSize(file.length());
            return resourceInfo;
        } catch (Exception e) {
            Log.e(TAG, "Error creating resource info from file: " + file.getName(), e);
            return null;
        }
    }
    /**
     * è§£æžBCD时间字符串
     * @param timeStr æ—¶é—´å­—符串(格式:YYMMDDHHmmss)
     * @return Date对象,如果解析失败返回null
     */
    private Date parseTime(String timeStr) {
        if (timeStr == null || timeStr.length() != 12) {
            return null;
        }
        try {
            SimpleDateFormat format = new SimpleDateFormat("yyMMddHHmmss", Locale.CHINA);
            return format.parse(timeStr);
        } catch (ParseException e) {
            Log.e(TAG, "Failed to parse time: " + timeStr, e);
            return null;
        }
    }
}
app/src/main/java/com/anyun/h264/H264Encoder.java
@@ -34,6 +34,7 @@
 * // è®¾ç½®UDP服务器地址(可选)
 * encoder.setServerAddress("192.168.1.100", 8888);
 * encoder.setProtocolParams("123456789012", (byte)1);
 * encoder.setEnableNetworkTransmission(true); // å¯ç”¨TCP/UDP网络传输,false表示禁用
 *
 * // åˆå§‹åŒ–并启动
 * int[] cameraIdRange = {0, 0};
@@ -74,6 +75,9 @@
    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);
@@ -83,6 +87,7 @@
    public H264Encoder() {
        this.usbCamera = new UsbCamera();
        this.protocolHelper = new JT1076ProtocolHelper();
        protocolHelper.setProtocolType(JT1076ProtocolHelper.PROTOCOL_TYPE_TCP);//设置为tcp传输
    }
    /**
@@ -133,6 +138,15 @@
    }
    /**
     * è®¾ç½®æ˜¯å¦å¯ç”¨TCP/UDP网络传输
     * @param enable true表示启用网络传输,false表示禁用
     */
    public void setEnableNetworkTransmission(boolean enable) {
        this.enableNetworkTransmission = enable;
        Log.d(TAG, "Network transmission " + (enable ? "enabled" : "disabled"));
    }
    /**
     * åˆå§‹åŒ–摄像头和编码器
     */
    public boolean initialize(int[] cameraIdRange, String cameraName, int[] resolution, boolean ayCamera) {
@@ -178,9 +192,14 @@
            // 3. åˆå§‹åŒ–H264编码器
            initEncoder();
            // 4. åˆå§‹åŒ–UDP Socket
            if (!protocolHelper.initializeUdpSocket()) {
                return false;
            // 4. åˆå§‹åŒ–Socket(UDP或TCP,根据协议类型自动选择)
            // åªæœ‰åœ¨å¯ç”¨ç½‘络传输时才初始化Socket
            if (enableNetworkTransmission) {
                if (!protocolHelper.initializeSocket()) {
                    return false;
                }
            } else {
                Log.d(TAG, "Network transmission disabled, skipping socket initialization");
            }
            // 5. åˆå§‹åŒ–文件输出(仅创建文件,SPS/PPS在第一次输出时写入)
@@ -397,11 +416,9 @@
            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);
@@ -462,6 +479,11 @@
     * å‘送编码后的数据(按JT/T 1076-2016协议打包)
     */
    private void sendEncodedData(byte[] data, long timestamp, boolean isKeyFrame) {
        // å¦‚果未启用网络传输,直接返回
        if (!enableNetworkTransmission) {
            return;
        }
        try {
            // è®¡ç®—æ—¶é—´é—´éš”
            long currentTime = System.currentTimeMillis();
@@ -502,8 +524,8 @@
                        packetData, timestamp, dataType, packetMark,
                        lastIFrameInterval, lastFrameInterval);
                // å‘送UDP包
                protocolHelper.sendUdpPacket(rtpPacket);
                // å‘送RTP包(UDP或TCP,根据协议类型自动选择)
                protocolHelper.sendPacket(rtpPacket);
                offset += packetDataSize;
            }
@@ -548,9 +570,10 @@
            }
        }
        // å…³é—­UDP Socket
        if (protocolHelper != null) {
            protocolHelper.closeUdpSocket();
        // å…³é—­Socket(UDP或TCP,根据协议类型自动选择)
        // åªæœ‰åœ¨å¯ç”¨ç½‘络传输时才需要关闭Socket
        if (enableNetworkTransmission && protocolHelper != null) {
            protocolHelper.closeSocket();
        }
        // å…³é—­æ–‡ä»¶è¾“出
app/src/main/java/com/anyun/h264/H264FileTransmitter.java
New file
@@ -0,0 +1,511 @@
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();
    }
}
app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java
@@ -10,7 +10,7 @@
/**
 * JT/T 1076-2016 åè®®å·¥å…·ç±»
 * æä¾›UDP发送、SIM卡号BCD转换、RTP包创建等公共功能
 * æä¾›UDP/TCP发送、SIM卡号BCD转换、RTP包创建等公共功能
 */
public class JT1076ProtocolHelper {
    private static final String TAG = "JT1076ProtocolHelper";
@@ -37,11 +37,23 @@
    public static final int RTP_PAYLOAD_TYPE_VIDEO = 96;  // è§†é¢‘负载类型
    public static final int RTP_PAYLOAD_TYPE_AUDIO = 97;  // éŸ³é¢‘负载类型
    
    // UDP参数
    // ä¼ è¾“协议类型
    public static final int PROTOCOL_TYPE_UDP = 0;  // UDP协议
    public static final int PROTOCOL_TYPE_TCP = 1;  // TCP协议
    // æœåŠ¡å™¨å‚æ•°
    private String serverIp;
    private int serverPort;
    // åè®®ç±»åž‹ï¼ˆé»˜è®¤UDP)
    private int protocolType = PROTOCOL_TYPE_UDP;
    // UDP参数
    private DatagramSocket udpSocket;
    private InetAddress serverAddress;
    // TCP参数
    private JT1076TcpClient tcpClient;
    
    // RTP协议参数
    private String simCardNumber = "123456789012"; // 12位SIM卡号
@@ -49,11 +61,45 @@
    private short sequenceNumber = 0; // åŒ…序号(自动递增)
    
    /**
     * è®¾ç½®UDP服务器地址
     * è®¾ç½®æœåŠ¡å™¨åœ°å€
     */
    public void setServerAddress(String ip, int port) {
        this.serverIp = ip;
        this.serverPort = port;
        // å¦‚æžœTCP客户端已存在,更新地址
        if (tcpClient != null) {
            tcpClient.setServerAddress(ip, port);
        }
    }
    /**
     * è®¾ç½®ä¼ è¾“协议类型(UDP或TCP)
     * @param protocolType PROTOCOL_TYPE_UDP æˆ– PROTOCOL_TYPE_TCP
     */
    public void setProtocolType(int protocolType) {
        if (protocolType != PROTOCOL_TYPE_UDP && protocolType != PROTOCOL_TYPE_TCP) {
            Log.w(TAG, "Invalid protocol type: " + protocolType + ", using UDP");
            protocolType = PROTOCOL_TYPE_UDP;
        }
        // å¦‚果协议类型改变,先关闭旧的连接
        if (this.protocolType != protocolType) {
            if (this.protocolType == PROTOCOL_TYPE_UDP) {
                closeUdpSocket();
            } else {
                closeTcpSocket();
            }
        }
        this.protocolType = protocolType;
        Log.d(TAG, "Protocol type set to: " + (protocolType == PROTOCOL_TYPE_UDP ? "UDP" : "TCP"));
    }
    /**
     * èŽ·å–å½“å‰åè®®ç±»åž‹
     */
    public int getProtocolType() {
        return protocolType;
    }
    
    /**
@@ -62,6 +108,17 @@
    public void setProtocolParams(String simCardNumber, byte logicalChannelNumber) {
        this.simCardNumber = simCardNumber;
        this.logicalChannelNumber = logicalChannelNumber;
    }
    /**
     * åˆå§‹åŒ–Socket(根据协议类型自动选择UDP或TCP)
     */
    public boolean initializeSocket() {
        if (protocolType == PROTOCOL_TYPE_UDP) {
            return initializeUdpSocket();
        } else {
            return initializeTcpSocket();
        }
    }
    
    /**
@@ -85,6 +142,59 @@
    }
    
    /**
     * åˆå§‹åŒ–TCP Socket
     */
    public boolean initializeTcpSocket() {
        try {
            if (serverIp == null || serverIp.isEmpty()) {
                Log.e(TAG, "Server IP not set");
                return false;
            }
            if (tcpClient == null) {
                tcpClient = new JT1076TcpClient();
                tcpClient.setServerAddress(serverIp, serverPort);
                // è®¾ç½®è¿žæŽ¥çŠ¶æ€ç›‘å¬å™¨
                tcpClient.setConnectionListener(new JT1076TcpClient.ConnectionListener() {
                    @Override
                    public void onConnected() {
                        Log.d(TAG, "TCP connection established");
                    }
                    @Override
                    public void onDisconnected() {
                        Log.d(TAG, "TCP connection disconnected");
                    }
                    @Override
                    public void onError(Throwable cause) {
                        Log.e(TAG, "TCP connection error", cause);
                    }
                });
            }
            tcpClient.connect();
            Log.d(TAG, "TCP socket initializing, target: " + serverIp + ":" + serverPort);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "Initialize TCP socket failed", e);
            return false;
        }
    }
    /**
     * å…³é—­Socket(根据协议类型自动选择)
     */
    public void closeSocket() {
        if (protocolType == PROTOCOL_TYPE_UDP) {
            closeUdpSocket();
        } else {
            closeTcpSocket();
        }
    }
    /**
     * å…³é—­UDP Socket
     */
    public void closeUdpSocket() {
@@ -100,7 +210,28 @@
    }
    
    /**
     * å‘送UDP包
     * å…³é—­TCP Socket
     */
    public void closeTcpSocket() {
        if (tcpClient != null) {
            tcpClient.disconnect();
            tcpClient = null;
        }
    }
    /**
     * å‘送RTP包(根据协议类型自动选择UDP或TCP)
     */
    public void sendPacket(byte[] packet) {
        if (protocolType == PROTOCOL_TYPE_UDP) {
            sendUdpPacket(packet);
        } else {
            sendTcpPacket(packet);
        }
    }
    /**
     * å‘送UDP包(保持向后兼容)
     */
    public void sendUdpPacket(byte[] packet) {
        try {
@@ -117,6 +248,17 @@
    }
    
    /**
     * å‘送TCP包
     */
    public void sendTcpPacket(byte[] packet) {
        if (tcpClient != null && tcpClient.isConnected()) {
            tcpClient.sendPacket(packet);
        } else {
            Log.w(TAG, "TCP socket not connected");
        }
    }
    /**
     * å°†SIM卡号转换为BCD格式(6字节)
     */
    public byte[] convertSimToBCD(String simNumber) {
app/src/main/java/com/anyun/h264/JT1076TcpClient.java
New file
@@ -0,0 +1,260 @@
package com.anyun.h264;
import android.util.Log;
import com.anyun.h264.util.BytesUtils;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.util.concurrent.TimeUnit;
/**
 * JT/T 1076-2016 TCP客户端工具类
 * ä½¿ç”¨Netty实现TCP连接和RTP包发送
 */
public class JT1076TcpClient {
    private static final String TAG = "JT1076TcpClient";
    private String serverIp;
    private int serverPort;
    private EventLoopGroup workerGroup;
    private Channel channel;
    private boolean isConnected = false;
    // è¿žæŽ¥çŠ¶æ€å›žè°ƒæŽ¥å£
    public interface ConnectionListener {
        void onConnected();
        void onDisconnected();
        void onError(Throwable cause);
    }
    private ConnectionListener connectionListener;
    /**
     * è®¾ç½®æœåŠ¡å™¨åœ°å€
     */
    public void setServerAddress(String ip, int port) {
        this.serverIp = ip;
        this.serverPort = port;
    }
    /**
     * è®¾ç½®è¿žæŽ¥çŠ¶æ€ç›‘å¬å™¨
     */
    public void setConnectionListener(ConnectionListener listener) {
        this.connectionListener = listener;
    }
    /**
     * åˆå§‹åŒ–TCP连接(异步)
     */
    public void connect() {
        if (serverIp == null || serverIp.isEmpty()) {
            Log.e(TAG, "Server IP not set");
            if (connectionListener != null) {
                connectionListener.onError(new IllegalArgumentException("Server IP not set"));
            }
            return;
        }
        if (workerGroup != null) {
            Log.w(TAG, "TCP client already initialized, disconnecting first");
            disconnect();
        }
        // åˆ›å»ºEventLoopGroup(使用单线程组即可)
        workerGroup = new NioEventLoopGroup(1);
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(workerGroup)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true) // ç¦ç”¨Nagle算法,降低延迟
                    .option(ChannelOption.SO_KEEPALIVE, true) // å¯ç”¨TCP keepalive
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // è¿žæŽ¥è¶…æ—¶5秒
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new TcpClientHandler());
                        }
                    });
            Log.d(TAG, "Connecting to TCP server: " + serverIp + ":" + serverPort);
            // å¼‚步连接
            ChannelFuture future = bootstrap.connect(serverIp, serverPort);
            future.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (future.isSuccess()) {
                        channel = future.channel();
                        isConnected = true;
                        Log.d(TAG, "TCP connection established: " + serverIp + ":" + serverPort);
                        if (connectionListener != null) {
                            connectionListener.onConnected();
                        }
                    } else {
                        isConnected = false;
                        Throwable cause = future.cause();
                        Log.e(TAG, "TCP connection failed: " + serverIp + ":" + serverPort, cause);
                        if (connectionListener != null) {
                            connectionListener.onError(cause);
                        }
                        // è¿žæŽ¥å¤±è´¥æ—¶æ¸…理资源
                        shutdown();
                    }
                }
            });
        } catch (Exception e) {
            Log.e(TAG, "Initialize TCP client failed", e);
            isConnected = false;
            if (connectionListener != null) {
                connectionListener.onError(e);
            }
            shutdown();
        }
    }
    /**
     * å‘送RTP包(TCP方式)
     */
    public void sendPacket(byte[] packet) {
        if (!isConnected || channel == null || !channel.isActive()) {
            Log.w(TAG, "TCP channel not connected, packet dropped");
            return;
        }
        try {
            // å°†å­—节数组包装为ByteBuf
            ByteBuf buffer = Unpooled.wrappedBuffer(packet);
            String str = BytesUtils.bytesToHexString(  BytesUtils.subArray(buffer.array(),0,30));
            Log.i(TAG, "Send TCP packet:"+ str);
            // å¼‚步写入
            ChannelFuture future = channel.writeAndFlush(buffer);
            // å¯é€‰ï¼šç›‘听发送结果
            future.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (!future.isSuccess()) {
                        Log.e(TAG, "Send TCP packet failed", future.cause());
                    }
                }
            });
        } catch (Exception e) {
            Log.e(TAG, "Send TCP packet error", e);
        }
    }
    /**
     * æ–­å¼€TCP连接
     */
    public void disconnect() {
        isConnected = false;
        if (channel != null) {
            try {
                ChannelFuture future = channel.close();
                future.await(2, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Log.w(TAG, "Close channel interrupted", e);
                Thread.currentThread().interrupt();
            } catch (Exception e) {
                Log.e(TAG, "Close channel error", e);
            }
            channel = null;
        }
        shutdown();
        if (connectionListener != null) {
            connectionListener.onDisconnected();
        }
        Log.d(TAG, "TCP connection closed");
    }
    /**
     * å…³é—­EventLoopGroup(清理资源)
     */
    private void shutdown() {
        if (workerGroup != null) {
            try {
                workerGroup.shutdownGracefully().await(3, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Log.w(TAG, "Shutdown worker group interrupted", e);
                Thread.currentThread().interrupt();
            } catch (Exception e) {
                Log.e(TAG, "Shutdown worker group error", e);
            }
            workerGroup = null;
        }
    }
    /**
     * æ£€æŸ¥æ˜¯å¦å·²è¿žæŽ¥
     */
    public boolean isConnected() {
        return isConnected && channel != null && channel.isActive();
    }
    /**
     * TCP客户端通道处理器
     */
    private class TcpClientHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            super.channelActive(ctx);
            Log.d(TAG, "TCP channel active: " + ctx.channel().remoteAddress());
            isConnected = true;
        }
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            super.channelInactive(ctx);
            Log.d(TAG, "TCP channel inactive: " + ctx.channel().remoteAddress());
            isConnected = false;
            channel = null;
            if (connectionListener != null) {
                connectionListener.onDisconnected();
            }
        }
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            Log.e(TAG, "TCP channel exception", cause);
            isConnected = false;
            if (connectionListener != null) {
                connectionListener.onError(cause);
            }
            ctx.close();
        }
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            // å¦‚果服务器有响应,可以在这里处理
            // å¯¹äºŽJT/T 1076-2016 RTP发送,通常不需要处理响应
            ByteBuf buf = (ByteBuf) msg;
            Log.d(TAG, "Received data from server: " + buf.readableBytes() + " bytes");
            buf.release(); // é‡Šæ”¾ByteBuf
        }
    }
}
app/src/main/java/com/anyun/h264/MainActivity.kt
@@ -69,8 +69,10 @@
            h264Encoder?.setEnableFileOutput(true) // å¯ç”¨æ–‡ä»¶è¾“出
            
            // è®¾ç½®UDP服务器地址(可选)
             h264Encoder?.setServerAddress("192.168.1.100", 8888)
             h264Encoder?.setProtocolParams("123456789012", 1)
//             h264Encoder?.setServerAddress("58.48.93.67", 11935)
            h264Encoder?.setEnableNetworkTransmission(false)
             h264Encoder?.setServerAddress("192.168.16.12", 11935)
             h264Encoder?.setProtocolParams("013120122580", 1)
            
            // åˆå§‹åŒ–并启动
            val cameraIdRange = intArrayOf(1, 2)
app/src/main/java/com/anyun/h264/model/ResourceInfo.java
New file
@@ -0,0 +1,157 @@
package com.anyun.h264.model;
import android.os.Parcel;
import android.os.Parcelable;
/**
 * éŸ³è§†é¢‘资源信息(根据JT/T 1076-2016表23定义)
 * ç»ˆç«¯ä¸Šä¼ éŸ³è§†é¢‘资源列表格式
 */
public class ResourceInfo implements Parcelable {
    /** é€»è¾‘通道号(根据JT/T 1076—2016表2) */
    private byte logicalChannelNumber;
    /** å¼€å§‹æ—¶é—´ï¼ˆBCD[6]格式:YY-MM-DD-HH-MM-SS) */
    private String startTime; // BCD编码的6字节,格式:YYMMDDHHmmss
    /** ç»“束时间(BCD[6]格式:YY-MM-DD-HH-MM-SS) */
    private String endTime; // BCD编码的6字节,格式:YYMMDDHHmmss
    /** æŠ¥è­¦æ ‡å¿—(64位) */
    private long alarmFlag;
    /** éŸ³è§†é¢‘资源类型:0-音视频,1-音频,2-视频 */
    private byte resourceType;
    /** ç æµç±»åž‹ï¼š1-主码流,2-子码流 */
    private byte streamType;
    /** å­˜å‚¨å™¨ç±»åž‹ï¼š1-主存储器,2-灾备存储器 */
    private byte storageType;
    /** æ–‡ä»¶å¤§å°ï¼ˆå•位:字节) */
    private long fileSize;
    public ResourceInfo() {
    }
    protected ResourceInfo(Parcel in) {
        logicalChannelNumber = in.readByte();
        startTime = in.readString();
        endTime = in.readString();
        alarmFlag = in.readLong();
        resourceType = in.readByte();
        streamType = in.readByte();
        storageType = in.readByte();
        fileSize = in.readLong();
    }
    public static final Creator<ResourceInfo> CREATOR = new Creator<ResourceInfo>() {
        @Override
        public ResourceInfo createFromParcel(Parcel in) {
            return new ResourceInfo(in);
        }
        @Override
        public ResourceInfo[] newArray(int size) {
            return new ResourceInfo[size];
        }
    };
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeByte(logicalChannelNumber);
        dest.writeString(startTime);
        dest.writeString(endTime);
        dest.writeLong(alarmFlag);
        dest.writeByte(resourceType);
        dest.writeByte(streamType);
        dest.writeByte(storageType);
        dest.writeLong(fileSize);
    }
    @Override
    public int describeContents() {
        return 0;
    }
    // Getters and Setters
    public byte getLogicalChannelNumber() {
        return logicalChannelNumber;
    }
    public void setLogicalChannelNumber(byte logicalChannelNumber) {
        this.logicalChannelNumber = logicalChannelNumber;
    }
    public String getStartTime() {
        return startTime;
    }
    public void setStartTime(String startTime) {
        this.startTime = startTime;
    }
    public String getEndTime() {
        return endTime;
    }
    public void setEndTime(String endTime) {
        this.endTime = endTime;
    }
    public long getAlarmFlag() {
        return alarmFlag;
    }
    public void setAlarmFlag(long alarmFlag) {
        this.alarmFlag = alarmFlag;
    }
    public byte getResourceType() {
        return resourceType;
    }
    public void setResourceType(byte resourceType) {
        this.resourceType = resourceType;
    }
    public byte getStreamType() {
        return streamType;
    }
    public void setStreamType(byte streamType) {
        this.streamType = streamType;
    }
    public byte getStorageType() {
        return storageType;
    }
    public void setStorageType(byte storageType) {
        this.storageType = storageType;
    }
    public long getFileSize() {
        return fileSize;
    }
    public void setFileSize(long fileSize) {
        this.fileSize = fileSize;
    }
    @Override
    public String toString() {
        return "ResourceInfo{" +
                "logicalChannelNumber=" + logicalChannelNumber +
                ", startTime='" + startTime + '\'' +
                ", endTime='" + endTime + '\'' +
                ", alarmFlag=" + alarmFlag +
                ", resourceType=" + resourceType +
                ", streamType=" + streamType +
                ", storageType=" + storageType +
                ", fileSize=" + fileSize +
                '}';
    }
}
app/src/main/java/com/anyun/h264/service/H264EncodeServiceClient.java
New file
@@ -0,0 +1,206 @@
package com.anyun.h264.service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.anyun.h264.IH264EncodeService;
import com.anyun.h264.model.ResourceInfo;
import java.util.List;
/**
 * H264编码服务客户端
 * ç”¨äºŽç»‘定服务并调用AIDL接口
 *
 * ä½¿ç”¨ç¤ºä¾‹ï¼š
 * <pre>
 * // åˆ›å»ºå®¢æˆ·ç«¯
 * H264EncodeServiceClient client = new H264EncodeServiceClient(context);
 *
 * // ç»‘定服务
 * client.bindService();
 *
 * // ç­‰å¾…服务绑定完成后,调用接口
 * // å¼€å¯æ–‡ä»¶ç¼–码(带配置参数)
 * String jsonConfig = "{\"width\":640,\"height\":480,\"framerate\":25}";
 * int result = client.controlEncode(0, jsonConfig);
 *
 * // å¼€å¯ç½‘络推送(带配置参数)
 * String networkConfig = "{\"ip\":\"192.168.1.100\",\"port\":8888,\"width\":1280,\"height\":720,\"framerate\":30}";
 * result = client.controlEncode(2, networkConfig);
 *
 * // åœæ­¢ç¼–码(不需要配置参数)
 * client.controlEncode(1, null);
 *
 * // èŽ·å–èµ„æºåˆ—è¡¨
 * List<ResourceInfo> resources = client.getResourceList("240101000000", "240101235959");
 *
 * // è§£ç»‘服务
 * client.unbindService();
 * </pre>
 */
public class H264EncodeServiceClient {
    private static final String TAG = "H264EncodeClient";
    private Context context;
    private IH264EncodeService service;
    private boolean isBound = false;
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            Log.d(TAG, "Service connected");
            service = IH264EncodeService.Stub.asInterface(binder);
            isBound = true;
            // å¯ä»¥åœ¨è¿™é‡Œæ·»åŠ æœåŠ¡è¿žæŽ¥æˆåŠŸçš„å›žè°ƒ
            if (onServiceConnectedListener != null) {
                onServiceConnectedListener.onConnected();
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.d(TAG, "Service disconnected");
            service = null;
            isBound = false;
            // å¯ä»¥åœ¨è¿™é‡Œæ·»åŠ æœåŠ¡æ–­å¼€è¿žæŽ¥çš„å›žè°ƒ
            if (onServiceDisconnectedListener != null) {
                onServiceDisconnectedListener.onDisconnected();
            }
        }
    };
    // æœåŠ¡è¿žæŽ¥å›žè°ƒæŽ¥å£
    public interface OnServiceConnectedListener {
        void onConnected();
    }
    public interface OnServiceDisconnectedListener {
        void onDisconnected();
    }
    private OnServiceConnectedListener onServiceConnectedListener;
    private OnServiceDisconnectedListener onServiceDisconnectedListener;
    public H264EncodeServiceClient(Context context) {
        this.context = context.getApplicationContext();
    }
    /**
     * è®¾ç½®æœåŠ¡è¿žæŽ¥ç›‘å¬å™¨
     */
    public void setOnServiceConnectedListener(OnServiceConnectedListener listener) {
        this.onServiceConnectedListener = listener;
    }
    /**
     * è®¾ç½®æœåŠ¡æ–­å¼€è¿žæŽ¥ç›‘å¬å™¨
     */
    public void setOnServiceDisconnectedListener(OnServiceDisconnectedListener listener) {
        this.onServiceDisconnectedListener = listener;
    }
    /**
     * ç»‘定服务
     * @return æ˜¯å¦æˆåŠŸå¯åŠ¨ç»‘å®š
     */
    public boolean bindService() {
        if (isBound) {
            Log.w(TAG, "Service is already bound");
            return true;
        }
        Intent intent = new Intent();
        intent.setComponent(new ComponentName(context, "com.anyun.h264.H264EncodeService"));
        boolean result = context.bindService(intent, connection, Context.BIND_AUTO_CREATE);
        if (!result) {
            Log.e(TAG, "Failed to bind service");
        }
        return result;
    }
    /**
     * è§£ç»‘服务
     */
    public void unbindService() {
        if (isBound) {
            context.unbindService(connection);
            isBound = false;
            service = null;
            Log.d(TAG, "Service unbound");
        }
    }
    /**
     * æ£€æŸ¥æœåŠ¡æ˜¯å¦å·²ç»‘å®š
     */
    public boolean isServiceBound() {
        return isBound && service != null;
    }
    /**
     * æŽ§åˆ¶H264编码
     * @param action æ“ä½œç±»åž‹ï¼š0-开启h264文件写入,1-停止h264编码并停止写入文件,2-开启网络推送h264(不写入文件),3-停止h264编码并停止网络推送
     * @param jsonConfig JSON格式的配置参数,包含:ip(服务器IP)、port(服务器端口)、width(视频宽度)、height(视频高度)、framerate(帧率)、simPhone(SIM卡号)
     *                  ç¤ºä¾‹ï¼š{"ip":"192.168.1.100","port":8888,"width":640,"height":480,"framerate":25,"simPhone":"013120122580"}
     *                  å¦‚æžœaction为1或3(停止操作),此参数可为null
     * @return 0-成功,1-失败
     */
    public int controlEncode(int action, String jsonConfig) {
        if (!isServiceBound()) {
            Log.e(TAG, "Service is not bound");
            return 1; // å¤±è´¥
        }
        try {
            int result = service.controlEncode(action, jsonConfig);
            Log.d(TAG, "controlEncode(" + action + ", " + jsonConfig + ") returned: " + result);
            return result;
        } catch (RemoteException e) {
            Log.e(TAG, "Error calling controlEncode", e);
            return 1; // å¤±è´¥
        }
    }
    /**
     * æŽ§åˆ¶H264编码(重载方法,兼容旧代码,使用null作为jsonConfig)
     * @param action æ“ä½œç±»åž‹ï¼š0-开启h264文件写入,1-停止h264编码并停止写入文件,2-开启网络推送h264(不写入文件),3-停止h264编码并停止网络推送
     * @return 0-成功,1-失败
     * @deprecated å»ºè®®ä½¿ç”¨ controlEncode(int action, String jsonConfig) æ–¹æ³•,传入完整的配置参数
     */
    @Deprecated
    public int controlEncode(int action) {
        return controlEncode(action, null);
    }
    /**
     * èŽ·å–èµ„æºåˆ—è¡¨
     * @param startTime å¼€å§‹æ—¶é—´ï¼ˆæ ¼å¼ï¼šYYMMDDHHmmss,例如:240101000000)
     * @param endTime ç»“束时间(格式:YYMMDDHHmmss,例如:240101235959)
     * @return èµ„源列表,如果失败返回null
     */
    public List<ResourceInfo> getResourceList(String startTime, String endTime) {
        if (!isServiceBound()) {
            Log.e(TAG, "Service is not bound");
            return null;
        }
        try {
            List<ResourceInfo> result = service.getResourceList(startTime, endTime);
            Log.d(TAG, "getResourceList returned " + (result != null ? result.size() : 0) + " resources");
            return result;
        } catch (RemoteException e) {
            Log.e(TAG, "Error calling getResourceList", e);
            return null;
        }
    }
}
app/src/main/java/com/anyun/h264/util/BytesUtils.java
New file
@@ -0,0 +1,212 @@
package com.anyun.h264.util;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public final class BytesUtils {
    public static String bytesToHexString(byte[] src,int len){
        return bytesToHexString(src,0,len);
    }
    public static String bytesToHexString(byte[] src){
        return bytesToHexString(src,0,src.length);
    }
    public static String byteToHexString(byte b)
    {
        int v = b & 0xFF;
        return Integer.toHexString(v);
    }
    public static float bytesToFloat(byte[] src, int start, int len) {
        ByteBuffer buffer = ByteBuffer.wrap(src, start, len);
        return buffer.getFloat();
    }
    public static String bytesToHexString(byte[] src,int start,int len){
        StringBuilder stringBuilder = new StringBuilder("");
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = start; i < start + len; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString().toUpperCase();
    }
    public static byte[] hexStringToBytes(String hexString) {
        if (hexString == null || hexString.equals("")) {
            return null;
        }
        hexString = hexString.toUpperCase();
        int length = hexString.length() / 2;
        char[] hexChars = hexString.toCharArray();
        byte[] d = new byte[length];
        for (int i = 0; i < length; i++) {
            int pos = i * 2;
            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
        }
        return d;
    }
    private static byte charToByte(char c) {
        return (byte) "0123456789ABCDEF".indexOf(c);
    }
    public static byte xor(byte[] bytes,int start,int size)
    {
        if (bytes.length < size)
        {
            throw new RuntimeException("计算xor的时候size不足");
        }
        int s1 = bytes[start];
        for (int i = start + 1; i < size; i++)
        {
            s1 = s1 ^ bytes[i];
        }
        return (byte)s1;
    }
    public static int ccitt(byte[] data, int start, int size) {
        final int []table = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5,
                0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b,
                0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210,
                0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
                0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c,
                0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401,
                0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b,
                0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
                0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6,
                0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738,
                0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5,
                0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
                0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969,
                0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96,
                0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc,
                0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
                0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03,
                0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd,
                0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6,
                0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
                0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a,
                0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb,
                0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1,
                0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
                0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c,
                0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2,
                0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb,
                0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
                0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447,
                0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8,
                0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2,
                0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
                0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9,
                0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827,
                0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c,
                0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
                0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0,
                0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d,
                0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07,
                0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
                0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba,
                0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74,
                0x2e93, 0x3eb2, 0x0ed1, 0x1ef0};
        int crc = 0;
        int finall = 0;
        for (int i = start; i < start + size; i++) {
            int temp = (data[i] ^ (crc >> 8)) & 0xff;
            crc = table[temp] ^ (crc << 8);
        }
        return crc ^ finall;
    }
    public static byte xor(byte[] bytes)
    {
        return xor(bytes,0,bytes.length);
    }
    public static String toHexString(byte b)
    {
        String hex = Integer.toHexString(b&0xff).toUpperCase();
        if(hex.length()==1)
        {
            return "0"+hex;
        }
        return hex;
    }
    public static int Sum(byte[] bytes)
    {
        int sum = 0;
        for(byte b:bytes)
        {
            sum+=b;
        }
        return sum;
    }
    public static boolean isAllZero(byte[] bytes)
    {
        for (byte b:bytes) {
            if (b != 0) {
                return false;
            }
        }
        return true;
    }
    public static int getInt32(byte[] bytes,int start){
        int result = 0;
        int a = (bytes[0+start] & 0xff) << 24;
        int b = (bytes[1+start] & 0xff) << 16;
        int c = (bytes[2+start] & 0xff) << 8;
        int d = (bytes[3+start] & 0xff);
        result = a | b | c | d;
        return result;
    }
    public static byte[] subArray(byte[] array,int start,int len)
    {
        byte[] result = new byte[len];
        System.arraycopy(array,start,result,0,len);
        return result;
    }
    private static int findPos(byte[] array,byte find,int start)
    {
        for(int i=start;i<array.length;i++)
        {
            if(array[i]==find) return i;
        }
        return -1;
    }
    public static List<byte[]> splitArray(byte[] bytes, byte spliter)
    {
        ArrayList<byte[]> list = new ArrayList();
        int start = 0;
        while (true) {
            int pos = findPos(bytes, spliter,start);
            if(pos>=0)
            {
                byte[] findbytes = subArray(bytes,start,pos - start);
                list.add(findbytes);
                start = pos + 1;
            }
            else
            {
                break;
            }
        }
        //添加最后一个数组
        byte[] findbytes = subArray(bytes,start,bytes.length - start);
        list.add(findbytes);
        return list;
    }
}
check_h264.py
New file
@@ -0,0 +1,209 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
H264文件检查工具
检查生成的test.h264文件是否符合播放要求
"""
import os
import sys
def check_h264_file(file_path):
    """检查H264文件是否符合播放要求"""
    print(f"检查文件: {file_path}\n")
    # 1. æ£€æŸ¥æ–‡ä»¶æ˜¯å¦å­˜åœ¨
    if not os.path.exists(file_path):
        print("❌ æ–‡ä»¶ä¸å­˜åœ¨ï¼")
        print(f"   é¢„期路径: {file_path}")
        print("\n提示:")
        print("   - å¦‚果是Android设备,需要先通过adb pull下载文件")
        print(f"   - æˆ–者将文件复制到当前目录")
        return False
    # 2. æ£€æŸ¥æ–‡ä»¶å¤§å°
    file_size = os.path.getsize(file_path)
    print(f"✓ æ–‡ä»¶å¤§å°: {file_size} å­—节 ({file_size / 1024:.2f} KB)")
    if file_size == 0:
        print("❌ æ–‡ä»¶ä¸ºç©ºï¼")
        return False
    if file_size < 100:
        print("⚠️  è­¦å‘Š: æ–‡ä»¶å¤ªå°ï¼Œå¯èƒ½åªæœ‰SPS/PPS或1帧数据")
    # 3. è¯»å–文件内容并分析
    try:
        with open(file_path, 'rb') as f:
            data = f.read()
    except Exception as e:
        print(f"❌ è¯»å–文件失败: {e}")
        return False
    # æ£€æŸ¥NALU单元
    nal_start_code_3 = bytes([0x00, 0x00, 0x01])
    nal_start_code_4 = bytes([0x00, 0x00, 0x00, 0x01])
    nalu_list = []
    i = 0
    # æŸ¥æ‰¾æ‰€æœ‰NALU单元
    while i < len(data):
        # æŸ¥æ‰¾èµ·å§‹ç 
        found = False
        # æŸ¥æ‰¾4字节起始码
        if i + 4 <= len(data) and data[i:i+4] == nal_start_code_4:
            start_pos = i + 4
            found = True
        # æŸ¥æ‰¾3字节起始码
        elif i + 3 <= len(data) and data[i:i+3] == nal_start_code_3:
            # ç¡®ä¿å‰é¢ä¸æ˜¯0x00 (避免误判)
            if i == 0 or data[i-1] != 0x00:
                start_pos = i + 3
                found = True
        if found:
            # æŸ¥æ‰¾ä¸‹ä¸€ä¸ªèµ·å§‹ç 
            next_start = -1
            # å…ˆæ‰¾4字节起始码
            for j in range(start_pos, len(data) - 3):
                if j + 4 <= len(data) and data[j:j+4] == nal_start_code_4:
                    next_start = j
                    break
            # å¦‚果没找到4字节的,找3字节的
            if next_start == -1:
                for j in range(start_pos, len(data) - 2):
                    if j + 3 <= len(data) and data[j:j+3] == nal_start_code_3:
                        # ç¡®ä¿å‰é¢ä¸æ˜¯0x00
                        if j == 0 or data[j-1] != 0x00:
                            next_start = j
                            break
            # æå–NALU数据
            if next_start == -1:
                nalu_data = data[start_pos:]
            else:
                nalu_data = data[start_pos:next_start]
            if len(nalu_data) > 0:
                # èŽ·å–NALU类型 (第一个字节的低5位)
                nal_type = nalu_data[0] & 0x1F
                nalu_list.append({
                    'type': nal_type,
                    'size': len(nalu_data),
                    'name': get_nalu_type_name(nal_type)
                })
            i = next_start if next_start != -1 else len(data)
        else:
            i += 1
    # 4. åˆ†æžNALU单元
    print(f"\n✓ æ‰¾åˆ° {len(nalu_list)} ä¸ªNALU单元\n")
    if len(nalu_list) == 0:
        print("❌ æœªæ‰¾åˆ°ä»»ä½•NALU单元!文件格式可能不正确")
        return False
    # ç»Ÿè®¡ä¸åŒç±»åž‹çš„NALU
    nal_type_count = {}
    for nalu in nalu_list:
        nal_type = nalu['type']
        if nal_type not in nal_type_count:
            nal_type_count[nal_type] = 0
        nal_type_count[nal_type] += 1
    print("NALU类型统计:")
    for nal_type in sorted(nal_type_count.keys()):
        count = nal_type_count[nal_type]
        name = get_nalu_type_name(nal_type)
        print(f"  {name} (类型{nal_type}): {count} ä¸ª")
    # 5. æ£€æŸ¥å…³é”®è¦æ±‚
    print("\n检查项:")
    has_sps = 7 in nal_type_count
    has_pps = 8 in nal_type_count
    has_idr = 5 in nal_type_count
    has_non_idr = 1 in nal_type_count
    # SPS/PPS检查
    if has_sps:
        print(f"  âœ“ åŒ…含SPS (序列参数集) - {nal_type_count[7]} ä¸ª")
    else:
        print("  âŒ ç¼ºå°‘SPS (序列参数集) - å¿…需!")
    if has_pps:
        print(f"  âœ“ åŒ…含PPS (图像参数集) - {nal_type_count[8]} ä¸ª")
    else:
        print("  âŒ ç¼ºå°‘PPS (图像参数集) - å¿…需!")
    # å…³é”®å¸§æ£€æŸ¥
    if has_idr:
        print(f"  âœ“ åŒ…含IDR关键帧 - {nal_type_count[5]} ä¸ª")
    else:
        print("  âŒ ç¼ºå°‘IDR关键帧")
    # éžå…³é”®å¸§æ£€æŸ¥
    if has_non_idr:
        print(f"  âœ“ åŒ…含非IDR帧 - {nal_type_count[1]} ä¸ª")
    else:
        print("  âš ï¸  æ²¡æœ‰éžIDR帧(只有关键帧)")
    # 6. æ€»ä½“评估
    print("\n" + "="*50)
    can_play = has_sps and has_pps and has_idr
    if can_play:
        if len(nalu_list) >= 3:
            print("✅ æ–‡ä»¶åº”该可以播放!")
            print(f"   åŒ…含完整的SPS/PPS和 {len(nalu_list)} ä¸ªNALU单元")
        else:
            print("⚠️  æ–‡ä»¶ç»“构完整,但帧数较少")
            print("   å»ºè®®å½•制更长时间以获得更多帧")
    else:
        print("❌ æ–‡ä»¶å¯èƒ½æ— æ³•播放")
        if not has_sps or not has_pps:
            print("   åŽŸå› : ç¼ºå°‘SPS/PPS参数集")
        if not has_idr:
            print("   åŽŸå› : ç¼ºå°‘IDR关键帧")
    print("="*50)
    return can_play
def get_nalu_type_name(nal_type):
    """获取NALU类型名称"""
    nal_names = {
        1: "非IDR编码片",
        2: "编码片数据分区A",
        3: "编码片数据分区B",
        4: "编码片数据分区C",
        5: "IDR图像编码片",
        6: "SEI (补充增强信息)",
        7: "SPS (序列参数集)",
        8: "PPS (图像参数集)",
        9: "访问单元分隔符",
        10: "序列结束",
        11: "流结束",
        12: "填充数据"
    }
    return nal_names.get(nal_type, f"未知类型{nal_type}")
if __name__ == "__main__":
    # é»˜è®¤æ–‡ä»¶è·¯å¾„
    default_path = "test.h264"
    # å¦‚果提供了命令行参数,使用参数作为文件路径
    file_path = sys.argv[1] if len(sys.argv) > 1 else default_path
    print("H264文件检查工具")
    print("="*50)
    success = check_h264_file(file_path)
    sys.exit(0 if success else 1)
gradle/libs.versions.toml
@@ -8,6 +8,7 @@
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.04.01"
netty = "4.1.48.Final"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -24,6 +25,7 @@
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
netty-all = { group = "io.netty", name = "netty-all", version.ref = "netty" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
gradlew
New file
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
##  Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
    echo "$*"
}
die () {
    echo
    echo "$*"
    echo
    exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
  CYGWIN* )
    cygwin=true
    ;;
  Darwin* )
    darwin=true
    ;;
  MINGW* )
    msys=true
    ;;
  NONSTOP* )
    nonstop=true
    ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD="$JAVA_HOME/jre/sh/java"
    else
        JAVACMD="$JAVA_HOME/bin/java"
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD="java"
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
            MAX_FD="$MAX_FD_LIMIT"
        fi
        ulimit -n $MAX_FD
        if [ $? -ne 0 ] ; then
            warn "Could not set maximum file descriptor limit: $MAX_FD"
        fi
    else
        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
    JAVACMD=`cygpath --unix "$JAVACMD"`
    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
        ROOTDIRS="$ROOTDIRS$SEP$dir"
        SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
        else
            eval `echo args$i`="\"$arg\""
        fi
        i=`expr $i + 1`
    done
    case $i in
        0) set -- ;;
        1) set -- "$args0" ;;
        2) set -- "$args0" "$args1" ;;
        3) set -- "$args0" "$args1" "$args2" ;;
        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
fi
# Escape application args
save () {
    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
    echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
gradlew.bat
New file
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
key/Verify.txt
New file
@@ -0,0 +1,41 @@
    private static PrivateKey getKey(String key, char[] password) throws Exception {
        byte[] cabuf = new BASE64Decoder().decodeBuffer(key);
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        keyStore.load(new ByteArrayInputStream(cabuf), password);
        Enumeration<String> aliases = keyStore.aliases();
        if (!aliases.hasMoreElements()) {
            throw new RuntimeException("no alias found");
        }
        String alias = aliases.nextElement();
        PrivateKey privateKey = (RSAPrivateCrtKeyImpl) keyStore.getKey(alias, password);
        return privateKey;
    }
    public static String sign(String data, PrivateKey key) throws Exception {
//        MessageDigest md = MessageDigest.getInstance("SHA-256");
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        md.update(data.getBytes("utf-8"));
        byte[] hash = md.digest();
//        Cipher cipher = Cipher.getInstance("RSA");
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] encrypted = cipher.doFinal(hash);
        return HexBin.encode(encrypted);
    }
    public static String sign(String data, String keyString ,String pwd ) throws Exception {
        char[] password = pwd.toCharArray();
        return sign(data,getKey(keyString,password));
    }
    public static void main(String[] args) {
        try{
            String sign = sign("xuanborobot123456nullnullnull","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","27U777");
            System.out.println(sign);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
key/bjkey.jks
Binary files differ
key/key.jks
Binary files differ
key/key.jks.ori
Binary files differ
key/keyrk3288.jks
Binary files differ
key/keysc200.jks
Binary files differ
key/keysc626.jks
Binary files differ
keystore.properties
New file
@@ -0,0 +1,4 @@
storePassword = 123456
keyPassword = 123456
keyAlias = key0
storeFile = ../key/key.jks
test.h264
Binary files differ
ÈçºÎ¼ì²étest.h264Îļþ.md
New file
@@ -0,0 +1,184 @@
# å¦‚何检查test.h264文件能否播放
## å¿«é€Ÿåˆ¤æ–­æ–¹æ³•
### æ–¹æ³•1: ç›´æŽ¥ç”¨æ’­æ”¾å™¨æµ‹è¯•(最简单)
1. **从Android设备下载文件**
   ```bash
   adb pull /storage/emulated/0/Android/data/com.anyun.h264/files/test.h264 ./test.h264
   ```
2. **用VLC播放器打开**
   - ä¸‹è½½VLC: https://www.videolan.org/vlc/
   - ç›´æŽ¥åŒå‡» `test.h264` æ–‡ä»¶
   - æˆ–者拖放到VLC窗口
3. **如果能播放** âœ…
   - è¯´æ˜Žæ–‡ä»¶æ ¼å¼æ­£ç¡®
   - å¯ä»¥çœ‹åˆ°è§†é¢‘内容
4. **如果不能播放** âŒ
   - å¯èƒ½ç¼ºå°‘SPS/PPS
   - å¯èƒ½åªæœ‰1帧
   - å¯èƒ½æ ¼å¼é”™è¯¯
### æ–¹æ³•2: ä½¿ç”¨Python工具检查(详细分析)
1. **确保安装了Python** (Python 3.6+)
2. **下载文件到本地**
   ```bash
   adb pull /storage/emulated/0/Android/data/com.anyun.h264/files/test.h264 ./test.h264
   ```
3. **运行检查工具**
   ```bash
   python check_h264.py test.h264
   ```
4. **查看检查结果**
   âœ… **可以播放的标志:**
   - âœ“ åŒ…含SPS (序列参数集)
   - âœ“ åŒ…含PPS (图像参数集)
   - âœ“ åŒ…含IDR关键帧
   - âœ“ æœ‰å¤šä¸ªNALU单元(建议>10个)
   âŒ **不能播放的标志:**
   - ç¼ºå°‘SPS或PPS
   - åªæœ‰1-2个NALU单元
   - æ–‡ä»¶å¤ªå°ï¼ˆ<100字节)
### æ–¹æ³•3: ä½¿ç”¨ffprobe检查(如果安装了FFmpeg)
```bash
ffprobe test.h264
```
如果能正确显示视频信息(分辨率、帧率等),说明文件可以播放。
### æ–¹æ³•4: æŸ¥çœ‹æ–‡ä»¶å¤§å°ï¼ˆç²—略判断)
```bash
# åœ¨Android设备上
adb shell "ls -lh /storage/emulated/0/Android/data/com.anyun.h264/files/test.h264"
```
**参考标准:**
- âœ… **可以播放**:通常 > 10KB(640x480@25fps,3秒约30-50KB)
- âš ï¸ **可能只有1帧**:< 5KB
- âŒ **文件异常**:= 0KB
## æ£€æŸ¥æ–‡ä»¶å†…容(高级)
### ä½¿ç”¨hexdump查看文件头
```bash
hexdump -C test.h264 | head -20
```
**正常文件应该看到:**
```
00000000  00 00 00 01 67 64 00 1f  ac 72 84 44 26 84 00 00  |....gd...r.D&...|
00000010  00 01 00 00 00 01 68 ee  3c b0 44 00 00 00 01 06  |......h.<.D.....|
```
- `00 00 00 01` = Annex-B起始码(4字节)
- `67` = SPS的NALU类型(0x67 & 0x1F = 7)
- `68` = PPS的NALU类型(0x68 & 0x1F = 8)
### ä½¿ç”¨adb在设备上直接检查
```bash
# æ£€æŸ¥æ–‡ä»¶å¤§å°
adb shell "stat -c '%s' /storage/emulated/0/Android/data/com.anyun.h264/files/test.h264"
# æŸ¥çœ‹æ–‡ä»¶å‰50字节
adb shell "hexdump -C /storage/emulated/0/Android/data/com.anyun.h264/files/test.h264 | head -5"
```
## å¸¸è§é—®é¢˜è¯Šæ–­
### âŒ é—®é¢˜1: æ–‡ä»¶åªæœ‰1帧、0秒
**原因:**
- ç¼–码器输出处理不完整
- SPS/PPS没有正确写入
- ç¼–码循环提前退出
**解决方案:**
✅ å·²ç»ä¿®å¤ï¼æ–°ä»£ç åŒ…含:
- æ­£ç¡®å¤„理SPS/PPS配置数据
- åœ¨å…³é”®å¸§æ—¶åˆå¹¶SPS/PPS
- æ”¹è¿›ç¼–码循环,处理所有输出
- æ·»åŠ æ¸…ç©ºç¼–ç å™¨åŠŸèƒ½
**建议:**
1. é‡æ–°ç¼–译运行应用
2. å½•制至少3-5秒
3. æ­£å¸¸åœæ­¢ç¼–码(不要强制退出)
### âŒ é—®é¢˜2: VLC无法播放
**检查步骤:**
1. **查看文件大小**
   ```bash
   adb shell "ls -lh /storage/emulated/0/Android/data/com.anyun.h264/files/test.h264"
   ```
   å¦‚果太小(<1KB),说明可能只有配置数据
2. **查看Logcat日志**
   ```bash
   adb logcat -s H264Encoder:D | grep -E "Frame encoded|SPS/PPS|NALU"
   ```
   åº”该看到:
   - "SPS/PPS included in key frame data"
   - "Frame encoded: ..." ï¼ˆå¤šæ¬¡ï¼‰
3. **检查文件格式**
   ```bash
   adb shell "hexdump -C /storage/emulated/0/Android/data/com.anyun.h264/files/test.h264 | head -3"
   ```
   åº”该看到 `00 00 00 01` æˆ– `00 00 01`
### âŒ é—®é¢˜3: æ–‡ä»¶å­˜åœ¨ä½†æ’­æ”¾å™¨æŠ¥é”™
**可能原因:**
1. æ–‡ä»¶æ ¼å¼ä¸æ˜¯çº¯Annex-B
2. ç¼ºå°‘SPS/PPS
3. æ•°æ®æŸå
**解决方法:**
1. ä½¿ç”¨ `check_h264.py` è¯¦ç»†æ£€æŸ¥
2. æŸ¥çœ‹Logcat确认编码过程正常
3. å°è¯•用ffmpeg转换:
   ```bash
   ffmpeg -i test.h264 -c copy test_fixed.h264
   ```
## éªŒè¯ä¿®å¤æ•ˆæžœ
修复后的代码应该能够:
✅ **生成可播放的H264文件**
- åŒ…含完整的SPS/PPS
- åŒ…含多个帧(IDR + éžIDR)
- æ­£ç¡®çš„Annex-B格式
✅ **文件特征:**
- æ–‡ä»¶å¤§å° > 10KB(3秒录制)
- NALU单元数量 > 10个
- åŒ…含SPS (类型7)
- åŒ…含PPS (类型8)
- åŒ…含IDR关键帧 (类型5)
- åŒ…含非IDR帧 (类型1)
## ä¸‹ä¸€æ­¥
1. **重新运行应用**,录制3-5秒视频
2. **下载文件**到本地
3. **用VLC播放**验证
4. **如果还有问题**,运行检查工具获取详细诊断