summaryrefslogtreecommitdiff
path: root/Source/Core/VideoCommon/FrameDumpFFMpeg.cpp
diff options
context:
space:
mode:
authorScott Mansell <phiren@gmail.com>2023-01-28 13:50:57 +1300
committerScott Mansell <phiren@gmail.com>2023-01-31 18:45:29 +1300
commite57eb04ed3b40e806fa081fc1bdaeaa0c03e4e66 (patch)
treec47db6dd4f9e8256764e5b9221dfd1dc3ae43651 /Source/Core/VideoCommon/FrameDumpFFMpeg.cpp
parent0d4537d60f695e50379c1542d6c4b1e643e8a5a0 (diff)
Rename FrameDump.{cpp,h} to FrameDumpFFMpeg.{cpp,h}
The name kind of conflicts with a new FrameDumper class
Diffstat (limited to 'Source/Core/VideoCommon/FrameDumpFFMpeg.cpp')
-rw-r--r--Source/Core/VideoCommon/FrameDumpFFMpeg.cpp546
1 files changed, 546 insertions, 0 deletions
diff --git a/Source/Core/VideoCommon/FrameDumpFFMpeg.cpp b/Source/Core/VideoCommon/FrameDumpFFMpeg.cpp
new file mode 100644
index 0000000000..09fdf4dbac
--- /dev/null
+++ b/Source/Core/VideoCommon/FrameDumpFFMpeg.cpp
@@ -0,0 +1,546 @@
+// Copyright 2009 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "VideoCommon/FrameDumpFFMpeg.h"
+
+#if defined(__FreeBSD__)
+#define __STDC_CONSTANT_MACROS 1
+#endif
+
+#include <array>
+#include <sstream>
+#include <string>
+
+#include <fmt/chrono.h>
+#include <fmt/format.h>
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libavutil/error.h>
+#include <libavutil/log.h>
+#include <libavutil/mathematics.h>
+#include <libavutil/opt.h>
+#include <libavutil/pixdesc.h>
+#include <libswscale/swscale.h>
+}
+
+#include "Common/ChunkFile.h"
+#include "Common/FileUtil.h"
+#include "Common/Logging/Log.h"
+#include "Common/Logging/LogManager.h"
+#include "Common/MsgHandler.h"
+#include "Common/StringUtil.h"
+
+#include "Core/Config/MainSettings.h"
+#include "Core/ConfigManager.h"
+#include "Core/HW/SystemTimers.h"
+#include "Core/HW/VideoInterface.h"
+
+#include "VideoCommon/FrameDumper.h"
+#include "VideoCommon/OnScreenDisplay.h"
+#include "VideoCommon/VideoConfig.h"
+
+struct FrameDumpContext
+{
+ AVFormatContext* format = nullptr;
+ AVStream* stream = nullptr;
+ AVCodecContext* codec = nullptr;
+ AVFrame* src_frame = nullptr;
+ AVFrame* scaled_frame = nullptr;
+ SwsContext* sws = nullptr;
+
+ s64 last_pts = AV_NOPTS_VALUE;
+
+ int width = 0;
+ int height = 0;
+
+ u64 start_ticks = 0;
+ u32 savestate_index = 0;
+
+ bool gave_vfr_warning = false;
+};
+
+namespace
+{
+AVRational GetTimeBaseForCurrentRefreshRate()
+{
+ int num;
+ int den;
+ av_reduce(&num, &den, int(VideoInterface::GetTargetRefreshRateDenominator()),
+ int(VideoInterface::GetTargetRefreshRateNumerator()), std::numeric_limits<int>::max());
+ return AVRational{num, den};
+}
+
+void InitAVCodec()
+{
+ static bool first_run = true;
+ if (first_run)
+ {
+ av_log_set_level(AV_LOG_DEBUG);
+ av_log_set_callback([](void* ptr, int level, const char* fmt, va_list vl) {
+ if (level < 0)
+ level = AV_LOG_DEBUG;
+ if (level >= 0)
+ level &= 0xff;
+
+ if (level > av_log_get_level())
+ return;
+
+ auto log_level = Common::Log::LogLevel::LNOTICE;
+ if (level >= AV_LOG_ERROR && level < AV_LOG_WARNING)
+ log_level = Common::Log::LogLevel::LERROR;
+ else if (level >= AV_LOG_WARNING && level < AV_LOG_INFO)
+ log_level = Common::Log::LogLevel::LWARNING;
+ else if (level >= AV_LOG_INFO && level < AV_LOG_DEBUG)
+ log_level = Common::Log::LogLevel::LINFO;
+ else if (level >= AV_LOG_DEBUG)
+ // keep libav debug messages visible in release build of dolphin
+ log_level = Common::Log::LogLevel::LINFO;
+
+ // Don't perform this formatting if the log level is disabled
+ auto* log_manager = Common::Log::LogManager::GetInstance();
+ if (log_manager != nullptr &&
+ log_manager->IsEnabled(Common::Log::LogType::FRAMEDUMP, log_level))
+ {
+ constexpr size_t MAX_MSGLEN = 1024;
+ char message[MAX_MSGLEN];
+ CharArrayFromFormatV(message, MAX_MSGLEN, fmt, vl);
+
+ GENERIC_LOG_FMT(Common::Log::LogType::FRAMEDUMP, log_level, "{}", message);
+ }
+ });
+
+ // TODO: We never call avformat_network_deinit.
+ avformat_network_init();
+
+ first_run = false;
+ }
+}
+
+std::string GetDumpPath(const std::string& extension, std::time_t time, u32 index)
+{
+ if (!g_Config.sDumpPath.empty())
+ return g_Config.sDumpPath;
+
+ const std::string path_prefix =
+ File::GetUserPath(D_DUMPFRAMES_IDX) + SConfig::GetInstance().GetGameID();
+
+ const std::string base_name =
+ fmt::format("{}_{:%Y-%m-%d_%H-%M-%S}_{}", path_prefix, fmt::localtime(time), index);
+
+ const std::string path = fmt::format("{}.{}", base_name, extension);
+
+ // Ask to delete file.
+ if (File::Exists(path))
+ {
+ if (Config::Get(Config::MAIN_MOVIE_DUMP_FRAMES_SILENT) ||
+ AskYesNoFmtT("Delete the existing file '{0}'?", path))
+ {
+ File::Delete(path);
+ }
+ else
+ {
+ // Stop and cancel dumping the video
+ return "";
+ }
+ }
+
+ return path;
+}
+
+std::string AVErrorString(int error)
+{
+ std::array<char, AV_ERROR_MAX_STRING_SIZE> msg;
+ av_make_error_string(&msg[0], msg.size(), error);
+ return fmt::format("{:8x} {}", (u32)error, &msg[0]);
+}
+
+} // namespace
+
+bool FFMpegFrameDump::Start(int w, int h, u64 start_ticks)
+{
+ if (IsStarted())
+ return true;
+
+ m_savestate_index = 0;
+ m_start_time = std::time(nullptr);
+ m_file_index = 0;
+
+ return PrepareEncoding(w, h, start_ticks, m_savestate_index);
+}
+
+bool FFMpegFrameDump::PrepareEncoding(int w, int h, u64 start_ticks, u32 savestate_index)
+{
+ m_context = std::make_unique<FrameDumpContext>();
+
+ m_context->width = w;
+ m_context->height = h;
+
+ m_context->start_ticks = start_ticks;
+ m_context->savestate_index = savestate_index;
+
+ InitAVCodec();
+ const bool success = CreateVideoFile();
+ if (!success)
+ {
+ CloseVideoFile();
+ OSD::AddMessage("FrameDump Start failed");
+ }
+ return success;
+}
+
+bool FFMpegFrameDump::CreateVideoFile()
+{
+ const std::string& format = g_Config.sDumpFormat;
+
+ const std::string dump_path = GetDumpPath(format, m_start_time, m_file_index);
+
+ if (dump_path.empty())
+ return false;
+
+ File::CreateFullPath(dump_path);
+
+ auto* const output_format = av_guess_format(format.c_str(), dump_path.c_str(), nullptr);
+ if (!output_format)
+ {
+ ERROR_LOG_FMT(FRAMEDUMP, "Invalid format {}", format);
+ return false;
+ }
+
+ if (avformat_alloc_output_context2(&m_context->format, output_format, nullptr,
+ dump_path.c_str()) < 0)
+ {
+ ERROR_LOG_FMT(FRAMEDUMP, "Could not allocate output context");
+ return false;
+ }
+
+ const std::string& codec_name = g_Config.bUseFFV1 ? "ffv1" : g_Config.sDumpCodec;
+
+ AVCodecID codec_id = output_format->video_codec;
+
+ if (!codec_name.empty())
+ {
+ const AVCodecDescriptor* const codec_desc = avcodec_descriptor_get_by_name(codec_name.c_str());
+ if (codec_desc)
+ codec_id = codec_desc->id;
+ else
+ WARN_LOG_FMT(FRAMEDUMP, "Invalid codec {}", codec_name);
+ }
+
+ const AVCodec* codec = nullptr;
+
+ if (!g_Config.sDumpEncoder.empty())
+ {
+ codec = avcodec_find_encoder_by_name(g_Config.sDumpEncoder.c_str());
+ if (!codec)
+ WARN_LOG_FMT(FRAMEDUMP, "Invalid encoder {}", g_Config.sDumpEncoder);
+ }
+ if (!codec)
+ codec = avcodec_find_encoder(codec_id);
+
+ m_context->codec = avcodec_alloc_context3(codec);
+ if (!codec || !m_context->codec)
+ {
+ ERROR_LOG_FMT(FRAMEDUMP, "Could not find encoder or allocate codec context");
+ return false;
+ }
+
+ // Force XVID FourCC for better compatibility when using H.263
+ if (codec->id == AV_CODEC_ID_MPEG4)
+ m_context->codec->codec_tag = MKTAG('X', 'V', 'I', 'D');
+
+ const auto time_base = GetTimeBaseForCurrentRefreshRate();
+
+ INFO_LOG_FMT(FRAMEDUMP, "Creating video file: {} x {} @ {}/{} fps", m_context->width,
+ m_context->height, time_base.den, time_base.num);
+
+ m_context->codec->codec_type = AVMEDIA_TYPE_VIDEO;
+ m_context->codec->bit_rate = static_cast<int64_t>(g_Config.iBitrateKbps) * 1000;
+ m_context->codec->width = m_context->width;
+ m_context->codec->height = m_context->height;
+ m_context->codec->time_base = time_base;
+ m_context->codec->gop_size = 1;
+ m_context->codec->level = 1;
+
+ AVPixelFormat pix_fmt = AV_PIX_FMT_NONE;
+
+ const std::string& pixel_format_string = g_Config.sDumpPixelFormat;
+ if (!pixel_format_string.empty())
+ {
+ pix_fmt = av_get_pix_fmt(pixel_format_string.c_str());
+ if (pix_fmt == AV_PIX_FMT_NONE)
+ WARN_LOG_FMT(FRAMEDUMP, "Invalid pixel format {}", pixel_format_string);
+ }
+
+ if (pix_fmt == AV_PIX_FMT_NONE)
+ {
+ if (m_context->codec->codec_id == AV_CODEC_ID_FFV1)
+ pix_fmt = AV_PIX_FMT_BGR0;
+ else if (m_context->codec->codec_id == AV_CODEC_ID_UTVIDEO)
+ pix_fmt = AV_PIX_FMT_GBRP;
+ else
+ pix_fmt = AV_PIX_FMT_YUV420P;
+ }
+
+ m_context->codec->pix_fmt = pix_fmt;
+
+ if (m_context->codec->codec_id == AV_CODEC_ID_UTVIDEO)
+ av_opt_set_int(m_context->codec->priv_data, "pred", 3, 0); // median
+
+ if (output_format->flags & AVFMT_GLOBALHEADER)
+ m_context->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+
+ if (avcodec_open2(m_context->codec, codec, nullptr) < 0)
+ {
+ ERROR_LOG_FMT(FRAMEDUMP, "Could not open codec");
+ return false;
+ }
+
+ m_context->src_frame = av_frame_alloc();
+ m_context->scaled_frame = av_frame_alloc();
+
+ m_context->scaled_frame->format = m_context->codec->pix_fmt;
+ m_context->scaled_frame->width = m_context->width;
+ m_context->scaled_frame->height = m_context->height;
+
+ if (av_frame_get_buffer(m_context->scaled_frame, 1))
+ return false;
+
+ m_context->stream = avformat_new_stream(m_context->format, codec);
+ if (!m_context->stream ||
+ avcodec_parameters_from_context(m_context->stream->codecpar, m_context->codec) < 0)
+ {
+ ERROR_LOG_FMT(FRAMEDUMP, "Could not create stream");
+ return false;
+ }
+
+ m_context->stream->time_base = m_context->codec->time_base;
+
+ NOTICE_LOG_FMT(FRAMEDUMP, "Opening file {} for dumping", dump_path);
+ if (avio_open(&m_context->format->pb, dump_path.c_str(), AVIO_FLAG_WRITE) < 0 ||
+ avformat_write_header(m_context->format, nullptr))
+ {
+ ERROR_LOG_FMT(FRAMEDUMP, "Could not open {}", dump_path);
+ return false;
+ }
+
+ if (av_cmp_q(m_context->stream->time_base, time_base) != 0)
+ {
+ WARN_LOG_FMT(FRAMEDUMP, "Stream time base differs at {}/{}", m_context->stream->time_base.den,
+ m_context->stream->time_base.num);
+ }
+
+ OSD::AddMessage(fmt::format("Dumping Frames to \"{}\" ({}x{})", dump_path, m_context->width,
+ m_context->height));
+ return true;
+}
+
+bool FFMpegFrameDump::IsFirstFrameInCurrentFile() const
+{
+ return m_context->last_pts == AV_NOPTS_VALUE;
+}
+
+void FFMpegFrameDump::AddFrame(const FrameData& frame)
+{
+ // Are we even dumping?
+ if (!IsStarted())
+ return;
+
+ CheckForConfigChange(frame);
+
+ // Handle failure after a config change.
+ if (!IsStarted())
+ return;
+
+ // Calculate presentation timestamp from ticks since start.
+ const s64 pts = av_rescale_q(frame.state.ticks - m_context->start_ticks,
+ AVRational{1, int(SystemTimers::GetTicksPerSecond())},
+ m_context->codec->time_base);
+
+ if (!IsFirstFrameInCurrentFile())
+ {
+ if (pts <= m_context->last_pts)
+ {
+ WARN_LOG_FMT(FRAMEDUMP, "PTS delta < 1. Current frame will not be dumped.");
+ return;
+ }
+ else if (pts > m_context->last_pts + 1 && !m_context->gave_vfr_warning)
+ {
+ WARN_LOG_FMT(FRAMEDUMP, "PTS delta > 1. Resulting file will have variable frame rate. "
+ "Subsequent occurrences will not be reported.");
+ m_context->gave_vfr_warning = true;
+ }
+ }
+
+ constexpr AVPixelFormat pix_fmt = AV_PIX_FMT_RGBA;
+
+ m_context->src_frame->data[0] = const_cast<u8*>(frame.data);
+ m_context->src_frame->linesize[0] = frame.stride;
+ m_context->src_frame->format = pix_fmt;
+ m_context->src_frame->width = m_context->width;
+ m_context->src_frame->height = m_context->height;
+
+ // Convert image from RGBA to desired pixel format.
+ m_context->sws = sws_getCachedContext(
+ m_context->sws, frame.width, frame.height, pix_fmt, m_context->width, m_context->height,
+ m_context->codec->pix_fmt, SWS_BICUBIC, nullptr, nullptr, nullptr);
+ if (m_context->sws)
+ {
+ sws_scale(m_context->sws, m_context->src_frame->data, m_context->src_frame->linesize, 0,
+ frame.height, m_context->scaled_frame->data, m_context->scaled_frame->linesize);
+ }
+
+ m_context->last_pts = pts;
+ m_context->scaled_frame->pts = pts;
+
+ if (const int error = avcodec_send_frame(m_context->codec, m_context->scaled_frame))
+ {
+ ERROR_LOG_FMT(FRAMEDUMP, "Error while encoding video: {}", AVErrorString(error));
+ return;
+ }
+
+ ProcessPackets();
+}
+
+void FFMpegFrameDump::ProcessPackets()
+{
+ auto pkt = std::unique_ptr<AVPacket, std::function<void(AVPacket*)>>(
+ av_packet_alloc(), [](AVPacket* packet) { av_packet_free(&packet); });
+
+ if (!pkt)
+ {
+ ERROR_LOG_FMT(FRAMEDUMP, "Could not allocate packet");
+ return;
+ }
+
+ while (true)
+ {
+ const int receive_error = avcodec_receive_packet(m_context->codec, pkt.get());
+
+ if (receive_error == AVERROR(EAGAIN) || receive_error == AVERROR_EOF)
+ {
+ // We have processed all available packets.
+ break;
+ }
+
+ if (receive_error)
+ {
+ ERROR_LOG_FMT(FRAMEDUMP, "Error receiving packet: {}", AVErrorString(receive_error));
+ break;
+ }
+
+ av_packet_rescale_ts(pkt.get(), m_context->codec->time_base, m_context->stream->time_base);
+ pkt->stream_index = m_context->stream->index;
+
+ if (const int write_error = av_interleaved_write_frame(m_context->format, pkt.get()))
+ {
+ ERROR_LOG_FMT(FRAMEDUMP, "Error writing packet: {}", AVErrorString(write_error));
+ break;
+ }
+ }
+}
+
+void FFMpegFrameDump::Stop()
+{
+ if (!IsStarted())
+ return;
+
+ // Signal end of stream to encoder.
+ if (const int flush_error = avcodec_send_frame(m_context->codec, nullptr))
+ WARN_LOG_FMT(FRAMEDUMP, "Error sending flush packet: {}", AVErrorString(flush_error));
+
+ ProcessPackets();
+ av_write_trailer(m_context->format);
+ CloseVideoFile();
+
+ NOTICE_LOG_FMT(FRAMEDUMP, "Stopping frame dump");
+ OSD::AddMessage("Stopped dumping frames");
+}
+
+bool FFMpegFrameDump::IsStarted() const
+{
+ return m_context != nullptr;
+}
+
+void FFMpegFrameDump::CloseVideoFile()
+{
+ av_frame_free(&m_context->src_frame);
+ av_frame_free(&m_context->scaled_frame);
+
+ avcodec_free_context(&m_context->codec);
+
+ if (m_context->format)
+ avio_closep(&m_context->format->pb);
+
+ avformat_free_context(m_context->format);
+
+ if (m_context->sws)
+ sws_freeContext(m_context->sws);
+
+ m_context.reset();
+}
+
+void FFMpegFrameDump::DoState(PointerWrap& p)
+{
+ if (p.IsReadMode())
+ ++m_savestate_index;
+}
+
+void FFMpegFrameDump::CheckForConfigChange(const FrameData& frame)
+{
+ bool restart_dump = false;
+
+ // We check here to see if the requested width and height have changed since the last frame which
+ // was dumped, then create a new file accordingly. However, is it possible for the height
+ // (possibly width as well, but no examples known) to have a value of zero. This can occur as the
+ // VI is able to be set to a zero value for height/width to disable output. If this is the case,
+ // simply keep the last known resolution of the video for the added frame.
+ if ((frame.width != m_context->width || frame.height != m_context->height) &&
+ (frame.width > 0 && frame.height > 0))
+ {
+ INFO_LOG_FMT(FRAMEDUMP, "Starting new dump on resolution change.");
+ restart_dump = true;
+ }
+ else if (!IsFirstFrameInCurrentFile() &&
+ frame.state.savestate_index != m_context->savestate_index)
+ {
+ INFO_LOG_FMT(FRAMEDUMP, "Starting new dump on savestate load.");
+ restart_dump = true;
+ }
+ else if (frame.state.refresh_rate_den != m_context->codec->time_base.num ||
+ frame.state.refresh_rate_num != m_context->codec->time_base.den)
+ {
+ INFO_LOG_FMT(FRAMEDUMP, "Starting new dump on refresh rate change {}/{} vs {}/{}.",
+ m_context->codec->time_base.den, m_context->codec->time_base.num,
+ frame.state.refresh_rate_num, frame.state.refresh_rate_den);
+ restart_dump = true;
+ }
+
+ if (restart_dump)
+ {
+ Stop();
+ ++m_file_index;
+ PrepareEncoding(frame.width, frame.height, frame.state.ticks, frame.state.savestate_index);
+ }
+}
+
+FrameState FFMpegFrameDump::FetchState(u64 ticks, int frame_number) const
+{
+ FrameState state;
+ state.ticks = ticks;
+ state.frame_number = frame_number;
+ state.savestate_index = m_savestate_index;
+
+ const auto time_base = GetTimeBaseForCurrentRefreshRate();
+ state.refresh_rate_num = time_base.den;
+ state.refresh_rate_den = time_base.num;
+ return state;
+}
+
+FFMpegFrameDump::FFMpegFrameDump() = default;
+
+FFMpegFrameDump::~FFMpegFrameDump()
+{
+ Stop();
+}