renderProject static method
Future<String>
renderProject({
- required List<
VideoEditorClip> clips, - required List<
VideoEditorAudioTrack> audioTracks, - required List<
VideoTextOverlay> textOverlays, - required List<
VideoEffectOverlay> effectOverlays, - required VideoAspectRatio aspectRatio,
- bool isPreview = false,
- ExportSettings settings = const ExportSettings(),
- RenderProgressCallback? onProgress,
- RenderJob? job,
Renders the whole project to an MP4 and returns its path.
isPreview renders a small, fast proxy without text (the editor draws
text live on top of it); exports honour settings. Throws
RenderException on failure and RenderCancelledException when job
is cancelled.
Implementation
static Future<String> renderProject({
required List<VideoEditorClip> clips,
required List<VideoEditorAudioTrack> audioTracks,
required List<VideoTextOverlay> textOverlays,
required List<VideoEffectOverlay> effectOverlays,
required VideoAspectRatio aspectRatio,
bool isPreview = false,
ExportSettings settings = const ExportSettings(),
RenderProgressCallback? onProgress,
RenderJob? job,
}) async {
if (clips.isEmpty) return '';
final directory = await getTemporaryDirectory();
final timestamp = DateTime.now().microsecondsSinceEpoch;
final workDir = Directory('${directory.path}/render_$timestamp');
await workDir.create(recursive: true);
final outputPath =
'${directory.path}/${isPreview ? "preview" : "export"}_$timestamp.mp4';
final canvas = canvasFor(
clips: clips,
aspectRatio: aspectRatio,
shortSide: isPreview ? previewShortSide : settings.resolution.height,
);
final fps = isPreview ? 30 : settings.frameRate.fps;
try {
// Text is rasterized with the same code the preview draws with, so the
// export matches the editor exactly (fonts, styles and rotation).
final images = <RenderImageOverlay>[];
if (!isPreview) {
for (var i = 0; i < textOverlays.length; i++) {
final overlay = textOverlays[i];
if (overlay.text.trim().isEmpty) continue;
final raster = await TextOverlayPaint.rasterizeToPng(overlay, canvas);
final file = File('${workDir.path}/text_$i.png');
await file.writeAsBytes(raster.png, flush: true);
images.add(RenderImageOverlay(
path: file.path,
centerX: overlay.x,
centerY: overlay.y,
start: overlay.startTime,
end: overlay.effectiveEndTime,
));
}
if (settings.watermark) {
images.add(await _watermark(canvas, workDir));
}
}
// Small proxies decode much faster than 4K originals for previews.
final sources = isPreview
? clips
.map((c) => c.proxyVideoFile != null &&
c.proxyVideoFile!.path != c.file.path &&
c.proxyVideoFile!.existsSync()
? c.copyWith(file: c.proxyVideoFile)
: c)
.toList()
: clips;
RenderPlan planFor({required bool hardware}) => RenderGraphBuilder.build(
clips: sources,
audioTracks: audioTracks,
effectOverlays: effectOverlays,
imageOverlays: images,
canvas: canvas,
fps: fps,
outputPath: outputPath,
audioBitrate: isPreview ? '96k' : '192k',
videoCodecArgs: videoCodecArgs(
canvas: canvas,
fps: fps,
quality: settings.quality,
preview: isPreview,
hardware: hardware,
),
);
// The phone's hardware encoder is several times faster than x264. Some
// devices advertise it but fail on certain sizes, so fall back once.
final hardware = await hardwareEncoderAvailable();
final plan = planFor(hardware: hardware);
if (!isPreview) debugPrint('Render: ${plan.arguments.join(' ')}');
try {
await executePlan(plan, onProgress: onProgress, job: job);
} on RenderException catch (e) {
if (!hardware) rethrow;
debugPrint('Hardware encoder failed, using x264: ${e.message}');
_mediaCodecWorks = false;
await executePlan(planFor(hardware: false), onProgress: onProgress, job: job);
}
if (!File(outputPath).existsSync()) {
throw const RenderException('Render produced no output file');
}
onProgress?.call(1);
return outputPath;
} catch (e) {
final partial = File(outputPath);
if (partial.existsSync()) {
try {
partial.deleteSync();
} catch (_) {}
}
rethrow;
} finally {
try {
await workDir.delete(recursive: true);
} catch (_) {}
}
}