video_util.dart 8.17 KB
import 'dart:io';
import 'dart:async';

import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
import 'package:ffmpeg_kit_flutter_new/statistics.dart';

class VideoUtil {
  ///
  /// 将视频格式转换为mp4
  /// 转码的同时,根据文件大小自动选择压缩参数
  /// iOS: h264_videotoolbox 硬件加速 + 按文件大小自动码率
  /// Android: libx264 + 按文件大小自动CRF
  /// [onProgress] 进度回调,值范围 0.0 ~ 1.0
  ///
  static Future<bool> convertToMp4(
    String inputPath,
    String outputPath, {
    void Function(int progress)? onProgress,
  }) async {
    // 先获取视频总时长(微秒)
    final duration = await _getVideoDuration(inputPath);
    final fileSize = await File(inputPath).length();

    String cmd;
    if (Platform.isIOS) {
      final bitrate = _getBitrateByFileSize(fileSize);
      cmd = '-i "$inputPath" '
          '-c:v h264_videotoolbox ' // 启用 iOS 硬件加速
          '-b:v ${bitrate}k ' // 根据文件大小自动计算码率
          '-vf scale=1280:-2 ' // 缩放到 720p (保持比例)
          '-c:a aac ' // 音频转为 AAC (兼容性最好)
          '-b:a 128k ' // 音频码率
          '"$outputPath"';
    } else {
      final crf = _getCrfByFileSize(fileSize);
      cmd = '-i "$inputPath" '
          '-c:v libx264 ' // 设置视频编码器为libx264(H.264)
          '-crf $crf ' // 根据文件大小自动计算CRF
          '-c:a aac ' // 设置音频编码器为AAC
          '-b:a 128k ' // 设置音频比特率为128kbps
          '-preset fast '
          '-threads 0 '
          '-strict experimental ' // 允许使用实验性编解码器功能
          '-movflags faststart ' // 优化MP4文件结构
          '-f mp4 ' // 指定输出格式为MP4
          '"$outputPath"'; // 指定输出文件路径
    }

    final completer = Completer<bool>();

    FFmpegKit.executeAsync(
      cmd,
      (session) async {
        final returnCode = await session.getReturnCode();
        completer.complete(ReturnCode.isSuccess(returnCode));
      },
      null,
      (Statistics statistics) {
        if (onProgress != null && duration > 0) {
          final currentTime = statistics.getTime();
          final progress = (currentTime / duration).clamp(0.0, 1.0);
          onProgress((progress * 100).floor());
        }
      },
    );

    return completer.future;
  }

  ///
  /// 通过 ffmpeg 压缩视频
  /// [quality] 压缩质量,可选值: 'low' | 'middle' | 'high'
  /// 未指定时根据文件大小自动计算CRF:文件越大压缩率越高,文件越小清晰度越高
  /// [onProgress] 进度回调,值范围 0.0 ~ 1.0
  ///
  static Future<bool> compressVideo(
    String inputPath,
    String outputPath,
    String? quality, {
    void Function(int progress)? onProgress,
  }) async {
    final duration = await _getVideoDuration(inputPath);

    // 使用CRF模式进行压缩,值范围0-51,建议值18-28
    // 高质量: CRF 18-20
    // 中等质量: CRF 23-26
    // 低质量: CRF 28-32
    int crf;
    if (quality != null && quality.isNotEmpty) {
      switch (quality) {
        case 'low':
          crf = 32;
          break;
        case 'middle':
          crf = 26;
          break;
        case 'high':
          crf = 20;
          break;
        default:
          throw Exception('参数错误');
      }
    } else {
      // 根据文件大小自动计算CRF
      final fileSize = await File(inputPath).length();
      crf = _getCrfByFileSize(fileSize);
    }
    String cmd = '-i "$inputPath" ' // 输入文件
        '-c:v libx264 ' // 视频编码器
        '-crf $crf ' // 恒定速率因子(质量控制)
        '-c:a aac ' // 音频编码器
        '-b:a 128k ' // 音频比特率
        // '-preset medium ' // 编码预设
        '-preset fast ' // 编码预设,编码速度显著提升,体积损失很小
        '-threads 0 ' // 让 libx264 自动使用所有可用 CPU 核心,默认行为可能只用单核
        '-movflags faststart ' // 优化MP4文件结构
        '"$outputPath"'; // 输出文件

    final completer = Completer<bool>();

    FFmpegKit.executeAsync(
      cmd,
      (session) async {
        final returnCode = await session.getReturnCode();
        completer.complete(ReturnCode.isSuccess(returnCode));
      },
      null,
      (Statistics statistics) {
        if (onProgress != null && duration > 0) {
          final currentTime = statistics.getTime();
          final progress = (currentTime / duration).clamp(0.0, 1.0);
          onProgress((progress * 100).floor());
        }
      },
    );

    return completer.future;
  }

