encodeMp4 function

Future<File> encodeMp4(
  1. List<File> frames,
  2. File output, {
  3. String? executable,
  4. int fps = 15,
  5. int? width,
  6. int? height,
  7. String paddingColor = 'black',
})

Encodes an ordered list of PNG frames as silent H.264 MP4.

Implementation

Future<File> encodeMp4(
  List<File> frames,
  File output, {
  String? executable,
  int fps = 15,
  int? width,
  int? height,
  String paddingColor = 'black',
}) async {
  if (frames.isEmpty) throw ArgumentError('At least one frame is required.');
  if (fps <= 0) throw ArgumentError.value(fps, 'fps', 'Must be positive.');
  if ((width == null) != (height == null) ||
      (width != null && (width <= 0 || height! <= 0))) {
    throw ArgumentError('Set both output width and height to positive values.');
  }
  final bin = executable ?? await _findFfmpeg();
  ProcessResult probe;
  try {
    probe = await Process.run(bin, ['-hide_banner', '-encoders']);
  } on ProcessException catch (error) {
    throw FfmpegException(
        'Could not run FFmpeg at "$bin": ${error.message}. Captured frames were preserved.');
  }
  final encoder = _availableH264Encoder('${probe.stdout}\n${probe.stderr}');
  if (probe.exitCode != 0 || encoder == null) {
    throw FfmpegException(
      'FFmpeg has no supported H.264 encoder. Captured frames were preserved.',
    );
  }
  final parent = output.absolute.parent;
  parent.createSync(recursive: true);
  final concat = File(
    p.join(parent.path, '.${output.uri.pathSegments.last}.frames.txt'),
  );
  String quote(File file) =>
      "'${file.absolute.path.replaceAll('\\', '/').replaceAll("'", "'\\''")}'";
  concat.writeAsStringSync(
    frames.map((f) => "file ${quote(f)}\nduration ${1 / fps}").join('\n') +
        '\nfile ${quote(frames.last)}\n',
  );
  try {
    final evenWidth = width == null ? null : (width + 1) & ~1;
    final evenHeight = height == null ? null : (height + 1) & ~1;
    final scale = evenWidth != null && evenHeight != null
        ? 'scale=$evenWidth:$evenHeight:force_original_aspect_ratio=decrease,pad=$evenWidth:$evenHeight:(ow-iw)/2:(oh-ih)/2:$paddingColor'
        : 'pad=ceil(iw/2)*2:ceil(ih/2)*2:0:0:$paddingColor';
    final result = await Process.run(bin, [
      '-hide_banner',
      '-y',
      '-f',
      'concat',
      '-safe',
      '0',
      '-i',
      concat.path,
      '-fps_mode',
      'vfr',
      '-vf',
      scale,
      '-an',
      '-c:v',
      encoder,
      '-pix_fmt',
      'yuv420p',
      output.absolute.path,
    ]);
    if (result.exitCode != 0)
      throw FfmpegException(
        'FFmpeg MP4 encoding failed (exit ${result.exitCode}):\n${result.stderr}',
      );
    File('${output.path}.wtc-owner')
        .writeAsStringSync('widget_test_capture:media:v1\n', flush: true);
    return output;
  } finally {
    if (concat.existsSync()) concat.deleteSync();
  }
}