Browse Source

Merge pull request #107188 from berarma/moviewriter-add-theora

Add Ogg Theora support to MovieWriter
Rémi Verschelde 2 months ago
parent
commit
cc9761c3f0

+ 4 - 2
doc/classes/MovieWriter.xml

@@ -5,13 +5,15 @@
 	</brief_description>
 	</brief_description>
 	<description>
 	<description>
 		Godot can record videos with non-real-time simulation. Like the [code]--fixed-fps[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url], this forces the reported [code]delta[/code] in [method Node._process] functions to be identical across frames, regardless of how long it actually took to render the frame. This can be used to record high-quality videos with perfect frame pacing regardless of your hardware's capabilities.
 		Godot can record videos with non-real-time simulation. Like the [code]--fixed-fps[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url], this forces the reported [code]delta[/code] in [method Node._process] functions to be identical across frames, regardless of how long it actually took to render the frame. This can be used to record high-quality videos with perfect frame pacing regardless of your hardware's capabilities.
-		Godot has 2 built-in [MovieWriter]s:
-		- AVI container with MJPEG for video and uncompressed audio ([code].avi[/code] file extension). Lossy compression, medium file sizes, fast encoding. The lossy compression quality can be adjusted by changing [member ProjectSettings.editor/movie_writer/mjpeg_quality]. The resulting file can be viewed in most video players, but it must be converted to another format for viewing on the web or by Godot with [VideoStreamPlayer]. MJPEG does not support transparency. AVI output is currently limited to a file of 4 GB in size at most.
+		Godot has 3 built-in [MovieWriter]s:
+		- OGV container with Theora for video and Vorbis for audio ([code].ogv[/code] file extension). Lossy compression, medium file sizes, fast encoding. The lossy compression quality can be adjusted by changing [member ProjectSettings.editor/movie_writer/video_quality] and [member ProjectSettings.editor/movie_writer/ogv/audio_quality]. The resulting file can be viewed in Godot with [VideoStreamPlayer] and most video players, but not web browsers as they don't support Theora.
+		- AVI container with MJPEG for video and uncompressed audio ([code].avi[/code] file extension). Lossy compression, medium file sizes, fast encoding. The lossy compression quality can be adjusted by changing [member ProjectSettings.editor/movie_writer/video_quality]. The resulting file can be viewed in most video players, but it must be converted to another format for viewing on the web or by Godot with [VideoStreamPlayer]. MJPEG does not support transparency. AVI output is currently limited to a file of 4 GB in size at most.
 		- PNG image sequence for video and WAV for audio ([code].png[/code] file extension). Lossless compression, large file sizes, slow encoding. Designed to be encoded to a video file with another tool such as [url=https://ffmpeg.org/]FFmpeg[/url] after recording. Transparency is currently not supported, even if the root viewport is set to be transparent.
 		- PNG image sequence for video and WAV for audio ([code].png[/code] file extension). Lossless compression, large file sizes, slow encoding. Designed to be encoded to a video file with another tool such as [url=https://ffmpeg.org/]FFmpeg[/url] after recording. Transparency is currently not supported, even if the root viewport is set to be transparent.
 		If you need to encode to a different format or pipe a stream through third-party software, you can extend the [MovieWriter] class to create your own movie writers. This should typically be done using GDExtension for performance reasons.
 		If you need to encode to a different format or pipe a stream through third-party software, you can extend the [MovieWriter] class to create your own movie writers. This should typically be done using GDExtension for performance reasons.
 		[b]Editor usage:[/b] A default movie file path can be specified in [member ProjectSettings.editor/movie_writer/movie_file]. Alternatively, for running single scenes, a [code]movie_file[/code] metadata can be added to the root node, specifying the path to a movie file that will be used when recording that scene. Once a path is set, click the video reel icon in the top-right corner of the editor to enable Movie Maker mode, then run any scene as usual. The engine will start recording as soon as the splash screen is finished, and it will only stop recording when the engine quits. Click the video reel icon again to disable Movie Maker mode. Note that toggling Movie Maker mode does not affect project instances that are already running.
 		[b]Editor usage:[/b] A default movie file path can be specified in [member ProjectSettings.editor/movie_writer/movie_file]. Alternatively, for running single scenes, a [code]movie_file[/code] metadata can be added to the root node, specifying the path to a movie file that will be used when recording that scene. Once a path is set, click the video reel icon in the top-right corner of the editor to enable Movie Maker mode, then run any scene as usual. The engine will start recording as soon as the splash screen is finished, and it will only stop recording when the engine quits. Click the video reel icon again to disable Movie Maker mode. Note that toggling Movie Maker mode does not affect project instances that are already running.
 		[b]Note:[/b] MovieWriter is available for use in both the editor and exported projects, but it is [i]not[/i] designed for use by end users to record videos while playing. Players wishing to record gameplay videos should install tools such as [url=https://obsproject.com/]OBS Studio[/url] or [url=https://www.maartenbaert.be/simplescreenrecorder/]SimpleScreenRecorder[/url] instead.
 		[b]Note:[/b] MovieWriter is available for use in both the editor and exported projects, but it is [i]not[/i] designed for use by end users to record videos while playing. Players wishing to record gameplay videos should install tools such as [url=https://obsproject.com/]OBS Studio[/url] or [url=https://www.maartenbaert.be/simplescreenrecorder/]SimpleScreenRecorder[/url] instead.
 		[b]Note:[/b] MJPEG support ([code].avi[/code] file extension) depends on the [code]jpg[/code] module being enabled at compile time (default behavior).
 		[b]Note:[/b] MJPEG support ([code].avi[/code] file extension) depends on the [code]jpg[/code] module being enabled at compile time (default behavior).
+		[b]Note:[/b] OGV support ([code].ogv[/code] file extension) depends on the [code]theora[/code] module being enabled at compile time (default behavior). Theora compression is only available in editor binaries.
 	</description>
 	</description>
 	<tutorials>
 	<tutorials>
 	</tutorials>
 	</tutorials>

+ 16 - 6
doc/classes/ProjectSettings.xml

@@ -1105,21 +1105,31 @@
 		<member name="editor/movie_writer/mix_rate" type="int" setter="" getter="" default="48000">
 		<member name="editor/movie_writer/mix_rate" type="int" setter="" getter="" default="48000">
 			The audio mix rate to use in the recorded audio when writing a movie (in Hz). This can be different from [member audio/driver/mix_rate], but this value must be divisible by [member editor/movie_writer/fps] to prevent audio from desynchronizing over time.
 			The audio mix rate to use in the recorded audio when writing a movie (in Hz). This can be different from [member audio/driver/mix_rate], but this value must be divisible by [member editor/movie_writer/fps] to prevent audio from desynchronizing over time.
 		</member>
 		</member>
-		<member name="editor/movie_writer/mjpeg_quality" type="float" setter="" getter="" default="0.75">
-			The JPEG quality to use when writing a video to an AVI file, between [code]0.01[/code] and [code]1.0[/code] (inclusive). Higher [code]quality[/code] values result in better-looking output at the cost of larger file sizes. Recommended [code]quality[/code] values are between [code]0.75[/code] and [code]0.9[/code]. Even at quality [code]1.0[/code], JPEG compression remains lossy.
-			[b]Note:[/b] This does not affect the audio quality or writing PNG image sequences.
-		</member>
 		<member name="editor/movie_writer/movie_file" type="String" setter="" getter="" default="&quot;&quot;">
 		<member name="editor/movie_writer/movie_file" type="String" setter="" getter="" default="&quot;&quot;">
 			The output path for the movie. The file extension determines the [MovieWriter] that will be used.
 			The output path for the movie. The file extension determines the [MovieWriter] that will be used.
-			Godot has 2 built-in [MovieWriter]s:
-			- AVI container with MJPEG for video and uncompressed audio ([code].avi[/code] file extension). Lossy compression, medium file sizes, fast encoding. The lossy compression quality can be adjusted by changing [member ProjectSettings.editor/movie_writer/mjpeg_quality]. The resulting file can be viewed in most video players, but it must be converted to another format for viewing on the web or by Godot with [VideoStreamPlayer]. MJPEG does not support transparency. AVI output is currently limited to a file of 4 GB in size at most.
+			Godot has 3 built-in [MovieWriter]s:
+			- OGV container with Theora for video and Vorbis for audio ([code].ogv[/code] file extension). Lossy compression, medium file sizes, fast encoding. The lossy compression quality can be adjusted by changing [member ProjectSettings.editor/movie_writer/video_quality] and [member ProjectSettings.editor/movie_writer/ogv/audio_quality]. The resulting file can be viewed in Godot with [VideoStreamPlayer] and most video players, but not web browsers as they don't support Theora.
+			- AVI container with MJPEG for video and uncompressed audio ([code].avi[/code] file extension). Lossy compression, medium file sizes, fast encoding. The lossy compression quality can be adjusted by changing [member ProjectSettings.editor/movie_writer/video_quality]. The resulting file can be viewed in most video players, but it must be converted to another format for viewing on the web or by Godot with [VideoStreamPlayer]. MJPEG does not support transparency. AVI output is currently limited to a file of 4 GB in size at most.
 			- PNG image sequence for video and WAV for audio ([code].png[/code] file extension). Lossless compression, large file sizes, slow encoding. Designed to be encoded to a video file with another tool such as [url=https://ffmpeg.org/]FFmpeg[/url] after recording. Transparency is currently not supported, even if the root viewport is set to be transparent.
 			- PNG image sequence for video and WAV for audio ([code].png[/code] file extension). Lossless compression, large file sizes, slow encoding. Designed to be encoded to a video file with another tool such as [url=https://ffmpeg.org/]FFmpeg[/url] after recording. Transparency is currently not supported, even if the root viewport is set to be transparent.
 			If you need to encode to a different format or pipe a stream through third-party software, you can extend this [MovieWriter] class to create your own movie writers.
 			If you need to encode to a different format or pipe a stream through third-party software, you can extend this [MovieWriter] class to create your own movie writers.
 			When using PNG output, the frame number will be appended at the end of the file name. It starts from 0 and is padded with 8 digits to ensure correct sorting and easier processing. For example, if the output path is [code]/tmp/hello.png[/code], the first two frames will be [code]/tmp/hello00000000.png[/code] and [code]/tmp/hello00000001.png[/code]. The audio will be saved at [code]/tmp/hello.wav[/code].
 			When using PNG output, the frame number will be appended at the end of the file name. It starts from 0 and is padded with 8 digits to ensure correct sorting and easier processing. For example, if the output path is [code]/tmp/hello.png[/code], the first two frames will be [code]/tmp/hello00000000.png[/code] and [code]/tmp/hello00000001.png[/code]. The audio will be saved at [code]/tmp/hello.wav[/code].
 		</member>
 		</member>
+		<member name="editor/movie_writer/ogv/audio_quality" type="float" setter="" getter="" default="0.5">
+			The audio encoding quality to use when writing Vorbis audio to a file, between [code]-0.1[/code] and [code]1.0[/code] (inclusive). Higher [code]quality[/code] values result in better-sounding output at the cost of larger file sizes. Even at quality [code]1.0[/code], compression remains lossy.
+			[b]Note:[/b] This does not affect video quality, which is controlled by [member editor/movie_writer/video_quality] instead.
+		</member>
+		<member name="editor/movie_writer/ogv/encoding_speed" type="int" setter="" getter="" default="4">
+			The tradeoff between encoding speed and compression efficiency. Speed [code]1[/code] is the slowest but provides the best compression. Speed [code]4[/code] is the fastest but provides the worst compression. Video quality is generally not affected significantly by this setting.
+		</member>
+		<member name="editor/movie_writer/ogv/keyframe_interval" type="int" setter="" getter="" default="64">
+			Forces keyframes at the specified interval (in frame count). Higher values can improve compression up to a certain level at the expense of higher latency when seeking.
+		</member>
 		<member name="editor/movie_writer/speaker_mode" type="int" setter="" getter="" default="0">
 		<member name="editor/movie_writer/speaker_mode" type="int" setter="" getter="" default="0">
 			The speaker mode to use in the recorded audio when writing a movie. See [enum AudioServer.SpeakerMode] for possible values.
 			The speaker mode to use in the recorded audio when writing a movie. See [enum AudioServer.SpeakerMode] for possible values.
 		</member>
 		</member>
+		<member name="editor/movie_writer/video_quality" type="float" setter="" getter="" default="0.75">
+			The video encoding quality to use when writing a Theora or AVI (MJPEG) video to a file, between [code]0.0[/code] and [code]1.0[/code] (inclusive). Higher [code]quality[/code] values result in better-looking output at the cost of larger file sizes. Recommended [code]quality[/code] values are between [code]0.75[/code] and [code]0.9[/code]. Even at quality [code]1.0[/code], compression remains lossy.
+		</member>
 		<member name="editor/naming/default_signal_callback_name" type="String" setter="" getter="" default="&quot;_on_{node_name}_{signal_name}&quot;">
 		<member name="editor/naming/default_signal_callback_name" type="String" setter="" getter="" default="&quot;_on_{node_name}_{signal_name}&quot;">
 			The format of the default signal callback name (in the Signal Connection Dialog). The following substitutions are available: [code]{NodeName}[/code], [code]{nodeName}[/code], [code]{node_name}[/code], [code]{SignalName}[/code], [code]{signalName}[/code], and [code]{signal_name}[/code].
 			The format of the default signal callback name (in the Signal Connection Dialog). The following substitutions are available: [code]{NodeName}[/code], [code]{nodeName}[/code], [code]{node_name}[/code], [code]{SignalName}[/code], [code]{signalName}[/code], and [code]{signal_name}[/code].
 		</member>
 		</member>

+ 1 - 0
editor/editor_property_name_processor.cpp

@@ -245,6 +245,7 @@ EditorPropertyNameProcessor::EditorPropertyNameProcessor() {
 	//capitalize_string_remaps["msec"] = "(msec)"; // Unit.
 	//capitalize_string_remaps["msec"] = "(msec)"; // Unit.
 	capitalize_string_remaps["navmesh"] = "NavMesh";
 	capitalize_string_remaps["navmesh"] = "NavMesh";
 	capitalize_string_remaps["nfc"] = "NFC";
 	capitalize_string_remaps["nfc"] = "NFC";
+	capitalize_string_remaps["ogv"] = "OGV";
 	capitalize_string_remaps["oidn"] = "OIDN";
 	capitalize_string_remaps["oidn"] = "OIDN";
 	capitalize_string_remaps["ok"] = "OK";
 	capitalize_string_remaps["ok"] = "OK";
 	capitalize_string_remaps["opengl"] = "OpenGL";
 	capitalize_string_remaps["opengl"] = "OpenGL";

+ 1 - 1
modules/jpg/movie_writer_mjpeg.cpp

@@ -259,5 +259,5 @@ void MovieWriterMJPEG::write_end() {
 MovieWriterMJPEG::MovieWriterMJPEG() {
 MovieWriterMJPEG::MovieWriterMJPEG() {
 	mix_rate = GLOBAL_GET("editor/movie_writer/mix_rate");
 	mix_rate = GLOBAL_GET("editor/movie_writer/mix_rate");
 	speaker_mode = AudioServer::SpeakerMode(int(GLOBAL_GET("editor/movie_writer/speaker_mode")));
 	speaker_mode = AudioServer::SpeakerMode(int(GLOBAL_GET("editor/movie_writer/speaker_mode")));
-	quality = GLOBAL_GET("editor/movie_writer/mjpeg_quality");
+	quality = GLOBAL_GET("editor/movie_writer/video_quality");
 }
 }

+ 36 - 25
modules/theora/SCsub

@@ -13,61 +13,68 @@ thirdparty_obj = []
 if env["builtin_libtheora"]:
 if env["builtin_libtheora"]:
     thirdparty_dir = "#thirdparty/libtheora/"
     thirdparty_dir = "#thirdparty/libtheora/"
     thirdparty_sources = [
     thirdparty_sources = [
-        # "analyze.c",
-        # "apiwrapper.c",
         "bitpack.c",
         "bitpack.c",
-        # "collect.c",
-        # "decapiwrapper.c",
         "decinfo.c",
         "decinfo.c",
         "decode.c",
         "decode.c",
         "dequant.c",
         "dequant.c",
-        # "encapiwrapper.c",
-        # "encfrag.c",
-        # "encinfo.c",
-        # "encode.c",
-        # "encoder_disabled.c",
-        # "enquant.c",
-        # "fdct.c",
         "fragment.c",
         "fragment.c",
         "huffdec.c",
         "huffdec.c",
-        # "huffenc.c",
         "idct.c",
         "idct.c",
         "info.c",
         "info.c",
         "internal.c",
         "internal.c",
-        # "mathops.c",
-        # "mcenc.c",
         "quant.c",
         "quant.c",
-        # "rate.c",
         "state.c",
         "state.c",
-        # "tokenize.c",
     ]
     ]
 
 
+    if env.editor_build:
+        thirdparty_sources += [
+            "analyze.c",
+            "encfrag.c",
+            "encinfo.c",
+            "encode.c",
+            "enquant.c",
+            "fdct.c",
+            "huffenc.c",
+            "mathops.c",
+            "mcenc.c",
+            "rate.c",
+            "tokenize.c",
+        ]
+
     thirdparty_sources_x86 = [
     thirdparty_sources_x86 = [
-        # "x86/mmxencfrag.c",
-        # "x86/mmxfdct.c",
         "x86/mmxfrag.c",
         "x86/mmxfrag.c",
         "x86/mmxidct.c",
         "x86/mmxidct.c",
         "x86/mmxstate.c",
         "x86/mmxstate.c",
-        # "x86/sse2encfrag.c",
-        # "x86/sse2fdct.c",
         "x86/sse2idct.c",
         "x86/sse2idct.c",
         "x86/x86cpu.c",
         "x86/x86cpu.c",
-        # "x86/x86enc.c",
-        # "x86/x86enquant.c"
         "x86/x86state.c",
         "x86/x86state.c",
     ]
     ]
 
 
+    if env.editor_build:
+        thirdparty_sources_x86 += [
+            "x86/mmxencfrag.c",
+            "x86/mmxfdct.c",
+            "x86/sse2encfrag.c",
+            "x86/sse2fdct.c",
+            "x86/x86enc.c",
+            "x86/x86enquant.c",
+        ]
+
     thirdparty_sources_x86_vc = [
     thirdparty_sources_x86_vc = [
-        # "x86_vc/mmxencfrag.c",
-        # "x86_vc/mmxfdct.c",
         "x86_vc/mmxfrag.c",
         "x86_vc/mmxfrag.c",
         "x86_vc/mmxidct.c",
         "x86_vc/mmxidct.c",
         "x86_vc/mmxstate.c",
         "x86_vc/mmxstate.c",
         "x86_vc/x86cpu.c",
         "x86_vc/x86cpu.c",
-        # "x86_vc/x86enc.c",
         "x86_vc/x86state.c",
         "x86_vc/x86state.c",
     ]
     ]
 
 
+    if env.editor_build:
+        thirdparty_sources_x86_vc += [
+            "x86_vc/mmxencfrag.c",
+            "x86_vc/mmxfdct.c",
+            "x86_vc/x86enc.c",
+        ]
+
     if env["x86_libtheora_opt_gcc"]:
     if env["x86_libtheora_opt_gcc"]:
         thirdparty_sources += thirdparty_sources_x86
         thirdparty_sources += thirdparty_sources_x86
 
 
@@ -98,6 +105,10 @@ if env["builtin_libtheora"]:
 module_obj = []
 module_obj = []
 
 
 env_theora.add_source_files(module_obj, "*.cpp")
 env_theora.add_source_files(module_obj, "*.cpp")
+
+if env.editor_build:
+    env_theora.add_source_files(module_obj, "editor/*.cpp")
+
 env.modules_sources += module_obj
 env.modules_sources += module_obj
 
 
 # Needed to force rebuilding the module files when the thirdparty library is updated.
 # Needed to force rebuilding the module files when the thirdparty library is updated.

+ 431 - 0
modules/theora/editor/movie_writer_ogv.cpp

@@ -0,0 +1,431 @@
+/**************************************************************************/
+/*  movie_writer_ogv.cpp                                                  */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "movie_writer_ogv.h"
+
+#include "core/config/project_settings.h"
+#include "rgb2yuv.h"
+
+void MovieWriterOGV::push_audio(const int32_t *p_audio_data) {
+	// Read and process more audio.
+	float **vorbis_buffer = vorbis_analysis_buffer(&vd, audio_frames);
+
+	// Deinterleave samples.
+	uint32_t count = 0;
+	for (uint32_t i = 0; i < audio_frames; i++) {
+		for (uint32_t j = 0; j < audio_ch; j++) {
+			vorbis_buffer[j][i] = p_audio_data[count] / 2147483647.f;
+			count++;
+		}
+	}
+
+	vorbis_analysis_wrote(&vd, audio_frames);
+}
+
+void MovieWriterOGV::pull_audio(bool p_last) {
+	ogg_packet op;
+
+	while (vorbis_analysis_blockout(&vd, &vb) > 0) {
+		// Analysis, assume we want to use bitrate management.
+		vorbis_analysis(&vb, nullptr);
+		vorbis_bitrate_addblock(&vb);
+
+		// Weld packets into the bitstream.
+		while (vorbis_bitrate_flushpacket(&vd, &op) > 0) {
+			ogg_stream_packetin(&vo, &op);
+		}
+	}
+
+	if (p_last) {
+		vorbis_analysis_wrote(&vd, 0);
+		pull_audio();
+	}
+}
+
+void MovieWriterOGV::push_video(const Ref<Image> &p_image) {
+	PackedByteArray data = p_image->get_data();
+	if (p_image->get_format() == Image::FORMAT_RGBA8) {
+		rgba2yuv420(y, u, v, data.ptrw(), p_image->get_width(), p_image->get_height());
+	} else {
+		rgb2yuv420(y, u, v, data.ptrw(), p_image->get_width(), p_image->get_height());
+	}
+	th_encode_ycbcr_in(td, ycbcr);
+}
+
+void MovieWriterOGV::pull_video(bool p_last) {
+	ogg_packet op;
+
+	int ret = 0;
+	do {
+		ret = th_encode_packetout(td, p_last, &op);
+		if (ret > 0) {
+			ogg_stream_packetin(&to, &op);
+		}
+	} while (ret > 0);
+}
+
+uint32_t MovieWriterOGV::get_audio_mix_rate() const {
+	return mix_rate;
+}
+
+AudioServer::SpeakerMode MovieWriterOGV::get_audio_speaker_mode() const {
+	return speaker_mode;
+}
+
+bool MovieWriterOGV::handles_file(const String &p_path) const {
+	return p_path.get_extension().to_lower() == "ogv";
+}
+
+void MovieWriterOGV::get_supported_extensions(List<String> *r_extensions) const {
+	r_extensions->push_back("ogv");
+}
+
+Error MovieWriterOGV::write_begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path) {
+	ERR_FAIL_COND_V_MSG((p_movie_size.width & 1) || (p_movie_size.height & 1), ERR_UNAVAILABLE, "Both video dimensions must be even.");
+	base_path = p_base_path.get_basename();
+	if (base_path.is_relative_path()) {
+		base_path = "res://" + base_path;
+	}
+	base_path += ".ogv";
+
+	f = FileAccess::open(base_path, FileAccess::WRITE_READ);
+	ERR_FAIL_COND_V(f.is_null(), ERR_CANT_OPEN);
+
+	fps = p_fps;
+
+	audio_ch = 2;
+	switch (speaker_mode) {
+		case AudioServer::SPEAKER_MODE_STEREO:
+			audio_ch = 2;
+			break;
+		case AudioServer::SPEAKER_SURROUND_31:
+			audio_ch = 4;
+			break;
+		case AudioServer::SPEAKER_SURROUND_51:
+			audio_ch = 6;
+			break;
+		case AudioServer::SPEAKER_SURROUND_71:
+			audio_ch = 8;
+			break;
+	}
+	audio_frames = mix_rate / fps;
+
+	// Set up Ogg output streams.
+	srand(time(nullptr));
+	ogg_stream_init(&to, rand()); // Video.
+	ogg_stream_init(&vo, rand()); // Audio.
+
+	// Initialize Vorbis audio encoding.
+	vorbis_info_init(&vi);
+	int ret = vorbis_encode_init_vbr(&vi, audio_ch, mix_rate, audio_quality);
+	ERR_FAIL_COND_V_MSG(ret, ERR_UNAVAILABLE, "The Ogg Vorbis encoder couldn't set up a mode according to the requested quality or bitrate.");
+
+	vorbis_comment_init(&vc);
+	vorbis_analysis_init(&vd, &vi);
+	vorbis_block_init(&vd, &vb);
+
+	// Set up Theora encoder.
+	// Theora has a divisible-by-16 restriction for the encoded frame size
+	// scale the picture size up to the nearest /16 and calculate offsets.
+	int pic_w = p_movie_size.width;
+	int pic_h = p_movie_size.height;
+	int frame_w = (pic_w + 15) & ~0xF;
+	int frame_h = (pic_h + 15) & ~0xF;
+	// Force the offsets to be even so that chroma samples line up like we expect.
+	int pic_x = (frame_w - pic_w) / 2 & ~1;
+	int pic_y = (frame_h - pic_h) / 2 & ~1;
+
+	y = (uint8_t *)memalloc(pic_w * pic_h);
+	u = (uint8_t *)memalloc(pic_w * pic_h / 4);
+	v = (uint8_t *)memalloc(pic_w * pic_h / 4);
+
+	// We submit the buffer using the size of the picture region.
+	// libtheora will pad the picture region out to the full frame size for us,
+	// whether we pass in a full frame or not.
+	ycbcr[0].width = pic_w;
+	ycbcr[0].height = pic_h;
+	ycbcr[0].stride = pic_w;
+	ycbcr[0].data = y;
+	ycbcr[1].width = pic_w / 2;
+	ycbcr[1].height = pic_h / 2;
+	ycbcr[1].stride = pic_w / 2;
+	ycbcr[1].data = u;
+	ycbcr[2].width = pic_w / 2;
+	ycbcr[2].height = pic_h / 2;
+	ycbcr[2].stride = pic_w / 2;
+	ycbcr[2].data = v;
+
+	th_info_init(&ti);
+	ti.frame_width = frame_w;
+	ti.frame_height = frame_h;
+	ti.pic_width = pic_w;
+	ti.pic_height = pic_h;
+	ti.pic_x = pic_x;
+	ti.pic_y = pic_y;
+	ti.fps_numerator = fps;
+	ti.fps_denominator = 1;
+	ti.aspect_numerator = 1;
+	ti.aspect_denominator = 1;
+	ti.colorspace = TH_CS_UNSPECIFIED;
+	// Account for the Ogg page overhead.
+	// This is 1 byte per 255 for lacing values, plus 26 bytes per 4096 bytes for
+	// the page header, plus approximately 1/2 byte per packet (not accounted for here).
+	ti.target_bitrate = (int)(64870 * (ogg_int64_t)video_bitrate >> 16);
+	ti.quality = video_quality * 63;
+	ti.pixel_fmt = TH_PF_420;
+	td = th_encode_alloc(&ti);
+	th_info_clear(&ti);
+	ERR_FAIL_NULL_V_MSG(td, ERR_UNCONFIGURED, "Couldn't create a Theora encoder instance. Check that the video parameters are valid.");
+
+	// Setting just the granule shift only allows power-of-two keyframe spacing.
+	// Set the actual requested spacing.
+	ret = th_encode_ctl(td, TH_ENCCTL_SET_KEYFRAME_FREQUENCY_FORCE, &keyframe_frequency, sizeof(keyframe_frequency));
+	if (ret < 0) {
+		ERR_PRINT("Couldn't set keyframe interval.");
+	}
+
+	// Speed should also be set after the current encoder mode is established,
+	// since the available speed levels may change depending on the encoder mode.
+	if (speed >= 0) {
+		int speed_max;
+		ret = th_encode_ctl(td, TH_ENCCTL_GET_SPLEVEL_MAX, &speed_max, sizeof(speed_max));
+		if (ret < 0) {
+			WARN_PRINT("Couldn't determine maximum speed level.");
+			speed_max = 0;
+		}
+		ret = th_encode_ctl(td, TH_ENCCTL_SET_SPLEVEL, &speed, sizeof(speed));
+		if (ret < 0) {
+			if (ret < 0) {
+				WARN_PRINT(vformat("Couldn't set speed level to %d of %d.", speed, speed_max));
+			}
+			if (speed > speed_max) {
+				WARN_PRINT(vformat("Setting speed level to %d instead.", speed_max));
+			}
+			ret = th_encode_ctl(td, TH_ENCCTL_SET_SPLEVEL, &speed_max, sizeof(speed_max));
+			if (ret < 0) {
+				WARN_PRINT(vformat("Couldn't set speed level to %d of %d.", speed_max, speed_max));
+			}
+		}
+	}
+
+	// Write the bitstream header packets with proper page interleave.
+	th_comment_init(&tc);
+	// The first packet will get its own page automatically.
+	ogg_packet op;
+	if (th_encode_flushheader(td, &tc, &op) <= 0) {
+		ERR_FAIL_V_MSG(ERR_UNCONFIGURED, "Internal Theora library error.");
+	}
+
+	ogg_stream_packetin(&to, &op);
+	if (ogg_stream_pageout(&to, &video_page) != 1) {
+		ERR_FAIL_V_MSG(ERR_UNCONFIGURED, "Internal Ogg library error.");
+	}
+	f->store_buffer(video_page.header, video_page.header_len);
+	f->store_buffer(video_page.body, video_page.body_len);
+
+	// Create the remaining Theora headers.
+	while (true) {
+		ret = th_encode_flushheader(td, &tc, &op);
+		if (ret < 0) {
+			ERR_FAIL_V_MSG(ERR_UNCONFIGURED, "Internal Theora library error.");
+		} else if (ret == 0) {
+			break;
+		}
+		ogg_stream_packetin(&to, &op);
+	}
+
+	// Vorbis streams start with 3 standard header packets.
+	ogg_packet id;
+	ogg_packet comment;
+	ogg_packet code;
+	if (vorbis_analysis_headerout(&vd, &vc, &id, &comment, &code) < 0) {
+		ERR_FAIL_V_MSG(ERR_UNCONFIGURED, "Internal Vorbis library error.");
+	}
+
+	// ID header is automatically placed in its own page.
+	ogg_stream_packetin(&vo, &id);
+	if (ogg_stream_pageout(&vo, &audio_page) != 1) {
+		ERR_FAIL_V_MSG(ERR_UNCONFIGURED, "Internal Ogg library error.");
+	}
+	f->store_buffer(audio_page.header, audio_page.header_len);
+	f->store_buffer(audio_page.body, audio_page.body_len);
+
+	// Append remaining Vorbis header packets.
+	ogg_stream_packetin(&vo, &comment);
+	ogg_stream_packetin(&vo, &code);
+
+	// Flush the rest of our headers. This ensures the actual data in each stream will start on a new page, as per spec.
+	while (true) {
+		ret = ogg_stream_flush(&to, &video_page);
+		if (ret < 0) {
+			ERR_FAIL_V_MSG(ERR_UNCONFIGURED, "Internal Ogg library error.");
+		} else if (ret == 0) {
+			break;
+		}
+		f->store_buffer(video_page.header, video_page.header_len);
+		f->store_buffer(video_page.body, video_page.body_len);
+	}
+
+	while (true) {
+		ret = ogg_stream_flush(&vo, &audio_page);
+		if (ret < 0) {
+			ERR_FAIL_V_MSG(ERR_UNCONFIGURED, "Internal Ogg library error.");
+		} else if (ret == 0) {
+			break;
+		}
+		f->store_buffer(audio_page.header, audio_page.header_len);
+		f->store_buffer(audio_page.body, audio_page.body_len);
+	}
+
+	return OK;
+}
+
+// The order of the operations has been chosen so we're one frame behind writing to the stream so we can put the eos
+// mark in the last frame.
+// Flushing streams to the file every X frames is done to improve audio/video page interleaving thus avoiding large runs
+// of video or audio pages.
+Error MovieWriterOGV::write_frame(const Ref<Image> &p_image, const int32_t *p_audio_data) {
+	ERR_FAIL_COND_V(f.is_null() || td == nullptr, ERR_UNCONFIGURED);
+
+	frame_count++;
+
+	pull_audio();
+	pull_video();
+
+	if ((frame_count % 8) == 0) {
+		write_to_file();
+	}
+
+	push_audio(p_audio_data);
+	push_video(p_image);
+
+	return OK;
+}
+
+void MovieWriterOGV::save_page(ogg_page page) {
+	unsigned int page_size = page.header_len + page.body_len;
+	if (page_size > backup_page_size) {
+		backup_page_data = (unsigned char *)memrealloc(backup_page_data, page_size);
+		backup_page_size = page_size;
+	}
+	backup_page.header = backup_page_data;
+	backup_page.header_len = page.header_len;
+	backup_page.body = backup_page_data + page.header_len;
+	backup_page.body_len = page.body_len;
+	memcpy(backup_page.header, page.header, page.header_len);
+	memcpy(backup_page.body, page.body, page.body_len);
+}
+
+void MovieWriterOGV::restore_page(ogg_page *page) {
+	page->header = backup_page.header;
+	page->header_len = backup_page.header_len;
+	page->body = backup_page.body;
+	page->body_len = backup_page.body_len;
+}
+
+// The added complexity here is because we have to ensure pages are written in ascending timestamp order.
+// libOgg doesn't allow checking the next page granulepos without requesting the page, and once requested it can't be
+// returned, thus, we need to save it so that it doesn't get erased by the next `ogg_stream_packetin` call.
+void MovieWriterOGV::write_to_file(bool p_finish) {
+	if (audio_flag) {
+		restore_page(&audio_page);
+	} else {
+		audio_flag = ogg_stream_flush(&vo, &audio_page);
+	}
+	if (video_flag) {
+		restore_page(&video_page);
+	} else {
+		video_flag = ogg_stream_flush(&to, &video_page);
+	}
+
+	bool finishing = p_finish && (audio_flag || video_flag);
+	while (finishing || (audio_flag && video_flag)) {
+		double audiotime = vorbis_granule_time(&vd, ogg_page_granulepos(&audio_page));
+		double videotime = th_granule_time(td, ogg_page_granulepos(&video_page));
+		bool video_first = audiotime >= videotime;
+
+		if (video_flag && video_first) {
+			// Flush a video page.
+			f->store_buffer(video_page.header, video_page.header_len);
+			f->store_buffer(video_page.body, video_page.body_len);
+			video_flag = ogg_stream_flush(&to, &video_page) > 0;
+		} else {
+			// Flush an audio page.
+			f->store_buffer(audio_page.header, audio_page.header_len);
+			f->store_buffer(audio_page.body, audio_page.body_len);
+			audio_flag = ogg_stream_flush(&vo, &audio_page) > 0;
+		}
+		finishing = p_finish && (audio_flag || video_flag);
+	}
+
+	if (video_flag) {
+		save_page(video_page);
+	} else if (audio_flag) {
+		save_page(audio_page);
+	}
+}
+
+void MovieWriterOGV::write_end() {
+	pull_audio(true);
+	pull_video(true);
+	write_to_file(true);
+
+	th_encode_free(td);
+
+	ogg_stream_clear(&vo);
+	vorbis_block_clear(&vb);
+	vorbis_dsp_clear(&vd);
+	vorbis_comment_clear(&vc);
+	vorbis_info_clear(&vi);
+
+	ogg_stream_clear(&to);
+	th_comment_clear(&tc);
+
+	memfree(y);
+	memfree(u);
+	memfree(v);
+
+	if (backup_page_data != nullptr) {
+		memfree(backup_page_data);
+	}
+
+	if (f.is_valid()) {
+		f.unref();
+	}
+}
+
+MovieWriterOGV::MovieWriterOGV() {
+	mix_rate = GLOBAL_GET("editor/movie_writer/mix_rate");
+	speaker_mode = AudioServer::SpeakerMode(int(GLOBAL_GET("editor/movie_writer/speaker_mode")));
+	video_quality = GLOBAL_GET("editor/movie_writer/video_quality");
+	audio_quality = GLOBAL_GET("editor/movie_writer/ogv/audio_quality");
+	speed = GLOBAL_GET("editor/movie_writer/ogv/encoding_speed");
+	keyframe_frequency = GLOBAL_GET("editor/movie_writer/ogv/keyframe_interval");
+}

+ 139 - 0
modules/theora/editor/movie_writer_ogv.h

@@ -0,0 +1,139 @@
+/**************************************************************************/
+/*  movie_writer_ogv.h                                                    */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "servers/audio_server.h"
+#include "servers/movie_writer/movie_writer.h"
+
+#include <theora/theoraenc.h>
+#include <vorbis/codec.h>
+#include <vorbis/vorbisenc.h>
+
+class MovieWriterOGV : public MovieWriter {
+	GDCLASS(MovieWriterOGV, MovieWriter)
+
+	uint32_t mix_rate = 48000;
+	AudioServer::SpeakerMode speaker_mode = AudioServer::SPEAKER_MODE_STEREO;
+	String base_path;
+	uint32_t frame_count = 0;
+	uint32_t fps = 0;
+	uint32_t audio_ch = 0;
+	uint32_t audio_frames = 0;
+
+	Ref<FileAccess> f;
+
+	// Vorbis quality -0.1 to 1 (-0.1 yields smallest files but lowest fidelity; 1 yields highest fidelity but large files. '0.2' is a reasonable default).
+	float audio_quality = 0.5;
+
+	// Bitrate target for Theora video.
+	int video_bitrate = 0;
+
+	// Theora quality selector from 0 to 1.0 (0 yields smallest files but lowest video quality. 1.0 yields highest fidelity but large files).
+	float video_quality = 0.75;
+
+	// Video stream keyframe frequency (one every N frames).
+	ogg_uint32_t keyframe_frequency = 64;
+
+	// Sets the encoder speed level. Higher speed levels favor quicker encoding over better quality per bit. Depending on the encoding
+	// mode, and the internal algorithms used, quality may actually improve with higher speeds, but in this case bitrate will also
+	// likely increase. The maximum value, and the meaning of each value, are implementation-specific and may change depending on the
+	// current encoding mode.
+	int speed = 4;
+
+	// Take physical pages, weld into a logical stream of packets.
+	ogg_stream_state to;
+
+	// Take physical pages, weld into a logical stream of packets.
+	ogg_stream_state vo;
+
+	// Theora encoding context.
+	th_enc_ctx *td;
+
+	// Theora bitstream information.
+	th_info ti;
+
+	// Theora comment information.
+	th_comment tc;
+
+	// Vorbis bitstream information.
+	vorbis_info vi;
+
+	// Vorbis comment information.
+	vorbis_comment vc;
+
+	// Central working state for the packet->PCM decoder.
+	vorbis_dsp_state vd;
+
+	// Local working space for packet->PCM decode.
+	vorbis_block vb;
+
+	// Video buffer.
+	uint8_t *y, *u, *v;
+	th_ycbcr_buffer ycbcr;
+
+	bool audio_flag = false;
+	bool video_flag = false;
+	ogg_page audio_page;
+	ogg_page video_page;
+	ogg_page backup_page;
+	unsigned int backup_page_size = 0;
+	unsigned char *backup_page_data = nullptr;
+
+	void write_to_file(bool p_finish = false);
+	void push_audio(const int32_t *p_audio_data);
+	void push_video(const Ref<Image> &p_image);
+	void pull_audio(bool p_last = false);
+	void pull_video(bool p_last = false);
+	void save_page(ogg_page page);
+	void restore_page(ogg_page *page);
+
+	inline int ilog(unsigned _v) {
+		int ret;
+		for (ret = 0; _v; ret++) {
+			_v >>= 1;
+		}
+		return ret;
+	}
+
+protected:
+	virtual uint32_t get_audio_mix_rate() const override;
+	virtual AudioServer::SpeakerMode get_audio_speaker_mode() const override;
+	virtual void get_supported_extensions(List<String> *r_extensions) const override;
+
+	virtual Error write_begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path) override;
+	virtual Error write_frame(const Ref<Image> &p_image, const int32_t *p_audio_data) override;
+	virtual void write_end() override;
+
+	virtual bool handles_file(const String &p_path) const override;
+
+public:
+	MovieWriterOGV();
+};

+ 76 - 0
modules/theora/editor/rgb2yuv.h

@@ -0,0 +1,76 @@
+/**************************************************************************/
+/*  rgb2yuv.h                                                             */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "core/typedefs.h"
+
+// For reference, see:
+// - https://stackoverflow.com/a/9467305
+// - https://en.wikipedia.org/wiki/YCbCr#Approximate_8-bit_matrices_for_BT.601
+
+static void _rgb2yuv420(uint8_t *y, uint8_t *u, uint8_t *v, uint8_t *rgb, size_t width, size_t height, size_t pixel_size) {
+	size_t uvpos = 0;
+	size_t i = 0;
+	for (size_t line = 0; line < height; line += 2) {
+		for (size_t x = 0; x < width; x += 2) {
+			uint8_t r = rgb[pixel_size * i];
+			uint8_t g = rgb[pixel_size * i + 1];
+			uint8_t b = rgb[pixel_size * i + 2];
+
+			y[i++] = ((66 * r + 129 * g + 25 * b) >> 8) + 16;
+
+			u[uvpos] = ((-38 * r + -74 * g + 112 * b) >> 8) + 128;
+			v[uvpos] = ((112 * r + -94 * g + -18 * b) >> 8) + 128;
+			uvpos++;
+
+			r = rgb[pixel_size * i];
+			g = rgb[pixel_size * i + 1];
+			b = rgb[pixel_size * i + 2];
+
+			y[i++] = ((66 * r + 129 * g + 25 * b) >> 8) + 16;
+		}
+		for (size_t x = 0; x < width; x += 1) {
+			uint8_t r = rgb[pixel_size * i];
+			uint8_t g = rgb[pixel_size * i + 1];
+			uint8_t b = rgb[pixel_size * i + 2];
+
+			y[i++] = ((66 * r + 129 * g + 25 * b) >> 8) + 16;
+		}
+	}
+}
+
+static void rgb2yuv420(uint8_t *y, uint8_t *u, uint8_t *v, uint8_t *rgb, size_t width, size_t height) {
+	_rgb2yuv420(y, u, v, rgb, width, height, 3);
+}
+
+static void rgba2yuv420(uint8_t *y, uint8_t *u, uint8_t *v, uint8_t *rgba, size_t width, size_t height) {
+	_rgb2yuv420(y, u, v, rgba, width, height, 4);
+}

+ 39 - 12
modules/theora/register_types.cpp

@@ -32,24 +32,51 @@
 
 
 #include "video_stream_theora.h"
 #include "video_stream_theora.h"
 
 
+#ifdef TOOLS_ENABLED
+#include "editor/movie_writer_ogv.h"
+#endif
+
 static Ref<ResourceFormatLoaderTheora> resource_loader_theora;
 static Ref<ResourceFormatLoaderTheora> resource_loader_theora;
+#ifdef TOOLS_ENABLED
+static MovieWriterOGV *writer_ogv = nullptr;
+#endif
 
 
 void initialize_theora_module(ModuleInitializationLevel p_level) {
 void initialize_theora_module(ModuleInitializationLevel p_level) {
-	if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
-		return;
-	}
-
-	resource_loader_theora.instantiate();
-	ResourceLoader::add_resource_format_loader(resource_loader_theora, true);
+	switch (p_level) {
+		case MODULE_INITIALIZATION_LEVEL_SERVERS: {
+#ifdef TOOLS_ENABLED
+			if (GD_IS_CLASS_ENABLED(MovieWriterOGV)) {
+				writer_ogv = memnew(MovieWriterOGV);
+				MovieWriter::add_writer(writer_ogv);
+			}
+#endif
+		} break;
 
 
-	GDREGISTER_CLASS(VideoStreamTheora);
+		case MODULE_INITIALIZATION_LEVEL_SCENE: {
+			resource_loader_theora.instantiate();
+			ResourceLoader::add_resource_format_loader(resource_loader_theora, true);
+			GDREGISTER_CLASS(VideoStreamTheora);
+		} break;
+		default:
+			break;
+	}
 }
 }
 
 
 void uninitialize_theora_module(ModuleInitializationLevel p_level) {
 void uninitialize_theora_module(ModuleInitializationLevel p_level) {
-	if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
-		return;
-	}
+	switch (p_level) {
+		case MODULE_INITIALIZATION_LEVEL_SCENE: {
+			ResourceLoader::remove_resource_format_loader(resource_loader_theora);
+			resource_loader_theora.unref();
+		} break;
 
 
-	ResourceLoader::remove_resource_format_loader(resource_loader_theora);
-	resource_loader_theora.unref();
+		case MODULE_INITIALIZATION_LEVEL_SERVERS: {
+#ifdef TOOLS_ENABLED
+			if (GD_IS_CLASS_ENABLED(MovieWriterOGV)) {
+				memdelete(writer_ogv);
+			}
+#endif
+		} break;
+		default:
+			break;
+	}
 }
 }

+ 6 - 2
modules/vorbis/SCsub

@@ -13,7 +13,6 @@ thirdparty_obj = []
 if env["builtin_libvorbis"]:
 if env["builtin_libvorbis"]:
     thirdparty_dir = "#thirdparty/libvorbis/"
     thirdparty_dir = "#thirdparty/libvorbis/"
     thirdparty_sources = [
     thirdparty_sources = [
-        # "analysis.c",
         # "barkmel.c",
         # "barkmel.c",
         "bitrate.c",
         "bitrate.c",
         "block.c",
         "block.c",
@@ -35,11 +34,16 @@ if env["builtin_libvorbis"]:
         "smallft.c",
         "smallft.c",
         "synthesis.c",
         "synthesis.c",
         # "tone.c",
         # "tone.c",
-        # "vorbisenc.c",
         "vorbisfile.c",
         "vorbisfile.c",
         "window.c",
         "window.c",
     ]
     ]
 
 
+    if env.editor_build:
+        thirdparty_sources += [
+            "analysis.c",
+            "vorbisenc.c",
+        ]
+
     thirdparty_sources = [thirdparty_dir + file for file in thirdparty_sources]
     thirdparty_sources = [thirdparty_dir + file for file in thirdparty_sources]
 
 
     env_vorbis.Prepend(CPPEXTPATH=[thirdparty_dir])
     env_vorbis.Prepend(CPPEXTPATH=[thirdparty_dir])

+ 8 - 2
platform/linuxbsd/detect.py

@@ -257,14 +257,20 @@ def configure(env: "SConsEnvironment"):
     if not env["builtin_libtheora"]:
     if not env["builtin_libtheora"]:
         env["builtin_libogg"] = False  # Needed to link against system libtheora
         env["builtin_libogg"] = False  # Needed to link against system libtheora
         env["builtin_libvorbis"] = False  # Needed to link against system libtheora
         env["builtin_libvorbis"] = False  # Needed to link against system libtheora
-        env.ParseConfig("pkg-config theora theoradec --cflags --libs")
+        if env.editor_build:
+            env.ParseConfig("pkg-config theora theoradec theoraenc --cflags --libs")
+        else:
+            env.ParseConfig("pkg-config theora theoradec --cflags --libs")
     else:
     else:
         if env["arch"] in ["x86_64", "x86_32"]:
         if env["arch"] in ["x86_64", "x86_32"]:
             env["x86_libtheora_opt_gcc"] = True
             env["x86_libtheora_opt_gcc"] = True
 
 
     if not env["builtin_libvorbis"]:
     if not env["builtin_libvorbis"]:
         env["builtin_libogg"] = False  # Needed to link against system libvorbis
         env["builtin_libogg"] = False  # Needed to link against system libvorbis
-        env.ParseConfig("pkg-config vorbis vorbisfile --cflags --libs")
+        if env.editor_build:
+            env.ParseConfig("pkg-config vorbis vorbisfile vorbisenc --cflags --libs")
+        else:
+            env.ParseConfig("pkg-config vorbis vorbisfile --cflags --libs")
 
 
     if not env["builtin_libogg"]:
     if not env["builtin_libogg"]:
         env.ParseConfig("pkg-config ogg --cflags --libs")
         env.ParseConfig("pkg-config ogg --cflags --libs")

+ 16 - 3
servers/movie_writer/movie_writer.cpp

@@ -126,6 +126,7 @@ void MovieWriter::begin(const Size2i &p_movie_size, uint32_t p_fps, const String
 
 
 	cpu_time = 0.0f;
 	cpu_time = 0.0f;
 	gpu_time = 0.0f;
 	gpu_time = 0.0f;
+	encoding_time_usec = 0;
 
 
 	mix_rate = get_audio_mix_rate();
 	mix_rate = get_audio_mix_rate();
 	AudioDriverDummy::get_dummy_singleton()->set_mix_rate(mix_rate);
 	AudioDriverDummy::get_dummy_singleton()->set_mix_rate(mix_rate);
@@ -155,7 +156,11 @@ void MovieWriter::_bind_methods() {
 
 
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/mix_rate", PROPERTY_HINT_RANGE, "8000,192000,1,suffix:Hz"), 48000);
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/mix_rate", PROPERTY_HINT_RANGE, "8000,192000,1,suffix:Hz"), 48000);
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/speaker_mode", PROPERTY_HINT_ENUM, "Stereo,3.1,5.1,7.1"), 0);
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/speaker_mode", PROPERTY_HINT_ENUM, "Stereo,3.1,5.1,7.1"), 0);
-	GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "editor/movie_writer/mjpeg_quality", PROPERTY_HINT_RANGE, "0.01,1.0,0.01"), 0.75);
+	GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "editor/movie_writer/video_quality", PROPERTY_HINT_RANGE, "0.0,1.0,0.01"), 0.75);
+	GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "editor/movie_writer/ogv/audio_quality", PROPERTY_HINT_RANGE, "-0.1,1.0,0.01"), 0.5);
+	GLOBAL_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);
+	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/movie_writer/ogv/keyframe_interval", PROPERTY_HINT_RANGE, "1,1024,1"), 64);
+
 	// Used by the editor.
 	// Used by the editor.
 	GLOBAL_DEF_BASIC("editor/movie_writer/movie_file", "");
 	GLOBAL_DEF_BASIC("editor/movie_writer/movie_file", "");
 	GLOBAL_DEF_BASIC("editor/movie_writer/disable_vsync", false);
 	GLOBAL_DEF_BASIC("editor/movie_writer/disable_vsync", false);
