build static method
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',
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()),
);
}