mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-01-18 09:09:47 +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:
parent
204eff8184
commit
43d241a5c2
|
@ -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
|
||||
|
|
|
@ -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%.
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!"
|
||||
|
||||
|
|
Loading…
Reference in a new issue