@@ -211,11 +216,18 @@ void MovieWriter::add_frame() {
 	gpu_time += RenderingServer::get_singleton()->viewport_get_measured_render_time_gpu(main_vp_rid);
 	gpu_time += RenderingServer::get_singleton()->viewport_get_measured_render_time_gpu(main_vp_rid);
 
 
 	AudioDriverDummy::get_dummy_singleton()->mix_audio(mix_rate / fps, audio_mix_buffer.ptr());
 	AudioDriverDummy::get_dummy_singleton()->mix_audio(mix_rate / fps, audio_mix_buffer.ptr());
+
+	uint64_t encoding_start_usec = Time::get_singleton()->get_ticks_usec();
 	write_frame(vp_tex, audio_mix_buffer.ptr());
 	write_frame(vp_tex, audio_mix_buffer.ptr());
+	uint64_t encoding_end_usec = Time::get_singleton()->get_ticks_usec();
+	encoding_time_usec += encoding_end_usec - encoding_start_usec;
 }
 }
 
 
 void MovieWriter::end() {
 void MovieWriter::end() {
+	uint64_t encoding_start_usec = Time::get_singleton()->get_ticks_usec();
 	write_end();
 	write_end();
+	uint64_t encoding_end_usec = Time::get_singleton()->get_ticks_usec();
+	encoding_time_usec += encoding_end_usec - encoding_start_usec;
 
 
 	// Print a report with various statistics.
 	// Print a report with various statistics.
 	print_line("--------------------------------------------------------------------------------");
 	print_line("--------------------------------------------------------------------------------");
@@ -242,7 +254,8 @@ void MovieWriter::end() {
 			String::num(real_time_seconds % 60, 0).pad_zeros(2));
 			String::num(real_time_seconds % 60, 0).pad_zeros(2));
 
 
 	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(MAX(1, movie_time_seconds)) / MAX(1, real_time_seconds)) * 100));
 	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(MAX(1, movie_time_seconds)) / MAX(1, real_time_seconds)) * 100));
