| 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 |  | 
|---|