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:
parent
204eff8184
commit
43d241a5c2
|
@ -4,10 +4,24 @@ enum ExportTab { IMAGE = 0, SPRITESHEET = 1 }
|
||||||
enum Orientation { ROWS = 0, COLUMNS = 1 }
|
enum Orientation { ROWS = 0, COLUMNS = 1 }
|
||||||
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
|
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
|
||||||
## See file_format_string, file_format_description, and ExportDialog.gd
|
## 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
|
## 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)
|
## A dictionary of custom exporter generators (received from extensions)
|
||||||
var custom_file_formats := {}
|
var custom_file_formats := {}
|
||||||
|
@ -262,6 +276,11 @@ func export_processed_images(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
if is_single_file_format(project):
|
if is_single_file_format(project):
|
||||||
|
if is_using_ffmpeg(project.file_format):
|
||||||
|
var video_exported := export_video(export_paths)
|
||||||
|
if not video_exported:
|
||||||
|
return false
|
||||||
|
else:
|
||||||
var exporter: AImgIOBaseExporter
|
var exporter: AImgIOBaseExporter
|
||||||
if project.file_format == FileFormat.APNG:
|
if project.file_format == FileFormat.APNG:
|
||||||
exporter = AImgIOAPNGExporter.new()
|
exporter = AImgIOAPNGExporter.new()
|
||||||
|
@ -334,6 +353,39 @@ func export_processed_images(
|
||||||
return true
|
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:
|
func export_animated(args: Dictionary) -> void:
|
||||||
var project: Project = args["project"]
|
var project: Project = args["project"]
|
||||||
var exporter: AImgIOBaseExporter = args["exporter"]
|
var exporter: AImgIOBaseExporter = args["exporter"]
|
||||||
|
@ -397,6 +449,16 @@ func file_format_string(format_enum: int) -> String:
|
||||||
return ".gif"
|
return ".gif"
|
||||||
FileFormat.APNG:
|
FileFormat.APNG:
|
||||||
return ".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 a file format description is not found, try generating one
|
||||||
if custom_exporter_generators.has(format_enum):
|
if custom_exporter_generators.has(format_enum):
|
||||||
|
@ -418,6 +480,16 @@ func file_format_description(format_enum: int) -> String:
|
||||||
return "GIF Image"
|
return "GIF Image"
|
||||||
FileFormat.APNG:
|
FileFormat.APNG:
|
||||||
return "APNG Image"
|
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
|
# If a file format description is not found, try generating one
|
||||||
for key in custom_file_formats.keys():
|
for key in custom_file_formats.keys():
|
||||||
|
@ -426,12 +498,25 @@ func file_format_description(format_enum: int) -> String:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
## True when exporting to .gif and .apng (and potentially video formats in the future)
|
## True when exporting to .gif, .apng and video
|
||||||
## False when exporting to .png, and other non-animated formats in the future
|
## False when exporting to .png, .jpg and static .webp
|
||||||
func is_single_file_format(project := Global.current_project) -> bool:
|
func is_single_file_format(project := Global.current_project) -> bool:
|
||||||
return animated_formats.has(project.file_format)
|
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:
|
func _create_export_path(multifile: bool, project: Project, frame := 0) -> String:
|
||||||
var path := project.file_name
|
var path := project.file_name
|
||||||
# Only append frame number when there are multiple files exported
|
# 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
|
var open_last_project := false
|
||||||
## Found in Preferences. If [code]true[/code], asks for permission to quit on exit.
|
## Found in Preferences. If [code]true[/code], asks for permission to quit on exit.
|
||||||
var quit_confirmation := false
|
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.
|
## Found in Preferences. If [code]true[/code], the zoom is smooth.
|
||||||
var smooth_zoom := true
|
var smooth_zoom := true
|
||||||
## Found in Preferences. If [code]true[/code], the zoom is restricted to integral multiples of 100%.
|
## 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(
|
Preference.new(
|
||||||
"quit_confirmation", "Startup/StartupContainer/QuitConfirmation", "button_pressed"
|
"quit_confirmation", "Startup/StartupContainer/QuitConfirmation", "button_pressed"
|
||||||
),
|
),
|
||||||
|
Preference.new("ffmpeg_path", "Startup/StartupContainer/FFMPEGPath", "text"),
|
||||||
Preference.new("shrink", "%ShrinkSlider", "value"),
|
Preference.new("shrink", "%ShrinkSlider", "value"),
|
||||||
Preference.new("font_size", "Interface/InterfaceOptions/FontSizeSlider", "value"),
|
Preference.new("font_size", "Interface/InterfaceOptions/FontSizeSlider", "value"),
|
||||||
Preference.new("dim_on_popup", "Interface/InterfaceOptions/DimCheckBox", "button_pressed"),
|
Preference.new("dim_on_popup", "Interface/InterfaceOptions/DimCheckBox", "button_pressed"),
|
||||||
|
@ -202,6 +203,10 @@ func _ready() -> void:
|
||||||
node.item_selected.connect(
|
node.item_selected.connect(
|
||||||
_on_Preference_value_changed.bind(pref, restore_default_button)
|
_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)
|
var global_value = Global.get(pref.prop_name)
|
||||||
if Global.config_cache.has_section_key("preferences", 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
|
mouse_default_cursor_shape = 2
|
||||||
text = "On"
|
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"]
|
[node name="Language" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
|
@ -13,7 +13,12 @@ var image_exports: Array[Export.FileFormat] = [
|
||||||
Export.FileFormat.WEBP,
|
Export.FileFormat.WEBP,
|
||||||
Export.FileFormat.JPEG,
|
Export.FileFormat.JPEG,
|
||||||
Export.FileFormat.GIF,
|
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] = [
|
var spritesheet_exports: Array[Export.FileFormat] = [
|
||||||
Export.FileFormat.PNG, Export.FileFormat.WEBP, Export.FileFormat.JPEG
|
Export.FileFormat.PNG, Export.FileFormat.WEBP, Export.FileFormat.JPEG
|
||||||
|
@ -188,6 +193,12 @@ func set_file_format_selector() -> void:
|
||||||
match Export.current_tab:
|
match Export.current_tab:
|
||||||
Export.ExportTab.IMAGE:
|
Export.ExportTab.IMAGE:
|
||||||
_set_file_format_selector_suitable_file_formats(image_exports)
|
_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:
|
Export.ExportTab.SPRITESHEET:
|
||||||
_set_file_format_selector_suitable_file_formats(spritesheet_exports)
|
_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:
|
func open_path_validation_alert_popup(path_or_name: int = -1) -> void:
|
||||||
# 0 is invalid path, 1 is invalid name
|
# 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:
|
if path_or_name == 0:
|
||||||
error_text = "DirAccess path is not valid!"
|
error_text = "Directory path is not valid!"
|
||||||
elif path_or_name == 1:
|
elif path_or_name == 1:
|
||||||
error_text = "File name is not valid!"
|
error_text = "File name is not valid!"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue