From a33dd83d72208f91e1a5f023aa50ae31a12058ee Mon Sep 17 00:00:00 2001 From: Michael Fabian 'Xaymar' Dirks Date: Mon, 13 Jan 2020 23:40:08 +0100 Subject: [PATCH] project: Merged FFmpeg Encoders Step 3 --- source/encoders/ffmpeg-encoder.cpp | 1368 +++++++++++++++++ source/encoders/ffmpeg-encoder.hpp | 221 +++ .../encoders/handlers/nvenc_h264_handler.cpp | 22 +- .../encoders/handlers/nvenc_hevc_handler.cpp | 27 +- source/encoders/handlers/nvenc_shared.cpp | 290 ++-- source/encoders/handlers/nvenc_shared.hpp | 8 + 6 files changed, 1764 insertions(+), 172 deletions(-) create mode 100644 source/encoders/ffmpeg-encoder.cpp create mode 100644 source/encoders/ffmpeg-encoder.hpp diff --git a/source/encoders/ffmpeg-encoder.cpp b/source/encoders/ffmpeg-encoder.cpp new file mode 100644 index 00000000..deb0452b --- /dev/null +++ b/source/encoders/ffmpeg-encoder.cpp @@ -0,0 +1,1368 @@ +// FFMPEG Video Encoder Integration for OBS Studio +// Copyright (c) 2019 Michael Fabian Dirks +// +// 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 "ffmpeg-encoder.hpp" +#include +#include "codecs/hevc.hpp" +#include "ffmpeg/tools.hpp" +#include "handlers/debug_handler.hpp" +#include "handlers/nvenc_h264_handler.hpp" +#include "handlers/nvenc_hevc_handler.hpp" +#include "handlers/prores_aw_handler.hpp" +#include "plugin.hpp" +#include "strings.hpp" +#include "utility.hpp" + +extern "C" { +#include +#include +#pragma warning(push) +#pragma warning(disable : 4244) +#include +#include +#include +#include +#include +#pragma warning(pop) +} + +#ifdef WIN32 +#define HARDWARE_ENCODING +#include "ffmpeg/hwapi/d3d11.hpp" +#endif + +// FFmpeg +#define ST_FFMPEG "FFmpeg" +#define ST_FFMPEG_CUSTOMSETTINGS "FFmpeg.CustomSettings" +#define ST_FFMPEG_THREADS "FFmpeg.Threads" +#define ST_FFMPEG_COLORFORMAT "FFmpeg.ColorFormat" +#define ST_FFMPEG_STANDARDCOMPLIANCE "FFmpeg.StandardCompliance" +#define ST_FFMPEG_GPU "FFmpeg.GPU" + +using namespace encoder::ffmpeg; + +enum class keyframe_type { SECONDS, FRAMES }; + +std::shared_ptr encoder::ffmpeg::ffmpeg_manager::_instance; + +ffmpeg_manager::ffmpeg_manager() +{ + // Handlers + _debug_handler = ::std::make_shared(); + register_handler("prores_aw", ::std::make_shared()); + register_handler("h264_nvenc", ::std::make_shared()); + register_handler("hevc_nvenc", ::std::make_shared()); + + void* iterator = nullptr; + const AVCodec* codec = nullptr; + for (codec = av_codec_iterate(&iterator); codec != nullptr; codec = av_codec_iterate(&iterator)) { + if (!av_codec_is_encoder(codec)) + continue; + if ((codec->type == AVMediaType::AVMEDIA_TYPE_AUDIO) || (codec->type == AVMediaType::AVMEDIA_TYPE_VIDEO)) { + try { + auto factory = std::make_shared(codec); + factory->register_encoder(); + _factories.emplace(codec, factory); + } catch (...) { + } + } + } +} + +ffmpeg_manager::~ffmpeg_manager() +{ + _factories.clear(); +} + +void ffmpeg_manager::register_handler(std::string const codec, std::shared_ptr const handler) {} + +std::shared_ptr const ffmpeg_manager::get_handler(std::string const codec) +{ + return nullptr; +} + +bool ffmpeg_manager::has_handler(std::string const codec) +{ + return false; +} + +static void* _create(obs_data_t* settings, obs_encoder_t* encoder) noexcept +try { + return reinterpret_cast(new ffmpeg_instance(settings, encoder)); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return nullptr; +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return nullptr; +} + +static void* _create_texture(obs_data_t* settings, obs_encoder_t* encoder) noexcept +try { + return reinterpret_cast(new ffmpeg_instance(settings, encoder, true)); +} catch (const std::exception& ex) { + ffmpeg_factory* fac = reinterpret_cast(obs_encoder_get_type_data(encoder)); + LOG_ERROR("<%s> Failed to create GPU assisted encoder due to exception: %s.", fac->get_avcodec()->name, ex.what()); + return obs_encoder_create_rerouted(encoder, fac->get_fallback().oei.id); +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return nullptr; +} + +static void _destroy(void* ptr) noexcept +try { + if (ptr) + delete reinterpret_cast(ptr); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); +} + +static const char* _get_name(void* type_data) noexcept +try { + return reinterpret_cast(type_data)->get_info().readable_name.c_str(); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return nullptr; +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return nullptr; +} + +static const char* _get_name_fallback(void* type_data) noexcept +try { + return reinterpret_cast(type_data)->get_fallback().readable_name.c_str(); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return nullptr; +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return nullptr; +} + +static void _get_defaults(obs_data_t* settings, void* type_data) noexcept +try { + reinterpret_cast(type_data)->get_defaults(settings); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); +}; + +static void _get_defaults_texture(obs_data_t* settings, void* type_data) noexcept +try { + reinterpret_cast(type_data)->get_defaults(settings, true); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); +}; + +static obs_properties_t* _get_properties(void* ptr, void* type_data) noexcept +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 (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return reinterpret_cast(0); +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return reinterpret_cast(0); +} + +static obs_properties_t* _get_properties_texture(void* ptr, void* type_data) noexcept +try { + obs_properties_t* props = obs_properties_create(); + if (type_data != nullptr) { + reinterpret_cast(type_data)->get_properties(props, true); + } + if (ptr != nullptr) { + reinterpret_cast(ptr)->get_properties(props, true); + } + return props; +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return reinterpret_cast(0); +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return reinterpret_cast(0); +} + +static bool _update(void* ptr, obs_data_t* settings) noexcept +try { + return reinterpret_cast(ptr)->update(settings); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; +} + +static bool _get_sei_data(void* ptr, uint8_t** sei_data, size_t* size) noexcept +try { + return reinterpret_cast(ptr)->get_sei_data(sei_data, size); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; +} + +static bool _get_extra_data(void* ptr, uint8_t** extra_data, size_t* size) noexcept +try { + return reinterpret_cast(ptr)->get_extra_data(extra_data, size); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; +} + +static void _get_video_info(void* ptr, struct video_scale_info* info) noexcept +try { + reinterpret_cast(ptr)->get_video_info(info); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); +} + +static bool _encode(void* ptr, struct encoder_frame* frame, struct encoder_packet* packet, + bool* received_packet) noexcept +try { + return reinterpret_cast(ptr)->video_encode(frame, packet, received_packet); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; +} + +static bool _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) noexcept +try { + return reinterpret_cast(ptr)->video_encode_texture(handle, pts, lock_key, next_key, packet, + received_packet); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; +} + +static void _get_audio_info(void* ptr, struct audio_convert_info* info) noexcept +try { + reinterpret_cast(ptr)->get_audio_info(info); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); +} + +static size_t _get_frame_size(void* ptr) noexcept +try { + return reinterpret_cast(ptr)->get_frame_size(); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return 0; +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return 0; +} + +static bool _encode_audio(void* ptr, struct encoder_frame* frame, struct encoder_packet* packet, + bool* received_packet) noexcept +try { + return reinterpret_cast(ptr)->audio_encode(frame, packet, received_packet); +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; +} + +ffmpeg_factory::ffmpeg_factory(const AVCodec* codec) : avcodec_ptr(codec), info(), info_fallback() +{ + // Find Codec UI handler. + _handler = ffmpeg_manager::get()->get_handler(avcodec_ptr->name); + + // Unique Id is FFmpeg name. + info.uid = std::string(PLUGIN_NAME "-") + avcodec_ptr->name; + + // Also generate a human readable name while we're at it. + { + std::stringstream sstr; + if (!ffmpeg_manager::get()->has_handler(avcodec_ptr->name)) { + sstr << "[UNSUPPORTED] "; + } + sstr << (avcodec_ptr->long_name ? avcodec_ptr->long_name : avcodec_ptr->name); + if (avcodec_ptr->long_name) { + sstr << " (" << avcodec_ptr->name << ")"; + } + info.readable_name = sstr.str(); + } + + // Assign Ids. + { + const AVCodecDescriptor* desc = avcodec_descriptor_get(avcodec_ptr->id); + if (desc) { + info.codec = desc->name; + } else { + // Fall back to encoder name in the case that FFmpeg itself doesn't know + // what codec this actually is. + info.codec = avcodec_ptr->name; + } + } + + info.oei.id = info.uid.c_str(); + info.oei.codec = info.codec.c_str(); + +#ifndef _DEBUG + // Is this a deprecated encoder? + if (!has_codec_handler(avcodec_ptr->name)) { + info.oei.caps |= OBS_ENCODER_CAP_DEPRECATED; + } +#endif + + // Hardware encoder? +#ifdef HARDWARE_ENCODING + if (::ffmpeg::tools::can_hardware_encode(avcodec_ptr)) { + info_fallback.uid = info.uid + "_sw"; + info_fallback.codec = info.codec; + info_fallback.readable_name = info.readable_name + " (Software)"; + + // Copy capabilities and hide from view. + info_fallback.oei.id = info_fallback.uid.c_str(); + info_fallback.oei.codec = info.oei.codec; + info_fallback.oei.caps = info.oei.caps; + //info_fallback.oei.caps |= OBS_ENCODER_CAP_DEPRECATED; + + info.oei.caps |= OBS_ENCODER_CAP_PASS_TEXTURE; + } +#endif +} + +ffmpeg_factory::~ffmpeg_factory() {} + +void ffmpeg_factory::register_encoder() +{ + // Detect encoder type (only Video and Audio supported) + if (avcodec_ptr->type == AVMediaType::AVMEDIA_TYPE_VIDEO) { + info.oei.type = obs_encoder_type::OBS_ENCODER_VIDEO; + } else if (avcodec_ptr->type == AVMediaType::AVMEDIA_TYPE_AUDIO) { + info.oei.type = obs_encoder_type::OBS_ENCODER_AUDIO; + } else { + throw std::invalid_argument("unsupported codec type"); + } + + // Register functions. + info.oei.destroy = _destroy; + info.oei.get_name = _get_name; + info.oei.get_defaults2 = _get_defaults; + info.oei.get_properties2 = _get_properties; + info.oei.update = _update; + info.oei.get_sei_data = _get_sei_data; + info.oei.get_extra_data = _get_extra_data; + + if (avcodec_ptr->type == AVMediaType::AVMEDIA_TYPE_VIDEO) { + info.oei.get_video_info = _get_video_info; + } else if (avcodec_ptr->type == AVMediaType::AVMEDIA_TYPE_AUDIO) { + info.oei.get_audio_info = _get_audio_info; + info.oei.get_frame_size = _get_frame_size; + info.oei.encode = _encode_audio; + } + + // Finally store ourself as type data. + info.oei.type_data = this; + + if (::ffmpeg::tools::can_hardware_encode(avcodec_ptr)) { + info.oei.create = _create_texture; + info.oei.encode_texture = _encode_texture; + info.oei.get_defaults2 = _get_defaults_texture; + info.oei.get_properties2 = _get_properties_texture; + + info_fallback.oei.type = info.oei.type; + info_fallback.oei.create = _create; + info_fallback.oei.destroy = _destroy; + info_fallback.oei.get_name = _get_name_fallback; + info_fallback.oei.get_defaults2 = _get_defaults; + info_fallback.oei.get_properties2 = _get_properties; + info_fallback.oei.update = _update; + info_fallback.oei.get_sei_data = _get_sei_data; + info_fallback.oei.get_extra_data = _get_extra_data; + info_fallback.oei.get_video_info = _get_video_info; + info_fallback.oei.encode = _encode; + info_fallback.oei.type_data = this; + + } else { + // Is not a GPU Encoder, don't implement fallback. + info.oei.create = _create; + info.oei.encode = _encode; + } + + if (_handler) + _handler->adjust_encoder_info(this, &info, &info_fallback); + + obs_register_encoder(&info.oei); + LOG_DEBUG("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); + if (info_fallback.uid.size() > 0) { + obs_register_encoder(&info_fallback.oei); + LOG_DEBUG("Registered software fallback for encoder #%llX", avcodec_ptr); + } +} + +void ffmpeg_factory::get_defaults(obs_data_t* settings, bool hw_encode) +{ + if (_handler) + _handler->get_defaults(settings, avcodec_ptr, nullptr, hw_encode); + + if ((avcodec_ptr->capabilities & AV_CODEC_CAP_INTRA_ONLY) == 0) { + obs_data_set_default_int(settings, S_KEYFRAMES_INTERVALTYPE, 0); + obs_data_set_default_double(settings, S_KEYFRAMES_INTERVAL_SECONDS, 2.0); + obs_data_set_default_int(settings, S_KEYFRAMES_INTERVAL_FRAMES, 300); + } + + { // Integrated Options + // FFmpeg + obs_data_set_default_string(settings, ST_FFMPEG_CUSTOMSETTINGS, ""); + if (!hw_encode) { + obs_data_set_default_int(settings, ST_FFMPEG_COLORFORMAT, static_cast(AV_PIX_FMT_NONE)); + obs_data_set_default_int(settings, ST_FFMPEG_THREADS, 0); + obs_data_set_default_int(settings, ST_FFMPEG_GPU, 0); + } + obs_data_set_default_int(settings, ST_FFMPEG_STANDARDCOMPLIANCE, FF_COMPLIANCE_STRICT); + } +} + +static bool modified_keyframes(obs_properties_t* props, obs_property_t*, obs_data_t* settings) +try { + bool is_seconds = obs_data_get_int(settings, S_KEYFRAMES_INTERVALTYPE) == 0; + obs_property_set_visible(obs_properties_get(props, S_KEYFRAMES_INTERVAL_FRAMES), !is_seconds); + obs_property_set_visible(obs_properties_get(props, S_KEYFRAMES_INTERVAL_SECONDS), is_seconds); + return true; +} catch (const std::exception& ex) { + LOG_ERROR("Unexpected exception in function '%s': %s.", __FUNCTION_NAME__, ex.what()); + return false; +} catch (...) { + LOG_ERROR("Unexpected exception in function '%s'.", __FUNCTION_NAME__); + return false; +} + +void ffmpeg_factory::get_properties(obs_properties_t* props, bool hw_encode) +{ + if (_handler) + _handler->get_properties(props, avcodec_ptr, nullptr, hw_encode); + + if ((avcodec_ptr->capabilities & AV_CODEC_CAP_INTRA_ONLY) == 0) { + // Key-Frame Options + obs_properties_t* grp = props; + if (!util::are_property_groups_broken()) { + grp = obs_properties_create(); + obs_properties_add_group(props, S_KEYFRAMES, D_TRANSLATE(S_KEYFRAMES), OBS_GROUP_NORMAL, grp); + } + + { + auto p = obs_properties_add_list(grp, S_KEYFRAMES_INTERVALTYPE, D_TRANSLATE(S_KEYFRAMES_INTERVALTYPE), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_long_description(p, D_TRANSLATE(D_DESC(S_KEYFRAMES_INTERVALTYPE))); + obs_property_set_modified_callback(p, modified_keyframes); + obs_property_list_add_int(p, D_TRANSLATE(S_KEYFRAMES_INTERVALTYPE_(Seconds)), 0); + obs_property_list_add_int(p, D_TRANSLATE(S_KEYFRAMES_INTERVALTYPE_(Frames)), 1); + } + { + auto p = obs_properties_add_float(grp, S_KEYFRAMES_INTERVAL_SECONDS, D_TRANSLATE(S_KEYFRAMES_INTERVAL), + 0.00, std::numeric_limits::max(), 0.01); + obs_property_set_long_description(p, D_TRANSLATE(D_DESC(S_KEYFRAMES_INTERVAL))); + obs_property_float_set_suffix(p, " seconds"); + } + { + auto p = obs_properties_add_int(grp, S_KEYFRAMES_INTERVAL_FRAMES, D_TRANSLATE(S_KEYFRAMES_INTERVAL), 0, + std::numeric_limits::max(), 1); + obs_property_set_long_description(p, D_TRANSLATE(D_DESC(S_KEYFRAMES_INTERVAL))); + obs_property_int_set_suffix(p, " frames"); + } + } + + { + obs_properties_t* grp = props; + if (!util::are_property_groups_broken()) { + auto prs = obs_properties_create(); + obs_properties_add_group(props, ST_FFMPEG, D_TRANSLATE(ST_FFMPEG), OBS_GROUP_NORMAL, prs); + grp = prs; + } + + { + auto p = obs_properties_add_text(grp, ST_FFMPEG_CUSTOMSETTINGS, D_TRANSLATE(ST_FFMPEG_CUSTOMSETTINGS), + obs_text_type::OBS_TEXT_DEFAULT); + obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_FFMPEG_CUSTOMSETTINGS))); + } + if (!hw_encode) { + { + auto p = obs_properties_add_int(grp, ST_FFMPEG_GPU, D_TRANSLATE(ST_FFMPEG_GPU), 0, + std::numeric_limits::max(), 1); + obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_FFMPEG_GPU))); + } + if (avcodec_ptr->pix_fmts) { + auto p = obs_properties_add_list(grp, ST_FFMPEG_COLORFORMAT, D_TRANSLATE(ST_FFMPEG_COLORFORMAT), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_FFMPEG_COLORFORMAT))); + obs_property_list_add_int(p, D_TRANSLATE(S_STATE_AUTOMATIC), static_cast(AV_PIX_FMT_NONE)); + for (auto ptr = avcodec_ptr->pix_fmts; *ptr != AV_PIX_FMT_NONE; ptr++) { + obs_property_list_add_int(p, ::ffmpeg::tools::get_pixel_format_name(*ptr), + static_cast(*ptr)); + } + } + if (avcodec_ptr->capabilities & (AV_CODEC_CAP_FRAME_THREADS | AV_CODEC_CAP_SLICE_THREADS)) { + auto p = obs_properties_add_int_slider(grp, ST_FFMPEG_THREADS, D_TRANSLATE(ST_FFMPEG_THREADS), 0, + std::thread::hardware_concurrency() * 2, 1); + obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_FFMPEG_THREADS))); + } + } + { + auto p = + obs_properties_add_list(grp, ST_FFMPEG_STANDARDCOMPLIANCE, D_TRANSLATE(ST_FFMPEG_STANDARDCOMPLIANCE), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_FFMPEG_STANDARDCOMPLIANCE))); + obs_property_list_add_int(p, D_TRANSLATE(ST_FFMPEG_STANDARDCOMPLIANCE ".VeryStrict"), + FF_COMPLIANCE_VERY_STRICT); + obs_property_list_add_int(p, D_TRANSLATE(ST_FFMPEG_STANDARDCOMPLIANCE ".Strict"), FF_COMPLIANCE_STRICT); + obs_property_list_add_int(p, D_TRANSLATE(ST_FFMPEG_STANDARDCOMPLIANCE ".Normal"), FF_COMPLIANCE_NORMAL); + obs_property_list_add_int(p, D_TRANSLATE(ST_FFMPEG_STANDARDCOMPLIANCE ".Unofficial"), + FF_COMPLIANCE_UNOFFICIAL); + obs_property_list_add_int(p, D_TRANSLATE(ST_FFMPEG_STANDARDCOMPLIANCE ".Experimental"), + FF_COMPLIANCE_EXPERIMENTAL); + } + }; +} + +const AVCodec* ffmpeg_factory::get_avcodec() +{ + return avcodec_ptr; +} + +const ffmpeg_info& ffmpeg_factory::get_info() +{ + return info; +} + +const ffmpeg_info& ffmpeg_factory::get_fallback() +{ + return info_fallback; +} + +void ffmpeg_instance::initialize_sw(obs_data_t* settings) +{ + if (_codec->type == AVMEDIA_TYPE_VIDEO) { + // Initialize Video Encoding + auto voi = video_output_get_info(obs_encoder_video(_self)); + + // Find a suitable Pixel Format. + AVPixelFormat _pixfmt_source = ::ffmpeg::tools::obs_videoformat_to_avpixelformat(voi->format); + AVPixelFormat _pixfmt_target = static_cast(obs_data_get_int(settings, ST_FFMPEG_COLORFORMAT)); + if (_pixfmt_target == AV_PIX_FMT_NONE) { + // Find the best conversion format. + _pixfmt_target = ::ffmpeg::tools::get_least_lossy_format(_codec->pix_fmts, _pixfmt_source); + + if (_handler) // Allow Handler to override the automatic color format for sanity reasons. + _handler->override_colorformat(_pixfmt_target, settings, _codec, _context); + } else { + // Use user override, guaranteed to be supported. + bool is_format_supported = false; + for (auto ptr = _codec->pix_fmts; *ptr != AV_PIX_FMT_NONE; ptr++) { + if (*ptr == _pixfmt_target) { + is_format_supported = true; + } + } + + if (!is_format_supported) { + std::stringstream sstr; + sstr << "Color Format '" << ::ffmpeg::tools::get_pixel_format_name(_pixfmt_target) + << "' is not supported by the encoder."; + throw std::exception(sstr.str().c_str()); + } + } + + _context->width = static_cast(obs_encoder_get_width(_self)); + _context->height = static_cast(obs_encoder_get_height(_self)); + ::ffmpeg::tools::setup_obs_color(voi->colorspace, voi->range, _context); + + _context->pix_fmt = _pixfmt_target; + _context->field_order = AV_FIELD_PROGRESSIVE; + _context->ticks_per_frame = 1; + _context->sample_aspect_ratio.num = _context->sample_aspect_ratio.den = 1; + _context->framerate.num = _context->time_base.den = voi->fps_num; + _context->framerate.den = _context->time_base.num = voi->fps_den; + + _swscale.set_source_size(_context->width, _context->height); + _swscale.set_source_color(_context->color_range == AVCOL_RANGE_JPEG, _context->colorspace); + _swscale.set_source_format(_pixfmt_source); + + _swscale.set_target_size(_context->width, _context->height); + _swscale.set_target_color(_context->color_range == AVCOL_RANGE_JPEG, _context->colorspace); + _swscale.set_target_format(_pixfmt_target); + + // Create Scaler + if (!_swscale.initialize(SWS_POINT)) { + std::stringstream sstr; + sstr << "Initializing scaler failed for conversion from '" + << ::ffmpeg::tools::get_pixel_format_name(_swscale.get_source_format()) << "' to '" + << ::ffmpeg::tools::get_pixel_format_name(_swscale.get_target_format()) << "' with color space '" + << ::ffmpeg::tools::get_color_space_name(_swscale.get_source_colorspace()) << "' and " + << (_swscale.is_source_full_range() ? "full" : "partial") << " range."; + throw std::runtime_error(sstr.str()); + } + } +} + +void ffmpeg_instance::initialize_hw(obs_data_t*) +{ + // Initialize Video Encoding + auto voi = video_output_get_info(obs_encoder_video(_self)); + + _context->width = voi->width; + _context->height = voi->height; + _context->field_order = AV_FIELD_PROGRESSIVE; + _context->ticks_per_frame = 1; + _context->sample_aspect_ratio.num = _context->sample_aspect_ratio.den = 1; + _context->framerate.num = _context->time_base.den = voi->fps_num; + _context->framerate.den = _context->time_base.num = voi->fps_den; + ::ffmpeg::tools::setup_obs_color(voi->colorspace, voi->range, _context); + _context->sw_pix_fmt = ::ffmpeg::tools::obs_videoformat_to_avpixelformat(voi->format); + +#ifdef WIN32 + _context->pix_fmt = AV_PIX_FMT_D3D11; +#endif + + _context->hw_device_ctx = _hwinst->create_device_context(); + + _context->hw_frames_ctx = av_hwframe_ctx_alloc(_context->hw_device_ctx); + if (!_context->hw_frames_ctx) + throw std::runtime_error("Failed to allocate AVHWFramesContext."); + + AVHWFramesContext* ctx = reinterpret_cast(_context->hw_frames_ctx->data); + ctx->width = _context->width; + ctx->height = _context->height; + ctx->format = _context->pix_fmt; + ctx->sw_format = _context->sw_pix_fmt; + + if (av_hwframe_ctx_init(_context->hw_frames_ctx) < 0) + throw std::runtime_error("Failed to initialize AVHWFramesContext."); +} + +void ffmpeg_instance::push_free_frame(std::shared_ptr frame) +{ + auto now = std::chrono::high_resolution_clock::now(); + if (_free_frames.size() > 0) { + if ((now - _free_frames_last_used) < std::chrono::seconds(1)) { + _free_frames.push(frame); + } + } else { + _free_frames.push(frame); + _free_frames_last_used = std::chrono::high_resolution_clock::now(); + } +} + +std::shared_ptr ffmpeg_instance::pop_free_frame() +{ + std::shared_ptr frame; + if (_free_frames.size() > 0) { + // Re-use existing frames first. + frame = _free_frames.top(); + _free_frames.pop(); + } else { + if (_hwinst) { + frame = _hwinst->allocate_frame(_context->hw_frames_ctx); + } else { + frame = std::shared_ptr(av_frame_alloc(), [](AVFrame* frame) { + av_frame_unref(frame); + av_frame_free(&frame); + }); + + frame->width = _context->width; + frame->height = _context->height; + frame->format = _context->pix_fmt; + + int res = av_frame_get_buffer(frame.get(), 32); + if (res < 0) { + throw std::runtime_error(::ffmpeg::tools::get_error_description(res)); + } + } + } + + return frame; +} + +void ffmpeg_instance::push_used_frame(std::shared_ptr frame) +{ + _used_frames.push(frame); +} + +std::shared_ptr ffmpeg_instance::pop_used_frame() +{ + auto frame = _used_frames.front(); + _used_frames.pop(); + return frame; +} + +ffmpeg_instance::ffmpeg_instance(obs_data_t* settings, obs_encoder_t* encoder, bool is_texture_encode) + : _self(encoder), _factory(reinterpret_cast(obs_encoder_get_type_data(_self))), + _codec(_factory->get_avcodec()), _context(nullptr), _lag_in_frames(0), _count_send_frames(0), + _have_first_frame(false) +{ + // Find a handler + _handler = ffmpeg_manager::get()->get_handler(_codec->name); + + // Initialize GPU Stuff + if (is_texture_encode) { +#ifdef WIN32 + auto gctx = util::obs_graphics(); + if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { + _hwapi = std::make_shared<::ffmpeg::hwapi::d3d11>(); + } +#endif + _hwinst = _hwapi->create_from_obs(); + } + + // Initialize context. + _context = avcodec_alloc_context3(_codec); + if (!_context) { + LOG_ERROR("Failed to create context for encoder '%s'.", _codec->name); + throw std::runtime_error("failed to create context"); + } + + // Create 8MB of precached Packet data for use later on. + av_init_packet(&_current_packet); + av_new_packet(&_current_packet, 8 * 1024 * 1024); // 8 MB precached Packet size. + + if (is_texture_encode) { + initialize_hw(settings); + } else { + initialize_sw(settings); + } + + // Update settings + update(settings); + + // Initialize Encoder + auto gctx = util::obs_graphics(); + int res = avcodec_open2(_context, _codec, NULL); + if (res < 0) { + std::stringstream sstr; + sstr << "Initializing encoder '" << _codec->name + << "' failed with error: " << ::ffmpeg::tools::get_error_description(res) << " (code " << res << ")"; + throw std::runtime_error(sstr.str()); + } +} + +ffmpeg_instance::~ffmpeg_instance() +{ + auto gctx = util::obs_graphics(); + if (_context) { + // Flush encoders that require it. + if ((_codec->capabilities & AV_CODEC_CAP_DELAY) != 0) { + avcodec_send_frame(_context, nullptr); + while (avcodec_receive_packet(_context, &_current_packet) >= 0) { + avcodec_send_frame(_context, nullptr); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + // Close and free context. + avcodec_close(_context); + avcodec_free_context(&_context); + } + + av_packet_unref(&_current_packet); + + _swscale.finalize(); +} + +void ffmpeg_instance::get_properties(obs_properties_t* props, bool hw_encode) +{ + if (_handler) + _handler->get_properties(props, _codec, _context, hw_encode); + + obs_property_set_enabled(obs_properties_get(props, S_KEYFRAMES), false); + obs_property_set_enabled(obs_properties_get(props, S_KEYFRAMES_INTERVALTYPE), false); + obs_property_set_enabled(obs_properties_get(props, S_KEYFRAMES_INTERVAL_SECONDS), false); + obs_property_set_enabled(obs_properties_get(props, S_KEYFRAMES_INTERVAL_FRAMES), false); + + obs_property_set_enabled(obs_properties_get(props, ST_FFMPEG_COLORFORMAT), false); + obs_property_set_enabled(obs_properties_get(props, ST_FFMPEG_THREADS), false); + obs_property_set_enabled(obs_properties_get(props, ST_FFMPEG_STANDARDCOMPLIANCE), false); + obs_property_set_enabled(obs_properties_get(props, ST_FFMPEG_GPU), false); +} + +bool ffmpeg_instance::update(obs_data_t* settings) +{ + // FFmpeg Options + _context->debug = 0; + _context->strict_std_compliance = static_cast(obs_data_get_int(settings, ST_FFMPEG_STANDARDCOMPLIANCE)); + + /// Threading + if (!_hwinst) { + _context->thread_type = 0; + if (_codec->capabilities & AV_CODEC_CAP_FRAME_THREADS) { + _context->thread_type |= FF_THREAD_FRAME; + } + if (_codec->capabilities & AV_CODEC_CAP_SLICE_THREADS) { + _context->thread_type |= FF_THREAD_SLICE; + } + if (_context->thread_type != 0) { + int64_t threads = obs_data_get_int(settings, ST_FFMPEG_THREADS); + if (threads > 0) { + _context->thread_count = static_cast(threads); + } else { + _context->thread_count = std::thread::hardware_concurrency(); + } + } else { + _context->thread_count = 1; + } + // Frame Delay (Lag In Frames) + _context->delay = _context->thread_count; + } else { + _context->delay = 0; + } + + // Apply GPU Selection + if (!_hwinst && ::ffmpeg::tools::can_hardware_encode(_codec)) { + av_opt_set_int(_context, "gpu", (int)obs_data_get_int(settings, ST_FFMPEG_GPU), AV_OPT_SEARCH_CHILDREN); + } + + // Keyframes + if (_handler && _handler->has_keyframe_support(this)) { + // Key-Frame Options + obs_video_info ovi; + if (!obs_get_video_info(&ovi)) { + throw std::runtime_error("obs_get_video_info failed, restart OBS Studio to fix it (hopefully)."); + } + + int64_t kf_type = obs_data_get_int(settings, S_KEYFRAMES_INTERVALTYPE); + bool is_seconds = (kf_type == 0); + + if (is_seconds) { + _context->gop_size = static_cast(obs_data_get_double(settings, S_KEYFRAMES_INTERVAL_SECONDS) + * (ovi.fps_num / ovi.fps_den)); + } else { + _context->gop_size = static_cast(obs_data_get_int(settings, S_KEYFRAMES_INTERVAL_FRAMES)); + } + _context->keyint_min = _context->gop_size; + } + + // Handler Options + if (_handler) + _handler->update(settings, _codec, _context); + + { // FFmpeg Custom Options + const char* opts = obs_data_get_string(settings, ST_FFMPEG_CUSTOMSETTINGS); + size_t opts_len = strnlen(opts, 65535); + + parse_ffmpeg_commandline(std::string{opts, opts + opts_len}); + } + + // Handler Overrides + if (_handler) + _handler->override_update(this, settings); + + // Handler Logging + if (_handler) { + LOG_INFO("[%s] Initializing...", _codec->name); + LOG_INFO("[%s] FFmpeg:", _codec->name); + LOG_INFO("[%s] Custom Settings: %s", _codec->name, obs_data_get_string(settings, ST_FFMPEG_CUSTOMSETTINGS)); + LOG_INFO("[%s] Standard Compliance: %s", _codec->name, + ::ffmpeg::tools::get_std_compliance_name(_context->strict_std_compliance)); + LOG_INFO("[%s] Threading: %s (with %i threads)", _codec->name, + ::ffmpeg::tools::get_thread_type_name(_context->thread_type), _context->thread_count); + + LOG_INFO("[%s] Video:", _codec->name); + if (_hwinst) { + LOG_INFO("[%s] Texture: %ldx%ld %s %s %s", _codec->name, _context->width, _context->height, + ::ffmpeg::tools::get_pixel_format_name(_context->sw_pix_fmt), + ::ffmpeg::tools::get_color_space_name(_context->colorspace), + av_color_range_name(_context->color_range)); + } else { + LOG_INFO("[%s] Input: %ldx%ld %s %s %s", _codec->name, _swscale.get_source_width(), + _swscale.get_source_height(), ::ffmpeg::tools::get_pixel_format_name(_swscale.get_source_format()), + ::ffmpeg::tools::get_color_space_name(_swscale.get_source_colorspace()), + _swscale.is_source_full_range() ? "Full" : "Partial"); + LOG_INFO("[%s] Output: %ldx%ld %s %s %s", _codec->name, _swscale.get_target_width(), + _swscale.get_target_height(), ::ffmpeg::tools::get_pixel_format_name(_swscale.get_target_format()), + ::ffmpeg::tools::get_color_space_name(_swscale.get_target_colorspace()), + _swscale.is_target_full_range() ? "Full" : "Partial"); + if (!_hwinst) + LOG_INFO("[%s] On GPU Index: %lli", _codec->name, obs_data_get_int(settings, ST_FFMPEG_GPU)); + } + LOG_INFO("[%s] Framerate: %ld/%ld (%f FPS)", _codec->name, _context->time_base.den, _context->time_base.num, + static_cast(_context->time_base.den) / static_cast(_context->time_base.num)); + + LOG_INFO("[%s] Keyframes: ", _codec->name); + if (_context->keyint_min != _context->gop_size) { + LOG_INFO("[%s] Minimum: %i frames", _codec->name, _context->keyint_min); + LOG_INFO("[%s] Maximum: %i frames", _codec->name, _context->gop_size); + } else { + LOG_INFO("[%s] Distance: %i frames", _codec->name, _context->gop_size); + } + _handler->log_options(settings, _codec, _context); + } + + return true; +} + +void ffmpeg_instance::get_audio_info(audio_convert_info*) {} + +size_t ffmpeg_instance::get_frame_size() +{ + return size_t(); +} + +bool ffmpeg_instance::audio_encode(encoder_frame*, encoder_packet*, bool*) +{ + return false; +} + +void ffmpeg_instance::get_video_info(video_scale_info* vsi) +{ + vsi->width = _swscale.get_source_width(); + vsi->height = _swscale.get_source_height(); + vsi->format = ::ffmpeg::tools::avpixelformat_to_obs_videoformat(_swscale.get_source_format()); +} + +bool ffmpeg_instance::get_sei_data(uint8_t** data, size_t* size) +{ + if (_sei_data.size() == 0) + return false; + + *data = _sei_data.data(); + *size = _sei_data.size(); + return true; +} + +bool ffmpeg_instance::get_extra_data(uint8_t** data, size_t* size) +{ + if (_extra_data.size() == 0) + return false; + + *data = _extra_data.data(); + *size = _extra_data.size(); + return true; +} + +static inline void copy_data(encoder_frame* frame, AVFrame* vframe) +{ + int h_chroma_shift, v_chroma_shift; + av_pix_fmt_get_chroma_sub_sample(static_cast(vframe->format), &h_chroma_shift, &v_chroma_shift); + + for (size_t idx = 0; idx < MAX_AV_PLANES; idx++) { + if (!frame->data[idx] || !vframe->data[idx]) + continue; + + size_t plane_height = vframe->height >> (idx ? v_chroma_shift : 0); + + if (static_cast(vframe->linesize[idx]) == frame->linesize[idx]) { + std::memcpy(vframe->data[idx], frame->data[idx], frame->linesize[idx] * plane_height); + } else { + size_t ls_in = frame->linesize[idx]; + size_t ls_out = vframe->linesize[idx]; + size_t bytes = ls_in < ls_out ? ls_in : ls_out; + + uint8_t* to = vframe->data[idx]; + uint8_t* from = frame->data[idx]; + + for (size_t y = 0; y < plane_height; y++) { + std::memcpy(to, from, bytes); + to += ls_out; + from += ls_in; + } + } + } +} + +bool ffmpeg_instance::video_encode(encoder_frame* frame, encoder_packet* packet, bool* received_packet) +{ + std::shared_ptr vframe = pop_free_frame(); // Retrieve an empty frame. + + // Convert frame. + { + vframe->height = _context->height; + vframe->format = _context->pix_fmt; + vframe->color_range = _context->color_range; + vframe->colorspace = _context->colorspace; + vframe->color_primaries = _context->color_primaries; + vframe->color_trc = _context->color_trc; + vframe->pts = frame->pts; + + if ((_swscale.is_source_full_range() == _swscale.is_target_full_range()) + && (_swscale.get_source_colorspace() == _swscale.get_target_colorspace()) + && (_swscale.get_source_format() == _swscale.get_target_format())) { + copy_data(frame, vframe.get()); + } else { + int res = + _swscale.convert(reinterpret_cast(frame->data), reinterpret_cast(frame->linesize), 0, + _context->height, vframe->data, vframe->linesize); + if (res <= 0) { + LOG_ERROR("Failed to convert frame: %s (%ld).", ::ffmpeg::tools::get_error_description(res), res); + return false; + } + } + } + + if (!encode_avframe(vframe, packet, received_packet)) + return false; + + return true; +} + +bool ffmpeg_instance::video_encode_texture(uint32_t handle, int64_t pts, uint64_t lock_key, uint64_t* next_lock_key, + encoder_packet* packet, bool* received_packet) +{ + if (handle == GS_INVALID_HANDLE) { + LOG_ERROR("Received invalid handle."); + *next_lock_key = lock_key; + return false; + } + + std::shared_ptr vframe = pop_free_frame(); + _hwinst->copy_from_obs(_context->hw_frames_ctx, handle, lock_key, next_lock_key, vframe); + + vframe->color_range = _context->color_range; + vframe->colorspace = _context->colorspace; + vframe->color_primaries = _context->color_primaries; + vframe->color_trc = _context->color_trc; + vframe->pts = pts; + + if (!encode_avframe(vframe, packet, received_packet)) + return false; + + *next_lock_key = lock_key; + + return true; +} + +int ffmpeg_instance::receive_packet(bool* received_packet, struct encoder_packet* packet) +{ + int res = 0; + + av_packet_unref(&_current_packet); + + { + auto gctx = util::obs_graphics(); + res = avcodec_receive_packet(_context, &_current_packet); + } + if (res != 0) { + return res; + } + + if (!_have_first_frame) { + if (_codec->id == AV_CODEC_ID_H264) { + uint8_t* tmp_packet; + uint8_t* tmp_header; + uint8_t* tmp_sei; + size_t sz_packet, sz_header, sz_sei; + + obs_extract_avc_headers(_current_packet.data, _current_packet.size, &tmp_packet, &sz_packet, &tmp_header, + &sz_header, &tmp_sei, &sz_sei); + + if (sz_header) { + _extra_data.resize(sz_header); + std::memcpy(_extra_data.data(), tmp_header, sz_header); + } + + if (sz_sei) { + _sei_data.resize(sz_sei); + std::memcpy(_sei_data.data(), tmp_sei, sz_sei); + } + + // Not required, we only need the Extra Data and SEI Data anyway. + //std::memcpy(_current_packet.data, tmp_packet, sz_packet); + //_current_packet.size = static_cast(sz_packet); + + bfree(tmp_packet); + bfree(tmp_header); + bfree(tmp_sei); + } else if (_codec->id == AV_CODEC_ID_HEVC) { + ::encoder::codec::hevc::extract_header_sei(_current_packet.data, _current_packet.size, _extra_data, + _sei_data); + } else if (_context->extradata != nullptr) { + _extra_data.resize(_context->extradata_size); + std::memcpy(_extra_data.data(), _context->extradata, _context->extradata_size); + } + _have_first_frame = true; + } + + // Allow Handler Post-Processing + if (_handler) + _handler->process_avpacket(_current_packet, _codec, _context); + + packet->type = OBS_ENCODER_VIDEO; + packet->pts = _current_packet.pts; + packet->dts = _current_packet.dts; + packet->data = _current_packet.data; + packet->size = _current_packet.size; + packet->keyframe = !!(_current_packet.flags & AV_PKT_FLAG_KEY); + packet->drop_priority = packet->keyframe ? 0 : 1; + *received_packet = true; + + push_free_frame(pop_used_frame()); + + return res; +} + +int ffmpeg_instance::send_frame(std::shared_ptr const frame) +{ + int res = 0; + { + auto gctx = util::obs_graphics(); + res = avcodec_send_frame(_context, frame.get()); + } + if (res == 0) { + push_used_frame(frame); + } + + return res; +} + +bool ffmpeg_instance::encode_avframe(std::shared_ptr frame, encoder_packet* packet, bool* received_packet) +{ + bool sent_frame = false; + bool recv_packet = false; + bool should_lag = (_count_send_frames >= _lag_in_frames); + + auto loop_begin = std::chrono::high_resolution_clock::now(); + auto loop_end = loop_begin + std::chrono::milliseconds(50); + + while ((!sent_frame || (should_lag && !recv_packet)) && !(std::chrono::high_resolution_clock::now() > loop_end)) { + bool eagain_is_stupid = false; + + if (!sent_frame) { + int res = send_frame(frame); + switch (res) { + case 0: + sent_frame = true; + frame = nullptr; + break; + case AVERROR(EAGAIN): + // This means we should call receive_packet again, but what do we do with that data? + // Why can't we queue on both? Do I really have to implement threading for this stuff? + if (*received_packet == true) { + LOG_WARNING("Skipped frame due to EAGAIN when a packet was already returned."); + sent_frame = true; + } + eagain_is_stupid = true; + break; + case AVERROR(EOF): + LOG_ERROR("Skipped frame due to end of stream."); + sent_frame = true; + break; + default: + LOG_ERROR("Failed to encode frame: %s (%ld).", ::ffmpeg::tools::get_error_description(res), res); + return false; + } + } + + if (!recv_packet) { + int res = receive_packet(received_packet, packet); + switch (res) { + case 0: + recv_packet = true; + break; + case AVERROR(EOF): + LOG_ERROR("Received end of file."); + recv_packet = true; + break; + case AVERROR(EAGAIN): + if (sent_frame) { + recv_packet = true; + } + if (eagain_is_stupid) { + LOG_ERROR("Both send and recieve returned EAGAIN, encoder is broken."); + return false; + } + break; + default: + LOG_ERROR("Failed to receive packet: %s (%ld).", ::ffmpeg::tools::get_error_description(res), res); + return false; + } + } + + if (!sent_frame || !recv_packet) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + if (!sent_frame) + push_free_frame(frame); + + return true; +} + +bool ffmpeg_instance::is_hardware_encode() +{ + return _hwinst != nullptr; +} + +const AVCodec* ffmpeg_instance::get_avcodec() +{ + return _codec; +} + +const AVCodecContext* ffmpeg_instance::get_avcodeccontext() +{ + return _context; +} + +void ffmpeg_instance::parse_ffmpeg_commandline(std::string text) +{ + // Steps to properly parse a command line: + // 1. Split by space and package by quotes. + // 2. Parse each resulting option individually. + + // First, we split by space and of course respect quotes while doing so. + // That means that "-foo= bar" is stored as std::string("-foo= bar"), + // and things like -foo="bar" is stored as std::string("-foo=\"bar\""). + // However "-foo"=bar" -foo2=bar" is stored as std::string("-foo=bar -foo2=bar") + // because the quote was not escaped. + std::list opts; + std::stringstream opt_stream{std::ios_base::in | std::ios_base::out | std::ios_base::binary}; + std::stack quote_stack; + for (size_t p = 0; p <= text.size(); p++) { + char here = p < text.size() ? text.at(p) : 0; + + if (here == '\\') { + size_t p2 = p + 1; + if (p2 < text.size()) { + char here2 = text.at(p2); + if (isdigit(here2)) { // Octal + // Not supported yet. + p++; + } else if (here2 == 'x') { // Hexadecimal + // Not supported yet. + p += 3; + } else if (here2 == 'u') { // 4 or 8 wide Unicode. + // Not supported yet. + } else if (here2 == 'a') { + opt_stream << '\a'; + p++; + } else if (here2 == 'b') { + opt_stream << '\b'; + p++; + } else if (here2 == 'f') { + opt_stream << '\f'; + p++; + } else if (here2 == 'n') { + opt_stream << '\n'; + p++; + } else if (here2 == 'r') { + opt_stream << '\r'; + p++; + } else if (here2 == 't') { + opt_stream << '\t'; + p++; + } else if (here2 == 'v') { + opt_stream << '\v'; + p++; + } else if (here2 == '\\') { + opt_stream << '\\'; + p++; + } else if (here2 == '\'') { + opt_stream << '\''; + p++; + } else if (here2 == '"') { + opt_stream << '"'; + p++; + } else if (here2 == '?') { + opt_stream << '\?'; + p++; + } + } + } else if ((here == '\'') || (here == '"')) { + if (quote_stack.size() > 1) { + opt_stream << here; + } + if (quote_stack.size() == 0) { + quote_stack.push(here); + } else if (quote_stack.top() == here) { + quote_stack.pop(); + } else { + quote_stack.push(here); + } + } else if ((here == 0) || ((here == ' ') && (quote_stack.size() == 0))) { + std::string ropt = opt_stream.str(); + if (ropt.size() > 0) { + opts.push_back(ropt); + opt_stream.str(std::string()); + opt_stream.clear(); + } + } else { + opt_stream << here; + } + } + + // Now that we have a list of parameters as neatly grouped strings, and + // have also dealt with escaping for the most part. We want to parse + // an FFmpeg commandline option set here, so the first character in + // the string must be a '-'. + for (std::string& opt : opts) { + // Skip empty options. + if (opt.size() == 0) + continue; + + // Skip options that don't start with a '-'. + if (opt.at(0) != '-') { + LOG_WARNING("Option '%s' is malformed, must start with a '-'.", opt.c_str()); + continue; + } + + // Skip options that don't contain a '='. + const char* cstr = opt.c_str(); + const char* eq_at = strchr(cstr, '='); + if (eq_at == nullptr) { + LOG_WARNING("Option '%s' is malformed, must contain a '='.", opt.c_str()); + continue; + } + + try { + std::string key = opt.substr(1, eq_at - cstr - 1); + std::string value = opt.substr(eq_at - cstr + 1); + + int res = av_opt_set(_context, key.c_str(), value.c_str(), AV_OPT_SEARCH_CHILDREN); + if (res < 0) { + LOG_WARNING("Option '%s' (key: '%s', value: '%s') encountered error: %s", opt.c_str(), key.c_str(), + value.c_str(), ::ffmpeg::tools::get_error_description(res)); + } + } catch (const std::exception& ex) { + LOG_ERROR("Option '%s' encountered exception: %s", opt.c_str(), ex.what()); + } + } +} diff --git a/source/encoders/ffmpeg-encoder.hpp b/source/encoders/ffmpeg-encoder.hpp new file mode 100644 index 00000000..09ad60b4 --- /dev/null +++ b/source/encoders/ffmpeg-encoder.hpp @@ -0,0 +1,221 @@ +// FFMPEG Video Encoder Integration for OBS Studio +// Copyright (c) 2019 Michael Fabian Dirks +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ffmpeg/avframe-queue.hpp" +#include "ffmpeg/hwapi/base.hpp" +#include "ffmpeg/swscale.hpp" +#include "handlers/handler.hpp" + +extern "C" { +#include +#include +#pragma warning(push) +#pragma warning(disable : 4244) +#include +#include +#pragma warning(pop) +} + +#define S_RATECONTROL "FFmpeg.RateControl" +#define S_RATECONTROL_MODE "FFmpeg.RateControl.Mode" +#define S_RATECONTROL_MODE_(x) "FFmpeg.RateControl.Mode." D_VSTR(x) +#define S_RATECONTROL_BITRATE_TARGET "FFmpeg.RateControl.Bitrate.Target" +#define S_RATECONTROL_BITRATE_MINIMUM "FFmpeg.RateControl.Bitrate.Minimum" +#define S_RATECONTROL_BITRATE_MAXIMUM "FFmpeg.RateControl.Bitrate.Maximum" +#define S_RATECONTROL_BUFFERSIZE "FFmpeg.RateControl.BufferSize" +#define S_RATECONTROL_QUALITY_TARGET "RateControl.Quality.Target" +#define S_RATECONTROL_QUALITY_MINIMUM "RateControl.Quality.Minimum" +#define S_RATECONTROL_QUALITY_MAXIMUM "RateControl.Quality.Maximum" +#define S_RATECONTROL_QP_I "RateControl.QP.I" +#define S_RATECONTROL_QP_P "RateControl.QP.P" +#define S_RATECONTROL_QP_B "RateControl.QP.B" +#define S_RATECONTROL_QP_I_INITIAL "RateControl.QP.I.Initial" +#define S_RATECONTROL_QP_P_INITIAL "RateControl.QP.P.Initial" +#define S_RATECONTROL_QP_B_INITIAL "RateControl.QP.B.Initial" + +#define S_KEYFRAMES "KeyFrames" +#define S_KEYFRAMES_INTERVALTYPE "KeyFrames.IntervalType" +#define S_KEYFRAMES_INTERVALTYPE_(x) "KeyFrames.IntervalType." D_VSTR(x) +#define S_KEYFRAMES_INTERVAL "KeyFrames.Interval" +#define S_KEYFRAMES_INTERVAL_SECONDS "KeyFrames.Interval.Seconds" +#define S_KEYFRAMES_INTERVAL_FRAMES "KeyFrames.Interval.Frames" + +namespace encoder::ffmpeg { + class ffmpeg_factory; + + class ffmpeg_manager { + static ::std::shared_ptr _instance; + + public: // Singleton + static void initialize() + { + _instance = ::std::make_shared(); + } + + static void finalize() + { + _instance.reset(); + } + + static std::shared_ptr get() + { + return _instance; + } + + private: + std::map> _factories; + std::map> _handlers; + std::shared_ptr _debug_handler; + + public: + ffmpeg_manager(); + ~ffmpeg_manager(); + + void register_handler(std::string const codec, std::shared_ptr const handler); + + std::shared_ptr const get_handler(std::string const codec); + + bool has_handler(std::string const codec); + }; + + struct ffmpeg_info { + std::string uid; + std::string codec; + std::string readable_name; + obs_encoder_info oei = {0}; + }; + + class ffmpeg_factory { + ffmpeg_info info; + ffmpeg_info info_fallback; + const AVCodec* avcodec_ptr; + + std::shared_ptr _handler; + + public: + ffmpeg_factory(const AVCodec* codec); + virtual ~ffmpeg_factory(); + + void register_encoder(); + + void get_defaults(obs_data_t* settings, bool hw_encoder = false); + + void get_properties(obs_properties_t* props, bool hw_encoder = false); + + const AVCodec* get_avcodec(); + + const ffmpeg_info& get_info(); + + const ffmpeg_info& get_fallback(); + }; + + class ffmpeg_instance { + obs_encoder_t* _self; + ffmpeg_factory* _factory; + + const AVCodec* _codec; + AVCodecContext* _context; + + std::shared_ptr _handler; + + std::shared_ptr<::ffmpeg::hwapi::base> _hwapi; + std::shared_ptr<::ffmpeg::hwapi::instance> _hwinst; + + ::ffmpeg::swscale _swscale; + AVPacket _current_packet; + + size_t _lag_in_frames; + size_t _count_send_frames; + + // Extra Data + bool _have_first_frame; + std::vector _extra_data; + std::vector _sei_data; + + // Frame Stack and Queue + std::stack> _free_frames; + std::queue> _used_frames; + std::chrono::high_resolution_clock::time_point _free_frames_last_used; + + void initialize_sw(obs_data_t* settings); + void initialize_hw(obs_data_t* settings); + + void push_free_frame(std::shared_ptr frame); + std::shared_ptr pop_free_frame(); + + void push_used_frame(std::shared_ptr frame); + std::shared_ptr pop_used_frame(); + + public: + ffmpeg_instance(obs_data_t* settings, obs_encoder_t* encoder, bool is_texture_encode = false); + virtual ~ffmpeg_instance(); + + public: // OBS API + // Shared + void get_properties(obs_properties_t* props, bool hw_encode = false); + + bool update(obs_data_t* settings); + + // Audio only + void get_audio_info(struct audio_convert_info* info); + + size_t get_frame_size(); + + bool audio_encode(struct encoder_frame* frame, struct encoder_packet* packet, bool* received_packet); + + // 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); + + bool video_encode(struct encoder_frame* frame, struct encoder_packet* packet, bool* received_packet); + + bool video_encode_texture(uint32_t handle, int64_t pts, uint64_t lock_key, uint64_t* next_key, + struct encoder_packet* packet, bool* received_packet); + + int receive_packet(bool* received_packet, struct encoder_packet* packet); + + int send_frame(std::shared_ptr frame); + + bool encode_avframe(std::shared_ptr frame, struct encoder_packet* packet, bool* received_packet); + + public: // Handler API + bool is_hardware_encode(); + + const AVCodec* get_avcodec(); + + const AVCodecContext* get_avcodeccontext(); + + void parse_ffmpeg_commandline(std::string text); + }; +} // namespace encoder::ffmpeg diff --git a/source/encoders/handlers/nvenc_h264_handler.cpp b/source/encoders/handlers/nvenc_h264_handler.cpp index 11abf6c9..5c2e3141 100644 --- a/source/encoders/handlers/nvenc_h264_handler.cpp +++ b/source/encoders/handlers/nvenc_h264_handler.cpp @@ -36,6 +36,9 @@ extern "C" { #pragma warning(pop) } +#define KEY_PROFILE "H264.Profile" +#define KEY_LEVEL "H264.Level" + using namespace encoder::ffmpeg::handler; using namespace encoder::codec::h264; @@ -55,16 +58,17 @@ std::map levels{ void nvenc_h264_handler::adjust_encoder_info(ffmpeg_factory*, ffmpeg_info* main, ffmpeg_info* fallback) { - main->readable_name = "H.264/AVC NVidia NVENC (Hardware)"; - fallback->readable_name = "H.264/AVC NVidia NVENC (Software)"; + main->readable_name = "H.264/AVC NVidia NVENC"; + fallback->readable_name = "H.264/AVC NVidia NVENC (Software Fallback)"; + fallback->oei.caps |= OBS_ENCODER_CAP_DEPRECATED; } void nvenc_h264_handler::get_defaults(obs_data_t* settings, const AVCodec* codec, AVCodecContext* context, bool) { nvenc::get_defaults(settings, codec, context); - obs_data_set_default_int(settings, P_H264_PROFILE, static_cast(profile::HIGH)); - obs_data_set_default_int(settings, P_H264_LEVEL, static_cast(level::UNKNOWN)); + obs_data_set_default_int(settings, KEY_PROFILE, static_cast(profile::HIGH)); + obs_data_set_default_int(settings, KEY_LEVEL, static_cast(level::UNKNOWN)); } bool nvenc_h264_handler::has_keyframe_support(ffmpeg_instance*) @@ -86,14 +90,14 @@ void nvenc_h264_handler::update(obs_data_t* settings, const AVCodec* codec, AVCo nvenc::update(settings, codec, context); { - auto found = profiles.find(static_cast(obs_data_get_int(settings, P_H264_PROFILE))); + auto found = profiles.find(static_cast(obs_data_get_int(settings, KEY_PROFILE))); if (found != profiles.end()) { av_opt_set(context->priv_data, "profile", found->second.c_str(), 0); } } { - auto found = levels.find(static_cast(obs_data_get_int(settings, P_H264_LEVEL))); + auto found = levels.find(static_cast(obs_data_get_int(settings, KEY_LEVEL))); if (found != levels.end()) { av_opt_set(context->priv_data, "level", found->second.c_str(), 0); } else { @@ -111,7 +115,7 @@ void nvenc_h264_handler::log_options(obs_data_t* settings, const AVCodec* codec, { nvenc::log_options(settings, codec, context); - LOG_INFO("[%s] H.265/HEVC:", codec->name); + LOG_INFO("[%s] H.264/AVC:", codec->name); ::ffmpeg::tools::print_av_option_string(context, "profile", " Profile", [](int64_t v) { profile val = static_cast(v); auto index = profiles.find(val); @@ -140,7 +144,7 @@ void nvenc_h264_handler::get_encoder_properties(obs_properties_t* props, const A } { - auto p = obs_properties_add_list(grp, P_H264_PROFILE, D_TRANSLATE(P_H264_PROFILE), OBS_COMBO_TYPE_LIST, + auto p = obs_properties_add_list(grp, KEY_PROFILE, D_TRANSLATE(P_H264_PROFILE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(P_H264_PROFILE))); obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DEFAULT), static_cast(profile::UNKNOWN)); @@ -150,7 +154,7 @@ void nvenc_h264_handler::get_encoder_properties(obs_properties_t* props, const A } } { - auto p = obs_properties_add_list(grp, P_H264_LEVEL, D_TRANSLATE(P_H264_LEVEL), OBS_COMBO_TYPE_LIST, + auto p = obs_properties_add_list(grp, KEY_LEVEL, D_TRANSLATE(P_H264_LEVEL), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(P_H264_LEVEL))); obs_property_list_add_int(p, D_TRANSLATE(S_STATE_AUTOMATIC), static_cast(level::UNKNOWN)); diff --git a/source/encoders/handlers/nvenc_hevc_handler.cpp b/source/encoders/handlers/nvenc_hevc_handler.cpp index 7c677837..bf07a42d 100644 --- a/source/encoders/handlers/nvenc_hevc_handler.cpp +++ b/source/encoders/handlers/nvenc_hevc_handler.cpp @@ -36,6 +36,10 @@ extern "C" { #pragma warning(pop) } +#define KEY_PROFILE "H265.Profile" +#define KEY_TIER "H265.Tier" +#define KEY_LEVEL "H265.Level" + using namespace encoder::ffmpeg::handler; using namespace encoder::codec::hevc; @@ -58,17 +62,18 @@ std::map levels{ void nvenc_hevc_handler::adjust_encoder_info(ffmpeg_factory*, ffmpeg_info* main, ffmpeg_info* fallback) { - main->readable_name = "H.265/HEVC Nvidia NVENC (Hardware)"; - fallback->readable_name = "H.265/HEVC Nvidia NVENC (Software)"; + main->readable_name = "H.265/HEVC Nvidia NVENC"; + fallback->readable_name = "H.265/HEVC Nvidia NVENC (Software Fallback)"; + fallback->oei.caps |= OBS_ENCODER_CAP_DEPRECATED; } void nvenc_hevc_handler::get_defaults(obs_data_t* settings, const AVCodec* codec, AVCodecContext* context, bool) { nvenc::get_defaults(settings, codec, context); - obs_data_set_default_int(settings, P_HEVC_PROFILE, static_cast(profile::MAIN)); - obs_data_set_default_int(settings, P_HEVC_TIER, static_cast(profile::MAIN)); - obs_data_set_default_int(settings, P_HEVC_LEVEL, static_cast(level::UNKNOWN)); + obs_data_set_default_int(settings, KEY_PROFILE, static_cast(profile::MAIN)); + obs_data_set_default_int(settings, KEY_TIER, static_cast(profile::MAIN)); + obs_data_set_default_int(settings, KEY_LEVEL, static_cast(level::UNKNOWN)); } bool nvenc_hevc_handler::has_keyframe_support(ffmpeg_instance*) @@ -90,19 +95,19 @@ void nvenc_hevc_handler::update(obs_data_t* settings, const AVCodec* codec, AVCo nvenc::update(settings, codec, context); { // HEVC Options - auto found = profiles.find(static_cast(obs_data_get_int(settings, P_HEVC_PROFILE))); + auto found = profiles.find(static_cast(obs_data_get_int(settings, KEY_PROFILE))); if (found != profiles.end()) { av_opt_set(context->priv_data, "profile", found->second.c_str(), 0); } } { - auto found = tiers.find(static_cast(obs_data_get_int(settings, P_HEVC_TIER))); + auto found = tiers.find(static_cast(obs_data_get_int(settings, KEY_TIER))); if (found != tiers.end()) { av_opt_set(context->priv_data, "tier", found->second.c_str(), 0); } } { - auto found = levels.find(static_cast(obs_data_get_int(settings, P_HEVC_LEVEL))); + auto found = levels.find(static_cast(obs_data_get_int(settings, KEY_LEVEL))); if (found != levels.end()) { av_opt_set(context->priv_data, "level", found->second.c_str(), 0); } else { @@ -156,7 +161,7 @@ void nvenc_hevc_handler::get_encoder_properties(obs_properties_t* props, const A } { - auto p = obs_properties_add_list(grp, P_HEVC_PROFILE, D_TRANSLATE(P_HEVC_PROFILE), OBS_COMBO_TYPE_LIST, + auto p = obs_properties_add_list(grp, KEY_PROFILE, D_TRANSLATE(P_HEVC_PROFILE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(P_HEVC_PROFILE))); obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DEFAULT), static_cast(profile::UNKNOWN)); @@ -166,7 +171,7 @@ void nvenc_hevc_handler::get_encoder_properties(obs_properties_t* props, const A } } { - auto p = obs_properties_add_list(grp, P_HEVC_TIER, D_TRANSLATE(P_HEVC_TIER), OBS_COMBO_TYPE_LIST, + auto p = obs_properties_add_list(grp, KEY_TIER, D_TRANSLATE(P_HEVC_TIER), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(P_HEVC_TIER))); obs_property_list_add_int(p, D_TRANSLATE(S_STATE_DEFAULT), static_cast(tier::UNKNOWN)); @@ -176,7 +181,7 @@ void nvenc_hevc_handler::get_encoder_properties(obs_properties_t* props, const A } } { - auto p = obs_properties_add_list(grp, P_HEVC_LEVEL, D_TRANSLATE(P_HEVC_LEVEL), OBS_COMBO_TYPE_LIST, + auto p = obs_properties_add_list(grp, KEY_LEVEL, D_TRANSLATE(P_HEVC_LEVEL), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(P_HEVC_LEVEL))); obs_property_list_add_int(p, D_TRANSLATE(S_STATE_AUTOMATIC), static_cast(level::UNKNOWN)); diff --git a/source/encoders/handlers/nvenc_shared.cpp b/source/encoders/handlers/nvenc_shared.cpp index 768de3af..71ce3312 100644 --- a/source/encoders/handlers/nvenc_shared.cpp +++ b/source/encoders/handlers/nvenc_shared.cpp @@ -36,45 +36,60 @@ extern "C" { #pragma warning(pop) } -#define ST_PRESET "NVENC.Preset" +#define ST_PRESET "Encoder.NVENC.Preset" #define ST_PRESET_(x) ST_PRESET "." D_VSTR(x) - -#define ST_RATECONTROL "NVENC.RateControl" +#define ST_RATECONTROL "Encoder.NVENC.RateControl" #define ST_RATECONTROL_MODE ST_RATECONTROL ".Mode" #define ST_RATECONTROL_MODE_(x) ST_RATECONTROL_MODE "." D_VSTR(x) #define ST_RATECONTROL_TWOPASS ST_RATECONTROL ".TwoPass" #define ST_RATECONTROL_LOOKAHEAD ST_RATECONTROL ".LookAhead" #define ST_RATECONTROL_ADAPTIVEI ST_RATECONTROL ".AdaptiveI" #define ST_RATECONTROL_ADAPTIVEB ST_RATECONTROL ".AdaptiveB" - #define ST_RATECONTROL_BITRATE ST_RATECONTROL ".Bitrate" #define ST_RATECONTROL_BITRATE_TARGET ST_RATECONTROL_BITRATE ".Target" #define ST_RATECONTROL_BITRATE_MAXIMUM ST_RATECONTROL_BITRATE ".Maximum" - #define ST_RATECONTROL_QUALITY ST_RATECONTROL ".Quality" #define ST_RATECONTROL_QUALITY_MINIMUM ST_RATECONTROL_QUALITY ".Minimum" #define ST_RATECONTROL_QUALITY_MAXIMUM ST_RATECONTROL_QUALITY ".Maximum" #define ST_RATECONTROL_QUALITY_TARGET ST_RATECONTROL_QUALITY ".Target" - #define ST_RATECONTROL_QP ST_RATECONTROL ".QP" #define ST_RATECONTROL_QP_I ST_RATECONTROL_QP ".I" -#define ST_RATECONTROL_QP_I_INITIAL ST_RATECONTROL_QP_I ".Initial" #define ST_RATECONTROL_QP_P ST_RATECONTROL_QP ".P" -#define ST_RATECONTROL_QP_P_INITIAL ST_RATECONTROL_QP_P ".Initial" #define ST_RATECONTROL_QP_B ST_RATECONTROL_QP ".B" -#define ST_RATECONTROL_QP_B_INITIAL ST_RATECONTROL_QP_B ".Initial" - -#define ST_AQ "NVENC.AQ" +#define ST_AQ "Encoder.NVENC.AQ" #define ST_AQ_SPATIAL ST_AQ ".Spatial" #define ST_AQ_TEMPORAL ST_AQ ".Temporal" #define ST_AQ_STRENGTH ST_AQ ".Strength" - -#define ST_OTHER "NVENC.Other" +#define ST_OTHER "Encoder.NVENC.Other" #define ST_OTHER_BFRAMES ST_OTHER ".BFrames" -#define ST_OTHER_BFRAME_REFERENCEMODE ST_OTHER ".BFrameReferenceMode" +#define ST_OTHER_BFRAMEREFERENCEMODE ST_OTHER ".BFrameReferenceMode" #define ST_OTHER_ZEROLATENCY ST_OTHER ".ZeroLatency" -#define ST_OTHER_WEIGHTED_PREDICTION ST_OTHER ".WeightedPrediction" -#define ST_OTHER_NONREFERENCE_PFRAMES ST_OTHER ".NonReferencePFrames" +#define ST_OTHER_WEIGHTEDPREDICTION ST_OTHER ".WeightedPrediction" +#define ST_OTHER_NONREFERENCEPFRAMES ST_OTHER ".NonReferencePFrames" + +#define KEY_PRESET "Preset" +#define KEY_RATECONTROL_MODE "RateControl.Mode" +#define KEY_RATECONTROL_TWOPASS "RateControl.TwoPass" +#define KEY_RATECONTROL_LOOKAHEAD "RateControl.LookAhead" +#define KEY_RATECONTROL_ADAPTIVEI "RateControl.AdaptiveI" +#define KEY_RATECONTROL_ADAPTIVEB "RateControl.AdaptiveB" +#define KEY_RATECONTROL_BITRATE_TARGET "RateControl.Bitrate.Target" +#define KEY_RATECONTROL_BITRATE_MAXIMUM "RateControl.Bitrate.Maximum" +#define KEY_RATECONTROL_QUALITY "RateControl.Quality" +#define KEY_RATECONTROL_QUALITY_TARGET "RateControl.Quality.Target" +#define KEY_RATECONTROL_QUALITY_MINIMUM "RateControl.Quality.Minimum" +#define KEY_RATECONTROL_QUALITY_MAXIMUM "RateControl.Quality.Maximum" +#define KEY_RATECONTROL_QP_I "RateControl.QP.I" +#define KEY_RATECONTROL_QP_P "RateControl.QP.P" +#define KEY_RATECONTROL_QP_B "RateControl.QP.B" +#define KEY_AQ_SPATIAL "AQ.Spatial" +#define KEY_AQ_TEMPORAL "AQ.Temporal" +#define KEY_AQ_STRENGTH "AQ.Strength" +#define KEY_OTHER_BFRAMES "Other.BFrames" +#define KEY_OTHER_BFRAMEREFERENCEMODE "Other.BFrameReferenceMode" +#define KEY_OTHER_ZEROLATENCY "Other.ZeroLatency" +#define KEY_OTHER_WEIGHTEDPREDICTION "Other.WeightedPrediction" +#define KEY_OTHER_NONREFERENCEPFRAMES "Other.NonReferencePFrames" using namespace encoder::ffmpeg::handler; using namespace ffmpeg; @@ -126,8 +141,8 @@ std::map nvenc::ratecontrolmode_to_opt{ std::map nvenc::b_ref_modes{ {nvenc::b_ref_mode::DISABLED, S_STATE_DISABLED}, - {nvenc::b_ref_mode::EACH, ST_OTHER_BFRAME_REFERENCEMODE ".Each"}, - {nvenc::b_ref_mode::MIDDLE, ST_OTHER_BFRAME_REFERENCEMODE ".Middle"}, + {nvenc::b_ref_mode::EACH, ST_OTHER_BFRAMEREFERENCEMODE ".Each"}, + {nvenc::b_ref_mode::MIDDLE, ST_OTHER_BFRAMEREFERENCEMODE ".Middle"}, }; std::map nvenc::b_ref_mode_to_opt{ @@ -168,38 +183,36 @@ void nvenc::override_update(ffmpeg_instance* instance, obs_data_t*) void nvenc::get_defaults(obs_data_t* settings, const AVCodec*, AVCodecContext*) { - obs_data_set_default_int(settings, ST_PRESET, static_cast(nvenc::preset::DEFAULT)); + obs_data_set_default_int(settings, KEY_PRESET, static_cast(nvenc::preset::DEFAULT)); - obs_data_set_default_int(settings, ST_RATECONTROL_MODE, static_cast(ratecontrolmode::CBR_HQ)); - obs_data_set_default_int(settings, ST_RATECONTROL_TWOPASS, -1); - obs_data_set_default_int(settings, ST_RATECONTROL_LOOKAHEAD, 0); - obs_data_set_default_int(settings, ST_RATECONTROL_ADAPTIVEI, -1); - obs_data_set_default_int(settings, ST_RATECONTROL_ADAPTIVEB, -1); + obs_data_set_default_int(settings, KEY_RATECONTROL_MODE, static_cast(ratecontrolmode::CBR_HQ)); + obs_data_set_default_int(settings, KEY_RATECONTROL_TWOPASS, -1); + obs_data_set_default_int(settings, KEY_RATECONTROL_LOOKAHEAD, 0); + obs_data_set_default_int(settings, KEY_RATECONTROL_ADAPTIVEI, -1); + obs_data_set_default_int(settings, KEY_RATECONTROL_ADAPTIVEB, -1); - obs_data_set_default_int(settings, ST_RATECONTROL_BITRATE_TARGET, 6000); - obs_data_set_default_int(settings, ST_RATECONTROL_BITRATE_MAXIMUM, 6000); + obs_data_set_default_int(settings, KEY_RATECONTROL_BITRATE_TARGET, 6000); + obs_data_set_default_int(settings, KEY_RATECONTROL_BITRATE_MAXIMUM, 6000); obs_data_set_default_int(settings, S_RATECONTROL_BUFFERSIZE, 12000); - obs_data_set_default_int(settings, ST_RATECONTROL_QUALITY_MINIMUM, 51); - obs_data_set_default_int(settings, ST_RATECONTROL_QUALITY_MAXIMUM, -1); - obs_data_set_default_int(settings, ST_RATECONTROL_QUALITY_TARGET, 0); + obs_data_set_default_bool(settings, KEY_RATECONTROL_QUALITY, false); + obs_data_set_default_int(settings, KEY_RATECONTROL_QUALITY_MINIMUM, 51); + obs_data_set_default_int(settings, KEY_RATECONTROL_QUALITY_MAXIMUM, -1); + obs_data_set_default_int(settings, KEY_RATECONTROL_QUALITY_TARGET, 0); - obs_data_set_default_int(settings, ST_RATECONTROL_QP_I, 21); - obs_data_set_default_int(settings, ST_RATECONTROL_QP_I_INITIAL, -1); - obs_data_set_default_int(settings, ST_RATECONTROL_QP_P, 21); - obs_data_set_default_int(settings, ST_RATECONTROL_QP_P_INITIAL, -1); - obs_data_set_default_int(settings, ST_RATECONTROL_QP_B, 21); - obs_data_set_default_int(settings, ST_RATECONTROL_QP_B_INITIAL, -1); + obs_data_set_default_int(settings, KEY_RATECONTROL_QP_I, 21); + obs_data_set_default_int(settings, KEY_RATECONTROL_QP_P, 21); + obs_data_set_default_int(settings, KEY_RATECONTROL_QP_B, 21); - obs_data_set_default_int(settings, ST_AQ_SPATIAL, -1); - obs_data_set_default_int(settings, ST_AQ_STRENGTH, 8); - obs_data_set_default_int(settings, ST_AQ_TEMPORAL, -1); + obs_data_set_default_int(settings, KEY_AQ_SPATIAL, -1); + obs_data_set_default_int(settings, KEY_AQ_STRENGTH, 8); + obs_data_set_default_int(settings, KEY_AQ_TEMPORAL, -1); - obs_data_set_default_int(settings, ST_OTHER_BFRAMES, 2); - obs_data_set_default_int(settings, ST_OTHER_BFRAME_REFERENCEMODE, static_cast(b_ref_mode::DISABLED)); - obs_data_set_default_int(settings, ST_OTHER_ZEROLATENCY, -1); - obs_data_set_default_int(settings, ST_OTHER_WEIGHTED_PREDICTION, -1); - obs_data_set_default_int(settings, ST_OTHER_NONREFERENCE_PFRAMES, -1); + obs_data_set_default_int(settings, KEY_OTHER_BFRAMES, 2); + obs_data_set_default_int(settings, KEY_OTHER_BFRAMEREFERENCEMODE, static_cast(b_ref_mode::DISABLED)); + obs_data_set_default_int(settings, KEY_OTHER_ZEROLATENCY, -1); + obs_data_set_default_int(settings, KEY_OTHER_WEIGHTEDPREDICTION, -1); + obs_data_set_default_int(settings, KEY_OTHER_NONREFERENCEPFRAMES, -1); // Replay Buffer obs_data_set_default_int(settings, "bitrate", 0); @@ -211,7 +224,6 @@ static bool modified_ratecontrol(obs_properties_t* props, obs_property_t*, obs_d bool have_bitrate_max = false; bool have_quality = false; bool have_qp = false; - bool have_qp_init = false; nvenc::ratecontrolmode rc = static_cast(obs_data_get_int(settings, ST_RATECONTROL_MODE)); switch (rc) { @@ -229,27 +241,24 @@ static bool modified_ratecontrol(obs_properties_t* props, obs_property_t*, obs_d have_bitrate = true; have_bitrate_max = true; have_quality = true; - have_qp_init = true; + have_qp = true; break; } obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_BITRATE), have_bitrate || have_bitrate_max); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_BITRATE_TARGET), have_bitrate); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_BITRATE_MAXIMUM), have_bitrate_max); + obs_property_set_visible(obs_properties_get(props, KEY_RATECONTROL_BITRATE_TARGET), have_bitrate); + obs_property_set_visible(obs_properties_get(props, KEY_RATECONTROL_BITRATE_MAXIMUM), have_bitrate_max); obs_property_set_visible(obs_properties_get(props, S_RATECONTROL_BUFFERSIZE), have_bitrate || have_bitrate_max); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QUALITY), have_quality); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QUALITY_MINIMUM), have_quality); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QUALITY_MAXIMUM), have_quality); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QUALITY_TARGET), have_quality); + obs_property_set_visible(obs_properties_get(props, KEY_RATECONTROL_QUALITY), have_quality); + obs_property_set_visible(obs_properties_get(props, KEY_RATECONTROL_QUALITY_MINIMUM), have_quality); + obs_property_set_visible(obs_properties_get(props, KEY_RATECONTROL_QUALITY_MAXIMUM), have_quality); + obs_property_set_visible(obs_properties_get(props, KEY_RATECONTROL_QUALITY_TARGET), have_quality); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QP), have_qp || have_qp_init); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QP_I), have_qp); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QP_P), have_qp); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QP_B), have_qp); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QP_I_INITIAL), have_qp_init); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QP_P_INITIAL), have_qp_init); - obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QP_B_INITIAL), have_qp_init); + obs_property_set_visible(obs_properties_get(props, ST_RATECONTROL_QP), have_qp); + obs_property_set_visible(obs_properties_get(props, KEY_RATECONTROL_QP_I), have_qp); + obs_property_set_visible(obs_properties_get(props, KEY_RATECONTROL_QP_P), have_qp); + obs_property_set_visible(obs_properties_get(props, KEY_RATECONTROL_QP_B), have_qp); return true; } @@ -257,22 +266,22 @@ static bool modified_ratecontrol(obs_properties_t* props, obs_property_t*, obs_d static bool modified_quality(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept { bool enabled = obs_data_get_bool(settings, ST_RATECONTROL_QUALITY); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QUALITY_MINIMUM), enabled); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QUALITY_MAXIMUM), enabled); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_QUALITY_MINIMUM), enabled); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_QUALITY_MAXIMUM), enabled); return true; } static bool modified_aq(obs_properties_t* props, obs_property_t*, obs_data_t* settings) noexcept { - bool spatial_aq = obs_data_get_int(settings, ST_AQ_SPATIAL) == 1; - obs_property_set_visible(obs_properties_get(props, ST_AQ_STRENGTH), spatial_aq); + bool spatial_aq = obs_data_get_int(settings, KEY_AQ_SPATIAL) == 1; + obs_property_set_visible(obs_properties_get(props, KEY_AQ_STRENGTH), spatial_aq); return true; } void nvenc::get_properties_pre(obs_properties_t* props, const AVCodec*) { { - auto p = obs_properties_add_list(props, ST_PRESET, D_TRANSLATE(ST_PRESET), OBS_COMBO_TYPE_LIST, + auto p = obs_properties_add_list(props, KEY_PRESET, D_TRANSLATE(ST_PRESET), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_PRESET))); for (auto kv : presets) { @@ -367,19 +376,19 @@ void nvenc::get_properties_post(obs_properties_t* props, const AVCodec* codec) } { - auto p = obs_properties_add_int_slider(grp, ST_RATECONTROL_QUALITY_MINIMUM, + auto p = obs_properties_add_int_slider(grp, KEY_RATECONTROL_QUALITY_MINIMUM, D_TRANSLATE(ST_RATECONTROL_QUALITY_MINIMUM), 0, 51, 1); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_RATECONTROL_QUALITY_MINIMUM))); } { - auto p = obs_properties_add_int_slider(grp, ST_RATECONTROL_QUALITY_MAXIMUM, + auto p = obs_properties_add_int_slider(grp, KEY_RATECONTROL_QUALITY_MAXIMUM, D_TRANSLATE(ST_RATECONTROL_QUALITY_MAXIMUM), -1, 51, 1); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_RATECONTROL_QUALITY_MAXIMUM))); } } { - auto p = obs_properties_add_float_slider(props, ST_RATECONTROL_QUALITY_TARGET, + auto p = obs_properties_add_float_slider(props, KEY_RATECONTROL_QUALITY_TARGET, D_TRANSLATE(ST_RATECONTROL_QUALITY_TARGET), 0, 100, 0.01); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_RATECONTROL_QUALITY_TARGET))); } @@ -389,40 +398,25 @@ void nvenc::get_properties_post(obs_properties_t* props, const AVCodec* codec) if (!util::are_property_groups_broken()) { grp = obs_properties_create(); auto p = obs_properties_add_group(props, ST_RATECONTROL_QP, D_TRANSLATE(ST_RATECONTROL_QP), - OBS_GROUP_CHECKABLE, grp); + OBS_GROUP_NORMAL, grp); obs_property_set_modified_callback(p, modified_quality); } { auto p = - obs_properties_add_int_slider(grp, ST_RATECONTROL_QP_I, D_TRANSLATE(ST_RATECONTROL_QP_I), 0, 51, 1); + obs_properties_add_int_slider(grp, KEY_RATECONTROL_QP_I, D_TRANSLATE(ST_RATECONTROL_QP_I), -1, 51, 1); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_RATECONTROL_QP_I))); } - { - auto p = obs_properties_add_int_slider(grp, ST_RATECONTROL_QP_I_INITIAL, - D_TRANSLATE(ST_RATECONTROL_QP_I_INITIAL), -1, 51, 1); - obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_RATECONTROL_QP_I_INITIAL))); - } { auto p = - obs_properties_add_int_slider(grp, ST_RATECONTROL_QP_P, D_TRANSLATE(ST_RATECONTROL_QP_P), 0, 51, 1); + obs_properties_add_int_slider(grp, KEY_RATECONTROL_QP_P, D_TRANSLATE(ST_RATECONTROL_QP_P), -1, 51, 1); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_RATECONTROL_QP_P))); } - { - auto p = obs_properties_add_int_slider(grp, ST_RATECONTROL_QP_P_INITIAL, - D_TRANSLATE(ST_RATECONTROL_QP_P_INITIAL), -1, 51, 1); - obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_RATECONTROL_QP_P_INITIAL))); - } { auto p = - obs_properties_add_int_slider(grp, ST_RATECONTROL_QP_B, D_TRANSLATE(ST_RATECONTROL_QP_B), 0, 51, 1); + obs_properties_add_int_slider(grp, KEY_RATECONTROL_QP_B, D_TRANSLATE(ST_RATECONTROL_QP_B), -1, 51, 1); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_RATECONTROL_QP_B))); } - { - auto p = obs_properties_add_int_slider(grp, ST_RATECONTROL_QP_B_INITIAL, - D_TRANSLATE(ST_RATECONTROL_QP_B_INITIAL), -1, 51, 1); - obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_RATECONTROL_QP_B_INITIAL))); - } } { @@ -433,16 +427,16 @@ void nvenc::get_properties_post(obs_properties_t* props, const AVCodec* codec) } { - auto p = util::obs_properties_add_tristate(grp, ST_AQ_SPATIAL, D_TRANSLATE(ST_AQ_SPATIAL)); + auto p = util::obs_properties_add_tristate(grp, KEY_AQ_SPATIAL, D_TRANSLATE(ST_AQ_SPATIAL)); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_AQ_SPATIAL))); obs_property_set_modified_callback(p, modified_aq); } { - auto p = obs_properties_add_int_slider(grp, ST_AQ_STRENGTH, D_TRANSLATE(ST_AQ_STRENGTH), 1, 15, 1); + auto p = obs_properties_add_int_slider(grp, KEY_AQ_STRENGTH, D_TRANSLATE(ST_AQ_STRENGTH), 1, 15, 1); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_AQ_STRENGTH))); } { - auto p = util::obs_properties_add_tristate(grp, ST_AQ_TEMPORAL, D_TRANSLATE(ST_AQ_TEMPORAL)); + auto p = util::obs_properties_add_tristate(grp, KEY_AQ_TEMPORAL, D_TRANSLATE(ST_AQ_TEMPORAL)); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_AQ_TEMPORAL))); } } @@ -455,80 +449,77 @@ void nvenc::get_properties_post(obs_properties_t* props, const AVCodec* codec) } { - auto p = obs_properties_add_int_slider(grp, ST_OTHER_BFRAMES, D_TRANSLATE(ST_OTHER_BFRAMES), 0, 4, 1); + auto p = obs_properties_add_int_slider(grp, KEY_OTHER_BFRAMES, D_TRANSLATE(ST_OTHER_BFRAMES), 0, 4, 1); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_OTHER_BFRAMES))); obs_property_int_set_suffix(p, " frames"); } { auto p = - obs_properties_add_list(grp, ST_OTHER_BFRAME_REFERENCEMODE, D_TRANSLATE(ST_OTHER_BFRAME_REFERENCEMODE), + obs_properties_add_list(grp, KEY_OTHER_BFRAMEREFERENCEMODE, D_TRANSLATE(ST_OTHER_BFRAMEREFERENCEMODE), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); - obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_OTHER_BFRAME_REFERENCEMODE))); + obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_OTHER_BFRAMEREFERENCEMODE))); for (auto kv : b_ref_modes) { obs_property_list_add_int(p, D_TRANSLATE(kv.second.c_str()), static_cast(kv.first)); } } { - auto p = util::obs_properties_add_tristate(grp, ST_OTHER_ZEROLATENCY, D_TRANSLATE(ST_OTHER_ZEROLATENCY)); + auto p = util::obs_properties_add_tristate(grp, KEY_OTHER_ZEROLATENCY, D_TRANSLATE(ST_OTHER_ZEROLATENCY)); obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_OTHER_ZEROLATENCY))); } { - auto p = util::obs_properties_add_tristate(grp, ST_OTHER_WEIGHTED_PREDICTION, - D_TRANSLATE(ST_OTHER_WEIGHTED_PREDICTION)); - obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_OTHER_WEIGHTED_PREDICTION))); + auto p = util::obs_properties_add_tristate(grp, KEY_OTHER_WEIGHTEDPREDICTION, + D_TRANSLATE(ST_OTHER_WEIGHTEDPREDICTION)); + obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_OTHER_WEIGHTEDPREDICTION))); } { - auto p = util::obs_properties_add_tristate(grp, ST_OTHER_NONREFERENCE_PFRAMES, - D_TRANSLATE(ST_OTHER_NONREFERENCE_PFRAMES)); - obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_OTHER_NONREFERENCE_PFRAMES))); + auto p = util::obs_properties_add_tristate(grp, KEY_OTHER_NONREFERENCEPFRAMES, + D_TRANSLATE(ST_OTHER_NONREFERENCEPFRAMES)); + obs_property_set_long_description(p, D_TRANSLATE(D_DESC(ST_OTHER_NONREFERENCEPFRAMES))); } } } void nvenc::get_runtime_properties(obs_properties_t* props, const AVCodec*, AVCodecContext*) { - obs_property_set_enabled(obs_properties_get(props, ST_PRESET), false); + obs_property_set_enabled(obs_properties_get(props, KEY_PRESET), false); obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_MODE), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_TWOPASS), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_LOOKAHEAD), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_ADAPTIVEI), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_ADAPTIVEB), false); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_MODE), false); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_TWOPASS), false); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_LOOKAHEAD), false); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_ADAPTIVEI), false); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_ADAPTIVEB), false); obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_BITRATE), true); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_BITRATE_TARGET), true); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_BITRATE_MAXIMUM), true); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_BITRATE_TARGET), true); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_BITRATE_MAXIMUM), true); obs_property_set_enabled(obs_properties_get(props, S_RATECONTROL_BUFFERSIZE), true); obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QUALITY), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QUALITY_MINIMUM), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QUALITY_MAXIMUM), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QUALITY_TARGET), false); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_QUALITY_MINIMUM), false); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_QUALITY_MAXIMUM), false); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_QUALITY_TARGET), false); obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QP), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QP_I), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QP_I_INITIAL), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QP_P), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QP_P_INITIAL), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QP_B), false); - obs_property_set_enabled(obs_properties_get(props, ST_RATECONTROL_QP_B_INITIAL), false); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_QP_I), false); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_QP_P), false); + obs_property_set_enabled(obs_properties_get(props, KEY_RATECONTROL_QP_B), false); obs_property_set_enabled(obs_properties_get(props, ST_AQ), false); - obs_property_set_enabled(obs_properties_get(props, ST_AQ_SPATIAL), false); - obs_property_set_enabled(obs_properties_get(props, ST_AQ_STRENGTH), false); - obs_property_set_enabled(obs_properties_get(props, ST_AQ_TEMPORAL), false); + obs_property_set_enabled(obs_properties_get(props, KEY_AQ_SPATIAL), false); + obs_property_set_enabled(obs_properties_get(props, KEY_AQ_STRENGTH), false); + obs_property_set_enabled(obs_properties_get(props, KEY_AQ_TEMPORAL), false); obs_property_set_enabled(obs_properties_get(props, ST_OTHER), false); - obs_property_set_enabled(obs_properties_get(props, ST_OTHER_BFRAMES), false); - obs_property_set_enabled(obs_properties_get(props, ST_OTHER_BFRAME_REFERENCEMODE), false); - obs_property_set_enabled(obs_properties_get(props, ST_OTHER_ZEROLATENCY), false); - obs_property_set_enabled(obs_properties_get(props, ST_OTHER_WEIGHTED_PREDICTION), false); - obs_property_set_enabled(obs_properties_get(props, ST_OTHER_NONREFERENCE_PFRAMES), false); + obs_property_set_enabled(obs_properties_get(props, KEY_OTHER_BFRAMES), false); + obs_property_set_enabled(obs_properties_get(props, KEY_OTHER_BFRAMEREFERENCEMODE), false); + obs_property_set_enabled(obs_properties_get(props, KEY_OTHER_ZEROLATENCY), false); + obs_property_set_enabled(obs_properties_get(props, KEY_OTHER_WEIGHTEDPREDICTION), false); + obs_property_set_enabled(obs_properties_get(props, KEY_OTHER_NONREFERENCEPFRAMES), false); } void nvenc::update(obs_data_t* settings, const AVCodec* codec, AVCodecContext* context) { { - preset c_preset = static_cast(obs_data_get_int(settings, ST_PRESET)); + preset c_preset = static_cast(obs_data_get_int(settings, KEY_PRESET)); auto found = preset_to_opt.find(c_preset); if (found != preset_to_opt.end()) { av_opt_set(context->priv_data, "preset", found->second.c_str(), 0); @@ -544,7 +535,7 @@ void nvenc::update(obs_data_t* settings, const AVCodec* codec, AVCodecContext* c bool have_qp = false; bool have_qp_init = false; - ratecontrolmode rc = static_cast(obs_data_get_int(settings, ST_RATECONTROL_MODE)); + ratecontrolmode rc = static_cast(obs_data_get_int(settings, KEY_RATECONTROL_MODE)); auto rcopt = ratecontrolmode_to_opt.find(rc); if (rcopt != ratecontrolmode_to_opt.end()) { av_opt_set(context->priv_data, "rc", rcopt->second.c_str(), 0); @@ -571,21 +562,21 @@ void nvenc::update(obs_data_t* settings, const AVCodec* codec, AVCodecContext* c break; } - int tp = static_cast(obs_data_get_int(settings, ST_RATECONTROL_TWOPASS)); + int tp = static_cast(obs_data_get_int(settings, KEY_RATECONTROL_TWOPASS)); if (tp >= 0) { av_opt_set_int(context->priv_data, "2pass", tp ? 1 : 0, 0); } - int la = static_cast(obs_data_get_int(settings, ST_RATECONTROL_LOOKAHEAD)); + int la = static_cast(obs_data_get_int(settings, KEY_RATECONTROL_LOOKAHEAD)); av_opt_set_int(context->priv_data, "rc-lookahead", la, 0); if (la > 0) { - int64_t adapt_i = obs_data_get_int(settings, ST_RATECONTROL_ADAPTIVEI); + int64_t adapt_i = obs_data_get_int(settings, KEY_RATECONTROL_ADAPTIVEI); if (!util::is_tristate_default(adapt_i)) { av_opt_set_int(context->priv_data, "no-scenecut", adapt_i, AV_OPT_SEARCH_CHILDREN); } if (strcmp(codec->name, "h264_nvenc")) { - int64_t adapt_b = obs_data_get_int(settings, ST_RATECONTROL_ADAPTIVEB); + int64_t adapt_b = obs_data_get_int(settings, KEY_RATECONTROL_ADAPTIVEB); if (!util::is_tristate_default(adapt_b)) { av_opt_set_int(context->priv_data, "b_adapt", adapt_b, AV_OPT_SEARCH_CHILDREN); } @@ -593,25 +584,25 @@ void nvenc::update(obs_data_t* settings, const AVCodec* codec, AVCodecContext* c } if (have_bitrate) { - context->bit_rate = static_cast(obs_data_get_int(settings, ST_RATECONTROL_BITRATE_TARGET) * 1000); + context->bit_rate = static_cast(obs_data_get_int(settings, KEY_RATECONTROL_BITRATE_TARGET) * 1000); // Support for Replay Buffer - obs_data_set_int(settings, "bitrate", obs_data_get_int(settings, ST_RATECONTROL_BITRATE_TARGET)); + obs_data_set_int(settings, "bitrate", obs_data_get_int(settings, KEY_RATECONTROL_BITRATE_TARGET)); } if (have_bitrate_max) - context->rc_max_rate = static_cast(obs_data_get_int(settings, ST_RATECONTROL_BITRATE_MAXIMUM) * 1000); + context->rc_max_rate = static_cast(obs_data_get_int(settings, KEY_RATECONTROL_BITRATE_MAXIMUM) * 1000); if (have_bitrate || have_bitrate_max) context->rc_buffer_size = static_cast(obs_data_get_int(settings, S_RATECONTROL_BUFFERSIZE) * 1000); - if (have_quality && obs_data_get_bool(settings, ST_RATECONTROL_QUALITY)) { - int qmin = static_cast(obs_data_get_int(settings, ST_RATECONTROL_QUALITY_MINIMUM)); + if (have_quality && obs_data_get_bool(settings, KEY_RATECONTROL_QUALITY)) { + int qmin = static_cast(obs_data_get_int(settings, KEY_RATECONTROL_QUALITY_MINIMUM)); context->qmin = qmin; if (qmin >= 0) { - context->qmax = static_cast(obs_data_get_int(settings, ST_RATECONTROL_QUALITY_MAXIMUM)); + context->qmax = static_cast(obs_data_get_int(settings, KEY_RATECONTROL_QUALITY_MAXIMUM)); } } { - double_t v = obs_data_get_double(settings, ST_RATECONTROL_QUALITY_TARGET) / 100.0 * 51.0; + double_t v = obs_data_get_double(settings, KEY_RATECONTROL_QUALITY_TARGET) / 100.0 * 51.0; if (v > 0) { av_opt_set_double(context->priv_data, "cq", v, 0); } @@ -619,22 +610,17 @@ void nvenc::update(obs_data_t* settings, const AVCodec* codec, AVCodecContext* c if (have_qp) { av_opt_set_int(context->priv_data, "init_qpI", - static_cast(obs_data_get_int(settings, ST_RATECONTROL_QP_I)), 0); + static_cast(obs_data_get_int(settings, KEY_RATECONTROL_QP_I)), 0); av_opt_set_int(context->priv_data, "init_qpP", - static_cast(obs_data_get_int(settings, ST_RATECONTROL_QP_P)), 0); + static_cast(obs_data_get_int(settings, KEY_RATECONTROL_QP_P)), 0); av_opt_set_int(context->priv_data, "init_qpB", - static_cast(obs_data_get_int(settings, ST_RATECONTROL_QP_B)), 0); - } - if (have_qp_init) { - av_opt_set_int(context->priv_data, "init_qpI", obs_data_get_int(settings, ST_RATECONTROL_QP_I_INITIAL), 0); - av_opt_set_int(context->priv_data, "init_qpP", obs_data_get_int(settings, ST_RATECONTROL_QP_P_INITIAL), 0); - av_opt_set_int(context->priv_data, "init_qpB", obs_data_get_int(settings, ST_RATECONTROL_QP_B_INITIAL), 0); + static_cast(obs_data_get_int(settings, KEY_RATECONTROL_QP_B)), 0); } } { // AQ - int64_t saq = obs_data_get_int(settings, ST_AQ_SPATIAL); - int64_t taq = obs_data_get_int(settings, ST_AQ_TEMPORAL); + int64_t saq = obs_data_get_int(settings, KEY_AQ_SPATIAL); + int64_t taq = obs_data_get_int(settings, KEY_AQ_TEMPORAL); if (strcmp(codec->name, "h264_nvenc") == 0) { if (!util::is_tristate_default(saq)) @@ -649,15 +635,15 @@ void nvenc::update(obs_data_t* settings, const AVCodec* codec, AVCodecContext* c } if (util::is_tristate_enabled(saq)) av_opt_set_int(context->priv_data, "aq-strength", - static_cast(obs_data_get_int(settings, ST_AQ_STRENGTH)), 0); + static_cast(obs_data_get_int(settings, KEY_AQ_STRENGTH)), 0); } { // Other - int64_t zl = obs_data_get_int(settings, ST_OTHER_ZEROLATENCY); - int64_t wp = obs_data_get_int(settings, ST_OTHER_WEIGHTED_PREDICTION); - int64_t nrp = obs_data_get_int(settings, ST_OTHER_NONREFERENCE_PFRAMES); + int64_t zl = obs_data_get_int(settings, KEY_OTHER_ZEROLATENCY); + int64_t wp = obs_data_get_int(settings, KEY_OTHER_WEIGHTEDPREDICTION); + int64_t nrp = obs_data_get_int(settings, KEY_OTHER_NONREFERENCEPFRAMES); - context->max_b_frames = static_cast(obs_data_get_int(settings, ST_OTHER_BFRAMES)); + context->max_b_frames = static_cast(obs_data_get_int(settings, KEY_OTHER_BFRAMES)); if (!util::is_tristate_default(zl)) av_opt_set_int(context->priv_data, "zerolatency", zl, 0); @@ -673,7 +659,7 @@ void nvenc::update(obs_data_t* settings, const AVCodec* codec, AVCodecContext* c { auto found = b_ref_mode_to_opt.find( - static_cast(obs_data_get_int(settings, ST_OTHER_BFRAME_REFERENCEMODE))); + static_cast(obs_data_get_int(settings, KEY_OTHER_BFRAMEREFERENCEMODE))); if (found != b_ref_mode_to_opt.end()) { av_opt_set(context->priv_data, "b_ref_mode", found->second.c_str(), 0); } diff --git a/source/encoders/handlers/nvenc_shared.hpp b/source/encoders/handlers/nvenc_shared.hpp index d82d5b8d..2344391b 100644 --- a/source/encoders/handlers/nvenc_shared.hpp +++ b/source/encoders/handlers/nvenc_shared.hpp @@ -33,6 +33,14 @@ extern "C" { #pragma warning(pop) } +/* NVENC has multiple compression modes: +- CBR: Constant Bitrate (rc=cbr) +- VBR: Variable Bitrate (rc=vbr) +- CQP: Constant QP (rc=cqp) +- VQP: Variable QP (rc=vbr b=0 minrate=0 maxrate=0 qmin=minqp qmax=maxqp cq=targetqp) +- TQ: Target Quality (rc=vbr b=0 minrate=0 maxrate=0 qmin=qp qmax=qp cq=qp) +*/ + using namespace encoder::ffmpeg; namespace encoder::ffmpeg::handler::nvenc {