Implement Running Godot as Movie Writer

* Allows running the game in "movie writer" mode.
* It ensures entirely stable framerate, so your run can be saved stable and with proper sound (which is impossible if your CPU/GPU can't sustain doing this in real-time).
* If disabling vsync, it can save movies faster than the game is run, but if you want to control the interaction it can get difficult.
* Implements a simple, default MJPEG writer.

This new features has two main use cases, which have high demand:
* Saving game videos in high quality and ensuring the frame rate is *completely* stable, always.
* Using Godot as a tool to make movies and animations (which is ideal if you want interaction, or creating them procedurally. No other software is as good for this).

**Note**: This feature **IS NOT** for capturing real-time footage. Use something like OBS, SimpleScreenRecorder or FRAPS to achieve that, as they do a much better job at intercepting the compositor than Godot can probably do using Vulkan or OpenGL natively. If your game runs near real-time when capturing, you can still use this feature but it will play no sound (sound will be saved directly).

Usage:

$ godot --write-movie movie.avi [scene_file.tscn]

Missing:

* Options for configuring video writing via GLOBAL_DEF
* UI Menu for launching with this mode from the editor.
* Add to list of command line options.
* Add a feature tag to override configurations when movie writing (fantastic for saving videos with highest quality settings).
This commit is contained in:
reduz 2022-06-17 00:55:19 +02:00
parent 362f53ff02
commit 5786516d4d
38 changed files with 2427 additions and 30 deletions

View File

@ -41,7 +41,6 @@
#include "core/os/keyboard.h"
#include "core/variant/variant_parser.h"
#include "core/version.h"
#include "modules/modules_enabled.gen.h" // For mono.
const String ProjectSettings::PROJECT_DATA_DIR_NAME_SUFFIX = "godot";

View File

@ -588,6 +588,7 @@ void register_global_constants() {
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_OBJECT_TOO_BIG);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_NODE_PATH_VALID_TYPES);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_SAVE_FILE);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_GLOBAL_SAVE_FILE);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_INT_IS_OBJECTID);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_INT_IS_POINTER);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_ARRAY_TYPE);

View File

