| 1 | /**************************************************************************/ |
| 2 | /* movie_writer.cpp */ |
| 3 | /**************************************************************************/ |
| 4 | /* This file is part of: */ |
| 5 | /* GODOT ENGINE */ |
| 6 | /* https://godotengine.org */ |
| 7 | /**************************************************************************/ |
| 8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
| 9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
| 10 | /* */ |
| 11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
| 12 | /* a copy of this software and associated documentation files (the */ |
| 13 | /* "Software"), to deal in the Software without restriction, including */ |
| 14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
| 15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
| 16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
| 17 | /* the following conditions: */ |
| 18 | /* */ |
| 19 | /* The above copyright notice and this permission notice shall be */ |
| 20 | /* included in all copies or substantial portions of the Software. */ |
| 21 | /* */ |
| 22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
| 23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
| 24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
| 25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
| 26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
| 27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
| 28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
| 29 | /**************************************************************************/ |
| 30 | |
| 31 | #include "movie_writer.h" |
| 32 | #include "core/config/project_settings.h" |
| 33 | #include "core/io/dir_access.h" |
| 34 | #include "core/os/time.h" |
| 35 | #include "servers/display_server.h" |
| 36 | #include "servers/rendering_server.h" |
| 37 | |
| 38 | MovieWriter *MovieWriter::writers[MovieWriter::MAX_WRITERS]; |
| 39 | uint32_t MovieWriter::writer_count = 0; |
| 40 | |
| 41 | void MovieWriter::add_writer(MovieWriter *p_writer) { |
| 42 | ERR_FAIL_COND(writer_count == MAX_WRITERS); |
| 43 | writers[writer_count++] = p_writer; |
| 44 | } |
| 45 | |
| 46 | MovieWriter *MovieWriter::find_writer_for_file(const String &p_file) { |
| 47 | for (int32_t i = writer_count - 1; i >= 0; i--) { // More recent last, to have override ability. |
| 48 | if (writers[i]->handles_file(p_file)) { |
| 49 | return writers[i]; |
| 50 | } |
| 51 | } |
| 52 | return nullptr; |
| 53 | } |
| 54 | |
| 55 | uint32_t MovieWriter::get_audio_mix_rate() const { |
| 56 | uint32_t ret = 48000; |
| 57 | GDVIRTUAL_REQUIRED_CALL(_get_audio_mix_rate, ret); |
| 58 | return ret; |
| 59 | } |
| 60 | AudioServer::SpeakerMode MovieWriter::get_audio_speaker_mode() const { |
| 61 | AudioServer::SpeakerMode ret = AudioServer::SPEAKER_MODE_STEREO; |
| 62 | GDVIRTUAL_REQUIRED_CALL(_get_audio_speaker_mode, ret); |
| 63 | return ret; |
| 64 | } |
| 65 | |
| 66 | Error MovieWriter::write_begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path) { |
| 67 | Error ret = ERR_UNCONFIGURED; |
| 68 | GDVIRTUAL_REQUIRED_CALL(_write_begin, p_movie_size, p_fps, p_base_path, ret); |
| 69 | return ret; |
| 70 | } |
| 71 | |
| 72 | Error MovieWriter::write_frame(const Ref<Image> &p_image, const int32_t *p_audio_data) { |
| 73 | Error ret = ERR_UNCONFIGURED; |
| 74 | GDVIRTUAL_REQUIRED_CALL(_write_frame, p_image, p_audio_data, ret); |
| 75 | return ret; |
| 76 | } |
| 77 | |
| 78 | void MovieWriter::write_end() { |
| 79 | GDVIRTUAL_REQUIRED_CALL(_write_end); |
| 80 | } |
| 81 | |
| 82 | bool MovieWriter::handles_file(const String &p_path) const { |
| 83 | bool ret = false; |
| 84 | GDVIRTUAL_REQUIRED_CALL(_handles_file, p_path, ret); |
| 85 | return ret; |
| 86 | } |
| 87 | |
| 88 | void MovieWriter::get_supported_extensions(List<String> *r_extensions) const { |
| 89 | Vector<String> exts; |
| 90 | GDVIRTUAL_REQUIRED_CALL(_get_supported_extensions, exts); |
| 91 | for (int i = 0; i < exts.size(); i++) { |
| 92 | r_extensions->push_back(exts[i]); |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | void MovieWriter::begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path) { |
| 97 | project_name = GLOBAL_GET("application/config/name" ); |
| 98 | |
| 99 | print_line(vformat("Movie Maker mode enabled, recording movie at %d FPS..." , p_fps)); |
| 100 | |
| 101 | // Check for available disk space and warn the user if needed. |
| 102 | Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); |
| 103 | String path = p_base_path.get_basename(); |
| 104 | if (path.is_relative_path()) { |
| 105 | path = "res://" + path; |
| 106 | } |
| 107 | dir->open(path); |
| 108 | if (dir->get_space_left() < 10 * Math::pow(1024.0, 3.0)) { |
| 109 | // Less than 10 GiB available. |
| 110 | WARN_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()))); |
| 111 | } |
| 112 | |
| 113 | cpu_time = 0.0f; |
| 114 | gpu_time = 0.0f; |
| 115 | |
| 116 | mix_rate = get_audio_mix_rate(); |
| 117 | AudioDriverDummy::get_dummy_singleton()->set_mix_rate(mix_rate); |
| 118 | AudioDriverDummy::get_dummy_singleton()->set_speaker_mode(AudioDriver::SpeakerMode(get_audio_speaker_mode())); |
| 119 | fps = p_fps; |
| 120 | if ((mix_rate % fps) != 0) { |
| 121 | WARN_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." ); |
| 122 | } |
| 123 | |
| 124 | audio_channels = AudioDriverDummy::get_dummy_singleton()->get_channels(); |
| 125 | audio_mix_buffer.resize(mix_rate * audio_channels / fps); |
| 126 | |
| 127 | write_begin(p_movie_size, p_fps, p_base_path); |
| 128 | } |
| 129 | |
| 130 | void MovieWriter::_bind_methods() { |
| 131 | ClassDB::bind_static_method("MovieWriter" , D_METHOD("add_writer" , "writer" ), &MovieWriter::add_writer); |
| 132 | |
| 133 | GDVIRTUAL_BIND(_get_audio_mix_rate) |
| 134 | GDVIRTUAL_BIND(_get_audio_speaker_mode) |
| 135 | |
| 136 | GDVIRTUAL_BIND(_handles_file, "path" ) |
| 137 | |
| 138 | GDVIRTUAL_BIND(_write_begin, "movie_size" , "fps" , "base_path" ) |
| 139 | GDVIRTUAL_BIND(_write_frame, "frame_image" , "audio_frame_block" ) |
| 140 | GDVIRTUAL_BIND(_write_end) |
| 141 | |
| 142 | GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/mix_rate" , PROPERTY_HINT_RANGE, "8000,192000,1,suffix:Hz" ), 48000); |
| 143 | GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/speaker_mode" , PROPERTY_HINT_ENUM, "Stereo,3.1,5.1,7.1" ), 0); |
| 144 | GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "editor/movie_writer/mjpeg_quality" , PROPERTY_HINT_RANGE, "0.01,1.0,0.01" ), 0.75); |
| 145 | // Used by the editor. |
| 146 | GLOBAL_DEF_BASIC("editor/movie_writer/movie_file" , "" ); |
| 147 | GLOBAL_DEF_BASIC("editor/movie_writer/disable_vsync" , false); |
| 148 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "editor/movie_writer/fps" , PROPERTY_HINT_RANGE, "1,300,1,suffix:FPS" ), 60); |
| 149 | } |
| 150 | |
| 151 | void MovieWriter::set_extensions_hint() { |
| 152 | RBSet<String> found; |
| 153 | for (uint32_t i = 0; i < writer_count; i++) { |
| 154 | List<String> extensions; |
| 155 | writers[i]->get_supported_extensions(&extensions); |
| 156 | for (const String &ext : extensions) { |
| 157 | found.insert(ext); |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | String ext_hint; |
| 162 | |
| 163 | for (const String &S : found) { |
| 164 | if (ext_hint != "" ) { |
| 165 | ext_hint += "," ; |
| 166 | } |
| 167 | ext_hint += "*." + S; |
| 168 | } |
| 169 | ProjectSettings::get_singleton()->set_custom_property_info(PropertyInfo(Variant::STRING, "editor/movie_writer/movie_file" , PROPERTY_HINT_GLOBAL_SAVE_FILE, ext_hint)); |
| 170 | } |
| 171 | |
| 172 | void MovieWriter::add_frame() { |
| 173 | const int movie_time_seconds = Engine::get_singleton()->get_frames_drawn() / fps; |
| 174 | const String movie_time = vformat("%s:%s:%s" , |
| 175 | String::num(movie_time_seconds / 3600).pad_zeros(2), |
| 176 | String::num((movie_time_seconds % 3600) / 60).pad_zeros(2), |
| 177 | String::num(movie_time_seconds % 60).pad_zeros(2)); |
| 178 | |
| 179 | #ifdef DEBUG_ENABLED |
| 180 | DisplayServer::get_singleton()->window_set_title(vformat("MovieWriter: Frame %d (time: %s) - %s (DEBUG)" , Engine::get_singleton()->get_frames_drawn(), movie_time, project_name)); |
| 181 | #else |
| 182 | DisplayServer::get_singleton()->window_set_title(vformat("MovieWriter: Frame %d (time: %s) - %s" , Engine::get_singleton()->get_frames_drawn(), movie_time, project_name)); |
| 183 | #endif |
| 184 | |
| 185 | RID main_vp_rid = RenderingServer::get_singleton()->viewport_find_from_screen_attachment(DisplayServer::MAIN_WINDOW_ID); |
| 186 | RID main_vp_texture = RenderingServer::get_singleton()->viewport_get_texture(main_vp_rid); |
| 187 | Ref<Image> vp_tex = RenderingServer::get_singleton()->texture_2d_get(main_vp_texture); |
| 188 | |
| 189 | RenderingServer::get_singleton()->viewport_set_measure_render_time(main_vp_rid, true); |
| 190 | cpu_time += RenderingServer::get_singleton()->viewport_get_measured_render_time_cpu(main_vp_rid); |
| 191 | cpu_time += RenderingServer::get_singleton()->get_frame_setup_time_cpu(); |
| 192 | gpu_time += RenderingServer::get_singleton()->viewport_get_measured_render_time_gpu(main_vp_rid); |
| 193 | |
| 194 | AudioDriverDummy::get_dummy_singleton()->mix_audio(mix_rate / fps, audio_mix_buffer.ptr()); |
| 195 | write_frame(vp_tex, audio_mix_buffer.ptr()); |
| 196 | } |
| 197 | |
| 198 | void MovieWriter::end() { |
| 199 | write_end(); |
| 200 | |
| 201 | // Print a report with various statistics. |
| 202 | print_line("----------------" ); |
| 203 | String movie_path = Engine::get_singleton()->get_write_movie_path(); |
| 204 | if (movie_path.is_relative_path()) { |
| 205 | // Print absolute path to make finding the file easier, |
| 206 | // and to make it clickable in terminal emulators that support this. |
| 207 | movie_path = ProjectSettings::get_singleton()->globalize_path("res://" ).path_join(movie_path); |
| 208 | } |
| 209 | print_line(vformat("Done recording movie at path: %s" , movie_path)); |
| 210 | |
| 211 | const int movie_time_seconds = Engine::get_singleton()->get_frames_drawn() / fps; |
| 212 | const String movie_time = vformat("%s:%s:%s" , |
| 213 | String::num(movie_time_seconds / 3600).pad_zeros(2), |
| 214 | String::num((movie_time_seconds % 3600) / 60).pad_zeros(2), |
| 215 | String::num(movie_time_seconds % 60).pad_zeros(2)); |
| 216 | |
| 217 | const int real_time_seconds = Time::get_singleton()->get_ticks_msec() / 1000; |
| 218 | const String real_time = vformat("%s:%s:%s" , |
| 219 | String::num(real_time_seconds / 3600).pad_zeros(2), |
| 220 | String::num((real_time_seconds % 3600) / 60).pad_zeros(2), |
| 221 | String::num(real_time_seconds % 60).pad_zeros(2)); |
| 222 | |
| 223 | print_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(movie_time_seconds) / real_time_seconds) * 100)); |
| 224 | print_line(vformat("CPU time: %.2f seconds (average: %.2f ms/frame)" , cpu_time / 1000, cpu_time / Engine::get_singleton()->get_frames_drawn())); |
| 225 | print_line(vformat("GPU time: %.2f seconds (average: %.2f ms/frame)" , gpu_time / 1000, gpu_time / Engine::get_singleton()->get_frames_drawn())); |
| 226 | print_line("----------------" ); |
| 227 | } |
| 228 | |