core/dumping: Allow format/encoder selection+configuration

The ParamPackage got modified so that we can use range-based for on it
This commit is contained in:
zhupengfei 2020-01-29 14:54:39 +08:00
parent 3c6765e87c
commit 016f8be0b8
No known key found for this signature in database
GPG key ID: DD129E108BD09378
8 changed files with 86 additions and 32 deletions

View file

@ -409,7 +409,7 @@ int main(int argc, char** argv) {
if (!dump_video.empty()) { if (!dump_video.empty()) {
Layout::FramebufferLayout layout{ Layout::FramebufferLayout layout{
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
system.VideoDumper().StartDumping(dump_video, "webm", layout); system.VideoDumper().StartDumping(dump_video, layout);
} }
std::thread render_thread([&emu_window] { emu_window->Present(); }); std::thread render_thread([&emu_window] { emu_window->Present(); });

View file

@ -976,7 +976,7 @@ void GMainWindow::BootGame(const QString& filename) {
Layout::FramebufferLayout layout{ Layout::FramebufferLayout layout{
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
Core::System::GetInstance().VideoDumper().StartDumping(video_dumping_path.toStdString(), Core::System::GetInstance().VideoDumper().StartDumping(video_dumping_path.toStdString(),
"webm", layout); layout);
video_dumping_on_start = false; video_dumping_on_start = false;
video_dumping_path.clear(); video_dumping_path.clear();
} }
@ -1815,7 +1815,7 @@ void GMainWindow::OnStartVideoDumping() {
if (emulation_running) { if (emulation_running) {
Layout::FramebufferLayout layout{ Layout::FramebufferLayout layout{
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), "webm", layout); Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), layout);
} else { } else {
video_dumping_on_start = true; video_dumping_on_start = true;
video_dumping_path = path; video_dumping_path = path;

View file

@ -135,4 +135,20 @@ void ParamPackage::Clear() {
data.clear(); data.clear();
} }
ParamPackage::DataType::iterator ParamPackage::begin() {
return data.begin();
}
ParamPackage::DataType::const_iterator ParamPackage::begin() const {
return data.begin();
}
ParamPackage::DataType::iterator ParamPackage::end() {
return data.end();
}
ParamPackage::DataType::const_iterator ParamPackage::end() const {
return data.end();
}
} // namespace Common } // namespace Common

View file

@ -5,15 +5,15 @@
#pragma once #pragma once
#include <initializer_list> #include <initializer_list>
#include <map>
#include <string> #include <string>
#include <unordered_map>
namespace Common { namespace Common {
/// A string-based key-value container supporting serializing to and deserializing from a string /// A string-based key-value container supporting serializing to and deserializing from a string
class ParamPackage { class ParamPackage {
public: public:
using DataType = std::unordered_map<std::string, std::string>; using DataType = std::map<std::string, std::string>;
ParamPackage() = default; ParamPackage() = default;
explicit ParamPackage(const std::string& serialized); explicit ParamPackage(const std::string& serialized);
@ -35,6 +35,12 @@ public:
void Erase(const std::string& key); void Erase(const std::string& key);
void Clear(); void Clear();
// For range-based for
DataType::iterator begin();
DataType::const_iterator begin() const;
DataType::iterator end();
DataType::const_iterator end() const;
private: private:
DataType data; DataType data;
}; };

View file

@ -28,8 +28,7 @@ public:
class Backend { class Backend {
public: public:
virtual ~Backend(); virtual ~Backend();
virtual bool StartDumping(const std::string& path, const std::string& format, virtual bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) = 0;
const Layout::FramebufferLayout& layout) = 0;
virtual void AddVideoFrame(VideoFrame frame) = 0; virtual void AddVideoFrame(VideoFrame frame) = 0;
virtual void AddAudioFrame(AudioCore::StereoFrame16 frame) = 0; virtual void AddAudioFrame(AudioCore::StereoFrame16 frame) = 0;
virtual void AddAudioSample(const std::array<s16, 2>& sample) = 0; virtual void AddAudioSample(const std::array<s16, 2>& sample) = 0;
@ -41,7 +40,7 @@ public:
class NullBackend : public Backend { class NullBackend : public Backend {
public: public:
~NullBackend() override; ~NullBackend() override;
bool StartDumping(const std::string& /*path*/, const std::string& /*format*/, bool StartDumping(const std::string& /*path*/,
const Layout::FramebufferLayout& /*layout*/) override { const Layout::FramebufferLayout& /*layout*/) override {
return false; return false;
} }

View file

@ -5,7 +5,9 @@
#include "common/assert.h" #include "common/assert.h"
#include "common/file_util.h" #include "common/file_util.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "common/param_package.h"
#include "core/dumping/ffmpeg_backend.h" #include "core/dumping/ffmpeg_backend.h"
#include "core/settings.h"
#include "video_core/renderer_base.h" #include "video_core/renderer_base.h"
#include "video_core/video_core.h" #include "video_core/video_core.h"
@ -27,6 +29,15 @@ void InitializeFFmpegLibraries() {
initialized = true; initialized = true;
} }
AVDictionary* ToAVDictionary(const std::string& serialized) {
Common::ParamPackage param_package{serialized};
AVDictionary* result = nullptr;
for (const auto& [key, value] : param_package) {
av_dict_set(&result, key.c_str(), value.c_str(), 0);
}
return result;
}
FFmpegStream::~FFmpegStream() { FFmpegStream::~FFmpegStream() {
Free(); Free();
} }
@ -100,9 +111,7 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou
frame_count = 0; frame_count = 0;
// Initialize video codec // Initialize video codec
// Ensure VP9 codec here, also to avoid patent issues const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.video_encoder.c_str());
constexpr AVCodecID codec_id = AV_CODEC_ID_VP9;
const AVCodec* codec = avcodec_find_encoder(codec_id);
codec_context.reset(avcodec_alloc_context3(codec)); codec_context.reset(avcodec_alloc_context3(codec));
if (!codec || !codec_context) { if (!codec || !codec_context) {
LOG_ERROR(Render, "Could not find video encoder or allocate video codec context"); LOG_ERROR(Render, "Could not find video encoder or allocate video codec context");
@ -111,23 +120,28 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou
// Configure video codec context // Configure video codec context
codec_context->codec_type = AVMEDIA_TYPE_VIDEO; codec_context->codec_type = AVMEDIA_TYPE_VIDEO;
codec_context->bit_rate = 2500000; codec_context->bit_rate = Settings::values.video_bitrate;
codec_context->width = layout.width; codec_context->width = layout.width;
codec_context->height = layout.height; codec_context->height = layout.height;
codec_context->time_base.num = 1; codec_context->time_base.num = 1;
codec_context->time_base.den = 60; codec_context->time_base.den = 60;
codec_context->gop_size = 12; codec_context->gop_size = 12;
codec_context->pix_fmt = AV_PIX_FMT_YUV420P; codec_context->pix_fmt = AV_PIX_FMT_YUV420P;
codec_context->thread_count = 8;
if (output_format->flags & AVFMT_GLOBALHEADER) if (output_format->flags & AVFMT_GLOBALHEADER)
codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
av_opt_set_int(codec_context.get(), "cpu-used", 5, 0);
if (avcodec_open2(codec_context.get(), codec, nullptr) < 0) { AVDictionary* options = ToAVDictionary(Settings::values.video_encoder_options);
if (avcodec_open2(codec_context.get(), codec, &options) < 0) {
LOG_ERROR(Render, "Could not open video codec"); LOG_ERROR(Render, "Could not open video codec");
return false; return false;
} }
if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
char* buf = nullptr;
av_dict_get_string(options, &buf, ':', ';');
LOG_WARNING(Render, "Video encoder options not found: {}", buf);
}
// Create video stream // Create video stream
stream = avformat_new_stream(format_context, codec); stream = avformat_new_stream(format_context, codec);
if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) { if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
@ -200,8 +214,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) {
sample_count = 0; sample_count = 0;
// Initialize audio codec // Initialize audio codec
constexpr AVCodecID codec_id = AV_CODEC_ID_VORBIS; const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.audio_encoder.c_str());
const AVCodec* codec = avcodec_find_encoder(codec_id);
codec_context.reset(avcodec_alloc_context3(codec)); codec_context.reset(avcodec_alloc_context3(codec));
if (!codec || !codec_context) { if (!codec || !codec_context) {
LOG_ERROR(Render, "Could not find audio encoder or allocate audio codec context"); LOG_ERROR(Render, "Could not find audio encoder or allocate audio codec context");
@ -210,17 +223,24 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) {
// Configure audio codec context // Configure audio codec context
codec_context->codec_type = AVMEDIA_TYPE_AUDIO; codec_context->codec_type = AVMEDIA_TYPE_AUDIO;
codec_context->bit_rate = 64000; codec_context->bit_rate = Settings::values.audio_bitrate;
codec_context->sample_fmt = codec->sample_fmts[0]; codec_context->sample_fmt = codec->sample_fmts[0];
codec_context->sample_rate = AudioCore::native_sample_rate; codec_context->sample_rate = AudioCore::native_sample_rate;
codec_context->channel_layout = AV_CH_LAYOUT_STEREO; codec_context->channel_layout = AV_CH_LAYOUT_STEREO;
codec_context->channels = 2; codec_context->channels = 2;
if (avcodec_open2(codec_context.get(), codec, nullptr) < 0) { AVDictionary* options = ToAVDictionary(Settings::values.audio_encoder_options);
if (avcodec_open2(codec_context.get(), codec, &options) < 0) {
LOG_ERROR(Render, "Could not open audio codec"); LOG_ERROR(Render, "Could not open audio codec");
return false; return false;
} }
if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
char* buf = nullptr;
av_dict_get_string(options, &buf, ':', ';');
LOG_WARNING(Render, "Audio encoder options not found: {}", buf);
}
// Create audio stream // Create audio stream
stream = avformat_new_stream(format_context, codec); stream = avformat_new_stream(format_context, codec);
if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) { if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
@ -305,8 +325,7 @@ FFmpegMuxer::~FFmpegMuxer() {
Free(); Free();
} }
bool FFmpegMuxer::Init(const std::string& path, const std::string& format, bool FFmpegMuxer::Init(const std::string& path, const Layout::FramebufferLayout& layout) {
const Layout::FramebufferLayout& layout) {
InitializeFFmpegLibraries(); InitializeFFmpegLibraries();
@ -315,9 +334,8 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format,
} }
// Get output format // Get output format
// Ensure webm here to avoid patent issues const auto format = Settings::values.output_format;
ASSERT_MSG(format == "webm", "Only webm is allowed for frame dumping"); auto* output_format = av_guess_format(format.c_str(), path.c_str(), nullptr);
auto* output_format = av_guess_format(format.c_str(), path.c_str(), "video/webm");
if (!output_format) { if (!output_format) {
LOG_ERROR(Render, "Could not get format {}", format); LOG_ERROR(Render, "Could not get format {}", format);
return false; return false;
@ -338,13 +356,19 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format,
if (!audio_stream.Init(format_context.get())) if (!audio_stream.Init(format_context.get()))
return false; return false;
AVDictionary* options = ToAVDictionary(Settings::values.format_options);
// Open video file // Open video file
if (avio_open(&format_context->pb, path.c_str(), AVIO_FLAG_WRITE) < 0 || if (avio_open(&format_context->pb, path.c_str(), AVIO_FLAG_WRITE) < 0 ||
avformat_write_header(format_context.get(), nullptr)) { avformat_write_header(format_context.get(), &options)) {
LOG_ERROR(Render, "Could not open {}", path); LOG_ERROR(Render, "Could not open {}", path);
return false; return false;
} }
if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
char* buf = nullptr;
av_dict_get_string(options, &buf, ':', ';');
LOG_WARNING(Render, "Format options not found: {}", buf);
}
LOG_INFO(Render, "Dumping frames to {} ({}x{})", path, layout.width, layout.height); LOG_INFO(Render, "Dumping frames to {} ({}x{})", path, layout.width, layout.height);
return true; return true;
@ -392,12 +416,11 @@ FFmpegBackend::~FFmpegBackend() {
ffmpeg.Free(); ffmpeg.Free();
} }
bool FFmpegBackend::StartDumping(const std::string& path, const std::string& format, bool FFmpegBackend::StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) {
const Layout::FramebufferLayout& layout) {
InitializeFFmpegLibraries(); InitializeFFmpegLibraries();
if (!ffmpeg.Init(path, format, layout)) { if (!ffmpeg.Init(path, layout)) {
ffmpeg.Free(); ffmpeg.Free();
return false; return false;
} }

View file

@ -129,8 +129,7 @@ class FFmpegMuxer {
public: public:
~FFmpegMuxer(); ~FFmpegMuxer();
bool Init(const std::string& path, const std::string& format, bool Init(const std::string& path, const Layout::FramebufferLayout& layout);
const Layout::FramebufferLayout& layout);
void Free(); void Free();
void ProcessVideoFrame(VideoFrame& frame); void ProcessVideoFrame(VideoFrame& frame);
void ProcessAudioFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1); void ProcessAudioFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1);
@ -161,8 +160,7 @@ class FFmpegBackend : public Backend {
public: public:
FFmpegBackend(); FFmpegBackend();
~FFmpegBackend() override; ~FFmpegBackend() override;
bool StartDumping(const std::string& path, const std::string& format, bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) override;
const Layout::FramebufferLayout& layout) override;
void AddVideoFrame(VideoFrame frame) override; void AddVideoFrame(VideoFrame frame) override;
void AddAudioFrame(AudioCore::StereoFrame16 frame) override; void AddAudioFrame(AudioCore::StereoFrame16 frame) override;
void AddAudioSample(const std::array<s16, 2>& sample) override; void AddAudioSample(const std::array<s16, 2>& sample) override;

View file

@ -204,6 +204,18 @@ struct Values {
std::string web_api_url; std::string web_api_url;
std::string citra_username; std::string citra_username;
std::string citra_token; std::string citra_token;
// Video Dumping
std::string output_format;
std::string format_options;
std::string video_encoder;
std::string video_encoder_options;
u64 video_bitrate;
std::string audio_encoder;
std::string audio_encoder_options;
u64 audio_bitrate;
} extern values; } extern values;
// a special value for Values::region_value indicating that citra will automatically select a region // a special value for Values::region_value indicating that citra will automatically select a region