Path: blob/master/servers/movie_writer/movie_writer.cpp
21981 views
/**************************************************************************/1/* movie_writer.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "movie_writer.h"31#include "core/config/project_settings.h"32#include "core/io/dir_access.h"33#include "core/os/time.h"34#include "scene/main/window.h"35#include "servers/audio/audio_driver_dummy.h"36#include "servers/display/display_server.h"37#include "servers/rendering/rendering_server.h"3839MovieWriter *MovieWriter::writers[MovieWriter::MAX_WRITERS];40uint32_t MovieWriter::writer_count = 0;4142void MovieWriter::add_writer(MovieWriter *p_writer) {43ERR_FAIL_COND(writer_count == MAX_WRITERS);44writers[writer_count++] = p_writer;45}4647MovieWriter *MovieWriter::find_writer_for_file(const String &p_file) {48for (int32_t i = writer_count - 1; i >= 0; i--) { // More recent last, to have override ability.49if (writers[i]->handles_file(p_file)) {50return writers[i];51}52}53return nullptr;54}5556uint32_t MovieWriter::get_audio_mix_rate() const {57uint32_t ret = 48000;58GDVIRTUAL_CALL(_get_audio_mix_rate, ret);59return ret;60}61AudioServer::SpeakerMode MovieWriter::get_audio_speaker_mode() const {62AudioServer::SpeakerMode ret = AudioServer::SPEAKER_MODE_STEREO;63GDVIRTUAL_CALL(_get_audio_speaker_mode, ret);64return ret;65}6667Error MovieWriter::write_begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path) {68Error ret = ERR_UNCONFIGURED;69GDVIRTUAL_CALL(_write_begin, p_movie_size, p_fps, p_base_path, ret);70return ret;71}7273Error MovieWriter::write_frame(const Ref<Image> &p_image, const int32_t *p_audio_data) {74Error ret = ERR_UNCONFIGURED;75GDVIRTUAL_CALL(_write_frame, p_image, p_audio_data, ret);76return ret;77}7879void MovieWriter::write_end() {80GDVIRTUAL_CALL(_write_end);81}8283bool MovieWriter::handles_file(const String &p_path) const {84bool ret = false;85GDVIRTUAL_CALL(_handles_file, p_path, ret);86return ret;87}8889void MovieWriter::get_supported_extensions(List<String> *r_extensions) const {90Vector<String> exts;91GDVIRTUAL_CALL(_get_supported_extensions, exts);92for (int i = 0; i < exts.size(); i++) {93r_extensions->push_back(exts[i]);94}95}9697void MovieWriter::begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path) {98project_name = GLOBAL_GET("application/config/name");99movie_size = p_movie_size;100101print_line(vformat(U"Movie Maker mode enabled, recording movie in %s×%s @ %d FPS...", movie_size.width, movie_size.height, p_fps));102103// Check for available disk space and warn the user if needed.104Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);105String path = p_base_path.get_base_dir();106if (path.is_relative_path()) {107path = "res://" + path;108}109dir->open(path);110if (dir->get_space_left() < 10 * Math::pow(1024.0, 3.0)) {111// Less than 10 GiB available.112WARN_PRINT(vformat("Current available space on disk is low (%s). MovieWriter will fail during movie recording if the disk runs out of available space.", String::humanize_size(dir->get_space_left())));113}114115cpu_time = 0.0f;116gpu_time = 0.0f;117encoding_time_usec = 0;118119mix_rate = get_audio_mix_rate();120AudioDriverDummy::get_dummy_singleton()->set_mix_rate(mix_rate);121AudioDriverDummy::get_dummy_singleton()->set_speaker_mode(AudioDriver::SpeakerMode(get_audio_speaker_mode()));122fps = p_fps;123if ((mix_rate % fps) != 0) {124WARN_PRINT("MovieWriter's audio mix rate (" + itos(mix_rate) + ") can not be divided by the recording FPS (" + itos(fps) + "). Audio may go out of sync over time.");125}126127audio_channels = AudioDriverDummy::get_dummy_singleton()->get_channels();128audio_mix_buffer.resize(mix_rate * audio_channels / fps);129130write_begin(movie_size, p_fps, p_base_path);131}132133void MovieWriter::_bind_methods() {134ClassDB::bind_static_method("MovieWriter", D_METHOD("add_writer", "writer"), &MovieWriter::add_writer);135136GDVIRTUAL_BIND(_get_audio_mix_rate)137GDVIRTUAL_BIND(_get_audio_speaker_mode)138139GDVIRTUAL_BIND(_handles_file, "path")140141GDVIRTUAL_BIND(_write_begin, "movie_size", "fps", "base_path")142GDVIRTUAL_BIND(_write_frame, "frame_image", "audio_frame_block")143GDVIRTUAL_BIND(_write_end)144145GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/mix_rate", PROPERTY_HINT_RANGE, "8000,192000,1,suffix:Hz"), 48000);146GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/speaker_mode", PROPERTY_HINT_ENUM, "Stereo,3.1,5.1,7.1"), 0);147GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "editor/movie_writer/video_quality", PROPERTY_HINT_RANGE, "0.0,1.0,0.01"), 0.75);148GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/audio_bit_depth", PROPERTY_HINT_ENUM, "16:16,32:32"), 16);149GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "editor/movie_writer/ogv/audio_quality", PROPERTY_HINT_RANGE, "-0.1,1.0,0.01"), 0.5);150GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/ogv/encoding_speed", PROPERTY_HINT_ENUM, "Fastest (Lowest Efficiency):4,Fast (Low Efficiency):3,Slow (High Efficiency):2,Slowest (Highest Efficiency):1"), 4);151GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/ogv/keyframe_interval", PROPERTY_HINT_RANGE, "1,1024,1"), 64);152153// Used by the editor.154GLOBAL_DEF_BASIC("editor/movie_writer/movie_file", "");155GLOBAL_DEF_BASIC("editor/movie_writer/disable_vsync", false);156GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "editor/movie_writer/fps", PROPERTY_HINT_RANGE, "1,300,1,suffix:FPS"), 60);157}158159void MovieWriter::set_extensions_hint() {160RBSet<String> found;161for (uint32_t i = 0; i < writer_count; i++) {162List<String> extensions;163writers[i]->get_supported_extensions(&extensions);164for (const String &ext : extensions) {165found.insert(ext);166}167}168169String ext_hint;170171for (const String &S : found) {172if (ext_hint != "") {173ext_hint += ",";174}175ext_hint += "*." + S;176}177ProjectSettings::get_singleton()->set_custom_property_info(PropertyInfo(Variant::STRING, "editor/movie_writer/movie_file", PROPERTY_HINT_GLOBAL_SAVE_FILE, ext_hint));178}179180void MovieWriter::add_frame() {181const int movie_time_seconds = Engine::get_singleton()->get_frames_drawn() / fps;182const int frame_remainder = Engine::get_singleton()->get_frames_drawn() % fps;183const String movie_time = vformat("%s:%s:%s:%s",184String::num(movie_time_seconds / 3600, 0).pad_zeros(2),185String::num((movie_time_seconds % 3600) / 60, 0).pad_zeros(2),186String::num(movie_time_seconds % 60, 0).pad_zeros(2),187String::num(frame_remainder, 0).pad_zeros(2));188189Window *main_window = Window::get_from_id(DisplayServer::MAIN_WINDOW_ID);190if (main_window) {191main_window->set_title(vformat("MovieWriter: Frame %d (time: %s) - %s", Engine::get_singleton()->get_frames_drawn(), movie_time, project_name));192}193194RID main_vp_rid = RenderingServer::get_singleton()->viewport_find_from_screen_attachment(DisplayServer::MAIN_WINDOW_ID);195RID main_vp_texture = RenderingServer::get_singleton()->viewport_get_texture(main_vp_rid);196Ref<Image> vp_tex = RenderingServer::get_singleton()->texture_2d_get(main_vp_texture);197198if (vp_tex->get_size() != movie_size) {199// Resize the texture to the output resolution if it differs from the current viewport size.200// This ensures all frames have the same resolution, as not all video formats and players201// support resolution changes during playback.202203const float src_aspect = vp_tex->get_size().aspect();204const float dst_aspect = movie_size.aspect();205206int crop_width = vp_tex->get_size().width;207int crop_height = vp_tex->get_size().height;208int crop_x = 0;209int crop_y = 0;210211// If the aspect ratio differs, crop the image to cover the base resolution's aspect ratio212// in a way similar to `TextureRect.STRETCH_KEEP_ASPECT_COVERED`.213if (src_aspect > dst_aspect) {214// Source is wider, crop horizontally.215crop_width = int(vp_tex->get_size().height * dst_aspect);216crop_x = (vp_tex->get_size().width - crop_width) / 2;217vp_tex->crop_from_point(crop_x, crop_y, crop_width, crop_height);218} else if (src_aspect < dst_aspect) {219// Source is taller, crop vertically.220crop_height = int(vp_tex->get_size().width / dst_aspect);221crop_y = (vp_tex->get_size().height - crop_height) / 2;222vp_tex->crop_from_point(crop_x, crop_y, crop_width, crop_height);223}224225vp_tex->resize(movie_size.width, movie_size.height, Image::INTERPOLATE_BILINEAR);226}227228if (RenderingServer::get_singleton()->viewport_is_using_hdr_2d(main_vp_rid)) {229vp_tex->convert(Image::FORMAT_RGBA8);230vp_tex->linear_to_srgb();231}232233RenderingServer::get_singleton()->viewport_set_measure_render_time(main_vp_rid, true);234cpu_time += RenderingServer::get_singleton()->viewport_get_measured_render_time_cpu(main_vp_rid);235cpu_time += RenderingServer::get_singleton()->get_frame_setup_time_cpu();236gpu_time += RenderingServer::get_singleton()->viewport_get_measured_render_time_gpu(main_vp_rid);237238AudioDriverDummy::get_dummy_singleton()->mix_audio(mix_rate / fps, audio_mix_buffer.ptr());239240uint64_t encoding_start_usec = Time::get_singleton()->get_ticks_usec();241write_frame(vp_tex, audio_mix_buffer.ptr());242uint64_t encoding_end_usec = Time::get_singleton()->get_ticks_usec();243encoding_time_usec += encoding_end_usec - encoding_start_usec;244}245246void MovieWriter::end() {247uint64_t encoding_start_usec = Time::get_singleton()->get_ticks_usec();248write_end();249uint64_t encoding_end_usec = Time::get_singleton()->get_ticks_usec();250encoding_time_usec += encoding_end_usec - encoding_start_usec;251252// Print a report with various statistics.253print_line("--------------------------------------------------------------------------------");254String movie_path = Engine::get_singleton()->get_write_movie_path();255if (movie_path.is_relative_path()) {256// Print absolute path to make finding the file easier,257// and to make it clickable in terminal emulators that support this.258movie_path = ProjectSettings::get_singleton()->globalize_path("res://").path_join(movie_path);259}260print_line(vformat("Done recording movie at path: %s", movie_path));261262const int movie_time_seconds = Engine::get_singleton()->get_frames_drawn() / fps;263const int frame_remainder = Engine::get_singleton()->get_frames_drawn() % fps;264const String movie_time = vformat("%s:%s:%s:%s",265String::num(movie_time_seconds / 3600, 0).pad_zeros(2),266String::num((movie_time_seconds % 3600) / 60, 0).pad_zeros(2),267String::num(movie_time_seconds % 60, 0).pad_zeros(2),268String::num(frame_remainder, 0).pad_zeros(2));269270const int real_time_seconds = Time::get_singleton()->get_ticks_msec() / 1000;271const String real_time = vformat("%s:%s:%s",272String::num(real_time_seconds / 3600, 0).pad_zeros(2),273String::num((real_time_seconds % 3600) / 60, 0).pad_zeros(2),274String::num(real_time_seconds % 60, 0).pad_zeros(2));275276print_line(vformat("%d frames at %d FPS (movie length: %s), recorded in %s (%d%% of real-time speed).", Engine::get_singleton()->get_frames_drawn(), fps, movie_time, real_time, (float(MAX(1, movie_time_seconds)) / MAX(1, real_time_seconds)) * 100));277print_line(vformat("CPU render time: %.2f seconds (average: %.2f ms/frame)", cpu_time / 1000, cpu_time / Engine::get_singleton()->get_frames_drawn()));278print_line(vformat("GPU render time: %.2f seconds (average: %.2f ms/frame)", gpu_time / 1000, gpu_time / Engine::get_singleton()->get_frames_drawn()));279print_line(vformat("Encoding time: %.2f seconds (average: %.2f ms/frame)", encoding_time_usec / 1000000.f, encoding_time_usec / 1000.f / Engine::get_singleton()->get_frames_drawn()));280print_line("--------------------------------------------------------------------------------");281}282283284