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 +#include #include -#include namespace Common { /// A string-based key-value container supporting serializing to and deserializing from a string class ParamPackage { public: - using DataType = std::unordered_map; + using DataType = std::map; 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& 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& 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