From 016f8be0b8a5778a2ead3fbecda4e5b4065a7591 Mon Sep 17 00:00:00 2001
From: zhupengfei <zhupf321@gmail.com>
Date: Wed, 29 Jan 2020 14:54:39 +0800
Subject: [PATCH] core/dumping: Allow format/encoder selection+configuration

The ParamPackage got modified so that we can use range-based for on it
---
 src/citra/citra.cpp                 |  2 +-
 src/citra_qt/main.cpp               |  4 +-
 src/common/param_package.cpp        | 16 ++++++++
 src/common/param_package.h          | 10 ++++-
 src/core/dumping/backend.h          |  5 +--
 src/core/dumping/ffmpeg_backend.cpp | 63 ++++++++++++++++++++---------
 src/core/dumping/ffmpeg_backend.h   |  6 +--
 src/core/settings.h                 | 12 ++++++
 8 files changed, 86 insertions(+), 32 deletions(-)

diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp
index ce06b31b7..a1455d373 100644
--- a/src/citra/citra.cpp
+++ b/src/citra/citra.cpp
@@ -409,7 +409,7 @@ int main(int argc, char** argv) {
     if (!dump_video.empty()) {
         Layout::FramebufferLayout layout{
             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(); });
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index 0c87e98e1..e106bf170 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -976,7 +976,7 @@ void GMainWindow::BootGame(const QString& filename) {
         Layout::FramebufferLayout layout{
             Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
         Core::System::GetInstance().VideoDumper().StartDumping(video_dumping_path.toStdString(),
-                                                               "webm", layout);
+                                                               layout);
         video_dumping_on_start = false;
         video_dumping_path.clear();
     }
@@ -1815,7 +1815,7 @@ void GMainWindow::OnStartVideoDumping() {
     if (emulation_running) {
         Layout::FramebufferLayout layout{
             Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
-        Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), "webm", layout);
+        Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), layout);
     } else {
         video_dumping_on_start = true;
         video_dumping_path = path;
diff --git a/src/common/param_package.cpp b/src/common/param_package.cpp
index 433b34b36..3a218efbc 100644
--- a/src/common/param_package.cpp
+++ b/src/common/param_package.cpp
@@ -135,4 +135,20 @@ void ParamPackage::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
diff --git a/src/common/param_package.h b/src/common/param_package.h
index 6a0a9b656..1fffb5035 100644
--- a/src/common/param_package.h
+++ b/src/common/param_package.h
@@ -5,15 +5,15 @@
 #pragma once
 
 #include <initializer_list>
+#include <map>
 #include <string>
-#include <unordered_map>
 
 namespace Common {
 
 /// A string-based key-value container supporting serializing to and deserializing from a string
 class ParamPackage {
 public:
-    using DataType = std::unordered_map<std::string, std::string>;
+    using DataType = std::map<std::string, std::string>;
 
     ParamPackage() = default;
     explicit ParamPackage(const std::string& serialized);
@@ -35,6 +35,12 @@ public:
     void Erase(const std::string& key);
     void Clear();
 
+    // For range-based for
+    DataType::iterator begin();
+    DataType::const_iterator begin() const;
+    DataType::iterator end();
+    DataType::const_iterator end() const;
+
 private:
     DataType data;
 };
diff --git a/src/core/dumping/backend.h b/src/core/dumping/backend.h
index 3dc89082c..b0b63ba66 100644
--- a/src/core/dumping/backend.h
+++ b/src/core/dumping/backend.h
@@ -28,8 +28,7 @@ public:
 class Backend {
 public:
     virtual ~Backend();
-    virtual bool StartDumping(const std::string& path, const std::string& format,
-                              const Layout::FramebufferLayout& layout) = 0;
+    virtual bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) = 0;
     virtual void AddVideoFrame(VideoFrame frame) = 0;
     virtual void AddAudioFrame(AudioCore::StereoFrame16 frame) = 0;
     virtual void AddAudioSample(const std::array<s16, 2>& sample) = 0;
@@ -41,7 +40,7 @@ public:
 class NullBackend : public Backend {
 public:
     ~NullBackend() override;
-    bool StartDumping(const std::string& /*path*/, const std::string& /*format*/,
+    bool StartDumping(const std::string& /*path*/,
                       const Layout::FramebufferLayout& /*layout*/) override {
         return false;
     }
diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp
index 9becaff7b..2e7ecbba5 100644
--- a/src/core/dumping/ffmpeg_backend.cpp
+++ b/src/core/dumping/ffmpeg_backend.cpp
@@ -5,7 +5,9 @@
 #include "common/assert.h"
 #include "common/file_util.h"
 #include "common/logging/log.h"
+#include "common/param_package.h"
 #include "core/dumping/ffmpeg_backend.h"
+#include "core/settings.h"
 #include "video_core/renderer_base.h"
 #include "video_core/video_core.h"
 
@@ -27,6 +29,15 @@ void InitializeFFmpegLibraries() {
     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() {
     Free();
 }
@@ -100,9 +111,7 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou
     frame_count = 0;
 
     // Initialize video codec
-    // Ensure VP9 codec here, also to avoid patent issues
-    constexpr AVCodecID codec_id = AV_CODEC_ID_VP9;
-    const AVCodec* codec = avcodec_find_encoder(codec_id);
+    const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.video_encoder.c_str());
     codec_context.reset(avcodec_alloc_context3(codec));
     if (!codec || !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
     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->height = layout.height;
     codec_context->time_base.num = 1;
     codec_context->time_base.den = 60;
     codec_context->gop_size = 12;
     codec_context->pix_fmt = AV_PIX_FMT_YUV420P;
-    codec_context->thread_count = 8;
     if (output_format->flags & AVFMT_GLOBALHEADER)
         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");
         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
     stream = avformat_new_stream(format_context, codec);
     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;
 
     // Initialize audio codec
-    constexpr AVCodecID codec_id = AV_CODEC_ID_VORBIS;
-    const AVCodec* codec = avcodec_find_encoder(codec_id);
+    const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.audio_encoder.c_str());
     codec_context.reset(avcodec_alloc_context3(codec));
     if (!codec || !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
     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_rate = AudioCore::native_sample_rate;
     codec_context->channel_layout = AV_CH_LAYOUT_STEREO;
     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");
         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
     stream = avformat_new_stream(format_context, codec);
     if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
@@ -305,8 +325,7 @@ FFmpegMuxer::~FFmpegMuxer() {
     Free();
 }
 
-bool FFmpegMuxer::Init(const std::string& path, const std::string& format,
-                       const Layout::FramebufferLayout& layout) {
+bool FFmpegMuxer::Init(const std::string& path, const Layout::FramebufferLayout& layout) {
 
     InitializeFFmpegLibraries();
 
@@ -315,9 +334,8 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format,
     }
 
     // Get output format
-    // Ensure webm here to avoid patent issues
-    ASSERT_MSG(format == "webm", "Only webm is allowed for frame dumping");
-    auto* output_format = av_guess_format(format.c_str(), path.c_str(), "video/webm");
+    const auto format = Settings::values.output_format;
+    auto* output_format = av_guess_format(format.c_str(), path.c_str(), nullptr);
     if (!output_format) {
         LOG_ERROR(Render, "Could not get format {}", format);
         return false;
@@ -338,13 +356,19 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format,
     if (!audio_stream.Init(format_context.get()))
         return false;
 
+    AVDictionary* options = ToAVDictionary(Settings::values.format_options);
     // Open video file
     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);
         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);
     return true;
@@ -392,12 +416,11 @@ FFmpegBackend::~FFmpegBackend() {
     ffmpeg.Free();
 }
 
-bool FFmpegBackend::StartDumping(const std::string& path, const std::string& format,
-                                 const Layout::FramebufferLayout& layout) {
+bool FFmpegBackend::StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) {
 
     InitializeFFmpegLibraries();
 
-    if (!ffmpeg.Init(path, format, layout)) {
+    if (!ffmpeg.Init(path, layout)) {
         ffmpeg.Free();
         return false;
     }
diff --git a/src/core/dumping/ffmpeg_backend.h b/src/core/dumping/ffmpeg_backend.h
index 8af74b0a8..f08f31d3d 100644
--- a/src/core/dumping/ffmpeg_backend.h
+++ b/src/core/dumping/ffmpeg_backend.h
@@ -129,8 +129,7 @@ class FFmpegMuxer {
 public:
     ~FFmpegMuxer();
 
-    bool Init(const std::string& path, const std::string& format,
-              const Layout::FramebufferLayout& layout);
+    bool Init(const std::string& path, const Layout::FramebufferLayout& layout);
     void Free();
     void ProcessVideoFrame(VideoFrame& frame);
     void ProcessAudioFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1);
@@ -161,8 +160,7 @@ class FFmpegBackend : public Backend {
 public:
     FFmpegBackend();
     ~FFmpegBackend() override;
-    bool StartDumping(const std::string& path, const std::string& format,
-                      const Layout::FramebufferLayout& layout) override;
+    bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) override;
     void AddVideoFrame(VideoFrame frame) override;
     void AddAudioFrame(AudioCore::StereoFrame16 frame) override;
     void AddAudioSample(const std::array<s16, 2>& sample) override;
diff --git a/src/core/settings.h b/src/core/settings.h
index 78b11912c..8739bcd1c 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -204,6 +204,18 @@ struct Values {
     std::string web_api_url;
     std::string citra_username;
     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;
 
 // a special value for Values::region_value indicating that citra will automatically select a region