ffmpeg: Add ListFormats and ListEncoders
These two functions allow the frontend to get a list of encoders/formats and their specific options. Retrieving the options is harder than it sounds due to FFmpeg's strange AVClass and AVOption system. For example, for integer and flags options, 'named constants' can be set. They are of type `AV_OPT_TYPE_CONST` and are categoried according to the `unit` field. An option can recognize all constants of the same `unit`.
This commit is contained in:
parent
4161163d9c
commit
8c4bcf9f59
|
@ -492,5 +492,5 @@ if (ARCHITECTURE_x86_64)
|
|||
endif()
|
||||
|
||||
if (ENABLE_FFMPEG_VIDEO_DUMPER)
|
||||
target_link_libraries(core PRIVATE FFmpeg::avcodec FFmpeg::avformat FFmpeg::swscale FFmpeg::swresample FFmpeg::avutil)
|
||||
target_link_libraries(core PUBLIC FFmpeg::avcodec FFmpeg::avformat FFmpeg::swscale FFmpeg::swresample FFmpeg::avutil)
|
||||
endif()
|
||||
|
|
|
@ -2,17 +2,19 @@
|
|||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <unordered_set>
|
||||
#include "common/assert.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/param_package.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/dumping/ffmpeg_backend.h"
|
||||
#include "core/settings.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
#include "video_core/video_core.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavutil/opt.h>
|
||||
#include <libavutil/pixdesc.h>
|
||||
}
|
||||
|
||||
namespace VideoDumper {
|
||||
|
@ -591,4 +593,243 @@ void FFmpegBackend::EndDumping() {
|
|||
processing_ended.Set();
|
||||
}
|
||||
|
||||
// To std string, but handles nullptr
|
||||
std::string ToStdString(const char* str, const std::string& fallback = "") {
|
||||
return str ? std::string{str} : fallback;
|
||||
}
|
||||
|
||||
std::string FormatDuration(s64 duration) {
|
||||
// The following is implemented according to libavutil code (opt.c)
|
||||
std::string out;
|
||||
if (duration < 0 && duration != std::numeric_limits<s64>::min()) {
|
||||
out.append("-");
|
||||
duration = -duration;
|
||||
}
|
||||
if (duration == std::numeric_limits<s64>::max()) {
|
||||
return "INT64_MAX";
|
||||
} else if (duration == std::numeric_limits<s64>::min()) {
|
||||
return "INT64_MIN";
|
||||
} else if (duration > 3600ll * 1000000ll) {
|
||||
out.append(fmt::format("{}:{:02d}:{:02d}.{:06d}", duration / 3600000000ll,
|
||||
((duration / 60000000ll) % 60), ((duration / 1000000ll) % 60),
|
||||
duration % 1000000));
|
||||
} else if (duration > 60ll * 1000000ll) {
|
||||
out.append(fmt::format("{}:{:02d}.{:06d}", duration / 60000000ll,
|
||||
((duration / 1000000ll) % 60), duration % 1000000));
|
||||
} else {
|
||||
out.append(fmt::format("{}.{:06d}", duration / 1000000ll, duration % 1000000));
|
||||
}
|
||||
while (out.back() == '0') {
|
||||
out.erase(out.size() - 1, 1);
|
||||
}
|
||||
if (out.back() == '.') {
|
||||
out.erase(out.size() - 1, 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string FormatDefaultValue(const AVOption* option,
|
||||
const std::vector<OptionInfo::NamedConstant>& named_constants) {
|
||||
// The following is taken and modified from libavutil code (opt.c)
|
||||
switch (option->type) {
|
||||
case AV_OPT_TYPE_BOOL: {
|
||||
const auto value = option->default_val.i64;
|
||||
if (value < 0) {
|
||||
return "auto";
|
||||
}
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
case AV_OPT_TYPE_FLAGS: {
|
||||
const auto value = option->default_val.i64;
|
||||
std::string out;
|
||||
for (const auto& constant : named_constants) {
|
||||
if (!(value & constant.value)) {
|
||||
continue;
|
||||
}
|
||||
if (!out.empty()) {
|
||||
out.append("+");
|
||||
}
|
||||
out.append(constant.name);
|
||||
}
|
||||
return out.empty() ? fmt::format("{}", value) : out;
|
||||
}
|
||||
case AV_OPT_TYPE_DURATION: {
|
||||
return FormatDuration(option->default_val.i64);
|
||||
}
|
||||
case AV_OPT_TYPE_INT:
|
||||
case AV_OPT_TYPE_UINT64:
|
||||
case AV_OPT_TYPE_INT64: {
|
||||
const auto value = option->default_val.i64;
|
||||
for (const auto& constant : named_constants) {
|
||||
if (constant.value == value) {
|
||||
return constant.name;
|
||||
}
|
||||
}
|
||||
return fmt::format("{}", value);
|
||||
}
|
||||
case AV_OPT_TYPE_DOUBLE:
|
||||
case AV_OPT_TYPE_FLOAT: {
|
||||
return fmt::format("{}", option->default_val.dbl);
|
||||
}
|
||||
case AV_OPT_TYPE_RATIONAL: {
|
||||
const auto q = av_d2q(option->default_val.dbl, std::numeric_limits<int>::max());
|
||||
return fmt::format("{}/{}", q.num, q.den);
|
||||
}
|
||||
case AV_OPT_TYPE_PIXEL_FMT: {
|
||||
const char* name = av_get_pix_fmt_name(static_cast<AVPixelFormat>(option->default_val.i64));
|
||||
return ToStdString(name, "none");
|
||||
}
|
||||
case AV_OPT_TYPE_SAMPLE_FMT: {
|
||||
const char* name =
|
||||
av_get_sample_fmt_name(static_cast<AVSampleFormat>(option->default_val.i64));
|
||||
return ToStdString(name, "none");
|
||||
}
|
||||
case AV_OPT_TYPE_COLOR:
|
||||
case AV_OPT_TYPE_IMAGE_SIZE:
|
||||
case AV_OPT_TYPE_STRING:
|
||||
case AV_OPT_TYPE_DICT:
|
||||
case AV_OPT_TYPE_VIDEO_RATE: {
|
||||
return ToStdString(option->default_val.str);
|
||||
}
|
||||
case AV_OPT_TYPE_CHANNEL_LAYOUT: {
|
||||
return fmt::format("{:#x}", option->default_val.i64);
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void GetOptionListSingle(std::vector<OptionInfo>& out, const AVClass* av_class) {
|
||||
if (av_class == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const AVOption* current = nullptr;
|
||||
std::unordered_map<std::string, std::vector<OptionInfo::NamedConstant>> named_constants_map;
|
||||
// First iteration: find and place all named constants
|
||||
while ((current = av_opt_next(&av_class, current))) {
|
||||
if (current->type != AV_OPT_TYPE_CONST || !current->unit) {
|
||||
continue;
|
||||
}
|
||||
named_constants_map[current->unit].push_back(
|
||||
{current->name, ToStdString(current->help), current->default_val.i64});
|
||||
}
|
||||
// Second iteration: find all options
|
||||
current = nullptr;
|
||||
while ((current = av_opt_next(&av_class, current))) {
|
||||
// Currently we cannot handle binary options
|
||||
if (current->type == AV_OPT_TYPE_CONST || current->type == AV_OPT_TYPE_BINARY) {
|
||||
continue;
|
||||
}
|
||||
std::vector<OptionInfo::NamedConstant> named_constants;
|
||||
if (current->unit && named_constants_map.count(current->unit)) {
|
||||
named_constants = named_constants_map.at(current->unit);
|
||||
}
|
||||
const auto default_value = FormatDefaultValue(current, named_constants);
|
||||
out.push_back({current->name, ToStdString(current->help), current->type, default_value,
|
||||
std::move(named_constants), current->min, current->max});
|
||||
}
|
||||
}
|
||||
|
||||
void GetOptionList(std::vector<OptionInfo>& out, const AVClass* av_class) {
|
||||
if (av_class == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
GetOptionListSingle(out, av_class);
|
||||
|
||||
const AVClass* child_class = nullptr;
|
||||
while ((child_class = av_opt_child_class_next(av_class, child_class))) {
|
||||
GetOptionListSingle(out, child_class);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<OptionInfo> GetOptionList(const AVClass* av_class) {
|
||||
std::vector<OptionInfo> out;
|
||||
GetOptionList(out, av_class);
|
||||
|
||||
// Filter out identical options (why do they exist in the first place?)
|
||||
std::unordered_set<std::string> option_name_set;
|
||||
std::vector<OptionInfo> final_out;
|
||||
for (auto& option : out) {
|
||||
if (option_name_set.count(option.name)) {
|
||||
continue;
|
||||
}
|
||||
option_name_set.emplace(option.name);
|
||||
final_out.emplace_back(std::move(option));
|
||||
}
|
||||
|
||||
return final_out;
|
||||
}
|
||||
|
||||
std::vector<EncoderInfo> ListEncoders(AVMediaType type) {
|
||||
InitializeFFmpegLibraries();
|
||||
|
||||
const auto general_options = GetOptionList(avcodec_get_class());
|
||||
|
||||
std::vector<EncoderInfo> out;
|
||||
|
||||
const AVCodec* current = nullptr;
|
||||
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100)
|
||||
while ((current = av_codec_next(current))) {
|
||||
#else
|
||||
void* data = nullptr; // For libavcodec to save the iteration state
|
||||
while ((current = av_codec_iterate(&data))) {
|
||||
#endif
|
||||
if (!av_codec_is_encoder(current) || current->type != type) {
|
||||
continue;
|
||||
}
|
||||
auto options = GetOptionList(current->priv_class);
|
||||
options.insert(options.end(), general_options.begin(), general_options.end());
|
||||
out.push_back(
|
||||
{current->name, ToStdString(current->long_name), current->id, std::move(options)});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<FormatInfo> ListFormats() {
|
||||
InitializeFFmpegLibraries();
|
||||
|
||||
const auto general_options = GetOptionList(avformat_get_class());
|
||||
|
||||
std::vector<FormatInfo> out;
|
||||
|
||||
const AVOutputFormat* current = nullptr;
|
||||
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
|
||||
while ((current = av_oformat_next(current))) {
|
||||
#else
|
||||
void* data = nullptr; // For libavformat to save the iteration state
|
||||
while ((current = av_muxer_iterate(&data))) {
|
||||
#endif
|
||||
auto options = GetOptionList(current->priv_class);
|
||||
options.insert(options.end(), general_options.begin(), general_options.end());
|
||||
|
||||
std::vector<std::string> extensions;
|
||||
Common::SplitString(ToStdString(current->extensions), ',', extensions);
|
||||
|
||||
std::set<AVCodecID> supported_video_codecs;
|
||||
std::set<AVCodecID> supported_audio_codecs;
|
||||
// Go through all codecs
|
||||
const AVCodecDescriptor* codec = nullptr;
|
||||
while ((codec = avcodec_descriptor_next(codec))) {
|
||||
if (avformat_query_codec(current, codec->id, FF_COMPLIANCE_NORMAL) == 1) {
|
||||
if (codec->type == AVMEDIA_TYPE_VIDEO) {
|
||||
supported_video_codecs.emplace(codec->id);
|
||||
} else if (codec->type == AVMEDIA_TYPE_AUDIO) {
|
||||
supported_audio_codecs.emplace(codec->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (supported_video_codecs.empty() || supported_audio_codecs.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push_back({current->name, ToStdString(current->long_name), std::move(extensions),
|
||||
std::move(supported_video_codecs), std::move(supported_audio_codecs),
|
||||
std::move(options)});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace VideoDumper
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
|
@ -19,6 +20,7 @@
|
|||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <libswscale/swscale.h>
|
||||
}
|
||||
|
@ -188,4 +190,43 @@ private:
|
|||
Common::Event processing_ended;
|
||||
};
|
||||
|
||||
/// Struct describing encoder/muxer options
|
||||
struct OptionInfo {
|
||||
std::string name;
|
||||
std::string description;
|
||||
AVOptionType type;
|
||||
std::string default_value;
|
||||
struct NamedConstant {
|
||||
std::string name;
|
||||
std::string description;
|
||||
s64 value;
|
||||
};
|
||||
std::vector<NamedConstant> named_constants;
|
||||
|
||||
// If this is a scalar type
|
||||
double min;
|
||||
double max;
|
||||
};
|
||||
|
||||
/// Struct describing an encoder
|
||||
struct EncoderInfo {
|
||||
std::string name;
|
||||
std::string long_name;
|
||||
AVCodecID codec;
|
||||
std::vector<OptionInfo> options;
|
||||
};
|
||||
|
||||
/// Struct describing a format
|
||||
struct FormatInfo {
|
||||
std::string name;
|
||||
std::string long_name;
|
||||
std::vector<std::string> extensions;
|
||||
std::set<AVCodecID> supported_video_codecs;
|
||||
std::set<AVCodecID> supported_audio_codecs;
|
||||
std::vector<OptionInfo> options;
|
||||
};
|
||||
|
||||
std::vector<EncoderInfo> ListEncoders(AVMediaType type);
|
||||
std::vector<FormatInfo> ListFormats();
|
||||
|
||||
} // namespace VideoDumper
|
||||
|
|
Reference in New Issue