@@ -4,10 +4,24 @@ enum ExportTab { IMAGE = 0, SPRITESHEET = 1 }
4
4
enum Orientation { ROWS = 0 , COLUMNS = 1 }
5
5
enum AnimationDirection { FORWARD = 0 , BACKWARDS = 1 , PING_PONG = 2 }
6
6
## See file_format_string, file_format_description, and ExportDialog.gd
7
- enum FileFormat { PNG , WEBP , JPEG , GIF , APNG }
7
+ enum FileFormat { PNG , WEBP , JPEG , GIF , APNG , MP4 , AVI , OGV , MKV , WEBM }
8
+
9
+ const TEMP_PATH := "user://tmp"
8
10
9
11
## List of animated formats
10
- var animated_formats := [FileFormat .GIF , FileFormat .APNG ]
12
+ var animated_formats := [
13
+ FileFormat .GIF ,
14
+ FileFormat .APNG ,
15
+ FileFormat .MP4 ,
16
+ FileFormat .AVI ,
17
+ FileFormat .OGV ,
18
+ FileFormat .MKV ,
19
+ FileFormat .WEBM
20
+ ]
21
+
22
+ var ffmpeg_formats := [
23
+ FileFormat .MP4 , FileFormat .AVI , FileFormat .OGV , FileFormat .MKV , FileFormat .WEBM
24
+ ]
11
25
12
26
## A dictionary of custom exporter generators (received from extensions)
13
27
var custom_file_formats := {}
@@ -262,23 +276,28 @@ func export_processed_images(
262
276
return result
263
277
264
278
if is_single_file_format (project ):
265
- var exporter : AImgIOBaseExporter
266
- if project .file_format == FileFormat .APNG :
267
- exporter = AImgIOAPNGExporter .new ()
268
- else :
269
- exporter = GIFAnimationExporter .new ()
270
- var details := {
271
- "exporter" : exporter ,
272
- "export_dialog" : export_dialog ,
273
- "export_paths" : export_paths ,
274
- "project" : project
275
- }
276
- if not _multithreading_enabled ():
277
- export_animated (details )
279
+ if is_using_ffmpeg (project .file_format ):
280
+ var video_exported := export_video (export_paths )
281
+ if not video_exported :
282
+ return false
278
283
else :
279
- if gif_export_thread .is_started ():
280
- gif_export_thread .wait_to_finish ()
281
- gif_export_thread .start (export_animated .bind (details ))
284
+ var exporter : AImgIOBaseExporter
285
+ if project .file_format == FileFormat .APNG :
286
+ exporter = AImgIOAPNGExporter .new ()
287
+ else :
288
+ exporter = GIFAnimationExporter .new ()
289
+ var details := {
290
+ "exporter" : exporter ,
291
+ "export_dialog" : export_dialog ,
292
+ "export_paths" : export_paths ,
293
+ "project" : project
294
+ }
295
+ if not _multithreading_enabled ():
296
+ export_animated (details )
297
+ else :
298
+ if gif_export_thread .is_started ():
299
+ gif_export_thread .wait_to_finish ()
300
+ gif_export_thread .start (export_animated .bind (details ))
282
301
else :
283
302
var succeeded := true
284
303
for i in range (processed_images .size ()):
@@ -334,6 +353,39 @@ func export_processed_images(
334
353
return true
335
354
336
355
356
+ ## Uses FFMPEG to export a video
357
+ func export_video (export_paths : PackedStringArray ) -> bool :
358
+ DirAccess .make_dir_absolute (TEMP_PATH )
359
+ var temp_path_real := ProjectSettings .globalize_path (TEMP_PATH )
360
+ var input_file_path := temp_path_real .path_join ("input.txt" )
361
+ var input_file := FileAccess .open (input_file_path , FileAccess .WRITE )
362
+ for i in range (processed_images .size ()):
363
+ var temp_file_name := str (i + 1 ).pad_zeros (number_of_digits ) + ".png"
364
+ var temp_file_path := temp_path_real .path_join (temp_file_name )
365
+ processed_images [i ].save_png (temp_file_path )
366
+ input_file .store_line ("file '" + temp_file_name + "'" )
367
+ input_file .store_line ("duration %s " % durations [i ])
368
+ input_file .close ()
369
+ var ffmpeg_execute : PackedStringArray = [
370
+ "-y" , "-f" , "concat" , "-i" , input_file_path , export_paths [0 ]
371
+ ]
372
+ var output := []
373
+ var success := OS .execute (Global .ffmpeg_path , ffmpeg_execute , output , true )
374
+ print (output )
375
+ var temp_dir := DirAccess .open (TEMP_PATH )
376
+ for file in temp_dir .get_files ():
377
+ temp_dir .remove (file )
378
+ DirAccess .remove_absolute (TEMP_PATH )
379
+ if success < 0 or success > 1 :
380
+ var fail_text := """Video failed to export. Make sure you have FFMPEG installed
381
+ and have set the correct path in the preferences."""
382
+ Global .error_dialog .set_text (tr (fail_text ))
383
+ Global .error_dialog .popup_centered ()
384
+ Global .dialog_open (true )
385
+ return false
386
+ return true
387
+
388
+
337
389
func export_animated (args : Dictionary ) -> void :
338
390
var project : Project = args ["project" ]
339
391
var exporter : AImgIOBaseExporter = args ["exporter" ]
@@ -397,6 +449,16 @@ func file_format_string(format_enum: int) -> String:
397
449
return ".gif"
398
450
FileFormat .APNG :
399
451
return ".apng"
452
+ FileFormat .MP4 :
453
+ return ".mp4"
454
+ FileFormat .AVI :
455
+ return ".avi"
456
+ FileFormat .OGV :
457
+ return ".ogv"
458
+ FileFormat .MKV :
459
+ return ".mkv"
460
+ FileFormat .WEBM :
461
+ return ".webm"
400
462
_ :
401
463
# If a file format description is not found, try generating one
402
464
if custom_exporter_generators .has (format_enum ):
@@ -418,6 +480,16 @@ func file_format_description(format_enum: int) -> String:
418
480
return "GIF Image"
419
481
FileFormat .APNG :
420
482
return "APNG Image"
483
+ FileFormat .MP4 :
484
+ return "MPEG-4 Video"
485
+ FileFormat .AVI :
486
+ return "AVI Video"
487
+ FileFormat .OGV :
488
+ return "OGV Video"
489
+ FileFormat .MKV :
490
+ return "Matroska Video"
491
+ FileFormat .WEBM :
492
+ return "WebM Video"
421
493
_ :
422
494
# If a file format description is not found, try generating one
423
495
for key in custom_file_formats .keys ():
@@ -426,12 +498,25 @@ func file_format_description(format_enum: int) -> String:
426
498
return ""
427
499
428
500
429
- ## True when exporting to .gif and .apng ( and potentially video formats in the future)
430
- ## False when exporting to .png, and other non-animated formats in the future
501
+ ## True when exporting to .gif, .apng and video
502
+ ## False when exporting to .png, .jpg and static .webp
431
503
func is_single_file_format (project := Global .current_project ) -> bool :
432
504
return animated_formats .has (project .file_format )
433
505
434
506
507
+ func is_using_ffmpeg (format : FileFormat ) -> bool :
508
+ return ffmpeg_formats .has (format )
509
+
510
+
511
+ func is_ffmpeg_installed () -> bool :
512
+ if Global .ffmpeg_path .is_empty ():
513
+ return false
514
+ var ffmpeg_executed := OS .execute (Global .ffmpeg_path , [])
515
+ if ffmpeg_executed == 0 or ffmpeg_executed == 1 :
516
+ return true
517
+ return false
518
+
519
+
435
520
func _create_export_path (multifile : bool , project : Project , frame := 0 ) -> String :
436
521
var path := project .file_name
437
522
# Only append frame number when there are multiple files exported
0 commit comments