新增 useTFCard 字段与 setUseTFCard,由两处服务在创建编码器时传入配置。H264EncodeService、H264EncodeService2 均调用了该 setter。
在创建新文件前(非 TF 卡模式)调用 ensureInternalFlashSpaceForH264;若清理后仍不足 800MB,停止文件输出并返回失败,不再在写帧时重复检查。
保留 TF 卡目录的清理逻辑,仅针对 TF 卡路径。
4个文件已修改
167 ■■■■■ 已修改文件
app/src/main/java/com/anyun/h264/H264EncodeService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264EncodeService2.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264Encoder.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/util/FileUtil.java 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264EncodeService.java
@@ -558,6 +558,7 @@
            
            // 设置 Context(用于清理 TF 卡文件)
            h264Encoder.setContext(this);
            h264Encoder.setUseTFCard(config.useTFCard);
            
            // 设置编码参数(使用配置中的参数)
            int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH;
app/src/main/java/com/anyun/h264/H264EncodeService2.java
@@ -290,6 +290,7 @@
            // 设置 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;
@@ -298,6 +299,7 @@
            // 获取输出文件目录(根据useTFCard配置)
            boolean useTFCard = config != null && config.useTFCard;
            h264Encoder.setUseTFCard(useTFCard);
            String outputDir = getOutputFileDirectory(useTFCard);
            
            // 设置输出文件目录(H264Encoder会自动管理文件创建,每分钟一个文件)
app/src/main/java/com/anyun/h264/H264Encoder.java
@@ -88,6 +88,7 @@
    // Context 和清理配置
    private Context context; // Context 对象,用于清理 TF 卡文件
    private long maxH264TotalSizeGB = 100; // 最大 H264 文件总大小(GB),默认 100GB
    private boolean useTFCard = false; // 是否使用 TF 卡输出
    // 网络传输控制
    private boolean enableNetworkTransmission = true; // 是否启用TCP/UDP网络传输