  /// 根据文件大小自动计算视频码率(kbps)
  /// 用于iOS h264_videotoolbox硬件加速模式(不支持CRF,需用码率控制质量)
  /// 文件越大码率越低(压缩率越高,清晰度越低)
  /// 文件越小码率越高(压缩率越低,清晰度越高)
  static int _getBitrateByFileSize(int fileSizeBytes) {
    final fileSizeMB = fileSizeBytes / (1024 * 1024);
    if (fileSizeMB < 5) {
      return 4000;  // 小文件:高质量
    } else if (fileSizeMB < 20) {
      return 2500;  // 中小文件:较高质量
    } else if (fileSizeMB < 50) {
      return 1500;  // 中等文件:中等质量
    } else if (fileSizeMB < 100) {
      return 1000;  // 较大文件:较低质量
    } else if (fileSizeMB < 200) {
      return 800;   // 大文件:低质量
    } else {
      return 600;   // 超大文件:最低质量
    }
  }

  /// 根据文件大小自动计算CRF值
  /// 文件越大CRF越高(压缩率越高,清晰度越低)
  /// 文件越小CRF越低(压缩率越低,清晰度越高)
  static int _getCrfByFileSize(int fileSizeBytes) {
    final fileSizeMB = fileSizeBytes / (1024 * 1024);
    if (fileSizeMB < 5) {
      return 20;   // 小文件:高质量
    } else if (fileSizeMB < 20) {
      return 23;   // 中小文件:较高质量
    } else if (fileSizeMB < 50) {
      return 26;   // 中等文件:中等质量
    } else if (fileSizeMB < 100) {
      return 28;   // 较大文件:较低质量
    } else if (fileSizeMB < 200) {
      return 30;   // 大文件:低质量
    } else {
      return 32;   // 超大文件:最低质量
    }
  }

  /// 获取视频总时长,返回毫秒
  static Future<int> _getVideoDuration(String videoPath) async {
    final session = await FFmpegKit.execute(
      '-i "$videoPath"',
    );
    final output = await session.getOutput();
    // 从 ffmpeg 输出中解析时长,格式如: Duration: 00:01:23.45
    final regex = RegExp(r'Duration:\s*(\d+):(\d+):(\d+)\.(\d+)');
    final match = regex.firstMatch(output ?? '');
    if (match != null) {
      final hours = int.parse(match.group(1)!);
      final minutes = int.parse(match.group(2)!);
      final seconds = int.parse(match.group(3)!);
      final ms = int.parse(match.group(4)!);
      return (hours * 3600 + minutes * 60 + seconds) * 1000 + ms * 10;
    }
    return 0;
  }

  /// 为视频文件生成缩略图
  /// [videoPath] 要生成缩略图的视频文件路径
  /// [thumbnailPath] 要生成的缩略图文件路径
  /// 返回缩略图路径
  static Future<String?> genVideoThumbnail(String videoPath, String thumbnailPath) async {
    try {
      // final thumbnailPath = '${dir.path}/video_thumb_${DateTime.now().millisecondsSinceEpoch}.jpg';

      // 使用 ffmpeg_kit_flutter_new 生成视频缩略图
      // 构建FFmpeg命令行参数
      String cmd = '-i "$videoPath" ' // 指定输入文件路径
          '-ss 1 ' // 从视频第1秒处截取画面
          '-vframes 1 ' // 只截取一帧画面
          '-vf scale=128:-1 ' // 设置缩略图宽度为128像素,高度按比例缩放
          '-y ' // 覆盖已存在的输出文件
          '"$thumbnailPath"'; // 指定输出文件路径

      final session = await FFmpegKit.execute(cmd);
      final returnCode = await session.getReturnCode();

      if (ReturnCode.isSuccess(returnCode)) {
        return thumbnailPath;
      } else {
        print('生成视频缩略图失败: ${await session.getFailStackTrace()}');
        return null;
      }
    } catch (e) {
      print('生成视频缩略图出错: $e');
      return null;
    }
  }
}