mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-30 21:30:04 +00:00 
			
		
		
		
	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:
		
							parent
							
								
									3c6765e87c
								
							
						
					
					
						commit
						016f8be0b8
					
				
					 8 changed files with 86 additions and 32 deletions
				
			
		|  | @ -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(); }); | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|  |  | ||||||
|  | @ -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
 | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|  |  | ||||||
|  | @ -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
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue