System: Add video capture feature

This commit is contained in:
Stenzek
2024-08-11 20:45:14 +10:00
parent 5f8082734e
commit af47eb6956
27 changed files with 2809 additions and 241 deletions

View File

@ -41,6 +41,7 @@
#include "util/ini_settings_interface.h"
#include "util/input_manager.h"
#include "util/iso_reader.h"
#include "util/media_capture.h"
#include "util/platform_misc.h"
#include "util/postprocessing.h"
#include "util/sockets.h"
@ -78,6 +79,7 @@ Log_SetChannel(System);
#ifdef _WIN32
#include "common/windows_headers.h"
#include <Objbase.h>
#include <mmsystem.h>
#include <objbase.h>
#endif
@ -302,6 +304,7 @@ static Common::Timer s_frame_timer;
static Threading::ThreadHandle s_cpu_thread_handle;
static std::unique_ptr<CheatList> s_cheat_list;
static std::unique_ptr<MediaCapture> s_media_capture;
// temporary save state, created when loading, used to undo load state
static std::optional<System::SaveStateBuffer> s_undo_load_state;
@ -445,6 +448,8 @@ void System::Internal::ProcessShutdown()
bool System::Internal::CPUThreadInitialize(Error* error)
{
Threading::SetNameOfCurrentThread("CPU Thread");
#ifdef _WIN32
// On Win32, we have a bunch of things which use COM (e.g. SDL, Cubeb, etc).
// We need to initialize COM first, before anything else does, because otherwise they might
@ -1690,8 +1695,8 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
if (parameters.load_image_to_ram || g_settings.cdrom_load_image_to_ram)
CDROM::PrecacheMedia();
if (parameters.start_audio_dump)
StartDumpingAudio();
if (parameters.start_media_capture)
StartMediaCapture({});
if (g_settings.start_paused || parameters.override_start_paused.value_or(false))
PauseSystem(true);
@ -1809,6 +1814,9 @@ void System::DestroySystem()
if (s_state == State::Shutdown)
return;
if (s_media_capture)
StopMediaCapture();
s_undo_load_state.reset();
#ifdef ENABLE_GDB_SERVER
@ -2003,6 +2011,13 @@ void System::FrameDone()
SaveRunaheadState();
}
// Kick off media capture early, might take a while.
if (s_media_capture && s_media_capture->IsCapturingVideo()) [[unlikely]]
{
if (!g_gpu->SendDisplayToMediaCapture(s_media_capture.get())) [[unlikely]]
StopMediaCapture();
}
Common::Timer::Value current_time = Common::Timer::GetCurrentValue();
// pre-frame sleep accounting (input lag reduction)
@ -3134,6 +3149,9 @@ void System::UpdatePerformanceCounters()
s_sw_thread_usage = static_cast<float>(static_cast<double>(sw_delta) * pct_divider);
s_sw_thread_time = static_cast<float>(static_cast<double>(sw_delta) * time_divider);
if (s_media_capture)
s_media_capture->UpdateCaptureThreadUsage(pct_divider, time_divider);
s_fps_timer.ResetTo(now_ticks);
if (g_gpu_device->IsGPUTimingEnabled())
@ -4896,61 +4914,6 @@ void System::UpdateVolume()
SPU::GetOutputStream()->SetOutputVolume(GetAudioOutputVolume());
}
bool System::IsDumpingAudio()
{
return SPU::IsDumpingAudio();
}
bool System::StartDumpingAudio(const char* filename)
{
if (System::IsShutdown())
return false;
std::string auto_filename;
if (!filename)
{
const auto& serial = System::GetGameSerial();
if (serial.empty())
{
auto_filename = Path::Combine(
EmuFolders::Dumps, fmt::format("audio" FS_OSPATH_SEPARATOR_STR "{}.wav", GetTimestampStringForFileName()));
}
else
{
auto_filename = Path::Combine(EmuFolders::Dumps, fmt::format("audio" FS_OSPATH_SEPARATOR_STR "{}_{}.wav", serial,
GetTimestampStringForFileName()));
}
filename = auto_filename.c_str();
}
if (SPU::StartDumpingAudio(filename))
{
Host::AddIconOSDMessage(
"audio_dumping", ICON_FA_VOLUME_UP,
fmt::format(TRANSLATE_FS("OSDMessage", "Started dumping audio to '{}'."), Path::GetFileName(filename)),
Host::OSD_INFO_DURATION);
return true;
}
else
{
Host::AddIconOSDMessage(
"audio_dumping", ICON_FA_VOLUME_UP,
fmt::format(TRANSLATE_FS("OSDMessage", "Failed to start dumping audio to '{}'."), Path::GetFileName(filename)),
Host::OSD_ERROR_DURATION);
return false;
}
}
void System::StopDumpingAudio()
{
if (System::IsShutdown() || !SPU::StopDumpingAudio())
return;
Host::AddIconOSDMessage("audio_dumping", ICON_FA_VOLUME_MUTE, TRANSLATE_STR("OSDMessage", "Stopped dumping audio."),
Host::OSD_INFO_DURATION);
}
bool System::SaveScreenshot(const char* filename, DisplayScreenshotMode mode, DisplayScreenshotFormat format,
u8 quality, bool compress_on_thread)
{
@ -4985,6 +4948,132 @@ bool System::SaveScreenshot(const char* filename, DisplayScreenshotMode mode, Di
return g_gpu->RenderScreenshotToFile(filename, mode, quality, compress_on_thread, true);
}
static std::string_view GetCaptureTypeForMessage(bool capture_video, bool capture_audio)
{
return capture_video ? (capture_audio ? TRANSLATE_SV("System", "capturing audio and video") :
TRANSLATE_SV("System", "capturing video")) :
TRANSLATE_SV("System", "capturing audio");
}
MediaCapture* System::GetMediaCapture()
{
return s_media_capture.get();
}
std::string System::GetNewMediaCapturePath(const std::string_view title, const std::string_view container)
{
const std::string sanitized_name = Path::SanitizeFileName(title);
std::string path;
if (sanitized_name.empty())
{
path = Path::Combine(EmuFolders::Videos, fmt::format("{}.{}", GetTimestampStringForFileName(), container));
}
else
{
path = Path::Combine(EmuFolders::Videos,
fmt::format("{} {}.{}", sanitized_name, GetTimestampStringForFileName(), container));
}
return path;
}
bool System::StartMediaCapture(std::string path, bool capture_video, bool capture_audio)
{
if (!IsValid())
return false;
if (s_media_capture)
StopMediaCapture();
// Need to work out the size.
u32 capture_width = g_settings.media_capture_video_width;
u32 capture_height = g_settings.media_capture_video_height;
const GPUTexture::Format capture_format =
g_gpu_device->HasSurface() ? g_gpu_device->GetWindowFormat() : GPUTexture::Format::RGBA8;
const float fps = g_gpu->ComputeVerticalFrequency();
if (capture_video)
{
// TODO: This will be a mess with GPU thread.
if (g_settings.media_capture_video_auto_size)
{
GSVector4i unused_display_rect, unused_draw_rect;
g_gpu->CalculateScreenshotSize(DisplayScreenshotMode::InternalResolution, &capture_width, &capture_height,
&unused_display_rect, &unused_draw_rect);
}
MediaCapture::AdjustVideoSize(&capture_width, &capture_height);
}
// TODO: Render anamorphic capture instead?
constexpr float aspect = 1.0f;
if (path.empty())
path = GetNewMediaCapturePath(GetGameTitle(), g_settings.media_capture_container);
Error error;
s_media_capture = MediaCapture::Create(g_settings.media_capture_backend, &error);
if (!s_media_capture ||
!s_media_capture->BeginCapture(
fps, aspect, capture_width, capture_height, capture_format, SPU::SAMPLE_RATE, std::move(path), capture_video,
g_settings.media_capture_video_codec, g_settings.media_capture_video_bitrate,
g_settings.media_capture_video_codec_use_args ? std::string_view(g_settings.media_capture_video_codec_args) :
std::string_view(),
capture_audio, g_settings.media_capture_audio_codec, g_settings.media_capture_audio_bitrate,
g_settings.media_capture_audio_codec_use_args ? std::string_view(g_settings.media_capture_audio_codec_args) :
std::string_view(),
&error))
{
Host::AddIconOSDMessage(
"MediaCapture", ICON_FA_EXCLAMATION_TRIANGLE,
fmt::format(TRANSLATE_FS("System", "Failed to create media capture: {0}"), error.GetDescription()),
Host::OSD_ERROR_DURATION);
s_media_capture.reset();
Host::OnMediaCaptureStopped();
return false;
}
Host::AddIconOSDMessage(
"MediaCapture", ICON_FA_CAMERA,
fmt::format(TRANSLATE_FS("System", "Starting {0} to '{1}'."),
GetCaptureTypeForMessage(s_media_capture->IsCapturingVideo(), s_media_capture->IsCapturingAudio()),
Path::GetFileName(s_media_capture->GetPath())),
Host::OSD_INFO_DURATION);
Host::OnMediaCaptureStarted();
return true;
}
void System::StopMediaCapture()
{
if (!s_media_capture)
return;
const bool was_capturing_audio = s_media_capture->IsCapturingAudio();
const bool was_capturing_video = s_media_capture->IsCapturingVideo();
Error error;
if (s_media_capture->EndCapture(&error))
{
Host::AddIconOSDMessage("MediaCapture", ICON_FA_CAMERA,
fmt::format(TRANSLATE_FS("System", "Stopped {0} to '{1}'."),
GetCaptureTypeForMessage(was_capturing_video, was_capturing_audio),
Path::GetFileName(s_media_capture->GetPath())),
Host::OSD_INFO_DURATION);
}
else
{
Host::AddIconOSDMessage(
"MediaCapture", ICON_FA_EXCLAMATION_TRIANGLE,
fmt::format(TRANSLATE_FS("System", "Stopped {0}: {1}."),
GetCaptureTypeForMessage(s_media_capture->IsCapturingVideo(), s_media_capture->IsCapturingAudio()),
error.GetDescription()),
Host::OSD_INFO_DURATION);
}
s_media_capture.reset();
Host::OnMediaCaptureStopped();
}
std::string System::GetGameSaveStateFileName(std::string_view serial, s32 slot)
{
if (slot < 0)