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