From 9a58a6a5ff3f5a578e4becf44a5ecb3829f7fc98 Mon Sep 17 00:00:00 2001 From: Michael Fabian 'Xaymar' Dirks Date: Sat, 6 Jul 2019 13:08:23 +0200 Subject: [PATCH] encoders/generic: Implement generic encoder for all ffmpeg encoders --- CMakeLists.txt | 4 +- data/locale/en-US.ini | 49 ++- source/encoders/generic.cpp | 742 ++++++++++++++++++++++++++++++++++++ source/encoders/generic.hpp | 95 +++++ source/plugin.cpp | 19 + 5 files changed, 898 insertions(+), 11 deletions(-) create mode 100644 source/encoders/generic.cpp create mode 100644 source/encoders/generic.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ae64a9d..56fa705 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,7 +52,7 @@ endif() # Define Project project( - xmr-ffmpeg-encoders + obs-ffmpeg-encoder VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.${VERSION_TWEAK} ) set(PROJECT_FULL_NAME "FFMPEG Encoder for OBS Studio") @@ -268,6 +268,8 @@ set(PROJECT_PRIVATE "${PROJECT_SOURCE_DIR}/source/plugin.hpp" "${PROJECT_SOURCE_DIR}/source/utility.cpp" "${PROJECT_SOURCE_DIR}/source/utility.hpp" + "${PROJECT_SOURCE_DIR}/source/encoders/generic.hpp" + "${PROJECT_SOURCE_DIR}/source/encoders/generic.cpp" "${PROJECT_SOURCE_DIR}/source/encoders/prores_aw.hpp" "${PROJECT_SOURCE_DIR}/source/encoders/prores_aw.cpp" "${PROJECT_SOURCE_DIR}/source/ffmpeg/avframe-queue.cpp" diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index e583f9c..7900104 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -1,15 +1,44 @@ -Bitrate="Bitrate" -Bitrate.Description="Bitrate in kbit/s" -KeyFrame.Type="Key Frame Type" -KeyFrame.Type.Frames="Frames" -KeyFrame.Type.Seconds="Seconds" -KeyFrame.Interval="Key Frame Interval" -KeyFrame.Interval.Description="Interval in which a Key Frame is placed." -CustomParameters="Custom Parameters" -CustomParameters.Description="Format: key1=val1 key2=val2 key3='val3 val4'" - # ProRes ProRes.Profile.Proxy="Proxy (PXY)" ProRes.Profile.Light="Light (LT)" ProRes.Profile.Standard="Standard" ProRes.Profile.HighQuality="High Quality (HQ)" + +# Rate Control +RateControl="Rate Control" +RateControl.Bitrate="Bitrate" +RateControl.Bitrate.Description="Bitrate in kbit/s. Not supported by all encoders." +RateControl.Profile="Profile" +RateControl.Profile.None="None" +RateControl.KeyFrame="Key-Frames / Group of Pictures (GOP)" +RateControl.KeyFrame.Type="Key-Frame Interval Type" +RateControl.KeyFrame.Type.Description="Defines how the interval value should be read." +RateControl.KeyFrame.Type.Frames="Frames" +RateControl.KeyFrame.Type.Seconds="Seconds" +RateControl.KeyFrame.Interval="Key Frame Interval" +RateControl.KeyFrame.Interval.Description="Interval in which a Key Frame is placed. Not all encoders support this." + +# Threading +MultiThreading="Multi-Threading" +MultiThreading.Model="Threading Model" +MultiThreading.Model.Description="Some encoders offer multi threading capabilities which can take advantage of multi-core systems." +MultiThreading.Model.Automatic="Automatic" +MultiThreading.Model.None="Single Threaded" +MultiThreading.Model.Frame="Frame-Threading" +MultiThreading.Model.Slice="Slice-Threading" +MultiThreading.ThreadCount="Number of Threads" +MultiThreading.ThreadCount.Description="The number of threads to use, with 0 being automatic.\nEncoders with 'Automatic-Threading' in the name control threads themselves and will behave different with the 0 value." +MultiThreading.FrameQueue="Enable Frame Queue" +MultiThreading.FrameQueue.Description="Some encoders require us to keep a frame in memory while it is being processed by the encoder.\nThis option fixes corruption due to this, but adds significant latency and memory usage." + +# FFmpeg +FFmpeg="FFmpeg Options" +FFmpeg.CustomSettings="Custom Settings" +FFmpeg.CustomSettings.Description="Custom settings that override any detected options above, use with caution.\nThe input should be in the format 'key=value;key=value;...'." +FFmpeg.StandardCompliance="Standard Compliance" +FFmpeg.StandardCompliance.Description="How strict should the encoder keep to the standard? A strictness below 'Normal' may cause issues with playback." +FFmpeg.StandardCompliance.VeryStrict="Very Strict" +FFmpeg.StandardCompliance.Strict="Strict" +FFmpeg.StandardCompliance.Normal="Normal" +FFmpeg.StandardCompliance.Unofficial="Unofficial" +FFmpeg.StandardCompliance.Experimental="Experimental" diff --git a/source/encoders/generic.cpp b/source/encoders/generic.cpp new file mode 100644 index 0000000..bd813fa --- /dev/null +++ b/source/encoders/generic.cpp @@ -0,0 +1,742 @@ +// FFMPEG Video Encoder Integration for OBS Studio +// Copyright (C) 2018 - 2019 Michael Fabian Dirks +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +#include "generic.hpp" +#include +#include +#include +#include "ffmpeg/tools.hpp" +#include "plugin.hpp" +#include "utility.hpp" + +extern "C" { +#include +#pragma warning(push) +#pragma warning(disable : 4244) +#include "libavutil/dict.h" +#include "libavutil/frame.h" +#include "libavutil/opt.h" +#pragma warning(pop) +} + +// Rate Control +#define P_RATECONTROL "RateControl" +#define P_RATECONTROL_PROFILE "RateControl.Profile" +#define P_RATECONTROL_BITRATE "RateControl.Bitrate" +#define P_RATECONTROL_MAXBITRATE "RateControl.MaxBitrate" +#define P_RATECONTROL_VBVBUFFER "RateControl.VBVBuffer" +#define P_RATECONTROL_KEYFRAME "RateControl.KeyFrame" +#define P_RATECONTROL_KEYFRAME_TYPE "RateControl.KeyFrame.Type" +#define P_RATECONTROL_KEYFRAME_INTERVAL "RateControl.KeyFrame.Interval" + +// Threading +#define P_MULTITHREADING "MultiThreading" +#define P_MULTITHREADING_MODEL "MultiThreading.Model" +#define P_MULTITHREADING_THREADCOUNT "MultiThreading.ThreadCount" +#define P_MULTITHREADING_FRAMEQUEUE "MultiThreading.FrameQueue" + +// FFmpeg +#define P_FFMPEG "FFmpeg" +#define P_FFMPEG_CUSTOMSETTINGS "FFmpeg.CustomSettings" +#define P_FFMPEG_STANDARDCOMPLIANCE "FFmpeg.StandardCompliance" + +enum class keyframe_type { Seconds, Frames }; + +encoder::generic_factory::generic_factory(AVCodec* codec) : avcodec_ptr(codec), info() {} + +encoder::generic_factory::~generic_factory() {} + +void encoder::generic_factory::register_encoder() +{ + // Generate unique name from given Id + { + std::stringstream sstr; + sstr << "ffmpeg-" << avcodec_ptr->name << "-0x" << std::uppercase << std::setfill('0') << std::setw(8) + << std::hex << avcodec_ptr->capabilities; + this->info.uid = sstr.str(); + } + + // Also generate a human readable name while we're at it. + // TODO: Figure out a way to translate from names to other names. + { + std::stringstream sstr; + sstr << "[FFmpeg] " << (avcodec_ptr->long_name ? avcodec_ptr->long_name : avcodec_ptr->name) << " (" + << avcodec_ptr->name << ")"; + std::string caps = ffmpeg::tools::translate_encoder_capabilities(avcodec_ptr->capabilities); + if (caps.length() != 0) { + sstr << " (" << caps << ")"; + } + this->info.readable_name = sstr.str(); + } + + // Assign codec (ffmpeg name). + this->info.codec = avcodec_ptr->name; + this->info.oei.id = this->info.uid.c_str(); + this->info.oei.codec = this->info.codec.c_str(); + + // Detect encoder type (only Video and Audio supported) + if (avcodec_ptr->type == AVMediaType::AVMEDIA_TYPE_VIDEO) { + this->info.oei.type = obs_encoder_type::OBS_ENCODER_VIDEO; + } else if (avcodec_ptr->type == AVMediaType::AVMEDIA_TYPE_AUDIO) { + this->info.oei.type = obs_encoder_type::OBS_ENCODER_AUDIO; + } else { + throw std::invalid_argument("unsupported codec type"); + } + + // Register functions. + this->info.oei.create = [](obs_data_t* settings, obs_encoder_t* encoder) { + try { + return reinterpret_cast(new generic(settings, encoder)); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.destroy = [](void* ptr) { + try { + delete reinterpret_cast(ptr); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.get_name = [](void* type_data) { + try { + return reinterpret_cast(type_data)->get_name(); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.get_defaults2 = [](obs_data_t* settings, void* type_data) { + try { + reinterpret_cast(type_data)->get_defaults(settings); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.get_properties2 = [](void* ptr, void* type_data) { + try { + obs_properties_t* props = obs_properties_create(); + if (type_data != nullptr) { + reinterpret_cast(type_data)->get_properties(props); + } + if (ptr != nullptr) { + reinterpret_cast(ptr)->get_properties(props); + } + return props; + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.update = [](void* ptr, obs_data_t* settings) { + try { + return reinterpret_cast(ptr)->update(settings); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.encode = [](void* ptr, struct encoder_frame* frame, struct encoder_packet* packet, + bool* received_packet) { + try { + return reinterpret_cast(ptr)->encode(frame, packet, received_packet); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.get_audio_info = [](void* ptr, struct audio_convert_info* info) { + try { + reinterpret_cast(ptr)->get_audio_info(info); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.get_frame_size = [](void* ptr) { + try { + return reinterpret_cast(ptr)->get_frame_size(); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.get_video_info = [](void* ptr, struct video_scale_info* info) { + try { + reinterpret_cast(ptr)->get_video_info(info); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.get_sei_data = [](void* ptr, uint8_t** sei_data, size_t* size) { + try { + return reinterpret_cast(ptr)->get_sei_data(sei_data, size); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.get_extra_data = [](void* ptr, uint8_t** extra_data, size_t* size) { + try { + return reinterpret_cast(ptr)->get_extra_data(extra_data, size); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + this->info.oei.encode_texture = [](void* ptr, uint32_t handle, int64_t pts, uint64_t lock_key, + uint64_t* next_key, struct encoder_packet* packet, bool* received_packet) { + try { + return reinterpret_cast(ptr)->encode_texture(handle, pts, lock_key, next_key, packet, + received_packet); + } catch (std::exception const& e) { + PLOG_ERROR("exception: %s", e.what()); + throw e; + } catch (...) { + PLOG_ERROR("unknown exception"); + throw; + } + }; + + // Finally store ourself as type data. + this->info.oei.type_data = this; + + obs_register_encoder(&this->info.oei); + PLOG_INFO("Registered encoder #%llX with name '%s' and long name '%s' and caps %llX", avcodec_ptr, + avcodec_ptr->name, avcodec_ptr->long_name, avcodec_ptr->capabilities); +} + +const char* encoder::generic_factory::get_name() +{ + return this->info.readable_name.c_str(); +} + +void encoder::generic_factory::get_defaults(obs_data_t* settings) +{ + AVCodecContext* ctx = avcodec_alloc_context3(this->avcodec_ptr); + if (ctx->priv_data) { + const AVOption* opt = nullptr; + while ((opt = av_opt_next(ctx->priv_data, opt)) != nullptr) { + if (opt->type == AV_OPT_TYPE_CONST && opt->unit != nullptr) { + continue; + } + + switch (opt->type) { + case AV_OPT_TYPE_BOOL: + obs_data_set_default_bool(settings, opt->name, !!opt->default_val.i64); + break; + case AV_OPT_TYPE_INT: + case AV_OPT_TYPE_INT64: + case AV_OPT_TYPE_UINT64: + obs_data_set_default_int(settings, opt->name, opt->default_val.i64); + break; + case AV_OPT_TYPE_FLOAT: + case AV_OPT_TYPE_DOUBLE: + obs_data_set_default_double(settings, opt->name, opt->default_val.dbl); + break; + case AV_OPT_TYPE_STRING: + obs_data_set_default_string(settings, opt->name, opt->default_val.str); + break; + } + } + } + + { // Integrated Options + // Rate Control + obs_data_set_default_int(settings, P_RATECONTROL_PROFILE, 0); + obs_data_set_default_int(settings, P_RATECONTROL_BITRATE, 2500); + obs_data_set_default_int(settings, P_RATECONTROL_KEYFRAME_TYPE, 0); + obs_data_set_default_double(settings, P_RATECONTROL_KEYFRAME_INTERVAL ".Seconds", 2.0); + obs_data_set_default_int(settings, P_RATECONTROL_KEYFRAME_INTERVAL ".Frames", 300); + + // Threading + obs_data_set_default_int(settings, P_MULTITHREADING_MODEL, -1); + obs_data_set_default_bool(settings, P_MULTITHREADING_FRAMEQUEUE, true); + obs_data_set_default_int(settings, P_MULTITHREADING_THREADCOUNT, 0); + + // FFmpeg + obs_data_set_default_string(settings, P_FFMPEG_CUSTOMSETTINGS, ""); + obs_data_set_default_int(settings, P_FFMPEG_STANDARDCOMPLIANCE, FF_COMPLIANCE_STRICT); + } +} + +void encoder::generic_factory::get_properties(obs_properties_t* props) +{ + // Encoder Options + AVCodecContext* ctx = avcodec_alloc_context3(this->avcodec_ptr); + if (ctx->priv_data) { + std::map> unit_property_map; + + const AVOption* opt = nullptr; + while ((opt = av_opt_next(ctx->priv_data, opt)) != nullptr) { + obs_property_t* p = nullptr; + + // Constants are parts of a unit, not actual options. + if (opt->type == AV_OPT_TYPE_CONST) { + auto unit = unit_property_map.find(opt->unit); + if (unit == unit_property_map.end()) { + continue; + } + + switch (unit->second.second->type) { + case AV_OPT_TYPE_INT: + case AV_OPT_TYPE_INT64: + obs_property_list_add_int(unit->second.first, opt->name, opt->default_val.i64); + break; + case AV_OPT_TYPE_FLOAT: + case AV_OPT_TYPE_DOUBLE: + obs_property_list_add_float(unit->second.first, opt->name, + opt->default_val.dbl); + break; + case AV_OPT_TYPE_STRING: + obs_property_list_add_string(unit->second.first, opt->name, + opt->default_val.str); + break; + default: + throw std::runtime_error("unhandled const type"); + } + + continue; + } + + switch (opt->type) { + case AV_OPT_TYPE_BOOL: + p = obs_properties_add_bool(props, opt->name, opt->name); + break; + case AV_OPT_TYPE_INT: + case AV_OPT_TYPE_INT64: + case AV_OPT_TYPE_UINT64: + if (opt->unit != nullptr) { + p = obs_properties_add_list(props, opt->name, opt->name, OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + unit_property_map.emplace(opt->unit, + std::pair{p, opt}); + } else { + p = obs_properties_add_int(props, opt->name, opt->name, opt->min, opt->max, 1); + } + break; + case AV_OPT_TYPE_FLOAT: + case AV_OPT_TYPE_DOUBLE: + if (opt->unit != nullptr) { + p = obs_properties_add_list(props, opt->name, opt->name, OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_FLOAT); + unit_property_map.emplace(opt->unit, + std::pair{p, opt}); + } else { + p = obs_properties_add_float(props, opt->name, opt->name, opt->min, opt->max, + 0.01); + } + break; + case AV_OPT_TYPE_STRING: + if (opt->unit != nullptr) { + p = obs_properties_add_list(props, opt->name, opt->name, OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); + unit_property_map.emplace(opt->unit, + std::pair{p, opt}); + } else { + p = obs_properties_add_text(props, opt->name, opt->name, + obs_text_type::OBS_TEXT_DEFAULT); + } + break; + case AV_OPT_TYPE_FLAGS: + case AV_OPT_TYPE_RATIONAL: + case AV_OPT_TYPE_VIDEO_RATE: + case AV_OPT_TYPE_BINARY: + case AV_OPT_TYPE_DICT: + case AV_OPT_TYPE_IMAGE_SIZE: + case AV_OPT_TYPE_PIXEL_FMT: + case AV_OPT_TYPE_SAMPLE_FMT: + case AV_OPT_TYPE_DURATION: + case AV_OPT_TYPE_COLOR: + case AV_OPT_TYPE_CHANNEL_LAYOUT: + PLOG_WARNING("Skipped option '%s' for codec '%s' as option type is not supported.", + opt->name, this->info.uid); + break; + } + + if ((opt->flags & AV_OPT_FLAG_READONLY) && (p != nullptr)) { + obs_property_set_enabled(p, false); + } + } + } + avcodec_free_context(&ctx); + + // FFmpeg Options + { /// Rate Control + auto prs = obs_properties_create(); + { + auto grp = obs_properties_create(); + auto p1 = obs_properties_add_list(grp, P_RATECONTROL_KEYFRAME_TYPE, + TRANSLATE(P_RATECONTROL_KEYFRAME_TYPE), OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + obs_property_set_long_description(p1, TRANSLATE(DESC(P_RATECONTROL_KEYFRAME_TYPE))); + obs_property_set_modified_callback2(p1, modified_ratecontrol_properties, this); + obs_property_list_add_int(p1, TRANSLATE(P_RATECONTROL_KEYFRAME_TYPE ".Seconds"), + static_cast(keyframe_type::Seconds)); + obs_property_list_add_int(p1, TRANSLATE(P_RATECONTROL_KEYFRAME_TYPE ".Frames"), + static_cast(keyframe_type::Frames)); + + auto p2 = obs_properties_add_float(grp, P_RATECONTROL_KEYFRAME_INTERVAL ".Seconds", + TRANSLATE(P_RATECONTROL_KEYFRAME_INTERVAL), 0.0, + std::numeric_limits::max(), 0.01); + obs_property_set_long_description(p2, TRANSLATE(DESC(P_RATECONTROL_KEYFRAME_INTERVAL))); + + auto p3 = obs_properties_add_int(grp, P_RATECONTROL_KEYFRAME_INTERVAL ".Frames", + TRANSLATE(P_RATECONTROL_KEYFRAME_INTERVAL), 0, + std::numeric_limits::max(), 1); + obs_property_set_long_description(p3, TRANSLATE(DESC(P_RATECONTROL_KEYFRAME_INTERVAL))); + obs_properties_add_group(prs, P_RATECONTROL_KEYFRAME, TRANSLATE(P_RATECONTROL_KEYFRAME), + OBS_GROUP_NORMAL, grp); + } + { + auto p = obs_properties_add_int(prs, P_RATECONTROL_BITRATE, TRANSLATE(P_RATECONTROL_BITRATE), 1, + std::numeric_limits::max(), 1); + obs_property_set_long_description(p, TRANSLATE(DESC(P_RATECONTROL_BITRATE))); + } + { + auto p = obs_properties_add_list(prs, P_RATECONTROL_PROFILE, TRANSLATE(P_RATECONTROL_PROFILE), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + if (this->avcodec_ptr->profiles) { + auto profile = this->avcodec_ptr->profiles; + obs_property_list_add_int(p, TRANSLATE(P_RATECONTROL_PROFILE ".None"), + FF_PROFILE_UNKNOWN); + while (profile->profile != FF_PROFILE_UNKNOWN) { + obs_property_list_add_int(p, profile->name, profile->profile); + profile++; + } + } + } + obs_properties_add_group(props, P_RATECONTROL, TRANSLATE(P_RATECONTROL), OBS_GROUP_NORMAL, prs); + }; + { /// Threading + auto prs = obs_properties_create(); + { + auto p = obs_properties_add_list(prs, P_MULTITHREADING_MODEL, TRANSLATE(P_MULTITHREADING_MODEL), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_long_description(p, TRANSLATE(DESC(P_MULTITHREADING_MODEL))); + obs_property_set_modified_callback2(p, modified_threading_properties, this); + obs_property_list_add_int(p, TRANSLATE(P_MULTITHREADING_MODEL ".Automatic"), -1); + obs_property_list_add_int(p, TRANSLATE(P_MULTITHREADING_MODEL ".None"), 0); + { + auto idx = obs_property_list_add_int(p, TRANSLATE(P_MULTITHREADING_MODEL ".Frame"), + FF_THREAD_FRAME); + obs_property_list_item_disable( + p, idx, !(this->avcodec_ptr->capabilities & AV_CODEC_CAP_FRAME_THREADS)); + } + { + auto idx = obs_property_list_add_int(p, TRANSLATE(P_MULTITHREADING_MODEL ".Slice"), + FF_THREAD_SLICE); + obs_property_list_item_disable( + p, idx, !(this->avcodec_ptr->capabilities & AV_CODEC_CAP_SLICE_THREADS)); + } + }; + { + auto p = obs_properties_add_int_slider(prs, P_MULTITHREADING_THREADCOUNT, + TRANSLATE(P_MULTITHREADING_THREADCOUNT), 0, + std::thread::hardware_concurrency() * 4, 1); + obs_property_set_long_description(p, TRANSLATE(DESC(P_MULTITHREADING_THREADCOUNT))); + }; + { + auto p = obs_properties_add_bool(prs, P_MULTITHREADING_FRAMEQUEUE, + TRANSLATE(P_MULTITHREADING_FRAMEQUEUE)); + obs_property_set_long_description(p, TRANSLATE(DESC(P_MULTITHREADING_FRAMEQUEUE))); + }; + + obs_properties_add_group(props, P_MULTITHREADING, TRANSLATE(P_MULTITHREADING), OBS_GROUP_NORMAL, prs); + }; + { + auto prs = obs_properties_create(); + { + auto p = + obs_properties_add_text(prs, P_FFMPEG_CUSTOMSETTINGS, TRANSLATE(P_FFMPEG_CUSTOMSETTINGS), + obs_text_type::OBS_TEXT_DEFAULT); + obs_property_set_long_description(p, TRANSLATE(DESC(P_FFMPEG_CUSTOMSETTINGS))); + } + { + auto p = obs_properties_add_list(prs, P_FFMPEG_STANDARDCOMPLIANCE, + TRANSLATE(P_FFMPEG_STANDARDCOMPLIANCE), OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + obs_property_set_long_description(p, TRANSLATE(DESC(P_FFMPEG_STANDARDCOMPLIANCE))); + obs_property_list_add_int(p, TRANSLATE(P_FFMPEG_STANDARDCOMPLIANCE ".VeryStrict"), + FF_COMPLIANCE_VERY_STRICT); + obs_property_list_add_int(p, TRANSLATE(P_FFMPEG_STANDARDCOMPLIANCE ".Strict"), + FF_COMPLIANCE_STRICT); + obs_property_list_add_int(p, TRANSLATE(P_FFMPEG_STANDARDCOMPLIANCE ".Normal"), + FF_COMPLIANCE_NORMAL); + obs_property_list_add_int(p, TRANSLATE(P_FFMPEG_STANDARDCOMPLIANCE ".Unofficial"), + FF_COMPLIANCE_UNOFFICIAL); + obs_property_list_add_int(p, TRANSLATE(P_FFMPEG_STANDARDCOMPLIANCE ".Experimental"), + FF_COMPLIANCE_EXPERIMENTAL); + } + obs_properties_add_group(props, P_FFMPEG, TRANSLATE(P_FFMPEG), OBS_GROUP_NORMAL, prs); + }; +} + +AVCodec* encoder::generic_factory::get_avcodec() +{ + return this->avcodec_ptr; +} + +bool encoder::generic_factory::modified_ratecontrol_properties(void* priv, obs_properties_t* props, + obs_property_t* prop, obs_data_t* settings) +{ + keyframe_type kft = static_cast(obs_data_get_int(settings, P_RATECONTROL_KEYFRAME_TYPE)); + obs_property_set_visible(obs_properties_get(props, P_RATECONTROL_KEYFRAME_INTERVAL ".Seconds"), + kft == keyframe_type::Seconds); + obs_property_set_visible(obs_properties_get(props, P_RATECONTROL_KEYFRAME_INTERVAL ".Frames"), + kft == keyframe_type::Frames); + return true; +} + +bool encoder::generic_factory::modified_threading_properties(void* priv, obs_properties_t* props, obs_property_t* prop, + obs_data_t* settings) +{ + int64_t tt = obs_data_get_int(settings, P_MULTITHREADING_MODEL); + obs_property_set_visible(obs_properties_get(props, P_MULTITHREADING_THREADCOUNT), tt != 0); + return true; +} + +encoder::generic::generic(obs_data_t* settings, obs_encoder_t* encoder) : self(encoder) +{ + this->factory = reinterpret_cast(obs_encoder_get_type_data(self)); + + // Verify that the codec actually still exists. + this->codec = avcodec_find_encoder(this->factory->get_avcodec()->id); + if (!this->codec) { + PLOG_ERROR("Failed to find encoder for codec '%s'.", this->factory->get_avcodec()->name); + throw std::runtime_error("failed to find codec"); + } + + // Initialize context. + this->context = avcodec_alloc_context3(this->codec); + if (!this->context) { + PLOG_ERROR("Failed to create context for encoder '%s'.", this->codec->name); + throw std::runtime_error("failed to create context"); + } + + // Settings + /// Rate Control + this->context->profile = obs_data_get_int(settings, P_RATECONTROL_PROFILE); + this->context->bit_rate = obs_data_get_int(settings, P_RATECONTROL_BITRATE); + this->context->strict_std_compliance = obs_data_get_int(settings, P_FFMPEG_STANDARDCOMPLIANCE); + this->context->debug = 0; + + /// Threading + { + int64_t tt = obs_data_get_int(settings, P_MULTITHREADING_MODEL); + if (tt == 0) { + this->context->thread_count = 1; + this->context->thread_type = 0; + } else if (tt == -1) { + if (this->codec->capabilities & AV_CODEC_CAP_SLICE_THREADS) { + this->context->thread_type = FF_THREAD_SLICE; + } else if (this->codec->capabilities & AV_CODEC_CAP_FRAME_THREADS) { + this->context->thread_type = FF_THREAD_FRAME; + } else { + this->context->thread_type = 0; + } + } else { + this->context->thread_type = tt; + } + if (tt != 0) { + this->context->thread_count = obs_data_get_int(settings, P_MULTITHREADING_THREADCOUNT); + if (!(this->codec->capabilities & AV_CODEC_CAP_AUTO_THREADS)) { + if (this->context->thread_count == 0) { + this->context->thread_count = std::thread::hardware_concurrency(); + } + } + } + } + + // Video and Audio exclusive setup + if (this->codec->type == AVMEDIA_TYPE_VIDEO) { + // FFmpeg Video Settings + auto encvideo = obs_encoder_video(this->self); + auto voi = video_output_get_info(encvideo); + /// Resolution and framerate. + this->context->width = voi->width; + this->context->height = voi->height; + this->context->time_base.num = voi->fps_num; + this->context->time_base.den = voi->fps_den; + this->context->ticks_per_frame = 1; + /// Group of Pictures + if (static_cast(obs_data_get_int(settings, P_RATECONTROL_KEYFRAME_TYPE)) + == keyframe_type::Frames) { + this->context->gop_size = obs_data_get_int(settings, P_RATECONTROL_KEYFRAME_INTERVAL ".Frames"); + } else { + double_t real_gop = obs_data_get_double(settings, P_RATECONTROL_KEYFRAME_INTERVAL ".Seconds"); + this->context->gop_size = static_cast(real_gop * voi->fps_num / voi->fps_den); + } + /// Color, Profile + this->context->colorspace = ffmpeg::tools::obs_videocolorspace_to_avcolorspace(voi->colorspace); + this->context->color_range = ffmpeg::tools::obs_videorangetype_to_avcolorrange(voi->range); + this->context->pix_fmt = ffmpeg::tools::obs_videoformat_to_avpixelformat(voi->format); + this->context->field_order = AV_FIELD_PROGRESSIVE; + } else if (this->codec->type == AVMEDIA_TYPE_AUDIO) { + } + + // Update settings + this->update(settings); + + // Initialize + int res = avcodec_open2(this->context, this->codec, NULL); + if (res < 0) { + PLOG_ERROR("Failed to initialize encoder '%s' due to error code %lld: %s", this->codec->name, res, + ffmpeg::tools::get_error_description(res)); + throw std::runtime_error(ffmpeg::tools::get_error_description(res)); + } + + // Video/Audio exclusive setup part 2. + if (this->codec->type == AVMEDIA_TYPE_VIDEO) { + // Create Scaler + swscale.set_source_size(this->context->width, this->context->height); + swscale.set_target_size(this->context->width, this->context->height); + swscale.set_source_format(this->context->pix_fmt); + swscale.set_target_format(this->context->pix_fmt); + swscale.set_source_color(this->context->color_range, this->context->colorspace); + swscale.set_target_color(this->context->color_range, this->context->colorspace); + + if (!swscale.initialize(SWS_FAST_BILINEAR)) { + PLOG_ERROR( + " Failed to initialize Software Scaler for pixel format '%s' with color space '%s' and " + "range '%s'.", + ffmpeg::tools::get_pixel_format_name(this->context->pix_fmt), + ffmpeg::tools::get_color_space_name(this->context->colorspace), + swscale.is_source_full_range() ? "Full" : "Partial"); + throw std::runtime_error("failed to initialize swscaler."); + } + + // Create Frame queue + frame_queue.set_pixel_format(this->context->pix_fmt); + frame_queue.set_resolution(this->context->width, this->context->height); + if (obs_data_get_bool(settings, P_MULTITHREADING_FRAMEQUEUE)) { + if (this->context->thread_count > 0) { + this->frame_queue.precache(this->context->thread_count); + } else { + if (this->context->thread_type != 0) { + this->frame_queue.precache(std::thread::hardware_concurrency()); + } else { + this->frame_queue.precache(1); + } + } + } else { + this->frame_queue.precache(1); + } + } else if (this->codec->type == AVMEDIA_TYPE_AUDIO) { + } + + // Create Packet + this->current_packet = av_packet_alloc(); + if (!this->current_packet) { + PLOG_ERROR("Failed to allocate packet storage."); + throw std::runtime_error("Failed to allocate packet storage."); + } +} + +encoder::generic::~generic() +{ + this->frame_queue.clear(); + this->frame_queue_used.clear(); + this->swscale.finalize(); + + if (this->context) { + avcodec_close(this->context); + avcodec_free_context(&this->context); + } +} + +void encoder::generic::get_properties(obs_properties_t* props) {} + +bool encoder::generic::update(obs_data_t* settings) +{ + return false; +} + +bool encoder::generic::encode(encoder_frame* frame, encoder_packet* packet, bool* received_packet) +{ + return false; +} + +void encoder::generic::get_audio_info(audio_convert_info* info) {} + +size_t encoder::generic::get_frame_size() +{ + return size_t(); +} + +void encoder::generic::get_video_info(video_scale_info* info) {} + +bool encoder::generic::get_sei_data(uint8_t** sei_data, size_t* size) +{ + return false; +} + +bool encoder::generic::get_extra_data(uint8_t** extra_data, size_t* size) +{ + if (!this->context->extradata) { + return false; + } + *extra_data = this->context->extradata; + *size = this->context->extradata_size; + return true; +} + +bool encoder::generic::encode_texture(uint32_t handle, int64_t pts, uint64_t lock_key, uint64_t* next_key, + encoder_packet* packet, bool* received_packet) +{ + return false; +} diff --git a/source/encoders/generic.hpp b/source/encoders/generic.hpp new file mode 100644 index 0000000..48dc6af --- /dev/null +++ b/source/encoders/generic.hpp @@ -0,0 +1,95 @@ +// FFMPEG Video Encoder Integration for OBS Studio +// Copyright (C) 2018 - 2019 Michael Fabian Dirks +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +#pragma once + +#include +#include "ffmpeg/avframe-queue.hpp" +#include "ffmpeg/swscale.hpp" + +namespace encoder { + class generic_factory { + struct info { + std::string uid; + std::string codec; + std::string readable_name; + obs_encoder_info oei; + } info; + AVCodec* avcodec_ptr; + + public: + generic_factory(AVCodec* codec); + virtual ~generic_factory(); + + void register_encoder(); + + const char* get_name(); + + void get_defaults(obs_data_t* settings); + + void get_properties(obs_properties_t* props); + + AVCodec* get_avcodec(); + + public: + static bool modified_ratecontrol_properties(void* priv, obs_properties_t* props, obs_property_t* prop, + obs_data_t* settings); + static bool modified_threading_properties(void* priv, obs_properties_t* props, obs_property_t* prop, + obs_data_t* settings); + }; + + class generic { + obs_encoder_t* self; + generic_factory* factory; + + AVCodec* codec; + AVCodecContext* context; + + ffmpeg::avframe_queue frame_queue; + ffmpeg::avframe_queue frame_queue_used; + ffmpeg::swscale swscale; + AVPacket* current_packet = nullptr; + + public: + generic(obs_data_t* settings, obs_encoder_t* encoder); + virtual ~generic(); + + // Shared + + void get_properties(obs_properties_t* props); + + bool update(obs_data_t* settings); + + bool encode(struct encoder_frame* frame, struct encoder_packet* packet, bool* received_packet); + + // Audio only + void get_audio_info(struct audio_convert_info* info); + + size_t get_frame_size(); + + // Video only + void get_video_info(struct video_scale_info* info); + + bool get_sei_data(uint8_t** sei_data, size_t* size); + + bool get_extra_data(uint8_t** extra_data, size_t* size); + + // GPU Video only + bool encode_texture(uint32_t handle, int64_t pts, uint64_t lock_key, uint64_t* next_key, + struct encoder_packet* packet, bool* received_packet); + }; +} // namespace encoder diff --git a/source/plugin.cpp b/source/plugin.cpp index b376bed..4ff00f6 100644 --- a/source/plugin.cpp +++ b/source/plugin.cpp @@ -16,10 +16,12 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA #include "plugin.hpp" +#include #include #include #include "utility.hpp" +#include "encoders/generic.hpp" #include "encoders/prores_aw.hpp" extern "C" { @@ -29,10 +31,27 @@ extern "C" { #pragma warning(pop) } +static std::map> generic_factories; + MODULE_EXPORT bool obs_module_load(void) { try { avcodec_register_all(); + + // Register all codecs. + AVCodec* cdc = nullptr; + while ((cdc = av_codec_next(cdc)) != nullptr) { + if ((!cdc->encode2) && (!cdc->send_frame)) + continue; + + if ((cdc->type == AVMediaType::AVMEDIA_TYPE_AUDIO) + || (cdc->type == AVMediaType::AVMEDIA_TYPE_VIDEO)) { + auto ptr = std::make_shared(cdc); + ptr->register_encoder(); + generic_factories.emplace(cdc, ptr); + } + } + obsffmpeg::encoder::prores_aw::initialize(); return true; } catch (std::exception ex) {