build static method

RenderPlan build({
  1. required List<VideoEditorClip> clips,
  2. required List<VideoEditorAudioTrack> audioTracks,
  3. required List<VideoEffectOverlay> effectOverlays,
  4. required Size canvas,
  5. required String outputPath,
  6. required List<String> videoCodecArgs,
  7. int fps = 30,
  8. List<RenderImageOverlay> imageOverlays = const [],
  9. String audioBitrate = '192k',
})

Implementation

static RenderPlan build({
  required List<VideoEditorClip> clips,
  required List<VideoEditorAudioTrack> audioTracks,
  required List<VideoEffectOverlay> effectOverlays,
  required Size canvas,
  required String outputPath,
  required List<String> videoCodecArgs,
  int fps = 30,
  List<RenderImageOverlay> imageOverlays = const [],
  String audioBitrate = '192k',
}) {
  if (clips.isEmpty) {
    throw ArgumentError('A render needs at least one clip.');
  }

  final width = canvas.width.round();
  final height = canvas.height.round();
  final inputs = <String>[];
  final filters = <String>[];
  var total = 0.0;

  // Each cut lands on the frame nearest its exact time, so every segment
  // is a whole number of frames (its sound exactly as long as its
  // pictures) and nothing drifts against the project timeline.
  final concatInputs = StringBuffer();
  var exactMs = 0;
  var usedFrames = 0;
  for (var i = 0; i < clips.length; i++) {
    exactMs += math.max(clips[i].effectiveDuration.inMilliseconds, 100);
    final frames = math.max(1, (exactMs * fps / 1000).round() - usedFrames);
    usedFrames += frames;
    total += _addClip(
      clip: clips[i],
      input: i,
      label: '$i',
      inputs: inputs,
      filters: filters,
      width: width,
      height: height,
      fps: fps,
      frames: frames,
      transitionIn: transitionInAt(clips, i),
      transitionOut: transitionOutAt(clips, i),
    );
    concatInputs.write('[v$i][a$i]');
  }
  var inputIndex = clips.length;

  filters.add(
    '${concatInputs}concat=n=${clips.length}:v=1:a=1[vcat][acat]',
  );
  var lastVideo = _addEffects(filters, 'vcat', effectOverlays);

  // Text and watermark images.
  var imageIndex = 0;
  for (final image in imageOverlays) {
    final input = inputIndex++;
    inputs.addAll(['-i', image.path]);
    final enable = image.end == null && image.start == Duration.zero
        ? ''
        : ':enable=${_enable(image.start, image.end ?? Duration(milliseconds: (total * 1000).round()))}';
    final next = 'vimg${imageIndex++}';
    filters.add(
      '[$lastVideo][$input:v]overlay='
      'x=main_w*${_num(image.centerX)}-overlay_w/2:'
      'y=main_h*${_num(image.centerY)}-overlay_h/2$enable[$next]',
    );
    lastVideo = next;
  }
  filters.add('[$lastVideo]format=yuv420p[vout]');

  final projectMs = (total * 1000).round();
  _addAudioMix(
    inputs: inputs,
    filters: filters,
    audioTracks: audioTracks,
    projectMs: projectMs,
    firstInput: inputIndex,
    base: 'acat',
  );

  final arguments = <String>[
    '-y',
    ...inputs,
    '-filter_complex', filters.join(';'),
    '-map', '[vout]',
    '-map', '[aout]',
    '-r', '$fps',
    ...videoCodecArgs,
    // Encoders that need another layout (hardware NV12) say so themselves.
    if (!videoCodecArgs.contains('-pix_fmt')) ...['-pix_fmt', 'yuv420p'],
    '-c:a', 'aac',
    '-b:a', audioBitrate,
    '-ar', '44100',
    '-movflags', '+faststart',
    '-t', _sec(total),
    outputPath,
  ];

  return RenderPlan(
    arguments: arguments,
    duration: Duration(milliseconds: projectMs),
    canvas: Size(width.toDouble(), height.toDouble()),
  );
}