1个文件已添加
15个文件已修改
1175 ■■■■ 已修改文件
README_H264_CHECK.md 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/build.gradle 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/aidl/com/anyun/h264/IH264EncodeService.aidl 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/aidl/com/anyun/h264/model/ResourceInfo.aidl 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264EncodeService.java 531 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264EncodeService2.java 292 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264Encoder.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/model/ResourceInfo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/model/WatermarkInfo.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/service/H264EncodeServiceClient.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/util/FileUtil.java 249 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/strings.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
check_h264.py 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
多进程方案使用说明.md 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
如何检查test.h264文件.md 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
README_H264_CHECK.md
@@ -118,3 +118,5 @@
   ```
   åº”该看到:`00 00 00 01` æˆ– `00 00 01`(Annex-B起始码)
app/build.gradle
@@ -15,7 +15,7 @@
        minSdk 21
        targetSdk 35
        versionCode 1
        versionName "1.0"
        versionName "1.0.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
app/src/main/aidl/com/anyun/h264/IH264EncodeService.aidl
@@ -32,12 +32,12 @@
     * @param endTime ç»“束时间(格式:YYMMDDHHmmss,BCD编码的6字节字符串)
     * @return èµ„源列表(根据JT/T 1076-2016表23定义)
     */
    List<ResourceInfo> getResourceList(String startTime, String endTime);
    List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard, String jsonConfig);
    
    /**
     * è®¾ç½®æ°´å°ä¿¡æ¯
     * @param watermarkInfo æ°´å°ä¿¡æ¯å­—符串
     */
    void setWatermarkInfo(String watermarkInfo);
    void setWatermarkInfo(String watermarkInfo, String jsonConfig);
}
app/src/main/aidl/com/anyun/h264/model/ResourceInfo.aidl
@@ -5,3 +5,5 @@
 */
parcelable ResourceInfo;
app/src/main/java/com/anyun/h264/H264EncodeService.java
@@ -11,6 +11,7 @@
import com.anyun.h264.model.ResourceInfo;
import com.anyun.h264.model.WatermarkInfo;
import com.anyun.h264.util.FileUtil;
import org.json.JSONException;
import org.json.JSONObject;
@@ -19,6 +20,7 @@
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@@ -36,7 +38,7 @@
    private H264FileTransmitter h264FileTransmitter; // H264文件传输器
    private String outputFileDirectory; // H264文件输出目录
    private WatermarkInfo currentWatermarkInfo; // å½“前水印信息
    private static final int H264_FILE_RETENTION_DAYS = 5; // å¯æ ¹æ®éœ€æ±‚调整为3或5天
    private static final int H264_FILE_RETENTION_DAYS = 1; // å¯æ ¹æ®éœ€æ±‚调整为3或5天
    
    // å¤šè¿›ç¨‹æ”¯æŒï¼šç¬¬äºŒä¸ªæ‘„像头的服务连接
    private IH264EncodeService camera2Service;
@@ -61,13 +63,13 @@
        }
        
        @Override
        public List<ResourceInfo> getResourceList(String startTime, String endTime) throws RemoteException {
            return H264EncodeService.this.getResourceList(startTime, endTime);
        public List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard,String jsonConfig) throws RemoteException {
            return H264EncodeService.this.getResourceList(startTime, endTime,useTFCard, jsonConfig);
        }
        
        @Override
        public void setWatermarkInfo(String watermarkInfo) throws RemoteException {
            H264EncodeService.this.setWatermarkInfo(watermarkInfo);
        public void setWatermarkInfo(String watermarkInfo,String jsonConfig) throws RemoteException {
            H264EncodeService.this.setWatermarkInfo(watermarkInfo,jsonConfig);
        }
    };
    
@@ -123,6 +125,9 @@
     * ç¼–码配置类
     */
    private static class EncodeConfig {
        boolean useTFCard=true;//如果为true,则在tfcard æ ¹ç›®å½•建一个h264文件夹,在h264目录下以当前日期为名字的文件夹(比如20250123),然后h264文件就写入以当前日前为名字的目录下
         boolean enableFileOutput; //是否开启h264文件写入
         boolean enableNetworkTransmit; //开启h264,网络实时推流
        String ip;
        int port;
        int width;
@@ -139,10 +144,13 @@
                config.width = DEFAULT_WIDTH;
                config.height = DEFAULT_HEIGHT;
                config.framerate = DEFAULT_FRAME_RATE;
                config.enableFileOutput = false;
                config.enableNetworkTransmit = false;
                config.ip = null;
                config.port = 0;
                config.simPhone = null;
                config.cameraId = 1; // é»˜è®¤ä½¿ç”¨ç¬¬ä¸€ä¸ªæ‘„像头
                config.useTFCard = false; // é»˜è®¤ä¸ä½¿ç”¨TF卡
                return config;
            }
            
@@ -150,9 +158,12 @@
            config.width = json.optInt("width", DEFAULT_WIDTH);
            config.height = json.optInt("height", DEFAULT_HEIGHT);
            config.framerate = json.optInt("framerate", DEFAULT_FRAME_RATE);
            config.enableFileOutput = json.optBoolean("enableFileOutput", false);
            config.enableNetworkTransmit = json.optBoolean("enableNetworkTransmit", false);
            config.ip = json.optString("ip", null);
            config.port = json.optInt("port", 0);
            config.simPhone = json.optString("simPhone", null);
            config.useTFCard = json.optBoolean("useTFCard", false);
            
            // è§£æžcameraId(如果未指定,默认为1)
            if (json.has("cameraId")) {
@@ -238,7 +249,9 @@
                case 0: // å¼€å¯h264文件写入
                    try {
                        EncodeConfig config0 = EncodeConfig.fromJson(jsonConfig);
                        return startFileEncode(config0);
                        config0.enableFileOutput = true;
                        config0.enableNetworkTransmit = false;
                        return startEncode(config0);
                    } catch (JSONException e) {
                        Timber.e(e, "Failed to parse JSON config: %s", jsonConfig);
                        return 1;
@@ -251,21 +264,35 @@
                    }
                    return stopEncoder();
                    
                case 2: // å¼€å¯ç½‘络推送h264(不写入文件)
                case 2: // å¼€å¯ç½‘络推送h264(可同时写入文件)
                    try {
                        EncodeConfig config2 = EncodeConfig.fromJson(jsonConfig);
                        return startNetworkEncode(config2);
                        // æ£€æŸ¥å¿…需的配置参数
                        if (config2 == null || config2.ip == null || config2.ip.trim().isEmpty() || config2.port <= 0) {
                            Timber.e("Network encode requires valid ip and port in config");
                            return 1; // å¤±è´¥
                        }
                        config2.enableNetworkTransmit = true;
                        return startEncode(config2);
                    } catch (JSONException e) {
                        Timber.e(e, "Failed to parse JSON config: %s", jsonConfig);
                        return 1;
                    }
                    
                case 3: // åœæ­¢h264编码并停止网络推送
                case 3: // åœæ­¢ç½‘络推送(保持文件写入)
                    // æ£€æŸ¥æ˜¯å¦æŒ‡å®šäº†cameraId=2
                    if (cameraId != null && cameraId == 2) {
                        return controlEncodeInProcess2(action, jsonConfig);
                    }
                    return stopEncoder();
                    // åªå…³é—­ç½‘络传输,保持文件写入
                    if (h264Encoder != null) {
                        h264Encoder.setEnableNetworkTransmission(false);
                        Timber.d("Network transmission stopped, file output continues");
                        return 0;
                    } else {
                        Timber.w("Encoder is not running");
                        return 0; // æˆåŠŸï¼ˆæ²¡æœ‰è¿è¡Œçš„ç¼–ç å™¨ï¼Œè§†ä¸ºæˆåŠŸï¼‰
                    }
                    
                case 4: // å¼€å§‹ä¼ è¾“H264文件
                    try {
@@ -317,6 +344,56 @@
        } catch (RemoteException e) {
            Timber.e(e, "Error calling camera2 service");
            return 1;
        }
    }
    /**
     * åœ¨ç¬¬äºŒä¸ªè¿›ç¨‹ï¼ˆcamera2)中获取资源列表
     */
    private List<ResourceInfo> getResourceListInProcess2(String startTime, String endTime, boolean useTFCard, String jsonConfig) {
        Timber.d("Routing to process 2 (camera2) for getResourceList");
        try {
            // ç¡®ä¿ç¬¬äºŒä¸ªè¿›ç¨‹çš„æœåŠ¡å·²ç»‘å®š
            if (!ensureCamera2ServiceBound()) {
                Timber.e("Failed to bind camera2 service");
                return new ArrayList<>();
            }
            // è°ƒç”¨ç¬¬äºŒä¸ªè¿›ç¨‹çš„æœåŠ¡
            if (camera2Service != null) {
                return camera2Service.getResourceList(startTime, endTime, useTFCard, jsonConfig);
            } else {
                Timber.e("Camera2 service is null");
                return new ArrayList<>();
            }
        } catch (RemoteException e) {
            Timber.e(e, "Error calling camera2 service for getResourceList");
            return new ArrayList<>();
        }
    }
    /**
     * åœ¨ç¬¬äºŒä¸ªè¿›ç¨‹ï¼ˆcamera2)中设置水印信息
     */
    private void setWatermarkInfoInProcess2(String watermarkInfoJson, String jsonConfig) {
        Timber.d("Routing to process 2 (camera2) for setWatermarkInfo");
        try {
            // ç¡®ä¿ç¬¬äºŒä¸ªè¿›ç¨‹çš„æœåŠ¡å·²ç»‘å®š
            if (!ensureCamera2ServiceBound()) {
                Timber.e("Failed to bind camera2 service");
                return;
            }
            // è°ƒç”¨ç¬¬äºŒä¸ªè¿›ç¨‹çš„æœåŠ¡
            if (camera2Service != null) {
                camera2Service.setWatermarkInfo(watermarkInfoJson, jsonConfig);
            } else {
                Timber.e("Camera2 service is null");
            }
        } catch (RemoteException e) {
            Timber.e(e, "Error calling camera2 service for setWatermarkInfo");
        }
    }
    
@@ -374,87 +451,113 @@
    }
    
    /**
     * å¯åŠ¨æ–‡ä»¶ç¼–ç æ¨¡å¼ï¼ˆåªå†™å…¥æ–‡ä»¶ï¼Œä¸è¿›è¡Œç½‘ç»œæŽ¨é€ï¼‰
     * èŽ·å–è¾“å‡ºæ–‡ä»¶ç›®å½•ï¼ˆæ ¹æ®useTFCard配置)
     * @param useTFCard æ˜¯å¦ä½¿ç”¨TF卡
     * @return è¾“出目录路径
     */
    private int startFileEncode(EncodeConfig config) {
        Timber.d("Starting file encode mode");
        // å¦‚果编码器已经在运行,先停止
        if (h264Encoder != null) {
            Timber.w("Encoder is already running, stopping it first");
            stopEncoder();
        }
        try {
            // åˆ›å»ºç¼–码器
            h264Encoder = new H264Encoder();
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH;
            int height = config != null && config.height > 0 ? config.height : DEFAULT_HEIGHT;
            int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            // è®¾ç½®è¾“出文件目录(H264Encoder会自动管理文件创建,每分钟一个文件)
            // ä½¿ç”¨ä¸€ä¸ªä¸´æ—¶æ–‡ä»¶åæ¥è®¾ç½®ç›®å½•,H264Encoder会在初始化时创建第一个文件
            File tempFile = new File(outputFileDirectory, "temp.h264");
            h264Encoder.setOutputFile(tempFile.getAbsolutePath());
            h264Encoder.setEnableFileOutput(true); // å¯ç”¨æ–‡ä»¶è¾“出
            // ç¦ç”¨ç½‘络传输
            h264Encoder.setEnableNetworkTransmission(false);
            // åˆå§‹åŒ–并启动(使用配置中的分辨率)
            // æ ¹æ®cameraId选择摄像头范围
            int[] cameraIdRange = DEFAULT_CAMERA_ID_RANGE;
            if (config != null && config.cameraId != null) {
                // å¦‚果指定了cameraId,使用对应的摄像头
                cameraIdRange = new int[]{config.cameraId, config.cameraId};
            }
            int[] resolution = {width, height};
            if (h264Encoder.initialize(cameraIdRange, null, resolution, false)) {
                // åº”用已保存的水印信息(如果有)
                if (currentWatermarkInfo != null) {
                    h264Encoder.setWatermarkInfo(currentWatermarkInfo);
                    Timber.d("Applied saved watermark info to encoder");
    private String getOutputFileDirectory(boolean useTFCard) {
        if (useTFCard) {
            // ä½¿ç”¨TF卡:/sdcard/h264/当前日期/
            try {
                String storagePath = FileUtil.getStoragePath(this, true);
                if (storagePath == null || storagePath.trim().isEmpty()) {
                    Timber.w("TF card storage path not available, fallback to app directory");
                    return outputFileDirectory;
                }
                h264Encoder.start();
                Timber.d("File encode started successfully, output directory: %s, resolution: %dx%d, framerate: %d",
                        outputFileDirectory, width, height, framerate);
                return 0; // æˆåŠŸ
            } else {
                Timber.e("Failed to initialize encoder");
                h264Encoder = null;
                return 1; // å¤±è´¥
                File externalStorage = new File(storagePath);
                if (!externalStorage.exists()) {
                    Timber.w("TF card storage directory does not exist: %s, fallback to app directory", storagePath);
                    return outputFileDirectory;
                }
                // èŽ·å–å½“å‰æ—¥æœŸï¼ˆæ ¼å¼ï¼šyyyyMMdd,如20250123)
                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA);
                String dateDir = dateFormat.format(new Date());
                // æž„建路径:/sdcard/h264/20250123/
                File h264Dir = new File(externalStorage, "h264");
                File dateDirFile = new File(h264Dir, dateDir);
                // åˆ›å»ºç›®å½•(如果不存在)
                if (!dateDirFile.exists()) {
                    boolean created = dateDirFile.mkdirs();
                    if (!created && !dateDirFile.exists()) {
                        Timber.e("Failed to create TF card directory: %s, fallback to app directory", dateDirFile.getAbsolutePath());
                        return outputFileDirectory;
                    }
                }
                String tfCardPath = dateDirFile.getAbsolutePath();
                Timber.d("Using TF card directory: %s", tfCardPath);
                return tfCardPath;
            } catch (Exception e) {
                Timber.e(e, "Error getting TF card directory, fallback to app directory");
                return outputFileDirectory;
            }
        } catch (Exception e) {
            Timber.e(e, "Failed to start file encode");
            h264Encoder = null;
            return 1; // å¤±è´¥
        } else {
            // ä½¿ç”¨åº”用外部存储目录
            return outputFileDirectory;
        }
    }
    
    /**
     * å¯åŠ¨ç½‘ç»œæŽ¨é€æ¨¡å¼ï¼ˆåªè¿›è¡Œç½‘ç»œæŽ¨é€ï¼Œä¸å†™å…¥æ–‡ä»¶ï¼‰
     * å¯åŠ¨ç¼–ç ï¼ˆç»Ÿä¸€æ–¹æ³•ï¼Œæ”¯æŒæ–‡ä»¶å†™å…¥å’Œç½‘ç»œä¼ è¾“çš„ç»„åˆï¼‰
     * @param config ç¼–码配置
     * @return 0-成功,1-失败
     */
    private int startNetworkEncode(EncodeConfig config) {
        Timber.d("Starting network encode mode");
    private int startEncode(EncodeConfig config) {
        if (config == null) {
            Timber.e("Encode config cannot be null");
            return 1;
        }
        Timber.d("Starting encode mode, fileOutput: %b, networkTransmit: %b, useTFCard: %b",
                config.enableFileOutput, config.enableNetworkTransmit, config.useTFCard);
        
        // å¦‚果编码器已经在运行,先停止
        // å¦‚果编码器已经在运行,只更新配置
        if (h264Encoder != null) {
            Timber.w("Encoder is already running, stopping it first");
            stopEncoder();
            Timber.d("Encoder is already running, updating configuration");
            try {
                h264Encoder.setEnableFileOutput(config.enableFileOutput);
                // å¦‚果开启网络传输,需要设置服务器地址和协议参数
                if (config.enableNetworkTransmit) {
                    if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) {
                        Timber.e("Network transmit requires valid ip and port in config");
                        return 1; // å¤±è´¥
                    }
                    h264Encoder.setServerAddress(config.ip, config.port);
                    // è®¾ç½®åè®®å‚数(使用配置中的simPhone,如果未提供则使用默认值)
                    String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty()
                            ? config.simPhone : "013120122580";
                    h264Encoder.setProtocolParams(simPhone, (byte)1);
                }
                h264Encoder.setEnableNetworkTransmission(config.enableNetworkTransmit);
                Timber.d("Encoder configuration updated successfully");
                return 0; // æˆåŠŸ
            } catch (Exception e) {
                Timber.e(e, "Error updating encoder configuration");
                return 1; // å¤±è´¥
            }
        }
        
        // æ£€æŸ¥å¿…需的配置参数
        if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) {
            Timber.e("Network encode requires valid ip and port in config");
            return 1; // å¤±è´¥
        }
        // ç¼–码器未运行,需要初始化并启动
        try {
            // å¦‚果开启网络传输,检查必需的配置参数
        if (config.enableNetworkTransmit) {
                if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) {
                    Timber.e("Network transmit requires valid ip and port in config");
                    return 1; // å¤±è´¥
                }
        }
            // åˆ›å»ºç¼–码器
            h264Encoder = new H264Encoder();
            // è®¾ç½® Context(用于清理 TF å¡æ–‡ä»¶ï¼‰
            h264Encoder.setContext(this);
            
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH;
@@ -462,29 +565,32 @@
            int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            // è®¾ç½®è¾“出文件目录(H264Encoder会自动管理文件创建,每分钟一个文件)
            // ä½¿ç”¨ä¸€ä¸ªä¸´æ—¶æ–‡ä»¶åæ¥è®¾ç½®ç›®å½•,H264Encoder会在初始化时创建第一个文件
            File tempFile = new File(outputFileDirectory, "temp.h264");
            h264Encoder.setOutputFile(tempFile.getAbsolutePath());
            h264Encoder.setEnableFileOutput(true); // å¯ç”¨æ–‡ä»¶è¾“出
            
            // å¯ç”¨ç½‘络传输并设置服务器地址
            h264Encoder.setEnableNetworkTransmission(true);
            h264Encoder.setServerAddress(config.ip, config.port);
            // èŽ·å–è¾“å‡ºæ–‡ä»¶ç›®å½•ï¼ˆæ ¹æ®useTFCard配置)
            String outputDir = getOutputFileDirectory(config.useTFCard);
            
            // è®¾ç½®åè®®å‚数(使用配置中的simPhone,如果未提供则使用默认值)
            String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty()
                    ? config.simPhone : "013120122580";
            h264Encoder.setProtocolParams(simPhone, (byte)1);
            // è®¾ç½®è¾“出文件目录(H264Encoder会自动管理文件创建,每分钟一个文件)
            // ä½¿ç”¨ä¸€ä¸ªä¸´æ—¶æ–‡ä»¶åæ¥è®¾ç½®ç›®å½•,H264Encoder会在初始化时创建第一个文件
            File tempFile = new File(outputDir, "temp.h264");
        h264Encoder.setOutputFile(tempFile.getAbsolutePath());
        h264Encoder.setEnableFileOutput(config.enableFileOutput);
            // è®¾ç½®ç½‘络传输
        h264Encoder.setEnableNetworkTransmission(config.enableNetworkTransmit);
        if (config.enableNetworkTransmit) {
                h264Encoder.setServerAddress(config.ip, config.port);
                // è®¾ç½®åè®®å‚数(使用配置中的simPhone,如果未提供则使用默认值)
                String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty()
                        ? config.simPhone : "013120122580";
                h264Encoder.setProtocolParams(simPhone, (byte)1);
            }
            
            // åˆå§‹åŒ–并启动(使用配置中的分辨率)
            // æ ¹æ®cameraId选择摄像头范围
            int[] cameraIdRange = DEFAULT_CAMERA_ID_RANGE;
            if (config != null && config.cameraId != null) {
                // å¦‚果指定了cameraId,使用对应的摄像头
                cameraIdRange = new int[]{config.cameraId, config.cameraId};
            }
            int[] resolution = {width, height};
            if (h264Encoder.initialize(cameraIdRange, null, resolution, false)) {
                // åº”用已保存的水印信息(如果有)
@@ -493,8 +599,11 @@
                    Timber.d("Applied saved watermark info to encoder");
                }
                h264Encoder.start();
                Timber.d("Network encode started successfully, server: %s:%d, resolution: %dx%d, framerate: %d",
                        config.ip, config.port, width, height, framerate);
                Timber.d("Encode started successfully, fileOutput: %b, networkTransmit: %b, resolution: %dx%d, framerate: %d",
                        config.enableFileOutput, config.enableNetworkTransmit, width, height, framerate);
                if (config.enableNetworkTransmit) {
                    Timber.d("Network server: %s:%d", config.ip, config.port);
                }
                return 0; // æˆåŠŸ
            } else {
                Timber.e("Failed to initialize encoder");
@@ -502,7 +611,7 @@
                return 1; // å¤±è´¥
            }
        } catch (Exception e) {
            Timber.e(e, "Failed to start network encode");
            Timber.e(e, "Failed to start encode");
            h264Encoder = null;
            return 1; // å¤±è´¥
        }
@@ -556,11 +665,48 @@
        }
        
        try {
            // æ£€æŸ¥æ–‡ä»¶æ˜¯å¦å­˜åœ¨
            File file = new File(config.filePath);
            // è§£æžå¾…传输文件路径,若不存在则尝试到TF卡目录按日期查找
            String resolvedFilePath = config.filePath;
            File file = new File(resolvedFilePath);
            if (!file.exists() || !file.isFile()) {
                Timber.e("File does not exist: %s", config.filePath);
                return 1; // å¤±è´¥
                Timber.w("File does not exist, try TF card lookup: %s", resolvedFilePath);
                String fileName = file.getName();
                String timestampStr = null;
                if (fileName.startsWith("h264_") && fileName.endsWith(".h264")) {
                    timestampStr = fileName.substring(5, fileName.length() - 5);
                }
                if (timestampStr == null || timestampStr.trim().isEmpty()) {
                    Timber.e("Cannot parse timestamp from file name: %s", fileName);
                    return 1; // å¤±è´¥
                }
                try {
                    long timestamp = Long.parseLong(timestampStr);
                    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA);
                    String dateDir = dateFormat.format(new Date(timestamp));
                    String storagePath = FileUtil.getStoragePath(this, true);
                    if (storagePath == null || storagePath.trim().isEmpty()) {
                        Timber.e("TF card storage path not available when searching file");
                        return 1; // å¤±è´¥
                    }
                    File tfRoot = new File(storagePath, "h264");
                    File candidate = new File(new File(tfRoot, dateDir), fileName);
                    if (candidate.exists() && candidate.isFile()) {
                        resolvedFilePath = candidate.getAbsolutePath();
                        file = candidate;
                        Timber.i("Found file on TF card: %s", resolvedFilePath);
                    } else {
                        Timber.e("File not found on TF card path: %s", candidate.getAbsolutePath());
                        return 1; // å¤±è´¥
                    }
                } catch (NumberFormatException e) {
                    Timber.e(e, "Failed to parse timestamp from file name: %s", fileName);
                    return 1; // å¤±è´¥
                }
            }
            
            // åˆ›å»ºæ–‡ä»¶ä¼ è¾“器
@@ -585,8 +731,8 @@
            h264FileTransmitter.setOnTransmitProgressCallback(new H264FileTransmitter.OnTransmitProgressCallback() {
                @Override
                public void onProgress(int currentFrame, int totalFrames) {
                    Timber.d("File transmit progress: frame %d%s", currentFrame,
                            totalFrames > 0 ? " of " + totalFrames : "");
//                    Timber.d("File transmit progress: frame %d%s", currentFrame,
//                            totalFrames > 0 ? " of " + totalFrames : "");
                }
                
                @Override
@@ -609,10 +755,10 @@
            }
            
            // å¼€å§‹ä¼ è¾“文件
            h264FileTransmitter.transmitFile(config.filePath);
            h264FileTransmitter.transmitFile(resolvedFilePath);
            
            Timber.d("File transmit started successfully, file: %s, server: %s:%d, protocol: %s, framerate: %d", 
                    config.filePath, config.ip, config.port,
                    resolvedFilePath, config.ip, config.port,
                    config.protocolType == JT1076ProtocolHelper.PROTOCOL_TYPE_UDP ? "UDP" : "TCP", framerate);
            return 0; // æˆåŠŸ
            
@@ -659,31 +805,110 @@
     * @param endTime ç»“束时间(格式:YYMMDDHHmmss)
     * @return èµ„源列表
     */
    private List<ResourceInfo> getResourceList(String startTime, String endTime) {
        Timber.d("getResourceList called, startTime: %s, endTime: %s", startTime, endTime);
    private List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard,String jsonConfig) {
        Timber.d("getResourceList called, startTime: %s, endTime: %s, useTFCard: %b", startTime, endTime, useTFCard);
        // è§£æžcameraId(如果配置中有)
        Integer cameraId = null;
        if (jsonConfig != null && !jsonConfig.trim().isEmpty()) {
            try {
                JSONObject json = new JSONObject(jsonConfig);
                if (json.has("cameraId")) {
                    cameraId = json.optInt("cameraId", 1);
                }
            } catch (JSONException e) {
                // å¿½ç•¥è§£æžé”™è¯¯ï¼Œç»§ç»­ä½¿ç”¨å½“前进程
                Timber.w(e, "Failed to parse cameraId from jsonConfig");
            }
        }
        // å¦‚果指定了cameraId=2,路由到第二个进程
        if (cameraId != null && cameraId == 2) {
            return getResourceListInProcess2(startTime, endTime, useTFCard, jsonConfig);
        }
        
        List<ResourceInfo> resourceList = new ArrayList<>();
        
        try {
            // æ‰«æè¾“出目录中的H264文件
            File dir = new File(outputFileDirectory);
            if (!dir.exists() || !dir.isDirectory()) {
                Timber.w("Output directory does not exist: %s", outputFileDirectory);
                return resourceList;
            }
            File[] files = dir.listFiles((dir1, name) -> name.toLowerCase().endsWith(".h264"));
            if (files == null || files.length == 0) {
                Timber.d("No H264 files found in directory");
                return resourceList;
            }
            // è§£æžæ—¶é—´èŒƒå›´
            Date startDate = parseTime(startTime);
            Date endDate = parseTime(endTime);
            
            if (startDate == null || endDate == null) {
                Timber.e("Invalid time format, startTime: %s, endTime: %s", startTime, endTime);
                return resourceList;
            }
            if (useTFCard) {
                // ä½¿ç”¨TF卡:扫描TF卡上的h264文件夹,根据日期范围过滤
                String storagePath = FileUtil.getStoragePath(this, true);
                if (storagePath == null || storagePath.trim().isEmpty()) {
                    Timber.w("TF card storage path not available, fallback to app directory");
                    // å›žé€€åˆ°åº”用目录
                    return getResourceListFromDirectory(outputFileDirectory, startDate, endDate);
                }
                File externalStorage = new File(storagePath);
                if (!externalStorage.exists()) {
                    Timber.w("TF card storage directory does not exist: %s, fallback to app directory", storagePath);
                    // å›žé€€åˆ°åº”用目录
                    return getResourceListFromDirectory(outputFileDirectory, startDate, endDate);
                }
                // TF卡上的h264文件夹路径:/sdcard/h264/
                File h264Dir = new File(externalStorage, "h264");
                if (!h264Dir.exists() || !h264Dir.isDirectory()) {
                    Timber.w("TF card h264 directory does not exist: %s", h264Dir.getAbsolutePath());
                    return resourceList;
                }
                // èŽ·å–æ—¥æœŸèŒƒå›´å†…çš„æ‰€æœ‰æ—¥æœŸæ–‡ä»¶å¤¹
                List<String> dateDirs = getDateDirectoriesInRange(startDate, endDate);
                Timber.d("Found %d date directories in range", dateDirs.size());
                // æ‰«ææ¯ä¸ªæ—¥æœŸæ–‡ä»¶å¤¹ä¸‹çš„h264文件
                for (String dateDir : dateDirs) {
                    File dateDirFile = new File(h264Dir, dateDir);
                    if (dateDirFile.exists() && dateDirFile.isDirectory()) {
                        List<ResourceInfo> dateResources = getResourceListFromDirectory(
                                dateDirFile.getAbsolutePath(), startDate, endDate);
                        resourceList.addAll(dateResources);
                    }
                }
                Timber.d("Found %d resources in TF card time range", resourceList.size());
                return resourceList;
            } else {
                // ä¸ä½¿ç”¨TF卡:扫描应用目录
                return getResourceListFromDirectory(outputFileDirectory, startDate, endDate);
            }
        } catch (Exception e) {
            Timber.e(e, "Error getting resource list");
            return resourceList;
        }
    }
    /**
     * ä»ŽæŒ‡å®šç›®å½•扫描H264文件并创建资源列表
     * @param directoryPath ç›®å½•路径
     * @param startDate å¼€å§‹æ—¥æœŸ
     * @param endDate ç»“束日期
     * @return èµ„源列表
     */
    private List<ResourceInfo> getResourceListFromDirectory(String directoryPath, Date startDate, Date endDate) {
        List<ResourceInfo> resourceList = new ArrayList<>();
        try {
            File dir = new File(directoryPath);
            if (!dir.exists() || !dir.isDirectory()) {
                Timber.w("Directory does not exist: %s", directoryPath);
                return resourceList;
            }
            File[] files = dir.listFiles((dir1, name) -> name.toLowerCase().endsWith(".h264"));
            if (files == null || files.length == 0) {
                Timber.d("No H264 files found in directory: %s", directoryPath);
                return resourceList;
            }
            
@@ -695,13 +920,57 @@
                }
            }
            
            Timber.d("Found %d resources in time range", resourceList.size());
            return resourceList;
            
        } catch (Exception e) {
            Timber.e(e, "Error getting resource list");
            Timber.e(e, "Error getting resource list from directory: %s", directoryPath);
            return resourceList;
        }
    }
    /**
     * èŽ·å–æ—¥æœŸèŒƒå›´å†…çš„æ‰€æœ‰æ—¥æœŸæ–‡ä»¶å¤¹åç§°åˆ—è¡¨ï¼ˆæ ¼å¼ï¼šyyyyMMdd)
     * @param startDate å¼€å§‹æ—¥æœŸ
     * @param endDate ç»“束日期
     * @return æ—¥æœŸæ–‡ä»¶å¤¹åç§°åˆ—表
     */
    private List<String> getDateDirectoriesInRange(Date startDate, Date endDate) {
        List<String> dateDirs = new ArrayList<>();
        try {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA);
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(startDate);
            calendar.set(Calendar.HOUR_OF_DAY, 0);
            calendar.set(Calendar.MINUTE, 0);
            calendar.set(Calendar.SECOND, 0);
            calendar.set(Calendar.MILLISECOND, 0);
            Date currentDate = calendar.getTime();
            Date endDateOnly = new Date(endDate.getTime());
            Calendar endCalendar = Calendar.getInstance();
            endCalendar.setTime(endDateOnly);
            endCalendar.set(Calendar.HOUR_OF_DAY, 23);
            endCalendar.set(Calendar.MINUTE, 59);
            endCalendar.set(Calendar.SECOND, 59);
            endCalendar.set(Calendar.MILLISECOND, 999);
            endDateOnly = endCalendar.getTime();
            // éåŽ†ä»Žå¼€å§‹æ—¥æœŸåˆ°ç»“æŸæ—¥æœŸçš„æ‰€æœ‰æ—¥æœŸ
            while (!currentDate.after(endDateOnly)) {
                String dateDir = dateFormat.format(currentDate);
                dateDirs.add(dateDir);
                // å¢žåР䏀天
                calendar.add(Calendar.DAY_OF_MONTH, 1);
                currentDate = calendar.getTime();
            }
        } catch (Exception e) {
            Timber.e(e, "Error getting date directories in range");
        }
        return dateDirs;
    }
    
    /**
@@ -711,9 +980,29 @@
     *                          ç¤ºä¾‹ï¼š{"plateNumber":"京A12345","student":"张三","coach":"李四",
     *                                "longitude":116.397128,"latitude":39.916527,"drivingSchool":"XX驾校","speed":60.5}
     */
    private void setWatermarkInfo(String watermarkInfoJson) {
    private void setWatermarkInfo(String watermarkInfoJson,String jsonConfig) {
        Timber.d("setWatermarkInfo called, watermarkInfoJson: %s", watermarkInfoJson);
        
        // è§£æžcameraId(如果配置中有)
        Integer cameraId = null;
        if (jsonConfig != null && !jsonConfig.trim().isEmpty()) {
            try {
                JSONObject json = new JSONObject(jsonConfig);
                if (json.has("cameraId")) {
                    cameraId = json.optInt("cameraId", 1);
                }
            } catch (JSONException e) {
                // å¿½ç•¥è§£æžé”™è¯¯ï¼Œç»§ç»­ä½¿ç”¨å½“前进程
                Timber.w(e, "Failed to parse cameraId from jsonConfig");
            }
        }
        // å¦‚果指定了cameraId=2,路由到第二个进程
        if (cameraId != null && cameraId == 2) {
            setWatermarkInfoInProcess2(watermarkInfoJson, jsonConfig);
            return;
        }
        try {
            if (watermarkInfoJson == null || watermarkInfoJson.trim().isEmpty()) {
                Timber.w("Watermark info JSON is null or empty, clearing watermark");
app/src/main/java/com/anyun/h264/H264EncodeService2.java
@@ -8,6 +8,7 @@
import com.anyun.h264.model.ResourceInfo;
import com.anyun.h264.model.WatermarkInfo;
import com.anyun.h264.util.FileUtil;
import org.json.JSONException;
import org.json.JSONObject;
@@ -16,6 +17,7 @@
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@@ -31,7 +33,7 @@
    private H264FileTransmitter h264FileTransmitter; // H264文件传输器
    private String outputFileDirectory; // H264文件输出目录
    private WatermarkInfo currentWatermarkInfo; // å½“前水印信息
    // é»˜è®¤ç¼–码参数
    private static final int DEFAULT_WIDTH = 640;
    private static final int DEFAULT_HEIGHT = 480;
@@ -47,15 +49,15 @@
        public int controlEncode(int action, String jsonConfig) throws RemoteException {
            return H264EncodeService2.this.controlEncode(action, jsonConfig);
        }
        @Override
        public List<ResourceInfo> getResourceList(String startTime, String endTime) throws RemoteException {
            return H264EncodeService2.this.getResourceList(startTime, endTime);
        public List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard,String jsonConfig) throws RemoteException {
            return H264EncodeService2.this.getResourceList(startTime, endTime,useTFCard,jsonConfig);
        }
        
        @Override
        public void setWatermarkInfo(String watermarkInfo) throws RemoteException {
            H264EncodeService2.this.setWatermarkInfo(watermarkInfo);
        public void setWatermarkInfo(String watermarkInfo,String jsonConfig) throws RemoteException {
            H264EncodeService2.this.setWatermarkInfo(watermarkInfo,jsonConfig);
        }
    };
    
@@ -102,6 +104,7 @@
        int height;
        int framerate;
        String simPhone;
        boolean useTFCard = false; // æ˜¯å¦ä½¿ç”¨TF卡
        
        // ä»ŽJSON解析配置
        static EncodeConfig fromJson(String jsonConfig) throws JSONException {
@@ -114,6 +117,7 @@
                config.ip = null;
                config.port = 0;
                config.simPhone = null;
                config.useTFCard = false; // é»˜è®¤ä¸ä½¿ç”¨TF卡
                return config;
            }
            
@@ -124,6 +128,7 @@
            config.ip = json.optString("ip", null);
            config.port = json.optInt("port", 0);
            config.simPhone = json.optString("simPhone", null);
            config.useTFCard = json.optBoolean("useTFCard", false);
            
            return config;
        }
@@ -216,10 +221,61 @@
    }
    
    /**
     * èŽ·å–è¾“å‡ºæ–‡ä»¶ç›®å½•ï¼ˆæ ¹æ®useTFCard配置)
     * @param useTFCard æ˜¯å¦ä½¿ç”¨TF卡
     * @return è¾“出目录路径
     */
    private String getOutputFileDirectory(boolean useTFCard) {
        if (useTFCard) {
            // ä½¿ç”¨TF卡:/sdcard/h264/当前日期/
            try {
                String storagePath = FileUtil.getStoragePath(this, true);
                if (storagePath == null || storagePath.trim().isEmpty()) {
                    Timber.w("TF card storage path not available, fallback to app directory (camera2)");
                    return outputFileDirectory;
                }
                File externalStorage = new File(storagePath);
                if (!externalStorage.exists()) {
                    Timber.w("TF card storage directory does not exist: %s, fallback to app directory (camera2)", storagePath);
                    return outputFileDirectory;
                }
                // èŽ·å–å½“å‰æ—¥æœŸï¼ˆæ ¼å¼ï¼šyyyyMMdd,如20250123)
                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA);
                String dateDir = dateFormat.format(new Date());
                // æž„建路径:/sdcard/h264/20250123/
                File h264Dir = new File(externalStorage, "h264");
                File dateDirFile = new File(h264Dir, dateDir);
                // åˆ›å»ºç›®å½•(如果不存在)
                if (!dateDirFile.exists()) {
                    boolean created = dateDirFile.mkdirs();
                    if (!created && !dateDirFile.exists()) {
                        Timber.e("Failed to create TF card directory: %s, fallback to app directory (camera2)", dateDirFile.getAbsolutePath());
                        return outputFileDirectory;
                    }
                }
                String tfCardPath = dateDirFile.getAbsolutePath();
                Timber.d("Using TF card directory (camera2): %s", tfCardPath);
                return tfCardPath;
            } catch (Exception e) {
                Timber.e(e, "Error getting TF card directory, fallback to app directory (camera2)");
                return outputFileDirectory;
            }
        } else {
            // ä½¿ç”¨åº”用外部存储目录
            return outputFileDirectory;
        }
    }
    /**
     * å¯åŠ¨æ–‡ä»¶ç¼–ç æ¨¡å¼ï¼ˆåªå†™å…¥æ–‡ä»¶ï¼Œä¸è¿›è¡Œç½‘ç»œæŽ¨é€ï¼‰
     */
    private int startFileEncode(EncodeConfig config) {
        Timber.d("Starting file encode mode (camera2)");
        Timber.d("Starting file encode mode (camera2), useTFCard: %b", config != null ? config.useTFCard : false);
        
        // å¦‚果编码器已经在运行,先停止
        if (h264Encoder != null) {
@@ -231,16 +287,22 @@
            // åˆ›å»ºç¼–码器
            h264Encoder = new H264Encoder();
            
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            // è®¾ç½® Context(用于清理 TF å¡æ–‡ä»¶ï¼‰
            h264Encoder.setContext(this);
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH;
            int height = config != null && config.height > 0 ? config.height : DEFAULT_HEIGHT;
            int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            // èŽ·å–è¾“å‡ºæ–‡ä»¶ç›®å½•ï¼ˆæ ¹æ®useTFCard配置)
            boolean useTFCard = config != null && config.useTFCard;
            String outputDir = getOutputFileDirectory(useTFCard);
            // è®¾ç½®è¾“出文件目录(H264Encoder会自动管理文件创建,每分钟一个文件)
            // ä½¿ç”¨ä¸€ä¸ªä¸´æ—¶æ–‡ä»¶åæ¥è®¾ç½®ç›®å½•,H264Encoder会在初始化时创建第一个文件
            File tempFile = new File(outputFileDirectory, "temp.h264");
            File tempFile = new File(outputDir, "temp.h264");
            h264Encoder.setOutputFile(tempFile.getAbsolutePath());
            h264Encoder.setEnableFileOutput(true); // å¯ç”¨æ–‡ä»¶è¾“出
            
@@ -293,15 +355,24 @@
            // åˆ›å»ºç¼–码器
            h264Encoder = new H264Encoder();
            
            // è®¾ç½® Context(用于清理 TF å¡æ–‡ä»¶ï¼‰
            h264Encoder.setContext(this);
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH;
            int height = config != null && config.height > 0 ? config.height : DEFAULT_HEIGHT;
            int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            // èŽ·å–è¾“å‡ºæ–‡ä»¶ç›®å½•ï¼ˆæ ¹æ®useTFCard配置)
            boolean useTFCard = config != null && config.useTFCard;
            String outputDir = getOutputFileDirectory(useTFCard);
            // è®¾ç½®è¾“出文件目录(H264Encoder会自动管理文件创建,每分钟一个文件)
            // ä½¿ç”¨ä¸€ä¸ªä¸´æ—¶æ–‡ä»¶åæ¥è®¾ç½®ç›®å½•,H264Encoder会在初始化时创建第一个文件
            File tempFile = new File(outputFileDirectory, "temp.h264");
            File tempFile = new File(outputDir, "temp.h264");
            h264Encoder.setOutputFile(tempFile.getAbsolutePath());
            h264Encoder.setEnableFileOutput(true); // å¯ç”¨æ–‡ä»¶è¾“出
@@ -387,11 +458,49 @@
        }
        
        try {
            // æ£€æŸ¥æ–‡ä»¶æ˜¯å¦å­˜åœ¨
            File file = new File(config.filePath);
            // è§£æžå¾…传输文件路径,若不存在则尝试到TF卡目录按日期查找
            String resolvedFilePath = config.filePath;
            File file = new File(resolvedFilePath);
            if (!file.exists() || !file.isFile()) {
                Timber.e("File does not exist: %s (camera2)", config.filePath);
                return 1; // å¤±è´¥
                Timber.w("File does not exist, try TF card lookup (camera2): %s", resolvedFilePath);
                String fileName = file.getName();
                String timestampStr = null;
                // camera2的文件名格式:h264_camera2_1234567890123.h264
                if (fileName.startsWith("h264_camera2_") && fileName.endsWith(".h264")) {
                    timestampStr = fileName.substring(13, fileName.length() - 5); // åŽ»æŽ‰ "h264_camera2_" å’Œ ".h264"
                }
                if (timestampStr == null || timestampStr.trim().isEmpty()) {
                    Timber.e("Cannot parse timestamp from file name (camera2): %s", fileName);
                    return 1; // å¤±è´¥
                }
                try {
                    long timestamp = Long.parseLong(timestampStr);
                    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA);
                    String dateDir = dateFormat.format(new Date(timestamp));
                    String storagePath = FileUtil.getStoragePath(this, true);
                    if (storagePath == null || storagePath.trim().isEmpty()) {
                        Timber.e("TF card storage path not available when searching file (camera2)");
                        return 1; // å¤±è´¥
                    }
                    File tfRoot = new File(storagePath, "h264");
                    File candidate = new File(new File(tfRoot, dateDir), fileName);
                    if (candidate.exists() && candidate.isFile()) {
                        resolvedFilePath = candidate.getAbsolutePath();
                        file = candidate;
                        Timber.i("Found file on TF card (camera2): %s", resolvedFilePath);
                    } else {
                        Timber.e("File not found on TF card path (camera2): %s", candidate.getAbsolutePath());
                        return 1; // å¤±è´¥
                    }
                } catch (NumberFormatException e) {
                    Timber.e(e, "Failed to parse timestamp from file name (camera2): %s", fileName);
                    return 1; // å¤±è´¥
                }
            }
            
            // åˆ›å»ºæ–‡ä»¶ä¼ è¾“器
@@ -440,10 +549,10 @@
            }
            
            // å¼€å§‹ä¼ è¾“文件
            h264FileTransmitter.transmitFile(config.filePath);
            h264FileTransmitter.transmitFile(resolvedFilePath);
            
            Timber.d("File transmit started successfully (camera2), file: %s, server: %s:%d, protocol: %s, framerate: %d", 
                    config.filePath, config.ip, config.port,
                    resolvedFilePath, config.ip, config.port,
                    config.protocolType == JT1076ProtocolHelper.PROTOCOL_TYPE_UDP ? "UDP" : "TCP", framerate);
            return 0; // æˆåŠŸ
            
@@ -487,32 +596,99 @@
    /**
     * èŽ·å–èµ„æºåˆ—è¡¨ï¼ˆæ ¹æ®JT/T 1076-2016表23定义)
     */
    private List<ResourceInfo> getResourceList(String startTime, String endTime) {
        Timber.d("getResourceList called (camera2), startTime: %s, endTime: %s", startTime, endTime);
    private List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard,String jsonConfig) {
        Timber.d("getResourceList called (camera2), startTime: %s, endTime: %s, useTFCard: %b", startTime, endTime, useTFCard);
        
        List<ResourceInfo> resourceList = new ArrayList<>();
        
        try {
            // æ‰«æè¾“出目录中的H264文件(只查找camera2的文件)
            File dir = new File(outputFileDirectory);
            if (!dir.exists() || !dir.isDirectory()) {
                Timber.w("Output directory does not exist: %s", outputFileDirectory);
                return resourceList;
            }
            File[] files = dir.listFiles((dir1, name) ->
                name.toLowerCase().endsWith(".h264") && name.contains("camera2"));
            if (files == null || files.length == 0) {
                Timber.d("No H264 files found for camera2 in directory");
                return resourceList;
            }
            // è§£æžæ—¶é—´èŒƒå›´
            Date startDate = parseTime(startTime);
            Date endDate = parseTime(endTime);
            
            if (startDate == null || endDate == null) {
                Timber.e("Invalid time format, startTime: %s, endTime: %s", startTime, endTime);
                return resourceList;
            }
            if (useTFCard) {
                // ä½¿ç”¨TF卡:扫描TF卡上的h264文件夹,根据日期范围过滤
                String storagePath = FileUtil.getStoragePath(this, true);
                if (storagePath == null || storagePath.trim().isEmpty()) {
                    Timber.w("TF card storage path not available, fallback to app directory");
                    // å›žé€€åˆ°åº”用目录
                    return getResourceListFromDirectory(outputFileDirectory, startDate, endDate, true);
                }
                File externalStorage = new File(storagePath);
                if (!externalStorage.exists()) {
                    Timber.w("TF card storage directory does not exist: %s, fallback to app directory", storagePath);
                    // å›žé€€åˆ°åº”用目录
                    return getResourceListFromDirectory(outputFileDirectory, startDate, endDate, true);
                }
                // TF卡上的h264文件夹路径:/sdcard/h264/
                File h264Dir = new File(externalStorage, "h264");
                if (!h264Dir.exists() || !h264Dir.isDirectory()) {
                    Timber.w("TF card h264 directory does not exist: %s", h264Dir.getAbsolutePath());
                    return resourceList;
                }
                // èŽ·å–æ—¥æœŸèŒƒå›´å†…çš„æ‰€æœ‰æ—¥æœŸæ–‡ä»¶å¤¹
                List<String> dateDirs = getDateDirectoriesInRange(startDate, endDate);
                Timber.d("Found %d date directories in range", dateDirs.size());
                // æ‰«ææ¯ä¸ªæ—¥æœŸæ–‡ä»¶å¤¹ä¸‹çš„h264文件(只查找camera2的文件)
                for (String dateDir : dateDirs) {
                    File dateDirFile = new File(h264Dir, dateDir);
                    if (dateDirFile.exists() && dateDirFile.isDirectory()) {
                        List<ResourceInfo> dateResources = getResourceListFromDirectory(
                                dateDirFile.getAbsolutePath(), startDate, endDate, true);
                        resourceList.addAll(dateResources);
                    }
                }
                Timber.d("Found %d resources for camera2 in TF card time range", resourceList.size());
                return resourceList;
            } else {
                // ä¸ä½¿ç”¨TF卡:扫描应用目录(只查找camera2的文件)
                return getResourceListFromDirectory(outputFileDirectory, startDate, endDate, true);
            }
        } catch (Exception e) {
            Timber.e(e, "Error getting resource list (camera2)");
            return resourceList;
        }
    }
    /**
     * ä»ŽæŒ‡å®šç›®å½•扫描H264文件并创建资源列表
     * @param directoryPath ç›®å½•路径
     * @param startDate å¼€å§‹æ—¥æœŸ
     * @param endDate ç»“束日期
     * @param camera2Only æ˜¯å¦åªæŸ¥æ‰¾camera2的文件
     * @return èµ„源列表
     */
    private List<ResourceInfo> getResourceListFromDirectory(String directoryPath, Date startDate, Date endDate, boolean camera2Only) {
        List<ResourceInfo> resourceList = new ArrayList<>();
        try {
            File dir = new File(directoryPath);
            if (!dir.exists() || !dir.isDirectory()) {
                Timber.w("Directory does not exist: %s", directoryPath);
                return resourceList;
            }
            File[] files = dir.listFiles((dir1, name) -> {
                boolean isH264 = name.toLowerCase().endsWith(".h264");
                if (camera2Only) {
                    return isH264 && name.contains("camera2");
                } else {
                    return isH264;
                }
            });
            if (files == null || files.length == 0) {
                Timber.d("No H264 files found in directory: %s", directoryPath);
                return resourceList;
            }
            
@@ -524,19 +700,63 @@
                }
            }
            
            Timber.d("Found %d resources for camera2 in time range", resourceList.size());
            return resourceList;
            
        } catch (Exception e) {
            Timber.e(e, "Error getting resource list (camera2)");
            Timber.e(e, "Error getting resource list from directory: %s", directoryPath);
            return resourceList;
        }
    }
    
    /**
     * èŽ·å–æ—¥æœŸèŒƒå›´å†…çš„æ‰€æœ‰æ—¥æœŸæ–‡ä»¶å¤¹åç§°åˆ—è¡¨ï¼ˆæ ¼å¼ï¼šyyyyMMdd)
     * @param startDate å¼€å§‹æ—¥æœŸ
     * @param endDate ç»“束日期
     * @return æ—¥æœŸæ–‡ä»¶å¤¹åç§°åˆ—表
     */
    private List<String> getDateDirectoriesInRange(Date startDate, Date endDate) {
        List<String> dateDirs = new ArrayList<>();
        try {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA);
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(startDate);
            calendar.set(Calendar.HOUR_OF_DAY, 0);
            calendar.set(Calendar.MINUTE, 0);
            calendar.set(Calendar.SECOND, 0);
            calendar.set(Calendar.MILLISECOND, 0);
            Date currentDate = calendar.getTime();
            Date endDateOnly = new Date(endDate.getTime());
            Calendar endCalendar = Calendar.getInstance();
            endCalendar.setTime(endDateOnly);
            endCalendar.set(Calendar.HOUR_OF_DAY, 23);
            endCalendar.set(Calendar.MINUTE, 59);
            endCalendar.set(Calendar.SECOND, 59);
            endCalendar.set(Calendar.MILLISECOND, 999);
            endDateOnly = endCalendar.getTime();
            // éåŽ†ä»Žå¼€å§‹æ—¥æœŸåˆ°ç»“æŸæ—¥æœŸçš„æ‰€æœ‰æ—¥æœŸ
            while (!currentDate.after(endDateOnly)) {
                String dateDir = dateFormat.format(currentDate);
                dateDirs.add(dateDir);
                // å¢žåР䏀天
                calendar.add(Calendar.DAY_OF_MONTH, 1);
                currentDate = calendar.getTime();
            }
        } catch (Exception e) {
            Timber.e(e, "Error getting date directories in range");
        }
        return dateDirs;
    }
    /**
     * è®¾ç½®æ°´å°ä¿¡æ¯
     */
    private void setWatermarkInfo(String watermarkInfoJson) {
    private void setWatermarkInfo(String watermarkInfoJson,String jsonConfig) {
        Timber.d("setWatermarkInfo called (camera2), watermarkInfoJson: %s", watermarkInfoJson);
        
        try {
@@ -635,7 +855,7 @@
            if (fileName.startsWith("h264_camera2_") && fileName.endsWith(".h264")) {
                try {
                    // æå–文件名中的时间戳
                    String timestampStr = fileName.substring(14, fileName.length() - 5); // åŽ»æŽ‰ "h264_camera2_" å’Œ ".h264"
                    String timestampStr = fileName.substring(13, fileName.length() - 5); // åŽ»æŽ‰ "h264_camera2_" å’Œ ".h264"
                    long timestamp = Long.parseLong(timestampStr);
                    startTimeFromFileName = new Date(timestamp);
                } catch (NumberFormatException e) {
app/src/main/java/com/anyun/h264/H264Encoder.java
@@ -1,11 +1,13 @@
package com.anyun.h264;
import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import com.anyun.libusbcamera.UsbCamera;
import com.anyun.libusbcamera.WatermarkParam;
import com.anyun.h264.model.WatermarkInfo;
import com.anyun.h264.util.FileUtil;
import java.io.File;
import java.io.FileOutputStream;
@@ -82,6 +84,10 @@
    private int cameraId = 1; // æ‘„像头ID,默认为1(第一个摄像头)
    private long currentFileStartTime = 0; // å½“前文件的开始时间(毫秒)
    private static final long FILE_DURATION_MS = 60 * 1000; // æ–‡ä»¶æ—¶é•¿ï¼š1分钟(毫秒)
    // Context å’Œæ¸…理配置
    private Context context; // Context å¯¹è±¡ï¼Œç”¨äºŽæ¸…理 TF å¡æ–‡ä»¶
    private long maxH264TotalSizeGB = 100; // æœ€å¤§ H264 æ–‡ä»¶æ€»å¤§å°ï¼ˆGB),默认 100GB
    // ç½‘络传输控制
    private boolean enableNetworkTransmission = true; // æ˜¯å¦å¯ç”¨TCP/UDP网络传输
@@ -158,6 +164,22 @@
    }
    /**
     * è®¾ç½® Context(用于清理 TF å¡æ–‡ä»¶ï¼‰
     * @param context Context å¯¹è±¡
     */
    public void setContext(Context context) {
        this.context = context;
    }
    /**
     * è®¾ç½®æœ€å¤§ H264 æ–‡ä»¶æ€»å¤§å°ï¼ˆGB)
     * @param maxTotalSizeGB æœ€å¤§æ€»å¤§å°ï¼ˆGB),默认 5GB
     */
    public void setMaxH264TotalSizeGB(long maxTotalSizeGB) {
        this.maxH264TotalSizeGB = maxTotalSizeGB;
    }
    /**
     * è®¾ç½®æ˜¯å¦å¯ç”¨æ–‡ä»¶è¾“出
     * @param enable true表示启用文件输出,false表示禁用
     */
@@ -173,6 +195,18 @@
    public void setEnableNetworkTransmission(boolean enable) {
        this.enableNetworkTransmission = enable;
        Timber.d("Network transmission " + (enable ? "enabled" : "disabled"));
        // å¦‚果在编码过程中动态开启网络传输,需要确保底层Socket已经建立
        if (enable) {
            if (!protocolHelper.initializeSocket()) {
                Timber.e("Failed to initialize socket when enabling network transmission");
            }
        } else {
            // åŠ¨æ€å…³é—­ç½‘ç»œä¼ è¾“æ—¶ï¼ŒåŠæ—¶é‡Šæ”¾åº•å±‚Socket资源
            if (protocolHelper != null) {
                protocolHelper.closeSocket();
            }
        }
    }
    /**
@@ -286,6 +320,7 @@
                    }
                    break;
                } else {
                    usbCamera.stopCamera();
                    // å¤±è´¥ï¼Œè®°å½•日志
                    Timber.w( "prepareCamera failed on attempt " + (attempt + 1) + ": " + result);
                    if (attempt < maxRetries - 1) {
@@ -417,6 +452,25 @@
                    Timber.e(e, "Error closing previous file");
                }
                fileOutputStream = null;
            }
            // æ£€æŸ¥å¹¶æ¸…理 TF å¡ä¸Šçš„ h264 æ–‡ä»¶ï¼ˆå¦‚果需要)
            if (context != null && outputFileDirectory != null && !outputFileDirectory.isEmpty()) {
                try {
                    // åˆ¤æ–­å½“前目录是否在 TF å¡çš„ h264 ç›®å½•下
                    // ç›®å½•结构:/sdcard/h264/yyyyMMdd/,h264 æ ¹ç›®å½•应该是父目录的父目录
                    File currentDir = new File(outputFileDirectory);
                    File parentDir = currentDir.getParentFile();
                    if (parentDir != null && "h264".equals(parentDir.getName())) {
                        // å½“前目录在 h264 ç›®å½•下,获取 h264 æ ¹ç›®å½•
                        String h264RootDir = parentDir.getAbsolutePath();
                        Timber.d("Checking and cleaning up h264 files in: %s", h264RootDir);
                        FileUtil.cleanupH264Files(context, h264RootDir, maxH264TotalSizeGB, 1);
                    }
                } catch (Exception e) {
                    Timber.e(e, "Error during h264 files cleanup check");
                    // æ¸…理失败不影响文件创建,继续执行
                }
            }
            
            // ç”Ÿæˆæ–°æ–‡ä»¶å
@@ -774,8 +828,8 @@
        // å¤åˆ¶ IDR å¸§
        System.arraycopy(idr, 0, combined, offset, idr.length);
        Timber.d("Combined SPS/PPS/IDR frame, total size: %d (SPS: %d, PPS: %d, IDR: %d)",
                 totalLength, sps.length, pps.length, idr.length);
//        Timber.d("Combined SPS/PPS/IDR frame, total size: %d (SPS: %d, PPS: %d, IDR: %d)",
//                 totalLength, sps.length, pps.length, idr.length);
        return combined;
    }
app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java
@@ -55,6 +55,8 @@
    
    // TCP参数
    private JT1076TcpClient tcpClient;
    // æŽ§åˆ¶æœªè¿žæŽ¥æ—¥å¿—的输出频率,避免刷屏
    private boolean tcpNotConnectedLogged = false;
    
    // RTP协议参数
    private String simCardNumber = "123456789012"; // 12位SIM卡号
@@ -162,6 +164,8 @@
                    @Override
                    public void onConnected() {
                        Timber.d("TCP connection established");
                        // è¿žæŽ¥æˆåŠŸåŽï¼Œå…è®¸ä¸‹æ¬¡æ–­å¼€æ—¶å†æ¬¡æ‰“å°æœªè¿žæŽ¥å‘Šè­¦
                        tcpNotConnectedLogged = false;
                    }
                    
                    @Override
@@ -255,8 +259,14 @@
    public void sendTcpPacket(byte[] packet) {
        if (tcpClient != null && tcpClient.isConnected()) {
            tcpClient.sendPacket(packet);
            // å‘送成功,重置未连接日志标记
            tcpNotConnectedLogged = false;
        } else {
            Timber.w("TCP socket not connected");
            // ä»…在第一次检测到未连接时打印warn,避免日志刷屏
            if (!tcpNotConnectedLogged) {
                Timber.w("TCP socket not connected");
                tcpNotConnectedLogged = true;
            }
        }
    }
    
app/src/main/java/com/anyun/h264/model/ResourceInfo.java
@@ -155,3 +155,6 @@
    }
}
app/src/main/java/com/anyun/h264/model/WatermarkInfo.java
@@ -138,3 +138,5 @@
    }
}
app/src/main/java/com/anyun/h264/service/H264EncodeServiceClient.java
@@ -191,14 +191,14 @@
     * @param endTime ç»“束时间(格式:YYMMDDHHmmss,例如:240101235959)
     * @return èµ„源列表,如果失败返回null
     */
    public List<ResourceInfo> getResourceList(String startTime, String endTime) {
    public List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard,String jsonConfig) {
        if (!isServiceBound()) {
            Log.e(TAG, "Service is not bound");
            return null;
        }
        
        try {
            List<ResourceInfo> result = service.getResourceList(startTime, endTime);
            List<ResourceInfo> result = service.getResourceList(startTime, endTime,useTFCard,jsonConfig);
            Log.d(TAG, "getResourceList returned " + (result != null ? result.size() : 0) + " resources");
            return result;
        } catch (RemoteException e) {
@@ -214,14 +214,14 @@
     *                      å¦‚果传入null或空字符串,将清除水印
     * @return true-成功,false-失败
     */
    public boolean setWatermarkInfo(String watermarkInfo) {
    public boolean setWatermarkInfo(String watermarkInfo,String jsonConfig) {
        if (!isServiceBound()) {
            Log.e(TAG, "Service is not bound");
            return false;
        }
        
        try {
            service.setWatermarkInfo(watermarkInfo);
            service.setWatermarkInfo(watermarkInfo,jsonConfig);
            Log.d(TAG, "setWatermarkInfo called with: " + watermarkInfo);
            return true;
        } catch (RemoteException e) {
app/src/main/java/com/anyun/h264/util/FileUtil.java
New file
@@ -0,0 +1,249 @@
package com.anyun.h264.util;
import android.content.Context;
import android.os.storage.StorageManager;
import android.os.StatFs;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import timber.log.Timber;
public class FileUtil {
    //获取插入的TFCard目录路径
    public static String getStoragePath(Context mContext, boolean is_removale) {
        if (mContext != null) {
            StorageManager mStorageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE);
            Class<?> storageVolumeClazz = null;
            try {
                storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
                Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
                Method getPath = storageVolumeClazz.getMethod("getPath");
                Method isRemovable = storageVolumeClazz.getMethod("isRemovable");
                Object result = getVolumeList.invoke(mStorageManager);
                final int length = Array.getLength(result);
                for (int i = 0; i < length; i++) {
                    Object storageVolumeElement = Array.get(result, i);
                    String path = (String) getPath.invoke(storageVolumeElement);
                    boolean removable = (Boolean) isRemovable.invoke(storageVolumeElement);
                    if (is_removale == removable) {
                        return path;
                    }
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            return  null;
        } else {
            return null;
        }
    }
    /**
     * æ¸…理 TF å¡ä¸Šçš„ h264 æ–‡ä»¶
     * å½“ h264 æ–‡ä»¶æ€»å¤§å°è¶…过指定阈值或 TF å¡å‰©ä½™ç©ºé—´å°äºŽæŒ‡å®šå€¼æ—¶ï¼Œåˆ é™¤æ—¥æœŸæœ€æ—©çš„æ–‡ä»¶å¤¹
     *
     * @param context Context å¯¹è±¡ï¼Œç”¨äºŽèŽ·å– TF å¡è·¯å¾„和剩余空间
     * @param h264RootDir h264 æ ¹ç›®å½•路径,例如 "/sdcard/h264"
     * @param maxTotalSizeGB æœ€å¤§æ€»å¤§å°ï¼ˆGB),默认 5GB
     * @param minFreeSpaceGB æœ€å°å‰©ä½™ç©ºé—´ï¼ˆGB),默认 1GB
     */
    public static void cleanupH264Files(Context context, String h264RootDir, long maxTotalSizeGB, long minFreeSpaceGB) {
        if (context == null || h264RootDir == null || h264RootDir.trim().isEmpty()) {
            Timber.w("Context or h264 root directory is null, skip cleanup");
            return;
        }
        File h264Root = new File(h264RootDir);
        if (!h264Root.exists() || !h264Root.isDirectory()) {
            Timber.d("H264 root directory does not exist: %s, skip cleanup", h264RootDir);
            return;
        }
        try {
            // èŽ·å– TF å¡è·¯å¾„
            String tfCardPath = getStoragePath(context, true);
            if (tfCardPath == null || tfCardPath.trim().isEmpty()) {
                Timber.w("TF card path not available, skip cleanup");
                return;
            }
            // èŽ·å– TF å¡å‰©ä½™ç©ºé—´ï¼ˆGB)
            long freeSpaceGB = getFreeSpaceGB(tfCardPath);
            Timber.d("TF card free space: %d GB", freeSpaceGB);
            // æ‰«ææ‰€æœ‰æ—¥æœŸæ–‡ä»¶å¤¹
            File[] dateDirs = h264Root.listFiles(File::isDirectory);
            if (dateDirs == null || dateDirs.length == 0) {
                Timber.d("No date directories found in h264 root: %s", h264RootDir);
                return;
            }
            // æŒ‰æ—¥æœŸæŽ’序(最早的在前)
            List<DateDirInfo> dateDirList = new ArrayList<>();
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA);
            for (File dateDir : dateDirs) {
                String dirName = dateDir.getName();
                // åªå¤„理符合日期格式的文件夹(yyyyMMdd)
                if (dirName.length() == 8 && dirName.matches("\\d{8}")) {
                    try {
                        Date date = dateFormat.parse(dirName);
                        long totalSize = calculateH264FilesSize(dateDir);
                        dateDirList.add(new DateDirInfo(dateDir, date, totalSize));
                    } catch (ParseException e) {
                        Timber.w("Invalid date directory name: %s", dirName);
                    }
                }
            }
            // æŒ‰æ—¥æœŸæŽ’序(最早的在前)
            Collections.sort(dateDirList, new Comparator<DateDirInfo>() {
                @Override
                public int compare(DateDirInfo o1, DateDirInfo o2) {
                    return o1.date.compareTo(o2.date);
                }
            });
            // è®¡ç®—所有 h264 æ–‡ä»¶çš„æ€»å¤§å°ï¼ˆGB)
            long totalSizeGB = 0;
            for (DateDirInfo info : dateDirList) {
                totalSizeGB += info.totalSize / (1024L * 1024L * 1024L);
            }
            Timber.d("Total h264 files size: %d GB, Max allowed: %d GB", totalSizeGB, maxTotalSizeGB);
            Timber.d("TF card free space: %d GB, Min required: %d GB", freeSpaceGB, minFreeSpaceGB);
            // æ£€æŸ¥æ˜¯å¦éœ€è¦æ¸…理
            boolean needCleanup = (totalSizeGB > maxTotalSizeGB) || (freeSpaceGB < minFreeSpaceGB);
            if (!needCleanup) {
                Timber.d("No cleanup needed");
                return;
            }
            // åˆ é™¤æœ€æ—©çš„æ—¥æœŸæ–‡ä»¶å¤¹ï¼Œç›´åˆ°æ»¡è¶³æ¡ä»¶
            int deletedCount = 0;
            while (!dateDirList.isEmpty() && ((totalSizeGB > maxTotalSizeGB) || (freeSpaceGB < minFreeSpaceGB))) {
                DateDirInfo oldestDir = dateDirList.remove(0);
                Timber.d("Deleting oldest date directory: %s (size: %d GB, date: %s)",
                        oldestDir.dir.getName(), oldestDir.totalSize / (1024L * 1024L * 1024L),
                        dateFormat.format(oldestDir.date));
                // åˆ é™¤æ–‡ä»¶å¤¹åŠå…¶æ‰€æœ‰å†…容
                if (deleteDirectory(oldestDir.dir)) {
                    deletedCount++;
                    totalSizeGB -= oldestDir.totalSize / (1024L * 1024L * 1024L);
                    // é‡æ–°èŽ·å–å‰©ä½™ç©ºé—´
                    freeSpaceGB = getFreeSpaceGB(tfCardPath);
                    Timber.d("After deletion - Total size: %d GB, Free space: %d GB",
                            totalSizeGB, freeSpaceGB);
                } else {
                    Timber.e("Failed to delete directory: %s", oldestDir.dir.getAbsolutePath());
                    break; // åˆ é™¤å¤±è´¥ï¼Œåœæ­¢æ¸…理
                }
            }
            Timber.i("Cleanup completed. Deleted %d date directories. Final total size: %d GB, Free space: %d GB",
                    deletedCount, totalSizeGB, freeSpaceGB);
        } catch (Exception e) {
            Timber.e(e, "Error during h264 files cleanup");
        }
    }
    /**
     * æ¸…理 TF å¡ä¸Šçš„ h264 æ–‡ä»¶ï¼ˆä½¿ç”¨é»˜è®¤å‚数:最大5GB,最小剩余空间1GB)
     */
    public static void cleanupH264Files(Context context, String h264RootDir) {
        cleanupH264Files(context, h264RootDir, 5, 1);
    }
    /**
     * è®¡ç®—目录下所有 .h264 æ–‡ä»¶çš„æ€»å¤§å°ï¼ˆå­—节)
     */
    private static long calculateH264FilesSize(File dir) {
        long totalSize = 0;
        File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isFile() && file.getName().toLowerCase().endsWith(".h264")) {
                    totalSize += file.length();
                }
            }
        }
        return totalSize;
    }
    /**
     * èŽ·å–æŒ‡å®šè·¯å¾„çš„å‰©ä½™ç©ºé—´ï¼ˆGB)
     */
    private static long getFreeSpaceGB(String path) {
        try {
            StatFs statFs = new StatFs(path);
            long blockSize = statFs.getBlockSizeLong();
            long availableBlocks = statFs.getAvailableBlocksLong();
            long freeBytes = availableBlocks * blockSize;
            return freeBytes / (1024L * 1024L * 1024L); // è½¬æ¢ä¸º GB
        } catch (Exception e) {
            Timber.e(e, "Error getting free space for path: %s", path);
            return 0;
        }
    }
    /**
     * é€’归删除目录及其所有内容
     */
    private static boolean deleteDirectory(File dir) {
        if (dir == null || !dir.exists()) {
            return true;
        }
        if (dir.isDirectory()) {
            File[] children = dir.listFiles();
            if (children != null) {
                for (File child : children) {
                    if (!deleteDirectory(child)) {
                        return false;
                    }
                }
            }
        }
        return dir.delete();
    }
    /**
     * æ—¥æœŸæ–‡ä»¶å¤¹ä¿¡æ¯
     */
    private static class DateDirInfo {
        File dir;
        Date date;
        long totalSize; // å­—节
        DateDirInfo(File dir, Date date, long totalSize) {
            this.dir = dir;
            this.date = date;
            this.totalSize = totalSize;
        }
    }
}
app/src/main/res/values/strings.xml
@@ -1,3 +1,3 @@
<resources>
    <string name="app_name">My Application</string>
    <string name="app_name">HuBeiVideo</string>
</resources>
check_h264.py
@@ -207,3 +207,6 @@
    
    sys.exit(0 if success else 1)
¶à½ø³Ì·½°¸Ê¹ÓÃ˵Ã÷.md
@@ -115,3 +115,6 @@
- ç¬¬äºŒä¸ªè¿›ç¨‹é€šè¿‡`android:process=":camera2"`配置在独立进程中运行
- æ‰€æœ‰æ“ä½œéƒ½é€šè¿‡AIDL接口进行进程间通信
ÈçºÎ¼ì²étest.h264Îļþ.md
@@ -182,3 +182,5 @@
3. **用VLC播放**验证
4. **如果还有问题**,运行检查工具获取详细诊断