@@ -174,6 +175,13 @@
     */
    public void setContext(Context context) {
        this.context = context;
    }
    /**
     * 设置是否使用 TF 卡
     */
    public void setUseTFCard(boolean useTFCard) {
        this.useTFCard = useTFCard;
    }
    /**
@@ -459,6 +467,16 @@
                fileOutputStream = null;
            }
            
            // 如果使用内部 Flash(非 TF 卡),在创建新文件前检查并清理空间
            if (!useTFCard && context != null) {
                int result = FileUtil.ensureInternalFlashSpaceForH264(context);
                if (result == -1) {
                    Timber.e("Insufficient internal flash space (<800MB) even after cleanup, stop file output");
                    enableFileOutput = false;
                    return false;
                }
            }
            // 检查并清理 TF 卡上的 h264 文件(如果需要)
            if (context != null && outputFileDirectory != null && !outputFileDirectory.isEmpty()) {
                try {
app/src/main/java/com/anyun/h264/util/FileUtil.java
@@ -179,6 +179,137 @@
    }
    /**
     * 检查内部 Flash(非 TF 卡)剩余空间,如果小于 800MB,则按时间顺序删除 h264_*.h264 文件
     * 目录:context.getExternalFilesDir(null).getAbsolutePath()
     * 文件名格式示例:h264_1735023032000.h264、h264_camera2_1735023032000.h264
     *
     * 删除规则:
     * - 按文件名中的时间戳从小到大(越早越先删)依次删除
     * - 每删除一次后重新计算剩余空间,直到 ≥ 800MB 或文件删完
     *
     * 返回值:
     * - 0:最终剩余空间 ≥ 800MB 或无需删除
     * - -1:删除完所有符合规则的文件后,剩余空间仍然 < 800MB
     */
    public static int ensureInternalFlashSpaceForH264(Context context) {
        if (context == null) {
            Timber.w("ensureInternalFlashSpaceForH264: context is null");
            return 0;
        }
        File externalDir = context.getExternalFilesDir(null);
        if (externalDir == null) {
            Timber.w("ensureInternalFlashSpaceForH264: external files dir is null");
            return 0;
        }
        String basePath = externalDir.getAbsolutePath();
        long minFreeBytes = 800L * 1024L * 1024L; // 800MB
        long freeBytes = getFreeSpaceBytes(basePath);
        Timber.d("ensureInternalFlashSpaceForH264: freeBytes=%d, minRequired=%d", freeBytes, minFreeBytes);
        if (freeBytes >= minFreeBytes) {
            // 空间充足,无需处理
            return 0;
        }
        // 收集符合命名规则的 h264 文件
        File[] files = externalDir.listFiles();
        if (files == null || files.length == 0) {
            Timber.w("ensureInternalFlashSpaceForH264: no files in dir -> %s", basePath);
            // 已经没有可删的文件,如果仍小于 800MB,则直接返回 -1
            return freeBytes >= minFreeBytes ? 0 : -1;
        }
        class H264FileInfo {
            File file;
            long timestamp;
            H264FileInfo(File file, long timestamp) {
                this.file = file;
                this.timestamp = timestamp;
            }
        }
        List<H264FileInfo> h264Files = new ArrayList<>();
        for (File file : files) {
            if (!file.isFile()) {
                continue;
            }
            String name = file.getName();
            // 只处理 .h264 结尾,且以 h264_ 开头的文件
            if (!name.toLowerCase(Locale.CHINA).endsWith(".h264")) {
                continue;
            }
            if (!name.startsWith("h264_") && !name.startsWith("h264_camera2_")) {
                continue;
            }
            // 提取时间戳部分
            String timePart = null;
            if (name.startsWith("h264_camera2_")) {
                // 前缀长度 13:"h264_camera2_"
                timePart = name.substring("h264_camera2_".length(), name.length() - ".h264".length());
            } else if (name.startsWith("h264_")) {
                // 前缀长度 5:"h264_"
                timePart = name.substring("h264_".length(), name.length() - ".h264".length());
            }
            if (timePart == null || timePart.isEmpty()) {
                continue;
            }
            try {
                long ts = Long.parseLong(timePart);
                h264Files.add(new H264FileInfo(file, ts));
            } catch (NumberFormatException e) {
                // 文件名不符合时间戳格式,跳过
                Timber.w("ensureInternalFlashSpaceForH264: invalid timestamp in file name -> %s", name);
            }
        }
        if (h264Files.isEmpty()) {
            Timber.w("ensureInternalFlashSpaceForH264: no matched h264 files in -> %s", basePath);
            return freeBytes >= minFreeBytes ? 0 : -1;
        }
        // 按时间戳升序排列(越早的越先删)
        Collections.sort(h264Files, new Comparator<H264FileInfo>() {
            @Override
            public int compare(H264FileInfo o1, H264FileInfo o2) {
                return Long.compare(o1.timestamp, o2.timestamp);
            }
        });
        int deletedCount = 0;
        for (H264FileInfo info : h264Files) {
            if (freeBytes >= minFreeBytes) {
                break;
            }
            File f = info.file;
            long size = f.length();
            Timber.d("ensureInternalFlashSpaceForH264: deleting file -> %s, size=%d, ts=%d",
                    f.getAbsolutePath(), size, info.timestamp);
            if (f.delete()) {
                deletedCount++;
                // 删除后重新获取剩余空间,更准确
                freeBytes = getFreeSpaceBytes(basePath);
                Timber.d("ensureInternalFlashSpaceForH264: after delete, freeBytes=%d", freeBytes);
            } else {
                Timber.e("ensureInternalFlashSpaceForH264: failed to delete file -> %s", f.getAbsolutePath());
            }
        }
        Timber.i("ensureInternalFlashSpaceForH264: deleted %d files, final freeBytes=%d", deletedCount, freeBytes);
        return freeBytes >= minFreeBytes ? 0 : -1;
    }
    /**
     * 计算目录下所有 .h264 文件的总大小(字节)
     */
    private static long calculateH264FilesSize(File dir) {
@@ -211,6 +342,21 @@
    }
    /**
     * 获取指定路径的剩余空间(字节)
     */
    private static long getFreeSpaceBytes(String path) {
        try {
            StatFs statFs = new StatFs(path);
            long blockSize = statFs.getBlockSizeLong();
            long availableBlocks = statFs.getAvailableBlocksLong();
            return availableBlocks * blockSize;
        } catch (Exception e) {
            Timber.e(e, "Error getting free space (bytes) for path: %s", path);
            return 0;
        }
    }
    /**
     * 递归删除目录及其所有内容
     */
    private static boolean deleteDirectory(File dir) {