1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 17:19:50 +00:00

Video exporting by calling FFMPEG externally (#980)

* Basic mp4 exporting, needs ffmpeg

* Add avi, ogv and mkv file exporting

* Add webm exporting

* Set ffmpeg path in the preferences

* Show an error message if the video fails to export

* Make sure to delete the temp files even if video exporting fails
This commit is contained in:
Emmanouil Papadeas 2024-01-24 02:00:17 +02:00 committed by GitHub
parent 204eff8184
commit 43d241a5c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 133 additions and 23 deletions

View file

@ -4,10 +4,24 @@ enum ExportTab { IMAGE = 0, SPRITESHEET = 1 }
enum Orientation { ROWS = 0, COLUMNS = 1 }
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
## See file_format_string, file_format_description, and ExportDialog.gd
enum FileFormat { PNG, WEBP, JPEG, GIF, APNG }
enum FileFormat { PNG, WEBP, JPEG, GIF, APNG, MP4, AVI, OGV, MKV, WEBM }
const TEMP_PATH := "user://tmp"
## List of animated formats
var animated_formats := [FileFormat.GIF, FileFormat.APNG]
var animated_formats := [
FileFormat.GIF,
FileFormat.APNG,
FileFormat.MP4,
FileFormat.AVI,
FileFormat.OGV,
FileFormat.MKV,
FileFormat.WEBM
]
var ffmpeg_formats := [
FileFormat.MP4, FileFormat.AVI, FileFormat.OGV, FileFormat.MKV, FileFormat.WEBM
]
## A dictionary of custom exporter generators (received from extensions)
var custom_file_formats := {}
@ -262,23 +276,28 @@ func export_processed_images(
return result
if is_single_file_format(project):
var exporter: AImgIOBaseExporter
if project.file_format == FileFormat.APNG:
exporter = AImgIOAPNGExporter.new()
if is_using_ffmpeg(project.file_format):
var video_exported := export_video(export_paths)
if not video_exported:
return false
else:
exporter = GIFAnimationExporter.new()
var details := {
"exporter": exporter,
"export_dialog": export_dialog,
"export_paths": export_paths,
"project": project
}
if not _multithreading_enabled():
export_animated(details)
else:
if gif_export_thread.is_started():
gif_export_thread.wait_to_finish()
gif_export_thread.start(export_animated.bind(details))
var exporter: AImgIOBaseExporter
if project.file_format == FileFormat.APNG:
exporter = AImgIOAPNGExporter.new()
else:
exporter = GIFAnimationExporter.new()
var details := {
"exporter": exporter,
"export_dialog": export_dialog,
"export_paths": export_paths,
"project": project
}
if not _multithreading_enabled():
export_animated(details)
else:
if gif_export_thread.is_started():
gif_export_thread.wait_to_finish()
gif_export_thread.start(export_animated.bind(details))
else:
var succeeded := true
for i in range(processed_images.size()):
@ -334,6 +353,39 @@ func export_processed_images(
return true
## Uses FFMPEG to export a video
func export_video(export_paths: PackedStringArray) -> bool:
DirAccess.make_dir_absolute(TEMP_PATH)
var temp_path_real := ProjectSettings.globalize_path(TEMP_PATH)
var input_file_path := temp_path_real.path_join("input.txt")
var input_file := FileAccess.open(input_file_path, FileAccess.WRITE)
for i in range(processed_images.size()):
var temp_file_name := str(i + 1).pad_zeros(number_of_digits) + ".png"
var temp_file_path := temp_path_real.path_join(temp_file_name)
processed_images[i].save_png(temp_file_path)
input_file.store_line("file '" + temp_file_name + "'")
input_file.store_line("duration %s" % durations[i])
input_file.close()
var ffmpeg_execute: PackedStringArray = [
"-y", "-f", "concat", "-i", input_file_path, export_paths[0]
]
var output := []
var success := OS.execute(Global.ffmpeg_path, ffmpeg_execute, output, true)
print(output)
var temp_dir := DirAccess.open(TEMP_PATH)
for file in temp_dir.get_files():
temp_dir.remove(file)
DirAccess.remove_absolute(TEMP_PATH)
if success < 0 or success > 1:
var fail_text := """Video failed to export. Make sure you have FFMPEG installed
and have set the correct path in the preferences."""
Global.error_dialog.set_text(tr(fail_text))
Global.error_dialog.popup_centered()
Global.dialog_open(true)
return false
return true
func export_animated(args: Dictionary) -> void:
var project: Project = args["project"]
var exporter: AImgIOBaseExporter = args["exporter"]
@ -397,6 +449,16 @@ func file_format_string(format_enum: int) -> String:
return ".gif"
FileFormat.APNG:
return ".apng"
FileFormat.MP4:
return ".mp4"
FileFormat.AVI:
return ".avi"
FileFormat.OGV:
return ".ogv"
FileFormat.MKV:
return ".mkv"
FileFormat.WEBM:
return ".webm"
_:
# If a file format description is not found, try generating one
if custom_exporter_generators.has(format_enum):
@ -418,6 +480,16 @@ func file_format_description(format_enum: int) -> String:
return "GIF Image"
FileFormat.APNG:
return "APNG Image"
FileFormat.MP4:
return "MPEG-4 Video"
FileFormat.AVI:
return "AVI Video"
FileFormat.OGV:
return "OGV Video"
FileFormat.MKV:
return "Matroska Video"
FileFormat.WEBM:
return "WebM Video"
_:
# If a file format description is not found, try generating one
for key in custom_file_formats.keys():
@ -426,12 +498,25 @@ func file_format_description(format_enum: int) -> String:
return ""
## True when exporting to .gif and .apng (and potentially video formats in the future)
## False when exporting to .png, and other non-animated formats in the future
## True when exporting to .gif, .apng and video
## False when exporting to .png, .jpg and static .webp
func is_single_file_format(project := Global.current_project) -> bool:
return animated_formats.has(project.file_format)
func is_using_ffmpeg(format: FileFormat) -> bool:
return ffmpeg_formats.has(format)
func is_ffmpeg_installed() -> bool:
if Global.ffmpeg_path.is_empty():
return false
var ffmpeg_executed := OS.execute(Global.ffmpeg_path, [])
if ffmpeg_executed == 0 or ffmpeg_executed == 1:
return true
return false
func _create_export_path(multifile: bool, project: Project, frame := 0) -> String:
var path := project.file_name
# Only append frame number when there are multiple files exported

View file

@ -131,6 +131,8 @@ var show_y_symmetry_axis := false
var open_last_project := false
## Found in Preferences. If [code]true[/code], asks for permission to quit on exit.
var quit_confirmation := false
## Found in Preferences. Refers to the ffmpeg location path.
var ffmpeg_path := ""
## Found in Preferences. If [code]true[/code], the zoom is smooth.
var smooth_zoom := true
## Found in Preferences. If [code]true[/code], the zoom is restricted to integral multiples of 100%.

View file

@ -7,6 +7,7 @@ var preferences: Array[Preference] = [
Preference.new(
"quit_confirmation", "Startup/StartupContainer/QuitConfirmation", "button_pressed"
),
Preference.new("ffmpeg_path", "Startup/StartupContainer/FFMPEGPath", "text"),
Preference.new("shrink", "%ShrinkSlider", "value"),
Preference.new("font_size", "Interface/InterfaceOptions/FontSizeSlider", "value"),
Preference.new("dim_on_popup", "Interface/InterfaceOptions/DimCheckBox", "button_pressed"),
@ -202,6 +203,10 @@ func _ready() -> void:
node.item_selected.connect(
_on_Preference_value_changed.bind(pref, restore_default_button)
)
"text":
node.text_changed.connect(
_on_Preference_value_changed.bind(pref, restore_default_button)
)
var global_value = Global.get(pref.prop_name)
if Global.config_cache.has_section_key("preferences", pref.prop_name):

View file

@ -92,6 +92,13 @@ layout_mode = 2
mouse_default_cursor_shape = 2
text = "On"
[node name="FFMPEGPathLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Startup/StartupContainer"]
layout_mode = 2
text = "FFMPEG path"
[node name="FFMPEGPath" type="LineEdit" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Startup/StartupContainer"]
layout_mode = 2
[node name="Language" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
visible = false
layout_mode = 2

View file

@ -13,7 +13,12 @@ var image_exports: Array[Export.FileFormat] = [
Export.FileFormat.WEBP,
Export.FileFormat.JPEG,
Export.FileFormat.GIF,
Export.FileFormat.APNG
Export.FileFormat.APNG,
Export.FileFormat.MP4,
Export.FileFormat.AVI,
Export.FileFormat.OGV,
Export.FileFormat.MKV,
Export.FileFormat.WEBM,
]
var spritesheet_exports: Array[Export.FileFormat] = [
Export.FileFormat.PNG, Export.FileFormat.WEBP, Export.FileFormat.JPEG
@ -188,6 +193,12 @@ func set_file_format_selector() -> void:
match Export.current_tab:
Export.ExportTab.IMAGE:
_set_file_format_selector_suitable_file_formats(image_exports)
if Export.is_ffmpeg_installed():
for format in Export.ffmpeg_formats:
file_format_options.set_item_disabled(format, false)
else:
for format in Export.ffmpeg_formats:
file_format_options.set_item_disabled(format, true)
Export.ExportTab.SPRITESHEET:
_set_file_format_selector_suitable_file_formats(spritesheet_exports)
@ -246,9 +257,9 @@ func update_dimensions_label() -> void:
func open_path_validation_alert_popup(path_or_name: int = -1) -> void:
# 0 is invalid path, 1 is invalid name
var error_text := "DirAccess path and file name are not valid!"
var error_text := "Directory path and file name are not valid!"
if path_or_name == 0:
error_text = "DirAccess path is not valid!"
error_text = "Directory path is not valid!"
elif path_or_name == 1:
error_text = "File name is not valid!"