@ -81,9 +81,11 @@ const char *Image::format_names[Image::FORMAT_MAX] = {
};
SavePNGFunc Image::save_png_func = nullptr;
SaveJPGFunc Image::save_jpg_func = nullptr;
SaveEXRFunc Image::save_exr_func = nullptr;
SavePNGBufferFunc Image::save_png_buffer_func = nullptr;
SaveJPGBufferFunc Image::save_jpg_buffer_func = nullptr;
void Image::_put_pixelb(int p_x, int p_y, uint32_t p_pixel_size, uint8_t *p_data, const uint8_t *p_pixel) {
uint32_t ofs = (p_y * width + p_x) * p_pixel_size;
@ -2286,6 +2288,14 @@ Error Image::save_png(const String &p_path) const {
return save_png_func(p_path, Ref<Image>((Image *)this));
}
Error Image::save_jpg(const String &p_path, float p_quality) const {
if (save_jpg_func == nullptr) {
return ERR_UNAVAILABLE;
}
return save_jpg_func(p_path, Ref<Image>((Image *)this), p_quality);
}
Vector<uint8_t> Image::save_png_to_buffer() const {
if (save_png_buffer_func == nullptr) {
return Vector<uint8_t>();
@ -2294,6 +2304,14 @@ Vector<uint8_t> Image::save_png_to_buffer() const {
return save_png_buffer_func(Ref<Image>((Image *)this));
}
Vector<uint8_t> Image::save_jpg_to_buffer(float p_quality) const {
if (save_jpg_buffer_func == nullptr) {
return Vector<uint8_t>();
}
return save_jpg_buffer_func(Ref<Image>((Image *)this), p_quality);
}
Error Image::save_exr(const String &p_path, bool p_grayscale) const {
if (save_exr_func == nullptr) {
return ERR_UNAVAILABLE;
@ -3138,6 +3156,8 @@ void Image::_bind_methods() {
ClassDB::bind_method(D_METHOD("load", "path"), &Image::load);
ClassDB::bind_method(D_METHOD("save_png", "path"), &Image::save_png);
ClassDB::bind_method(D_METHOD("save_png_to_buffer"), &Image::save_png_to_buffer);
ClassDB::bind_method(D_METHOD("save_jpg", "path", "quality"), &Image::save_jpg, DEFVAL(0.75));
ClassDB::bind_method(D_METHOD("save_jpg_to_buffer", "quality"), &Image::save_jpg_to_buffer, DEFVAL(0.75));
ClassDB::bind_method(D_METHOD("save_exr", "path", "grayscale"), &Image::save_exr, DEFVAL(false));
ClassDB::bind_method(D_METHOD("detect_alpha"), &Image::detect_alpha);

View File

@ -45,6 +45,8 @@ class Image;
typedef Error (*SavePNGFunc)(const String &p_path, const Ref<Image> &p_img);
typedef Vector<uint8_t> (*SavePNGBufferFunc)(const Ref<Image> &p_img);
typedef Error (*SaveJPGFunc)(const String &p_path, const Ref<Image> &p_img, float p_quality);
typedef Vector<uint8_t> (*SaveJPGBufferFunc)(const Ref<Image> &p_img, float p_quality);
typedef Ref<Image> (*ImageMemLoadFunc)(const uint8_t *p_png, int p_size);
typedef Error (*SaveEXRFunc)(const String &p_path, const Ref<Image> &p_img, bool p_grayscale);
@ -54,8 +56,10 @@ class Image : public Resource {
public:
static SavePNGFunc save_png_func;
static SaveJPGFunc save_jpg_func;
static SaveEXRFunc save_exr_func;
static SavePNGBufferFunc save_png_buffer_func;
static SaveJPGBufferFunc save_jpg_buffer_func;
enum {
MAX_WIDTH = (1 << 24), // force a limit somehow
@ -281,7 +285,9 @@ public:
Error load(const String &p_path);
Error save_png(const String &p_path) const;
Error save_jpg(const String &p_path, float p_quality = 0.75) const;
Vector<uint8_t> save_png_to_buffer() const;
Vector<uint8_t> save_jpg_to_buffer(float p_quality = 0.75) const;
Error save_exr(const String &p_path, bool p_grayscale) const;
void create_empty(int p_width, int p_height, bool p_use_mipmaps, Format p_format) {

View File

@ -85,6 +85,7 @@ enum PropertyHint {
PROPERTY_HINT_OBJECT_TOO_BIG, ///< object is too big to send
PROPERTY_HINT_NODE_PATH_VALID_TYPES,
PROPERTY_HINT_SAVE_FILE, ///< a file path must be passed, hint_text (optionally) is a filter "*.png,*.wav,*.doc,". This opens a save dialog
PROPERTY_HINT_GLOBAL_SAVE_FILE, ///< a file path must be passed, hint_text (optionally) is a filter "*.png,*.wav,*.doc,". This opens a save dialog
PROPERTY_HINT_INT_IS_OBJECTID,
PROPERTY_HINT_ARRAY_TYPE,
PROPERTY_HINT_INT_IS_POINTER,

View File

@ -388,6 +388,10 @@ bool OS::has_feature(const String &p_feature) {
return true;
}
if (p_feature == "movie") {
return _writing_movie;
}
#ifdef DEBUG_ENABLED
if (p_feature == "debug") {
return true;

View File

@ -58,6 +58,7 @@ class OS {
bool _allow_layered = false;
bool _stdout_enabled = true;
bool _stderr_enabled = true;
bool _writing_movie = false;
CompositeLogger *_logger = nullptr;

View File

@ -2512,19 +2512,21 @@
</constant>
<constant name="PROPERTY_HINT_SAVE_FILE" value="38" enum="PropertyHint">
</constant>
<constant name="PROPERTY_HINT_INT_IS_OBJECTID" value="39" enum="PropertyHint">
<constant name="PROPERTY_HINT_GLOBAL_SAVE_FILE" value="39" enum="PropertyHint">
</constant>
<constant name="PROPERTY_HINT_INT_IS_POINTER" value="41" enum="PropertyHint">
<constant name="PROPERTY_HINT_INT_IS_OBJECTID" value="40" enum="PropertyHint">
</constant>
<constant name="PROPERTY_HINT_ARRAY_TYPE" value="40" enum="PropertyHint">
<constant name="PROPERTY_HINT_INT_IS_POINTER" value="42" enum="PropertyHint">
</constant>
<constant name="PROPERTY_HINT_LOCALE_ID" value="42" enum="PropertyHint">
<constant name="PROPERTY_HINT_ARRAY_TYPE" value="41" enum="PropertyHint">
</constant>
<constant name="PROPERTY_HINT_LOCALE_ID" value="43" enum="PropertyHint">
Hints that a string property is a locale code. Editing it will show a locale dialog for picking language and country.
</constant>
<constant name="PROPERTY_HINT_LOCALIZABLE_STRING" value="43" enum="PropertyHint">
<constant name="PROPERTY_HINT_LOCALIZABLE_STRING" value="44" enum="PropertyHint">
Hints that a dictionary property is string translation map. Dictionary keys are locale codes and, values are translated strings.
</constant>
<constant name="PROPERTY_HINT_MAX" value="44" enum="PropertyHint">
<constant name="PROPERTY_HINT_MAX" value="45" enum="PropertyHint">
</constant>
<constant name="PROPERTY_USAGE_NONE" value="0" enum="PropertyUsageFlags">
</constant>

View File

@ -380,6 +380,19 @@
[b]Note:[/b] The TinyEXR module is disabled in non-editor builds, which means [method save_exr] will return [constant ERR_UNAVAILABLE] when it is called from an exported project.
</description>
</method>
<method name="save_jpg" qualifiers="const">
<return type="int" enum="Error" />
<argument index="0" name="path" type="String" />
<argument index="1" name="quality" type="float" default="0.75" />
<description>
</description>
</method>
<method name="save_jpg_to_buffer" qualifiers="const">
<return type="PackedByteArray" />
<argument index="0" name="quality" type="float" default="0.75" />
<description>
</description>
</method>
<method name="save_png" qualifiers="const">
<return type="int" enum="Error" />
<argument index="0" name="path" type="String" />

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="MovieWriter" inherits="Object" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
</brief_description>
<description>
</description>
<tutorials>
</tutorials>
<methods>
<method name="_get_audio_mix_rate" qualifiers="virtual const">
<return type="int" />
<description>
</description>
</method>
<method name="_get_audio_speaker_mode" qualifiers="virtual const">
<return type="int" enum="AudioServer.SpeakerMode" />
<description>
</description>
</method>
<method name="_handles_file" qualifiers="virtual const">
<return type="bool" />
<argument index="0" name="path" type="String" />
<description>
</description>
</method>
<method name="_write_begin" qualifiers="virtual">
<return type="int" enum="Error" />
<argument index="0" name="movie_size" type="Vector2i" />
<argument index="1" name="fps" type="int" />
<argument index="2" name="base_path" type="String" />
<description>
</description>
</method>
<method name="_write_end" qualifiers="virtual">
<return type="void" />
<description>
</description>
</method>
<method name="_write_frame" qualifiers="virtual">
<return type="int" enum="Error" />
<argument index="0" name="frame_image" type="Image" />
<argument index="1" name="audio_frame_block" type="const void*" />
<description>
</description>
</method>
<method name="add_writer" qualifiers="static">
<return type="void" />
<argument index="0" name="writer" type="MovieWriter" />
<description>
</description>
</method>
</methods>
</class>

View File

@ -547,6 +547,18 @@
See [enum DisplayServer.VSyncMode] for possible values and how they affect the behavior of your application.
Depending on the platform and used renderer, the engine will fall back to [code]Enabled[/code], if the desired mode is not supported.
</member>
<member name="editor/movie_writer/disable_vsync" type="bool" setter="" getter="" default="false">
</member>
<member name="editor/movie_writer/fps" type="int" setter="" getter="" default="60">
</member>
<member name="editor/movie_writer/mix_rate_hz" type="int" setter="" getter="" default="48000">
</member>
<member name="editor/movie_writer/mjpeg_quality" type="float" setter="" getter="" default="0.75">
</member>
<member name="editor/movie_writer/movie_file" type="String" setter="" getter="" default="&quot;&quot;">
</member>
<member name="editor/movie_writer/speaker_mode" type="int" setter="" getter="" default="0">
</member>
<member name="editor/node_naming/name_casing" type="int" setter="" getter="" default="0">
When creating node names automatically, set the type of casing in this project. This is mostly an editor setting.
</member>

View File

@ -2342,6 +2342,20 @@ void EditorNode::_run(bool p_current, const String &p_custom) {
return;
}
String write_movie_file;
if (write_movie_button->is_pressed()) {
if (p_current && get_tree()->get_edited_scene_root() && get_tree()->get_edited_scene_root()->has_meta("movie_file")) {
// If the scene file has a movie_file metadata set, use this as file. Quick workaround if you want to have multiple scenes that write to multiple movies.
write_movie_file = get_tree()->get_edited_scene_root()->get_meta("movie_file");
} else {
write_movie_file = GLOBAL_GET("editor/movie_writer/movie_file");
}
if (write_movie_file == String()) {
show_accept(TTR("Movie Maker mode is enabled, but no movie file path has been specified.\nA default movie file path can be specified in the project settings under the 'Editor/Movie Writer' category.\nAlternatively, for running single scenes, a 'movie_path' metadata can be added to the root node,\nspecifying the path to a movie file that will be used when recording that scene."), TTR("OK"));
return;
}
}
play_button->set_pressed(false);
play_button->set_icon(gui_base->get_theme_icon(SNAME("MainPlay"), SNAME("EditorIcons")));
play_scene_button->set_pressed(false);
@ -2405,7 +2419,7 @@ void EditorNode::_run(bool p_current, const String &p_custom) {
}
EditorDebuggerNode::get_singleton()->start();
Error error = editor_run.run(run_filename);
Error error = editor_run.run(run_filename, write_movie_file);
if (error != OK) {
EditorDebuggerNode::get_singleton()->stop();
show_accept(TTR("Could not start subprocess(es)!"), TTR("OK"));
@ -2788,6 +2802,9 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) {
case RUN_SETTINGS: {
project_settings_editor->popup_project_settings();
} break;
case RUN_WRITE_MOVIE: {
_update_write_movie_icon();
} break;
case FILE_INSTALL_ANDROID_SOURCE: {
if (p_confirmed) {
export_template_manager->install_android_template();
@ -4949,6 +4966,14 @@ String EditorNode::get_run_playing_scene() const {
return run_filename;
}
void EditorNode::_update_write_movie_icon() {
if (write_movie_button->is_pressed()) {
write_movie_button->set_icon(gui_base->get_theme_icon(SNAME("MainMovieWriteEnabled"), SNAME("EditorIcons")));
} else {
write_movie_button->set_icon(gui_base->get_theme_icon(SNAME("MainMovieWrite"), SNAME("EditorIcons")));
}
}
void EditorNode::_immediate_dialog_confirmed() {
immediate_dialog_confirmed = true;
}
@ -6704,6 +6729,23 @@ EditorNode::EditorNode() {
ED_SHORTCUT_OVERRIDE("editor/play_custom_scene", "macos", KeyModifierMask::CMD | KeyModifierMask::SHIFT | Key::R);
play_custom_scene_button->set_shortcut(ED_GET_SHORTCUT("editor/play_custom_scene"));
write_movie_button = memnew(Button);
write_movie_button->set_flat(true);
write_movie_button->set_toggle_mode(true);
play_hb->add_child(write_movie_button);
write_movie_button->set_pressed(false);
write_movie_button->set_icon(gui_base->get_theme_icon(SNAME("MainMovieWrite"), SNAME("EditorIcons")));
write_movie_button->set_focus_mode(Control::FOCUS_NONE);
write_movie_button->connect("pressed", callable_mp(this, &EditorNode::_menu_option), make_binds(RUN_WRITE_MOVIE));
write_movie_button->set_tooltip(TTR("Enable Movie Maker mode.\nThe project will run at stable FPS and the visual and audio output will be recorded to a video file."));
// Restore these values to something more useful so it ignores the theme
write_movie_button->add_theme_color_override("icon_normal_color", Color(1, 1, 1, 0.4));
write_movie_button->add_theme_color_override("icon_pressed_color", Color(1, 1, 1, 1));
write_movie_button->add_theme_color_override("icon_hover_color", Color(1.2, 1.2, 1.2, 0.4));
write_movie_button->add_theme_color_override("icon_hover_pressed_color", Color(1.2, 1.2, 1.2, 1));
write_movie_button->add_theme_color_override("icon_focus_color", Color(1, 1, 1, 1));
write_movie_button->add_theme_color_override("icon_disabled_color", Color(1, 1, 1, 0.4));
HBoxContainer *right_menu_hb = memnew(HBoxContainer);
menu_hb->add_child(right_menu_hb);

View File

@ -173,6 +173,7 @@ private:
RUN_PLAY_CUSTOM_SCENE,
RUN_SETTINGS,
RUN_USER_DATA_FOLDER,
RUN_WRITE_MOVIE,
RELOAD_CURRENT_PROJECT,
RUN_PROJECT_MANAGER,
RUN_VCS_METADATA,
@ -333,6 +334,7 @@ private:
Button *play_scene_button = nullptr;
Button *play_custom_scene_button = nullptr;
Button *search_button = nullptr;
Button *write_movie_button = nullptr;
TextureProgressBar *audio_vu = nullptr;
Timer *screenshot_timer = nullptr;
@ -667,7 +669,7 @@ private:
void _pick_main_scene_custom_action(const String &p_custom_action_name);
void _immediate_dialog_confirmed();
void _update_write_movie_icon();
void _select_default_main_screen_plugin();
void _bottom_panel_switch(bool p_enable, int p_idx);

View File

@ -3747,11 +3747,11 @@ EditorProperty *EditorInspectorDefaultPlugin::get_editor_for_property(Object *p_
EditorPropertyLocale *editor = memnew(EditorPropertyLocale);
editor->setup(p_hint_text);
return editor;
} else if (p_hint == PROPERTY_HINT_DIR || p_hint == PROPERTY_HINT_FILE || p_hint == PROPERTY_HINT_SAVE_FILE || p_hint == PROPERTY_HINT_GLOBAL_DIR || p_hint == PROPERTY_HINT_GLOBAL_FILE) {
} else if (p_hint == PROPERTY_HINT_DIR || p_hint == PROPERTY_HINT_FILE || p_hint == PROPERTY_HINT_SAVE_FILE || p_hint == PROPERTY_HINT_GLOBAL_SAVE_FILE || p_hint == PROPERTY_HINT_GLOBAL_DIR || p_hint == PROPERTY_HINT_GLOBAL_FILE) {
Vector<String> extensions = p_hint_text.split(",");
bool global = p_hint == PROPERTY_HINT_GLOBAL_DIR || p_hint == PROPERTY_HINT_GLOBAL_FILE;
bool global = p_hint == PROPERTY_HINT_GLOBAL_DIR || p_hint == PROPERTY_HINT_GLOBAL_FILE || p_hint == PROPERTY_HINT_GLOBAL_SAVE_FILE;
bool folder = p_hint == PROPERTY_HINT_DIR || p_hint == PROPERTY_HINT_GLOBAL_DIR;
bool save = p_hint == PROPERTY_HINT_SAVE_FILE;
bool save = p_hint == PROPERTY_HINT_SAVE_FILE || p_hint == PROPERTY_HINT_GLOBAL_SAVE_FILE;
EditorPropertyPath *editor = memnew(EditorPropertyPath);
editor->setup(extensions, folder, global);
if (save) {

View File

@ -43,7 +43,7 @@ String EditorRun::get_running_scene() const {
return running_scene;
}
Error EditorRun::run(const String &p_scene) {
Error EditorRun::run(const String &p_scene, const String &p_write_movie) {
List<String> args;
String resource_path = ProjectSettings::get_singleton()->get_resource_path();
@ -68,6 +68,16 @@ Error EditorRun::run(const String &p_scene) {
args.push_back("--debug-navigation");
}
if (p_write_movie != "") {
args.push_back("--write-movie");
args.push_back(p_write_movie);
args.push_back("--fixed-fps");
args.push_back(itos(GLOBAL_GET("editor/movie_writer/fps")));
if (bool(GLOBAL_GET("editor/movie_writer/disable_vsync"))) {
args.push_back("--disable-vsync");
}
}
int screen = EditorSettings::get_singleton()->get("run/window_placement/screen");
if (screen == 0) {
// Same as editor

View File

@ -50,7 +50,7 @@ private:
public:
Status get_status() const;
String get_running_scene() const;
Error run(const String &p_scene);
Error run(const String &p_scene, const String &p_write_movie = "");
void run_native_notify() { status = STATUS_PLAY; }
void stop();

View File

@ -0,0 +1 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M8 2a6 6 0 0 0-6 6 6 6 0 0 0 6 6 6 6 0 0 0 4-1.535V14h.002a2 2 0 0 0 .266 1A2 2 0 0 0 14 16h1v-2h-.5a.5.5 0 0 1-.5-.5V8a6 6 0 0 0-6-6zm0 1a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1zm3.441 2a1 1 0 0 1 .89.5 1 1 0 0 1-.366 1.365 1 1 0 0 1-1.367-.365 1 1 0 0 1 .367-1.365A1 1 0 0 1 11.44 5zm-6.953.002a1 1 0 0 1 .547.133A1 1 0 0 1 5.402 6.5a1 1 0 0 1-1.367.365A1 1 0 0 1 3.67 5.5a1 1 0 0 1 .818-.498zM4.512 9a1 1 0 0 1 .89.5 1 1 0 0 1-.367 1.365A1 1 0 0 1 3.67 10.5a1 1 0 0 1 .365-1.365A1 1 0 0 1 4.512 9zm6.904.002a1 1 0 0 1 .549.133 1 1 0 0 1 .365 1.365 1 1 0 0 1-1.365.365 1 1 0 0 1-.367-1.365 1 1 0 0 1 .818-.498zM8 11a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#e0e0e0"/></svg>

After

Width:  |  Height:  |  Size: 787 B

View File

@ -0,0 +1 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M8 2a6 6 0 0 0-6 6 6 6 0 0 0 6 6 6 6 0 0 0 4-1.535V14h.002a2 2 0 0 0 .266 1A2 2 0 0 0 14 16h1v-2h-.5a.5.5 0 0 1-.5-.5V8a6 6 0 0 0-6-6zm0 1a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1zm3.441 2a1 1 0 0 1 .89.5 1 1 0 0 1-.366 1.365 1 1 0 0 1-1.367-.365 1 1 0 0 1 .367-1.365A1 1 0 0 1 11.44 5zm-6.953.002a1 1 0 0 1 .547.133A1 1 0 0 1 5.402 6.5a1 1 0 0 1-1.367.365A1 1 0 0 1 3.67 5.5a1 1 0 0 1 .818-.498zM4.512 9a1 1 0 0 1 .89.5 1 1 0 0 1-.367 1.365A1 1 0 0 1 3.67 10.5a1 1 0 0 1 .365-1.365A1 1 0 0 1 4.512 9zm6.904.002a1 1 0 0 1 .549.133 1 1 0 0 1 .365 1.365 1 1 0 0 1-1.365.365 1 1 0 0 1-.367-1.365 1 1 0 0 1 .818-.498zM8 11a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#e0e0e0" style="fill:#ee5353;fill-opacity:1"/></svg>

After

Width:  |  Height:  |  Size: 823 B

View File

@ -35,6 +35,7 @@
#include "editor/editor_log.h"
#include "editor/editor_node.h"
#include "editor/editor_scale.h"
#include "servers/movie_writer/movie_writer.h"
ProjectSettingsEditor *ProjectSettingsEditor::singleton = nullptr;
@ -261,6 +262,7 @@ void ProjectSettingsEditor::_add_feature_overrides() {
presets.insert("standalone");
presets.insert("32");
presets.insert("64");
presets.insert("movie");
EditorExport *ee = EditorExport::get_singleton();
@ -698,4 +700,6 @@ ProjectSettingsEditor::ProjectSettingsEditor(EditorData *p_data) {
import_defaults_editor->set_name(TTR("Import Defaults"));
tab_container->add_child(import_defaults_editor);
import_defaults_editor->connect("project_settings_changed", callable_mp(this, &ProjectSettingsEditor::queue_save));
MovieWriter::set_extensions_hint(); // ensure extensions are properly displayed.
}

View File

@ -65,6 +65,8 @@
#include "servers/audio_server.h"
#include "servers/camera_server.h"
#include "servers/display_server.h"
#include "servers/movie_writer/movie_writer.h"
#include "servers/movie_writer/movie_writer_mjpeg.h"
#include "servers/navigation_server_2d.h"
#include "servers/navigation_server_3d.h"
#include "servers/physics_server_2d.h"
@ -178,6 +180,9 @@ static bool debug_navigation = false;
static int frame_delay = 0;
static bool disable_render_loop = false;
static int fixed_fps = -1;
static String write_movie_path;
static MovieWriter *movie_writer = nullptr;
static bool disable_vsync = false;
static bool print_fps = false;
#ifdef TOOLS_ENABLED
static bool dump_extension_api = false;
@ -325,6 +330,8 @@ void Main::print_help(const char *p_binary) {
OS::get_singleton()->print(" --text-driver <driver> Text driver (Fonts, BiDi, shaping)\n");
OS::get_singleton()->print(" --tablet-driver <driver> Pen tablet input driver.\n");
OS::get_singleton()->print(" --headless Enable headless mode (--display-driver headless --audio-driver Dummy). Useful for servers and with --script.\n");
OS::get_singleton()->print(" --write-movie <file> Run the engine in a way that a movie is written (by default .avi MJPEG). Fixed FPS is forced when enabled, but can be used to change movie FPS. Disabling vsync can speed up movie writing but makes interaction more difficult.\n");
OS::get_singleton()->print(" --disable-vsync Force disabling of vsync. Run the engine in a way that a movie is written (by default .avi MJPEG). Fixed FPS is forced when enabled, but can be used to change movie FPS.\n");
OS::get_singleton()->print("\n");
@ -1136,6 +1143,20 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
OS::get_singleton()->print("Missing fixed-fps argument, aborting.\n");
goto error;
}
} else if (I->get() == "--write-movie") {
if (I->next()) {
write_movie_path = I->next()->get();
N = I->next()->next();
if (fixed_fps == -1) {
fixed_fps = 60;
}
OS::get_singleton()->_writing_movie = true;
} else {
OS::get_singleton()->print("Missing write-movie argument, aborting.\n");
goto error;
}
} else if (I->get() == "--disable-vsync") {
disable_vsync = true;
} else if (I->get() == "--print-fps") {
print_fps = true;
} else if (I->get() == "--profile-gpu") {
@ -1462,7 +1483,13 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
}
if (audio_driver_idx < 0) {
audio_driver_idx = 0;
audio_driver_idx = 0; // 0 Is always available as the dummy driver (no sound)
}
if (write_movie_path != String()) {
// Always use dummy driver for audio driver (which is last), also in no threaded mode.
audio_driver_idx = AudioDriverManager::get_driver_count() - 1;
AudioDriverDummy::get_dummy_singleton()->set_use_threads(false);
}
{
@ -1470,6 +1497,9 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
}
{
window_vsync_mode = DisplayServer::VSyncMode(int(GLOBAL_DEF("display/window/vsync/vsync_mode", DisplayServer::VSyncMode::VSYNC_ENABLED)));
if (disable_vsync) {
window_vsync_mode = DisplayServer::VSyncMode::VSYNC_DISABLED;
}
}
Engine::get_singleton()->set_physics_ticks_per_second(GLOBAL_DEF_BASIC("physics/common/physics_ticks_per_second", 60));
ProjectSettings::get_singleton()->set_custom_property_info("physics/common/physics_ticks_per_second",
@ -1553,6 +1583,7 @@ error:
display_driver = "";
audio_driver = "";
tablet_driver = "";
write_movie_path = "";
project_path = "";
args.clear();
@ -1725,6 +1756,14 @@ Error Main::setup2(Thread::ID p_main_tid_override) {
rendering_server->set_print_gpu_profile(true);
}
if (write_movie_path != String()) {
movie_writer = MovieWriter::find_writer_for_file(write_movie_path);
if (movie_writer == nullptr) {
ERR_PRINT("Can't find movie writer for file type, aborting: " + write_movie_path);
write_movie_path = String();
}
}
#ifdef UNIX_ENABLED
// Print warning after initializing the renderer but before initializing audio.
if (OS::get_singleton()->get_environment("USER") == "root" && !OS::get_singleton()->has_environment("GODOT_SILENCE_ROOT_WARNING")) {
@ -2650,6 +2689,9 @@ bool Main::start() {
OS::get_singleton()->set_main_loop(main_loop);
if (movie_writer) {
movie_writer->begin(DisplayServer::get_singleton()->window_get_size(), fixed_fps, write_movie_path);
}
return true;
}
@ -2836,6 +2878,13 @@ bool Main::iteration() {
Input::get_singleton()->flush_buffered_events();
}
if (movie_writer) {
RID main_vp_rid = RenderingServer::get_singleton()->viewport_find_from_screen_attachment(DisplayServer::MAIN_WINDOW_ID);
RID main_vp_texture = RenderingServer::get_singleton()->viewport_get_texture(main_vp_rid);
Ref<Image> vp_tex = RenderingServer::get_singleton()->texture_2d_get(main_vp_texture);
movie_writer->add_frame(vp_tex);
}
if (fixed_fps != -1) {
return exit;
}
@ -2875,6 +2924,10 @@ void Main::cleanup(bool p_force) {
ERR_FAIL_COND(!_start_success);
}
if (movie_writer) {
movie_writer->end();
}
ResourceLoader::remove_custom_loaders();
ResourceSaver::remove_custom_savers();

View File

@ -13,6 +13,7 @@ thirdparty_obj = []
thirdparty_dir = "#thirdparty/jpeg-compressor/"
thirdparty_sources = [
"jpgd.cpp",
"jpge.cpp",
]
thirdparty_sources = [thirdparty_dir + file for file in thirdparty_sources]

View File

@ -33,7 +33,8 @@
#include "core/os/os.h"
#include "core/string/print_string.h"
#include <jpgd.h>
#include "thirdparty/jpeg-compressor/jpgd.h"
#include "thirdparty/jpeg-compressor/jpge.h"
#include <string.h>
Error jpeg_load_image_from_buffer(Image *p_image, const uint8_t *p_buffer, int p_buffer_len) {
@ -131,6 +132,59 @@ static Ref<Image> _jpegd_mem_loader_func(const uint8_t *p_png, int p_size) {
return img;
}
static Error _jpgd_save_func(const String &p_path, const Ref<Image> &p_img, float p_quality) {
return OK;
}
class ImageLoaderJPGOSFile : public jpge::output_stream {
public:
Ref<FileAccess> f;
virtual bool put_buf(const void *Pbuf, int len) {
f->store_buffer((const uint8_t *)Pbuf, len);
return true;
}
};
class ImageLoaderJPGOSBuffer : public jpge::output_stream {
public:
Vector<uint8_t> *buffer = nullptr;
virtual bool put_buf(const void *Pbuf, int len) {
uint32_t base = buffer->size();
buffer->resize(base + len);
memcpy(buffer->ptrw() + base, Pbuf, len);
return true;
}
};
static Vector<uint8_t> _jpgd_buffer_save_func(const Ref<Image> &p_img, float p_quality) {
ERR_FAIL_COND_V(p_img.is_null() || p_img->is_empty(), Vector<uint8_t>());
Ref<Image> image = p_img;
if (image->get_format() != Image::FORMAT_RGB8) {
image->convert(Image::FORMAT_ETC2_RGB8);
}
jpge::params p;
p.m_quality = CLAMP(p_quality * 100, 1, 100);
Vector<uint8_t> output;
ImageLoaderJPGOSBuffer ob;
ob.buffer = &output;
jpge::jpeg_encoder enc;
enc.init(&ob, image->get_width(), image->get_height(), 3, p);
const uint8_t *src_data = image->get_data().ptr();
for (int i = 0; i < image->get_height(); i++) {
enc.process_scanline(&src_data[i * image->get_width() * 3]);
}
enc.process_scanline(nullptr);
return output;
}
ImageLoaderJPG::ImageLoaderJPG() {
Image::_jpg_mem_loader_func = _jpegd_mem_loader_func;
Image::save_jpg_func = _jpgd_save_func;
Image::save_jpg_buffer_func = _jpgd_buffer_save_func;
}

View File

@ -14,6 +14,7 @@ SConscript("audio/SCsub")
SConscript("text/SCsub")
SConscript("debugger/SCsub")
SConscript("extensions/SCsub")
SConscript("movie_writer/SCsub")
lib = env.add_library("servers", env.servers_sources)

View File

@ -33,22 +33,24 @@
#include "core/config/project_settings.h"
#include "core/os/os.h"
AudioDriverDummy *AudioDriverDummy::singleton = nullptr;
Error AudioDriverDummy::init() {
active = false;
thread_exited = false;
exit_thread = false;
samples_in = nullptr;
if (mix_rate == -1) {
mix_rate = GLOBAL_GET("audio/driver/mix_rate");
speaker_mode = SPEAKER_MODE_STEREO;
channels = 2;
int latency = GLOBAL_GET("audio/driver/output_latency");
buffer_frames = closest_power_of_2(latency * mix_rate / 1000);
}
channels = get_channels();
samples_in = memnew_arr(int32_t, (size_t)buffer_frames * channels);
if (use_threads) {
thread.start(AudioDriverDummy::thread_func, this);
}
return OK;
};
@ -93,11 +95,56 @@ void AudioDriverDummy::unlock() {
mutex.unlock();
};
void AudioDriverDummy::set_use_threads(bool p_use_threads) {
use_threads = p_use_threads;
}
void AudioDriverDummy::set_speaker_mode(SpeakerMode p_mode) {
speaker_mode = p_mode;
}
void AudioDriverDummy::set_mix_rate(int p_rate) {
mix_rate = p_rate;
}
uint32_t AudioDriverDummy::get_channels() const {
static const int channels_for_mode[4] = { 2, 4, 8, 16 };
return channels_for_mode[speaker_mode];
}
void AudioDriverDummy::mix_audio(int p_frames, int32_t *p_buffer) {
ERR_FAIL_COND(!active); // If not active, should not mix.
ERR_FAIL_COND(use_threads == true); // If using threads, this will not work well.
uint32_t todo = p_frames;
while (todo) {
uint32_t to_mix = MIN(buffer_frames, todo);
lock();
audio_server_process(to_mix, samples_in);
unlock();
uint32_t total_samples = to_mix * channels;
for (uint32_t i = 0; i < total_samples; i++) {
p_buffer[i] = samples_in[i];
}
todo -= to_mix;
p_buffer += total_samples;
}
}
void AudioDriverDummy::finish() {
if (use_threads) {
exit_thread = true;
thread.wait_to_finish();
}
if (samples_in) {
memdelete_arr(samples_in);
};
};
}
AudioDriverDummy::AudioDriverDummy() {
singleton = this;
}

View File

@ -44,9 +44,9 @@ class AudioDriverDummy : public AudioDriver {
static void thread_func(void *p_udata);
unsigned int buffer_frames;
unsigned int mix_rate;
SpeakerMode speaker_mode;
uint32_t buffer_frames = 4096;
int32_t mix_rate = -1;
SpeakerMode speaker_mode = SPEAKER_MODE_STEREO;
int channels;
@ -54,6 +54,10 @@ class AudioDriverDummy : public AudioDriver {
bool thread_exited;
mutable bool exit_thread;
bool use_threads = true;
static AudioDriverDummy *singleton;
public:
const char *get_name() const {
return "Dummy";
@ -67,7 +71,17 @@ public:
virtual void unlock();
virtual void finish();
AudioDriverDummy() {}
void set_use_threads(bool p_use_threads);
void set_speaker_mode(SpeakerMode p_mode);
void set_mix_rate(int p_rate);
uint32_t get_channels() const;
void mix_audio(int p_frames, int32_t *p_buffer);
static AudioDriverDummy *get_dummy_singleton() { return singleton; }
AudioDriverDummy();
~AudioDriverDummy() {}
};

View File

@ -0,0 +1,5 @@
#!/usr/bin/env python
Import("env")
env.add_source_files(env.servers_sources, "*.cpp")

View File

@ -0,0 +1,306 @@
/*************************************************************************/
/* movie_writer.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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.h"
#include "core/config/project_settings.h"
#include "core/io/dir_access.h"
MovieWriter *MovieWriter::writers[MovieWriter::MAX_WRITERS];
uint32_t MovieWriter::writer_count = 0;
void MovieWriter::add_writer(MovieWriter *p_writer) {
ERR_FAIL_COND(writer_count == MAX_WRITERS);
writers[writer_count++] = p_writer;
}
MovieWriter *MovieWriter::find_writer_for_file(const String &p_file) {
for (int32_t i = writer_count - 1; i >= 0; i--) { // More recent last, to have override ability.
if (writers[i]->handles_file(p_file)) {
return writers[i];
}
}
return nullptr;
}
uint32_t MovieWriter::get_audio_mix_rate() const {
uint32_t ret = 0;
if (GDVIRTUAL_REQUIRED_CALL(_get_audio_mix_rate, ret)) {
return ret;
}
return 48000;
}
AudioServer::SpeakerMode MovieWriter::get_audio_speaker_mode() const {
AudioServer::SpeakerMode ret = AudioServer::SPEAKER_MODE_STEREO;
if (GDVIRTUAL_REQUIRED_CALL(_get_audio_speaker_mode, ret)) {
return ret;
}
return AudioServer::SPEAKER_MODE_STEREO;
}
Error MovieWriter::write_begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path) {
Error ret = OK;
if (GDVIRTUAL_REQUIRED_CALL(_write_begin, p_movie_size, p_fps, p_base_path, ret)) {
return ret;
}
return ERR_UNCONFIGURED;
}
Error MovieWriter::write_frame(const Ref<Image> &p_image, const int32_t *p_audio_data) {
Error ret = OK;
if (GDVIRTUAL_REQUIRED_CALL(_write_frame, p_image, p_audio_data, ret)) {
return ret;
}
return ERR_UNCONFIGURED;
}
void MovieWriter::write_end() {
GDVIRTUAL_REQUIRED_CALL(_write_end);
}
bool MovieWriter::handles_file(const String &p_path) const {
bool ret = false;
if (GDVIRTUAL_REQUIRED_CALL(_handles_file, p_path, ret)) {
return ret;
}
return false;
}
void MovieWriter::get_supported_extensions(List<String> *r_extensions) const {
Vector<String> exts;
if (GDVIRTUAL_REQUIRED_CALL(_get_supported_extensions, exts)) {
for (int i = 0; i < exts.size(); i++) {
r_extensions->push_back(exts[i]);
}
}
}
void MovieWriter::begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path) {
mix_rate = get_audio_mix_rate();
AudioDriverDummy::get_dummy_singleton()->set_mix_rate(mix_rate);
AudioDriverDummy::get_dummy_singleton()->set_speaker_mode(AudioDriver::SpeakerMode(get_audio_speaker_mode()));
fps = p_fps;
if ((mix_rate % fps) != 0) {
WARN_PRINT("Audio mix rate (" + itos(mix_rate) + ") can not be divided by fps (" + itos(fps) + "). Audio may go out of sync over time.");
}
audio_channels = AudioDriverDummy::get_dummy_singleton()->get_channels();
audio_mix_buffer.resize(mix_rate * audio_channels / fps);
write_begin(p_movie_size, p_fps, p_base_path);
}
void MovieWriter::_bind_methods() {
ClassDB::bind_static_method("MovieWriter", D_METHOD("add_writer", "writer"), &MovieWriter::add_writer);
GDVIRTUAL_BIND(_get_audio_mix_rate)
GDVIRTUAL_BIND(_get_audio_speaker_mode)
GDVIRTUAL_BIND(_handles_file, "path")
GDVIRTUAL_BIND(_write_begin, "movie_size", "fps", "base_path")
GDVIRTUAL_BIND(_write_frame, "frame_image", "audio_frame_block")
GDVIRTUAL_BIND(_write_end)
GLOBAL_DEF("editor/movie_writer/mix_rate_hz", 48000);
GLOBAL_DEF("editor/movie_writer/speaker_mode", 0);
ProjectSettings::get_singleton()->set_custom_property_info("editor/movie_writer/speaker_mode", PropertyInfo(Variant::INT, "editor/movie_writer/speaker_mode", PROPERTY_HINT_ENUM, "Stereo,3.1,5.1,7.1"));
GLOBAL_DEF("editor/movie_writer/mjpeg_quality", 0.75);
// used by the editor
GLOBAL_DEF_BASIC("editor/movie_writer/movie_file", "");
GLOBAL_DEF_BASIC("editor/movie_writer/disable_vsync", false);
GLOBAL_DEF_BASIC("editor/movie_writer/fps", 60);
ProjectSettings::get_singleton()->set_custom_property_info("editor/movie_writer/fps", PropertyInfo(Variant::INT, "editor/movie_writer/fps", PROPERTY_HINT_RANGE, "1,300,1"));
}
void MovieWriter::set_extensions_hint() {
RBSet<String> found;
for (uint32_t i = 0; i < writer_count; i++) {
List<String> extensions;
writers[i]->get_supported_extensions(&extensions);
for (const String &ext : extensions) {
found.insert(ext);
}
}
String ext_hint;
for (const String &S : found) {
if (ext_hint != "") {
ext_hint += ",";
}
ext_hint += "*." + S;
}
ProjectSettings::get_singleton()->set_custom_property_info("editor/movie_writer/movie_file", PropertyInfo(Variant::STRING, "editor/movie_writer/movie_file", PROPERTY_HINT_GLOBAL_SAVE_FILE, ext_hint));
}
void MovieWriter::add_frame(const Ref<Image> &p_image) {
AudioDriverDummy::get_dummy_singleton()->mix_audio(mix_rate / fps, audio_mix_buffer.ptr());
write_frame(p_image, audio_mix_buffer.ptr());
}
void MovieWriter::end() {
write_end();
}
/////////////////////////////////////////
uint32_t MovieWriterPNGWAV::get_audio_mix_rate() const {
return mix_rate;
}
AudioServer::SpeakerMode MovieWriterPNGWAV::get_audio_speaker_mode() const {
return speaker_mode;
}
void MovieWriterPNGWAV::get_supported_extensions(List<String> *r_extensions) const {
r_extensions->push_back("png");
}
bool MovieWriterPNGWAV::handles_file(const String &p_path) const {
return p_path.get_extension().to_lower() == "png";
}
String MovieWriterPNGWAV::zeros_str(uint32_t p_index) {
char zeros[MAX_TRAILING_ZEROS + 1];
for (uint32_t i = 0; i < MAX_TRAILING_ZEROS; i++) {
uint32_t idx = MAX_TRAILING_ZEROS - i - 1;
uint32_t digit = (p_index / uint32_t(Math::pow(double(10), double(idx)))) % 10;
zeros[i] = '0' + digit;
}
zeros[MAX_TRAILING_ZEROS] = 0;
return zeros;
}
Error MovieWriterPNGWAV::write_begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path) {
// Quick & Dirty PNGWAV Code based on - https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference
base_path = p_base_path.get_basename();
if (base_path.is_relative_path()) {
base_path = "res://" + base_path;
}
{
//Remove existing files before writing anew
uint32_t idx = 0;
Ref<DirAccess> d = DirAccess::open(base_path.get_base_dir());
String file = base_path.get_file();
while (true) {
String path = file + zeros_str(idx) + ".png";
if (d->remove(path) != OK) {
break;
}
}
}
f_wav = FileAccess::open(base_path + ".wav", FileAccess::WRITE_READ);
ERR_FAIL_COND_V(f_wav.is_null(), ERR_CANT_OPEN);
fps = p_fps;
f_wav->store_buffer((const uint8_t *)"RIFF", 4);
int total_size = 4 /* WAVE */ + 8 /* fmt+size */ + 16 /* format */ + 8 /* data+size */;
f_wav->store_32(total_size); //will store final later
f_wav->store_buffer((const uint8_t *)"WAVE", 4);
/* FORMAT CHUNK */
f_wav->store_buffer((const uint8_t *)"fmt ", 4);
uint32_t channels = 2;
switch (speaker_mode) {
case AudioServer::SPEAKER_MODE_STEREO:
channels = 2;
break;
case AudioServer::SPEAKER_SURROUND_31:
channels = 4;
break;
case AudioServer::SPEAKER_SURROUND_51:
channels = 6;
break;
case AudioServer::SPEAKER_SURROUND_71:
channels = 8;
break;
}
f_wav->store_32(16); //standard format, no extra fields
f_wav->store_16(1); // compression code, standard PCM
f_wav->store_16(channels); //CHANNELS: 2
f_wav->store_32(mix_rate);
/* useless stuff the format asks for */
int bits_per_sample = 32;
int blockalign = bits_per_sample / 8 * channels;
int bytes_per_sec = mix_rate * blockalign;
audio_block_size = (mix_rate / fps) * blockalign;
f_wav->store_32(bytes_per_sec);
f_wav->store_16(blockalign); // block align (unused)
f_wav->store_16(bits_per_sample);
/* DATA CHUNK */
f_wav->store_buffer((const uint8_t *)"data", 4);
f_wav->store_32(0); //data size... wooh
wav_data_size_pos = f_wav->get_position();
return OK;
}
Error MovieWriterPNGWAV::write_frame(const Ref<Image> &p_image, const int32_t *p_audio_data) {
ERR_FAIL_COND_V(!f_wav.is_valid(), ERR_UNCONFIGURED);
Vector<uint8_t> png_buffer = p_image->save_png_to_buffer();
Ref<FileAccess> fi = FileAccess::open(base_path + zeros_str(frame_count) + ".png", FileAccess::WRITE);
fi->store_buffer(png_buffer.ptr(), png_buffer.size());
f_wav->store_buffer((const uint8_t *)p_audio_data, audio_block_size);
frame_count++;
return OK;
}
void MovieWriterPNGWAV::write_end() {
if (f_wav.is_valid()) {
uint32_t total_size = 4 /* WAVE */ + 8 /* fmt+size */ + 16 /* format */ + 8 /* data+size */;
uint32_t datasize = f_wav->get_position() - wav_data_size_pos;
f_wav->seek(4);
f_wav->store_32(total_size + datasize);
f_wav->seek(0x28);
f_wav->store_32(datasize);
}
}
MovieWriterPNGWAV::MovieWriterPNGWAV() {
mix_rate = GLOBAL_GET("editor/movie_writer/mix_rate_hz");
speaker_mode = AudioServer::SpeakerMode(int(GLOBAL_GET("editor/movie_writer/speaker_mode")));
}

View File

@ -0,0 +1,123 @@
/*************************************************************************/
/* movie_writer.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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. */
/*************************************************************************/
#ifndef MOVIE_WRITER_H
#define MOVIE_WRITER_H
#include "core/templates/local_vector.h"
#include "servers/audio/audio_driver_dummy.h"
#include "servers/audio_server.h"
class MovieWriter : public Object {
GDCLASS(MovieWriter, Object);
uint64_t fps = 0;
uint64_t mix_rate = 0;
uint32_t audio_channels = 0;
LocalVector<int32_t> audio_mix_buffer;
enum {
MAX_WRITERS = 8
};
static MovieWriter *writers[];
static uint32_t writer_count;
protected:
virtual uint32_t get_audio_mix_rate() const;
virtual AudioServer::SpeakerMode get_audio_speaker_mode() const;
virtual Error write_begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path);
virtual Error write_frame(const Ref<Image> &p_image, const int32_t *p_audio_data);
virtual void write_end();
GDVIRTUAL0RC(uint32_t, _get_audio_mix_rate)
GDVIRTUAL0RC(AudioServer::SpeakerMode, _get_audio_speaker_mode)
GDVIRTUAL1RC(bool, _handles_file, const String &)
GDVIRTUAL0RC(Vector<String>, _get_supported_extensions)
GDVIRTUAL3R(Error, _write_begin, const Size2i &, uint32_t, const String &)
GDVIRTUAL2R(Error, _write_frame, const Ref<Image> &, GDNativeConstPtr<int32_t>)
GDVIRTUAL0(_write_end)
static void _bind_methods();
public:
virtual bool handles_file(const String &p_path) const;
virtual void get_supported_extensions(List<String> *r_extensions) const;
static void add_writer(MovieWriter *p_writer);
static MovieWriter *find_writer_for_file(const String &p_file);
void begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path);
void add_frame(const Ref<Image> &p_image);
static void set_extensions_hint();
void end();
};
class MovieWriterPNGWAV : public MovieWriter {
GDCLASS(MovieWriterPNGWAV, MovieWriter)
enum {
MAX_TRAILING_ZEROS = 8 // more than 10 days at 60fps, no hard drive can put up with this anyway :)
};
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_block_size = 0;
Ref<FileAccess> f_wav;
uint32_t wav_data_size_pos = 0;
String zeros_str(uint32_t p_index);
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:
MovieWriterPNGWAV();
};
#endif // VIDEO_WRITER_H

View File

@ -0,0 +1,263 @@
/*************************************************************************/
/* movie_writer_mjpeg.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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_mjpeg.h"
#include "core/config/project_settings.h"
uint32_t MovieWriterMJPEG::get_audio_mix_rate() const {
return mix_rate;
}
AudioServer::SpeakerMode MovieWriterMJPEG::get_audio_speaker_mode() const {
return speaker_mode;
}
bool MovieWriterMJPEG::handles_file(const String &p_path) const {
return p_path.get_extension().to_lower() == "avi";
}
void MovieWriterMJPEG::get_supported_extensions(List<String> *r_extensions) const {
r_extensions->push_back("avi");
}
Error MovieWriterMJPEG::write_begin(const Size2i &p_movie_size, uint32_t p_fps, const String &p_base_path) {
// Quick & Dirty MJPEG Code based on - https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference
base_path = p_base_path.get_basename();
if (base_path.is_relative_path()) {
base_path = "res://" + base_path;
}
base_path += ".avi";
f = FileAccess::open(base_path, FileAccess::WRITE_READ);
fps = p_fps;
ERR_FAIL_COND_V(f.is_null(), ERR_CANT_OPEN);
f->store_buffer((const uint8_t *)"RIFF", 4);
f->store_32(0); // Total length (update later)
f->store_buffer((const uint8_t *)"AVI ", 4);
f->store_buffer((const uint8_t *)"LIST", 4);
f->store_32(300); // 4 + 4 + 4 + 56 + 4 + 4 + 132 + 4 + 4 + 84
f->store_buffer((const uint8_t *)"hdrl", 4);
f->store_buffer((const uint8_t *)"avih", 4);
f->store_32(56);
f->store_32(1000000 / p_fps); // Microsecs per frame.
f->store_32(7000); // Max bytes per second
f->store_32(0); // Padding Granularity
f->store_32(16);
total_frames_ofs = f->get_position();
f->store_32(0); // Total frames (update later)
f->store_32(0); // Initial frames
f->store_32(1); // Streams
f->store_32(0); // Suggested buffer size
f->store_32(p_movie_size.width); // Movie Width
f->store_32(p_movie_size.height); // Movie Height
for (uint32_t i = 0; i < 4; i++) {
f->store_32(0); // Reserved.
}
f->store_buffer((const uint8_t *)"LIST", 4);
f->store_32(132); // 4 + 4 + 4 + 48 + 4 + 4 + 40 + 4 + 4 + 16
f->store_buffer((const uint8_t *)"strl", 4);
f->store_buffer((const uint8_t *)"strh", 4);
f->store_32(48);
f->store_buffer((const uint8_t *)"vids", 4);
f->store_buffer((const uint8_t *)"MJPG", 4);
f->store_32(0); // Flags
f->store_16(0); // Priority
f->store_16(0); // Language
f->store_32(0); // Initial Frames
f->store_32(1); // Scale
f->store_32(p_fps); // FPS
f->store_32(0); // Start
total_frames_ofs2 = f->get_position();
f->store_32(0); // Number of frames (to be updated later)
f->store_32(0); // Suggested Buffer Size
f->store_32(0); // Quality
f->store_32(0); // Sample Size
f->store_buffer((const uint8_t *)"strf", 4);
f->store_32(40); // Size.
f->store_32(40); // Size.
f->store_32(p_movie_size.width); // Width
f->store_32(p_movie_size.height); // Width
f->store_16(1); // Planes
f->store_16(24); // Bitcount
f->store_buffer((const uint8_t *)"MJPG", 4); // Compression
f->store_32(((p_movie_size.width * 24 / 8 + 3) & 0xFFFFFFFC) * p_movie_size.height); // SizeImage
f->store_32(0); // XPelsXMeter
f->store_32(0); // YPelsXMeter
f->store_32(0); // ClrUsed
f->store_32(0); // ClrImportant
f->store_buffer((const uint8_t *)"LIST", 4);
f->store_32(16);
f->store_buffer((const uint8_t *)"odml", 4);
f->store_buffer((const uint8_t *)"dmlh", 4);
f->store_32(4); // sizes
total_frames_ofs3 = f->get_position();
f->store_32(0); // Number of frames (to be updated later)
// Audio //
const uint32_t bit_depth = 32;
uint32_t channels = 2;
switch (speaker_mode) {
case AudioServer::SPEAKER_MODE_STEREO:
channels = 2;
break;
case AudioServer::SPEAKER_SURROUND_31:
channels = 4;
break;
case AudioServer::SPEAKER_SURROUND_51:
channels = 6;
break;
case AudioServer::SPEAKER_SURROUND_71:
channels = 8;
break;
}
uint32_t blockalign = bit_depth / 8 * channels;
f->store_buffer((const uint8_t *)"LIST", 4);
f->store_32(84); // 4 + 4 + 4 + 48 + 4 + 4 + 16
f->store_buffer((const uint8_t *)"strl", 4);
f->store_buffer((const uint8_t *)"strh", 4);
f->store_32(48);
f->store_buffer((const uint8_t *)"auds", 4);
f->store_32(0); // Handler
f->store_32(0); // Flags
f->store_16(0); // Priority
f->store_16(0); // Language
f->store_32(0); // Initial Frames
f->store_32(blockalign); // Scale
f->store_32(mix_rate * blockalign); // mix rate
f->store_32(0); // Start
total_audio_frames_ofs4 = f->get_position();
f->store_32(0); // Number of frames (to be updated later)
f->store_32(12288); // Suggested Buffer Size
f->store_32(0xFFFFFFFF); // Quality
f->store_32(blockalign); // Block Align to 32 bits
audio_block_size = (mix_rate / fps) * blockalign;
f->store_buffer((const uint8_t *)"strf", 4);
f->store_32(16); // Standard format, no extra fields
f->store_16(1); // Compression code, standard PCM
f->store_16(channels);
f->store_32(mix_rate); // Samples (frames) / Sec
f->store_32(mix_rate * blockalign); // Bytes / sec
f->store_16(blockalign); // Bytes / sec
f->store_16(bit_depth); // Bytes / sec
f->store_buffer((const uint8_t *)"LIST", 4);
movi_data_ofs = f->get_position();
f->store_32(0); // Number of frames (to be updated later)
f->store_buffer((const uint8_t *)"movi", 4);
return OK;
}
Error MovieWriterMJPEG::write_frame(const Ref<Image> &p_image, const int32_t *p_audio_data) {
ERR_FAIL_COND_V(!f.is_valid(), ERR_UNCONFIGURED);
Vector<uint8_t> jpg_buffer = p_image->save_jpg_to_buffer(quality);
uint32_t s = jpg_buffer.size();
f->store_buffer((const uint8_t *)"00db", 4); // Stream 0, Video
f->store_32(jpg_buffer.size()); // sizes
f->store_buffer(jpg_buffer.ptr(), jpg_buffer.size());
if (jpg_buffer.size() & 1) {
f->store_8(0);
s++;
}
jpg_frame_sizes.push_back(s);
f->store_buffer((const uint8_t *)"01wb", 4); // Stream 1, Audio.
f->store_32(audio_block_size);
f->store_buffer((const uint8_t *)p_audio_data, audio_block_size);
frame_count++;
return OK;
}
void MovieWriterMJPEG::write_end() {
if (f.is_valid()) {
// Finalize the file (frame indices)
f->store_buffer((const uint8_t *)"idx1", 4);
f->store_32(8 * 4 * frame_count);
uint32_t ofs = 4;
uint32_t all_data_size = 0;
for (uint32_t i = 0; i < frame_count; i++) {
f->store_buffer((const uint8_t *)"00db", 4);
f->store_32(16); // AVI_KEYFRAME
f->store_32(ofs);
f->store_32(jpg_frame_sizes[i]);
ofs += jpg_frame_sizes[i] + 8;
f->store_buffer((const uint8_t *)"01wb", 4);
f->store_32(16); // AVI_KEYFRAME
f->store_32(ofs);
f->store_32(audio_block_size);
ofs += audio_block_size + 8;
all_data_size += jpg_frame_sizes[i] + audio_block_size;
}
uint32_t file_size = f->get_position();
f->seek(4);
f->store_32(file_size - 78);
f->seek(total_frames_ofs);
f->store_32(frame_count);
f->seek(total_frames_ofs2);
f->store_32(frame_count);
f->seek(total_frames_ofs3);
f->store_32(frame_count);
f->seek(total_audio_frames_ofs4);
f->store_32(frame_count * mix_rate / fps);
f->seek(movi_data_ofs);
f->store_32(all_data_size + 4 + 16 * frame_count);
f.unref();
}
}
MovieWriterMJPEG::MovieWriterMJPEG() {
mix_rate = GLOBAL_GET("editor/movie_writer/mix_rate_hz");
speaker_mode = AudioServer::SpeakerMode(int(GLOBAL_GET("editor/movie_writer/speaker_mode")));
quality = GLOBAL_GET("editor/movie_writer/mjpeg_quality");
}

View File

@ -0,0 +1,73 @@
/*************************************************************************/
/* movie_writer_mjpeg.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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. */
/*************************************************************************/
#ifndef MOVIE_WRITER_MJPEG_H
#define MOVIE_WRITER_MJPEG_H
#include "servers/movie_writer/movie_writer.h"
class MovieWriterMJPEG : public MovieWriter {
GDCLASS(MovieWriterMJPEG, 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;
float quality = 0.75;
uint32_t audio_block_size = 0;
Vector<uint32_t> jpg_frame_sizes;
uint64_t total_frames_ofs = 0;
uint64_t total_frames_ofs2 = 0;
uint64_t total_frames_ofs3 = 0;
uint64_t total_audio_frames_ofs4 = 0;
uint64_t movi_data_ofs = 0;
Ref<FileAccess> f;
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:
MovieWriterMJPEG();
};
#endif // MOVIE_WRITER_AVIJPEG_H

View File

@ -57,6 +57,8 @@
#include "camera_server.h"
#include "debugger/servers_debugger.h"
#include "display_server.h"
#include "movie_writer/movie_writer.h"
#include "movie_writer/movie_writer_mjpeg.h"
#include "navigation_server_2d.h"
#include "navigation_server_3d.h"
#include "physics_2d/godot_physics_server_2d.h"
@ -107,6 +109,9 @@ static bool has_server_feature_callback(const String &p_feature) {
return false;
}
static MovieWriterMJPEG *writer_mjpeg = nullptr;
static MovieWriterPNGWAV *writer_pngwav = nullptr;
void register_server_types() {
shader_types = memnew(ShaderTypes);
@ -239,6 +244,8 @@ void register_server_types() {
GDREGISTER_CLASS(PhysicsTestMotionParameters3D);
GDREGISTER_CLASS(PhysicsTestMotionResult3D);
GDREGISTER_VIRTUAL_CLASS(MovieWriter);
ServersDebugger::initialize();
// Physics 2D
@ -254,11 +261,19 @@ void register_server_types() {
PhysicsServer3DManager::register_server("GodotPhysics3D", &_createGodotPhysics3DCallback);
PhysicsServer3DManager::set_default_server("GodotPhysics3D");
writer_mjpeg = memnew(MovieWriterMJPEG);
MovieWriter::add_writer(writer_mjpeg);
writer_pngwav = memnew(MovieWriterPNGWAV);
MovieWriter::add_writer(writer_pngwav);
}
void unregister_server_types() {
ServersDebugger::deinitialize();
memdelete(shader_types);
memdelete(writer_mjpeg);
memdelete(writer_pngwav);
}
void register_server_singletons() {

View File

@ -1193,6 +1193,20 @@ void RendererViewport::viewport_set_sdf_oversize_and_scale(RID p_viewport, RS::V
RSG::texture_storage->render_target_set_sdf_size_and_scale(viewport->render_target, p_size, p_scale);
}
RID RendererViewport::viewport_find_from_screen_attachment(DisplayServer::WindowID p_id) const {
RID *rids = nullptr;
uint32_t rid_count = viewport_owner.get_rid_count();
rids = (RID *)alloca(sizeof(RID *) * rid_count);
viewport_owner.fill_owned_buffer(rids);
for (uint32_t i = 0; i < rid_count; i++) {
Viewport *viewport = viewport_owner.get_or_null(rids[i]);
if (viewport->viewport_to_screen == p_id) {
return rids[i];
}
}
return RID();
}
bool RendererViewport::free(RID p_rid) {
if (viewport_owner.owns(p_rid)) {
Viewport *viewport = viewport_owner.get_or_null(p_rid);

View File

@ -282,6 +282,8 @@ public:
void viewport_set_sdf_oversize_and_scale(RID p_viewport, RS::ViewportSDFOversize p_over_size, RS::ViewportSDFScale p_scale);
virtual RID viewport_find_from_screen_attachment(DisplayServer::WindowID p_id = DisplayServer::MAIN_WINDOW_ID) const;
void handle_timestamp(String p_timestamp, uint64_t p_cpu_time, uint64_t p_gpu_time);
void set_default_clear_color(const Color &p_color);

View File

@ -629,6 +629,7 @@ public:
FUNC2(viewport_set_measure_render_time, RID, bool)
FUNC1RC(double, viewport_get_measured_render_time_cpu, RID)
FUNC1RC(double, viewport_get_measured_render_time_gpu, RID)
FUNC1RC(RID, viewport_find_from_screen_attachment, DisplayServer::WindowID)
FUNC2(call_set_vsync_mode, DisplayServer::VSyncMode, DisplayServer::WindowID)

View File

@ -944,6 +944,8 @@ public:
virtual double viewport_get_measured_render_time_cpu(RID p_viewport) const = 0;
virtual double viewport_get_measured_render_time_gpu(RID p_viewport) const = 0;
virtual RID viewport_find_from_screen_attachment(DisplayServer::WindowID p_id = DisplayServer::MAIN_WINDOW_ID) const = 0;
/* SKY API */
enum SkyMode {

View File

@ -254,6 +254,7 @@ Files generated from upstream source:
Files extracted from upstream source:
- `jpgd*.{c,h}`
- `jpge*.{c,h}`
## libogg

1076
thirdparty/jpeg-compressor/jpge.cpp vendored Normal file

File diff suppressed because it is too large Load Diff

174
thirdparty/jpeg-compressor/jpge.h vendored Normal file
View File

@ -0,0 +1,174 @@
// jpge.h - C++ class for JPEG compression.
// Public Domain or Apache 2.0, Richard Geldreich <richgel99@gmail.com>
// Alex Evans: Added RGBA support, linear memory allocator.
#ifndef JPEG_ENCODER_H
#define JPEG_ENCODER_H
namespace jpge
{
typedef unsigned char uint8;
typedef signed short int16;
typedef signed int int32;
typedef unsigned short uint16;
typedef unsigned int uint32;
typedef unsigned int uint;
// JPEG chroma subsampling factors. Y_ONLY (grayscale images) and H2V2 (color images) are the most common.
enum subsampling_t { Y_ONLY = 0, H1V1 = 1, H2V1 = 2, H2V2 = 3 };
// JPEG compression parameters structure.
struct params
{
inline params() : m_quality(85), m_subsampling(H2V2), m_no_chroma_discrim_flag(false), m_two_pass_flag(false), m_use_std_tables(false) { }
inline bool check() const
{
if ((m_quality < 1) || (m_quality > 100)) return false;
if ((uint)m_subsampling > (uint)H2V2) return false;
return true;
}
// Quality: 1-100, higher is better. Typical values are around 50-95.
int m_quality;
// m_subsampling:
// 0 = Y (grayscale) only
// 1 = YCbCr, no subsampling (H1V1, YCbCr 1x1x1, 3 blocks per MCU)
// 2 = YCbCr, H2V1 subsampling (YCbCr 2x1x1, 4 blocks per MCU)
// 3 = YCbCr, H2V2 subsampling (YCbCr 4x1x1, 6 blocks per MCU-- very common)
subsampling_t m_subsampling;
// Disables CbCr discrimination - only intended for testing.
// If true, the Y quantization table is also used for the CbCr channels.
bool m_no_chroma_discrim_flag;
bool m_two_pass_flag;
// By default we use the same quantization tables as mozjpeg's default.
// Set to true to use the traditional tables from JPEG Annex K.
bool m_use_std_tables;
};
// Writes JPEG image to a file.
// num_channels must be 1 (Y) or 3 (RGB), image pitch must be width*num_channels.
bool compress_image_to_jpeg_file(const char* pFilename, int width, int height, int num_channels, const uint8* pImage_data, const params& comp_params = params());
// Writes JPEG image to memory buffer.
// On entry, buf_size is the size of the output buffer pointed at by pBuf, which should be at least ~1024 bytes.
// If return value is true, buf_size will be set to the size of the compressed data.
bool compress_image_to_jpeg_file_in_memory(void* pBuf, int& buf_size, int width, int height, int num_channels, const uint8* pImage_data, const params& comp_params = params());
// Output stream abstract class - used by the jpeg_encoder class to write to the output stream.
// put_buf() is generally called with len==JPGE_OUT_BUF_SIZE bytes, but for headers it'll be called with smaller amounts.
class output_stream
{
public:
virtual ~output_stream() { };
virtual bool put_buf(const void* Pbuf, int len) = 0;
template<class T> inline bool put_obj(const T& obj) { return put_buf(&obj, sizeof(T)); }
};
// Lower level jpeg_encoder class - useful if more control is needed than the above helper functions.
class jpeg_encoder
{
public:
jpeg_encoder();
~jpeg_encoder();
// Initializes the compressor.
// pStream: The stream object to use for writing compressed data.
// params - Compression parameters structure, defined above.
// width, height - Image dimensions.
// channels - May be 1, or 3. 1 indicates grayscale, 3 indicates RGB source data.
// Returns false on out of memory or if a stream write fails.
bool init(output_stream* pStream, int width, int height, int src_channels, const params& comp_params = params());
const params& get_params() const { return m_params; }
// Deinitializes the compressor, freeing any allocated memory. May be called at any time.
void deinit();
uint get_total_passes() const { return m_params.m_two_pass_flag ? 2 : 1; }
inline uint get_cur_pass() { return m_pass_num; }
// Call this method with each source scanline.
// width * src_channels bytes per scanline is expected (RGB or Y format).
// You must call with NULL after all scanlines are processed to finish compression.
// Returns false on out of memory or if a stream write fails.
bool process_scanline(const void* pScanline);
private:
jpeg_encoder(const jpeg_encoder&);
jpeg_encoder& operator =(const jpeg_encoder&);
typedef int32 sample_array_t;
output_stream* m_pStream;
params m_params;
uint8 m_num_components;
uint8 m_comp_h_samp[3], m_comp_v_samp[3];
int m_image_x, m_image_y, m_image_bpp, m_image_bpl;
int m_image_x_mcu, m_image_y_mcu;
int m_image_bpl_xlt, m_image_bpl_mcu;
int m_mcus_per_row;
int m_mcu_x, m_mcu_y;
uint8* m_mcu_lines[16];
uint8 m_mcu_y_ofs;
sample_array_t m_sample_array[64];
int16 m_coefficient_array[64];
int32 m_quantization_tables[2][64];
uint m_huff_codes[4][256];
uint8 m_huff_code_sizes[4][256];
uint8 m_huff_bits[4][17];
uint8 m_huff_val[4][256];
uint32 m_huff_count[4][256];
int m_last_dc_val[3];
enum { JPGE_OUT_BUF_SIZE = 2048 };
uint8 m_out_buf[JPGE_OUT_BUF_SIZE];
uint8* m_pOut_buf;
uint m_out_buf_left;
uint32 m_bit_buffer;
uint m_bits_in;
uint8 m_pass_num;
bool m_all_stream_writes_succeeded;
void optimize_huffman_table(int table_num, int table_len);
void emit_byte(uint8 i);
void emit_word(uint i);
void emit_marker(int marker);
void emit_jfif_app0();
void emit_dqt();
void emit_sof();
void emit_dht(uint8* bits, uint8* val, int index, bool ac_flag);
void emit_dhts();
void emit_sos();
void emit_markers();
void compute_huffman_table(uint* codes, uint8* code_sizes, uint8* bits, uint8* val);
void compute_quant_table(int32* dst, int16* src);
void adjust_quant_table(int32* dst, int32* src);
void first_pass_init();
bool second_pass_init();
bool jpg_open(int p_x_res, int p_y_res, int src_channels);
void load_block_8_8_grey(int x);
void load_block_8_8(int x, int y, int c);
void load_block_16_8(int x, int c);
void load_block_16_8_8(int x, int c);
void load_quantized_coefficients(int component_num);
void flush_output_buffer();
void put_bits(uint bits, uint len);
void code_coefficients_pass_one(int component_num);
void code_coefficients_pass_two(int component_num);
void code_block(int component_num);
void process_mcu_row();
bool terminate_pass_one();
bool terminate_pass_two();
bool process_end_of_image();
void load_mcu(const void* src);
void clear();
void init();
};
} // namespace jpge
#endif // JPEG_ENCODER