-	print_line(vformat("CPU time: %.2f seconds (average: %.2f ms/frame)", cpu_time / 1000, cpu_time / Engine::get_singleton()->get_frames_drawn()));
-	print_line(vformat("GPU time: %.2f seconds (average: %.2f ms/frame)", gpu_time / 1000, gpu_time / Engine::get_singleton()->get_frames_drawn()));
+	print_line(vformat("CPU render time: %.2f seconds (average: %.2f ms/frame)", cpu_time / 1000, cpu_time / Engine::get_singleton()->get_frames_drawn()));
+	print_line(vformat("GPU render time: %.2f seconds (average: %.2f ms/frame)", gpu_time / 1000, gpu_time / Engine::get_singleton()->get_frames_drawn()));
+	print_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()));
 	print_line("--------------------------------------------------------------------------------");
 	print_line("--------------------------------------------------------------------------------");
 }
 }

+ 1 - 0
servers/movie_writer/movie_writer.h

@@ -43,6 +43,7 @@ class MovieWriter : public Object {
 
 
 	float cpu_time = 0.0f;
 	float cpu_time = 0.0f;
 	float gpu_time = 0.0f;
 	float gpu_time = 0.0f;
+	uint64_t encoding_time_usec = 0;
 
 
 	String project_name;
 	String project_name;