// 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 "tools.hpp" #include #include #include #include #include "plugin.hpp" #include "utility.hpp" extern "C" { #pragma warning(push) #pragma warning(disable : 4244) #include #include #include #include #pragma warning(pop) } std::string ffmpeg::tools::translate_encoder_capabilities(int capabilities) { // Sorted by relative importance. std::pair caps[] = { {AV_CODEC_CAP_EXPERIMENTAL, "Experimental"}, // Quality {AV_CODEC_CAP_LOSSLESS, "Lossless"}, // Features {AV_CODEC_CAP_PARAM_CHANGE, "Dynamic Parameter Change"}, {AV_CODEC_CAP_SUBFRAMES, "Sub-Frames"}, {AV_CODEC_CAP_VARIABLE_FRAME_SIZE, "Variable Frame Size"}, {AV_CODEC_CAP_SMALL_LAST_FRAME, "Small Final Frame"}, // Other {AV_CODEC_CAP_TRUNCATED, "Truncated"}, {AV_CODEC_CAP_CHANNEL_CONF, "AV_CODEC_CAP_CHANNEL_CONF"}, {AV_CODEC_CAP_DRAW_HORIZ_BAND, "AV_CODEC_CAP_DRAW_HORIZ_BAND"}, {AV_CODEC_CAP_AVOID_PROBING, "AV_CODEC_CAP_AVOID_PROBING"}, }; std::stringstream sstr; for (auto const kv : caps) { if (capabilities & kv.first) { capabilities &= ~kv.first; sstr << kv.second; if (capabilities != 0) { sstr << ", "; } else { break; } } } return sstr.str(); } const char* ffmpeg::tools::get_pixel_format_name(AVPixelFormat v) { return av_get_pix_fmt_name(v); } const char* ffmpeg::tools::get_color_space_name(AVColorSpace v) { switch (v) { case AVCOL_SPC_RGB: return "RGB"; case AVCOL_SPC_BT709: return "BT.709"; case AVCOL_SPC_FCC: return "FCC Title 47 CoFR 73.682 (a)(20)"; case AVCOL_SPC_BT470BG: return "BT.601 625"; case AVCOL_SPC_SMPTE170M: case AVCOL_SPC_SMPTE240M: return "BT.601 525"; case AVCOL_SPC_YCGCO: return "ITU-T SG16"; case AVCOL_SPC_BT2020_NCL: return "BT.2020 NCL"; case AVCOL_SPC_BT2020_CL: return "BT.2020 CL"; case AVCOL_SPC_SMPTE2085: return "SMPTE 2085"; case AVCOL_SPC_CHROMA_DERIVED_NCL: return "Chroma NCL"; case AVCOL_SPC_CHROMA_DERIVED_CL: return "Chroma CL"; case AVCOL_SPC_ICTCP: return "BT.2100"; case AVCOL_SPC_NB: return "Not Part of ABI"; } return "Unknown"; } const char* ffmpeg::tools::get_error_description(int error) { thread_local char error_buf[AV_ERROR_MAX_STRING_SIZE + 1]; if (av_strerror(error, error_buf, AV_ERROR_MAX_STRING_SIZE) < 0) { snprintf(error_buf, AV_ERROR_MAX_STRING_SIZE, "Unknown Error (%i)", error); } return error_buf; } static std::map obs_to_av_format_map = { {VIDEO_FORMAT_I420, AV_PIX_FMT_YUV420P}, // YUV 4:2:0 {VIDEO_FORMAT_NV12, AV_PIX_FMT_NV12}, // NV12 Packed YUV {VIDEO_FORMAT_YVYU, AV_PIX_FMT_YVYU422}, // YVYU Packed YUV {VIDEO_FORMAT_YUY2, AV_PIX_FMT_YUYV422}, // YUYV Packed YUV {VIDEO_FORMAT_UYVY, AV_PIX_FMT_UYVY422}, // UYVY Packed YUV {VIDEO_FORMAT_RGBA, AV_PIX_FMT_RGBA}, // {VIDEO_FORMAT_BGRA, AV_PIX_FMT_BGRA}, // {VIDEO_FORMAT_BGRX, AV_PIX_FMT_BGR0}, // {VIDEO_FORMAT_Y800, AV_PIX_FMT_GRAY8}, // {VIDEO_FORMAT_I444, AV_PIX_FMT_YUV444P}, // {VIDEO_FORMAT_BGR3, AV_PIX_FMT_BGR24}, // {VIDEO_FORMAT_I422, AV_PIX_FMT_YUV422P}, // {VIDEO_FORMAT_I40A, AV_PIX_FMT_YUVA420P}, // {VIDEO_FORMAT_I42A, AV_PIX_FMT_YUVA422P}, // {VIDEO_FORMAT_YUVA, AV_PIX_FMT_YUVA444P}, // //{VIDEO_FORMAT_AYUV, AV_PIX_FMT_AYUV444P}, // }; AVPixelFormat ffmpeg::tools::obs_videoformat_to_avpixelformat(video_format v) { auto found = obs_to_av_format_map.find(v); if (found != obs_to_av_format_map.end()) { return found->second; } return AV_PIX_FMT_NONE; } video_format ffmpeg::tools::avpixelformat_to_obs_videoformat(AVPixelFormat v) { for (const auto& kv : obs_to_av_format_map) { if (kv.second == v) return kv.first; } return VIDEO_FORMAT_NONE; } AVPixelFormat ffmpeg::tools::get_least_lossy_format(const AVPixelFormat* haystack, AVPixelFormat needle) { int data_loss = 0; return avcodec_find_best_pix_fmt_of_list(haystack, needle, 0, &data_loss); } AVColorSpace ffmpeg::tools::obs_videocolorspace_to_avcolorspace(video_colorspace v) { switch (v) { case VIDEO_CS_DEFAULT: case VIDEO_CS_709: return AVCOL_SPC_BT709; case VIDEO_CS_601: return AVCOL_SPC_BT470BG; } throw std::invalid_argument("unknown color space"); } AVColorRange ffmpeg::tools::obs_videorangetype_to_avcolorrange(video_range_type v) { switch (v) { case VIDEO_RANGE_DEFAULT: case VIDEO_RANGE_PARTIAL: return AVCOL_RANGE_MPEG; case VIDEO_RANGE_FULL: return AVCOL_RANGE_JPEG; } throw std::invalid_argument("unknown range"); } bool ffmpeg::tools::can_hardware_encode(const AVCodec* codec) { AVPixelFormat hardware_formats[] = {AV_PIX_FMT_D3D11}; for (const AVPixelFormat* fmt = codec->pix_fmts; (fmt != nullptr) && (*fmt != AV_PIX_FMT_NONE); fmt++) { for (auto cmp : hardware_formats) { if (*fmt == cmp) { return true; } } } return false; } std::vector ffmpeg::tools::get_software_formats(const AVPixelFormat* list) { AVPixelFormat hardware_formats[] = { #if FF_API_VAAPI AV_PIX_FMT_VAAPI_MOCO, AV_PIX_FMT_VAAPI_IDCT, #endif AV_PIX_FMT_VAAPI, AV_PIX_FMT_DXVA2_VLD, AV_PIX_FMT_VDPAU, AV_PIX_FMT_QSV, AV_PIX_FMT_MMAL, AV_PIX_FMT_D3D11VA_VLD, AV_PIX_FMT_CUDA, AV_PIX_FMT_XVMC, AV_PIX_FMT_VIDEOTOOLBOX, AV_PIX_FMT_MEDIACODEC, AV_PIX_FMT_D3D11, AV_PIX_FMT_OPENCL, }; std::vector fmts; for (auto fmt = list; fmt && (*fmt != AV_PIX_FMT_NONE); fmt++) { bool is_blacklisted = false; for (auto blacklisted : hardware_formats) { if (*fmt == blacklisted) is_blacklisted = true; } if (!is_blacklisted) fmts.push_back(*fmt); } fmts.push_back(AV_PIX_FMT_NONE); return std::move(fmts); } void ffmpeg::tools::setup_obs_color(video_colorspace colorspace, video_range_type range, AVCodecContext* context) { std::map> colorspaces = { {VIDEO_CS_DEFAULT, {AVCOL_SPC_BT470BG, AVCOL_PRI_BT470BG, AVCOL_TRC_SMPTE170M}}, {VIDEO_CS_601, {AVCOL_SPC_BT470BG, AVCOL_PRI_BT470BG, AVCOL_TRC_SMPTE170M}}, {VIDEO_CS_709, {AVCOL_SPC_BT709, AVCOL_PRI_BT709, AVCOL_TRC_BT709}}, }; std::map colorranges = { {VIDEO_RANGE_DEFAULT, AVCOL_RANGE_MPEG}, {VIDEO_RANGE_PARTIAL, AVCOL_RANGE_MPEG}, {VIDEO_RANGE_FULL, AVCOL_RANGE_JPEG}, }; { auto found = colorspaces.find(colorspace); if (found != colorspaces.end()) { context->colorspace = std::get(found->second); context->color_primaries = std::get(found->second); context->color_trc = std::get(found->second); } } { auto found = colorranges.find(range); if (found != colorranges.end()) { context->color_range = found->second; } } // Downscaling should result in downscaling, not pixelation context->chroma_sample_location = AVCHROMA_LOC_CENTER; } const char* ffmpeg::tools::get_std_compliance_name(int compliance) { switch (compliance) { case FF_COMPLIANCE_VERY_STRICT: return "Very Strict"; case FF_COMPLIANCE_STRICT: return "Strict"; case FF_COMPLIANCE_NORMAL: return "Normal"; case FF_COMPLIANCE_UNOFFICIAL: return "Unofficial"; case FF_COMPLIANCE_EXPERIMENTAL: return "Experimental"; } return "Invalid"; } const char* ffmpeg::tools::get_thread_type_name(int thread_type) { switch (thread_type) { case FF_THREAD_FRAME | FF_THREAD_SLICE: return "Slice & Frame"; case FF_THREAD_FRAME: return "Frame"; case FF_THREAD_SLICE: return "Slice"; default: return "None"; } } void ffmpeg::tools::print_av_option_bool(AVCodecContext* context, const char* option, std::string text) { if (av_opt_is_set_to_default_by_name(context, option, AV_OPT_SEARCH_CHILDREN) == 0) { int64_t v = 0; if (av_opt_get_int(context, option, AV_OPT_SEARCH_CHILDREN, &v) == 0) { PLOG_INFO("[%s] %s: %s", context->codec->name, text.c_str(), v == 0 ? "Disabled" : "Enabled"); } else { PLOG_INFO("[%s] %s: ", context->codec->name, text.c_str()); } } else { PLOG_INFO("[%s] %s: ", context->codec->name, text.c_str()); } } void ffmpeg::tools::print_av_option_int(AVCodecContext* context, const char* option, std::string text, std::string suffix) { if (av_opt_is_set_to_default_by_name(context, option, AV_OPT_SEARCH_CHILDREN) == 0) { int64_t v = 0; if (av_opt_get_int(context, option, AV_OPT_SEARCH_CHILDREN, &v) == 0) { PLOG_INFO("[%s] %s: %lld %s", context->codec->name, text.c_str(), v, suffix.c_str()); } else { PLOG_INFO("[%s] %s: ", context->codec->name, text.c_str()); } } else { PLOG_INFO("[%s] %s: ", context->codec->name, text.c_str()); } } void ffmpeg::tools::print_av_option_string(AVCodecContext* context, const char* option, std::string text, std::function decoder) { if (av_opt_is_set_to_default_by_name(context, option, AV_OPT_SEARCH_CHILDREN) == 0) { int64_t v = 0; if (av_opt_get_int(context, option, AV_OPT_SEARCH_CHILDREN, &v) == 0) { std::string name = ""; if (decoder) name = decoder(v); PLOG_INFO("[%s] %s: %s", context->codec->name, text.c_str(), name.c_str()); } else { PLOG_INFO("[%s] %s: ", context->codec->name, text.c_str()); } } else { PLOG_INFO("[%s] %s: ", context->codec->name, text.c_str()); } }