| | |
| | | } |
| | | |
| | | /** |
| | | * 清理 TF 卡上的 h264 文件 |
| | | * 当 h264 文件总大小超过指定阈值或 TF 卡剩余空间小于指定值时,删除日期最早的文件夹 |
| | | * 清理 TF 卡上的 MP4 文件(例如 AnYun_VIDEO 目录) |
| | | * 当 MP4 文件总大小超过指定阈值或 TF 卡剩余空间小于指定值时,删除日期最早的文件夹 |
| | | * |
| | | * @param context Context 对象,用于获取 TF 卡路径和剩余空间 |
| | | * @param h264RootDir h264 根目录路径,例如 "/sdcard/h264" |
| | | * @param mp4RootDir MP4 根目录路径,例如 "/storage/XXXX-XXXX/AnYun_VIDEO" |
| | | * @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"); |
| | | public static void cleanupH264Files(Context context, String mp4RootDir, long maxTotalSizeGB, long minFreeSpaceGB) { |
| | | // 注意:为了兼容旧代码,方法名仍然叫 cleanupH264Files,但已经改为清理 MP4 文件 |
| | | if (context == null || mp4RootDir == null || mp4RootDir.trim().isEmpty()) { |
| | | Timber.w("Context or mp4 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); |
| | | File mp4Root = new File(mp4RootDir); |
| | | if (!mp4Root.exists() || !mp4Root.isDirectory()) { |
| | | Timber.d("MP4 root directory does not exist: %s, skip cleanup", mp4RootDir); |
| | | return; |
| | | } |
| | | |
| | |
| | | Timber.d("TF card free space: %d GB", freeSpaceGB); |
| | | |
| | | // 扫描所有日期文件夹 |
| | | File[] dateDirs = h264Root.listFiles(File::isDirectory); |
| | | File[] dateDirs = mp4Root.listFiles(File::isDirectory); |
| | | if (dateDirs == null || dateDirs.length == 0) { |
| | | Timber.d("No date directories found in h264 root: %s", h264RootDir); |
| | | Timber.d("No date directories found in mp4 root: %s", mp4RootDir); |
| | | return; |
| | | } |
| | | |
| | | // 按日期排序(最早的在前) |
| | | List<DateDirInfo> dateDirList = new ArrayList<>(); |
| | | SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA); |
| | | // AnYun_VIDEO 使用的是 yyyy_MM_dd 目录名,例如 2026_01_30 |
| | | SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy_MM_dd", Locale.CHINA); |
| | | |
| | | for (File dateDir : dateDirs) { |
| | | String dirName = dateDir.getName(); |
| | | // 只处理符合日期格式的文件夹(yyyyMMdd) |
| | | if (dirName.length() == 8 && dirName.matches("\\d{8}")) { |
| | | // 只处理符合日期格式的文件夹(yyyy_MM_dd,例如 2026_01_30) |
| | | if (dirName.matches("\\d{4}_\\d{2}_\\d{2}")) { |
| | | try { |
| | | Date date = dateFormat.parse(dirName); |
| | | long totalSize = calculateH264FilesSize(dateDir); |
| | | long totalSize = calculateMp4FilesSize(dateDir); |
| | | dateDirList.add(new DateDirInfo(dateDir, date, totalSize)); |
| | | } catch (ParseException e) { |
| | | Timber.w("Invalid date directory name: %s", dirName); |
| | |
| | | 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("Total mp4 files size: %d GB, Max allowed: %d GB", totalSizeGB, maxTotalSizeGB); |
| | | Timber.d("TF card free space: %d GB, Min required: %d GB", freeSpaceGB, minFreeSpaceGB); |
| | | |
| | | // 检查是否需要清理 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 清理 TF 卡上的 h264 文件(使用默认参数:最大5GB,最小剩余空间1GB) |
| | | * 清理 TF 卡上的 MP4 文件(使用默认参数:最大5GB,最小剩余空间1GB) |
| | | */ |
| | | public static void cleanupH264Files(Context context, String h264RootDir) { |
| | | cleanupH264Files(context, h264RootDir, 5, 1); |
| | | public static void cleanupH264Files(Context context, String mp4RootDir) { |
| | | // 为兼容旧调用保留方法名,内部已改为处理 MP4 |
| | | cleanupH264Files(context, mp4RootDir, 5, 1); |
| | | } |
| | | |
| | | /** |
| | | * 检查内部 Flash(非 TF 卡)剩余空间,如果小于 800MB,则按时间顺序删除 h264_*.h264 文件 |
| | | * 目录:context.getExternalFilesDir(null).getAbsolutePath() |
| | | * 文件名格式示例:h264_1735023032000.h264、h264_camera2_1735023032000.h264 |
| | | * 检查内部 Flash(非 TF 卡)剩余空间,如果小于 800MB,则按时间顺序删除 AnYun_VIDEO 下最早的 MP4 文件 |
| | | * 目录结构示例:/sdcard/AnYun_VIDEO/yyMMdd/HHmmss_xxx.mp4 |
| | | * |
| | | * 删除规则: |
| | | * - 按文件名中的时间戳从小到大(越早越先删)依次删除 |
| | | * - 递归遍历 AnYun_VIDEO 目录,收集所有 .mp4 文件 |
| | | * - 按 lastModified 时间从早到晚排序(越早越先删) |
| | | * - 每删除一次后重新计算剩余空间,直到 ≥ 800MB 或文件删完 |
| | | * |
| | | * 返回值: |
| | |
| | | return 0; |
| | | } |
| | | |
| | | File externalDir = context.getExternalFilesDir(null); |
| | | if (externalDir == null) { |
| | | Timber.w("ensureInternalFlashSpaceForH264: external files dir is null"); |
| | | // 内部存储根目录(与 VideoFileUtils 中保持一致) |
| | | File externalRoot = android.os.Environment.getExternalStorageDirectory(); |
| | | if (externalRoot == null) { |
| | | Timber.w("ensureInternalFlashSpaceForH264: external storage dir is null"); |
| | | return 0; |
| | | } |
| | | |
| | | String basePath = externalDir.getAbsolutePath(); |
| | | // AnYun_VIDEO 根目录 |
| | | File anyunRoot = new File(externalRoot, "AnYun_VIDEO"); |
| | | if (!anyunRoot.exists() || !anyunRoot.isDirectory()) { |
| | | Timber.w("ensureInternalFlashSpaceForH264: AnYun_VIDEO dir not found -> %s", anyunRoot.getAbsolutePath()); |
| | | return 0; |
| | | } |
| | | |
| | | String basePath = anyunRoot.getAbsolutePath(); |
| | | long minFreeBytes = 800L * 1024L * 1024L; // 800MB |
| | | |
| | | long freeBytes = getFreeSpaceBytes(basePath); |
| | |
| | | return 0; |
| | | } |
| | | |
| | | // 收集符合命名规则的 h264 文件 |
| | | File[] files = externalDir.listFiles(); |
| | | if (files == null || files.length == 0) { |
| | | Timber.w("ensureInternalFlashSpaceForH264: no files in dir -> %s", basePath); |
| | | // 收集所有 .mp4 文件 |
| | | List<File> mp4Files = new ArrayList<>(); |
| | | collectMp4Files(anyunRoot, mp4Files); |
| | | if (mp4Files.isEmpty()) { |
| | | Timber.w("ensureInternalFlashSpaceForH264: no mp4 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>() { |
| | | // 按 lastModified 升序排列(越早的越先删) |
| | | Collections.sort(mp4Files, new Comparator<File>() { |
| | | @Override |
| | | public int compare(H264FileInfo o1, H264FileInfo o2) { |
| | | return Long.compare(o1.timestamp, o2.timestamp); |
| | | public int compare(File o1, File o2) { |
| | | return Long.compare(o1.lastModified(), o2.lastModified()); |
| | | } |
| | | }); |
| | | |
| | | int deletedCount = 0; |
| | | for (H264FileInfo info : h264Files) { |
| | | for (File f : mp4Files) { |
| | | 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); |
| | | Timber.d("ensureInternalFlashSpaceForH264: deleting mp4 file -> %s, size=%d", |
| | | f.getAbsolutePath(), size); |
| | | |
| | | if (f.delete()) { |
| | | deletedCount++; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 计算目录下所有 .h264 文件的总大小(字节) |
| | | * 计算目录下所有 .mp4 文件的总大小(字节) |
| | | */ |
| | | private static long calculateH264FilesSize(File dir) { |
| | | private static long calculateMp4FilesSize(File dir) { |
| | | long totalSize = 0; |
| | | File[] files = dir.listFiles(); |
| | | if (files != null) { |
| | | for (File file : files) { |
| | | if (file.isFile() && file.getName().toLowerCase().endsWith(".h264")) { |
| | | if (file.isFile() && file.getName().toLowerCase().endsWith(".mp4")) { |
| | | totalSize += file.length(); |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 递归收集目录下所有 .mp4 文件 |
| | | */ |
| | | private static void collectMp4Files(File dir, List<File> outList) { |
| | | if (dir == null || !dir.exists()) { |
| | | return; |
| | | } |
| | | |
| | | File[] files = dir.listFiles(); |
| | | if (files == null) { |
| | | return; |
| | | } |
| | | |
| | | for (File f : files) { |
| | | if (f.isDirectory()) { |
| | | collectMp4Files(f, outList); |
| | | } else if (f.isFile() && f.getName().toLowerCase(Locale.CHINA).endsWith(".mp4")) { |
| | | outList.add(f); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取指定路径的剩余空间(字节) |
| | | */ |
| | | private static long getFreeSpaceBytes(String path) { |