pcsx2/pcsx2/GS/GSCapture.cpp
2024-08-06 18:51:00 +02:00

1543 lines
47 KiB
C++

// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "GS/GSCapture.h"
#include "GS/GSPng.h"
#include "GS/GSUtil.h"
#include "GS/GSExtra.h"
#include "GS/Renderers/Common/GSDevice.h"
#include "GS/Renderers/Common/GSTexture.h"
#include "SPU2/spu2.h"
#include "Host.h"
#include "Host/AudioStream.h"
#include "IconsFontAwesome5.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/BitUtils.h"
#include "common/DynamicLibrary.h"
#include "common/Path.h"
#include "common/SmallString.h"
#include "common/StringUtil.h"
#include "common/Threading.h"
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <string>
// We're using deprecated fields because we're targeting multiple ffmpeg versions.
#if defined(_MSC_VER)
#pragma warning(disable : 4996) // warning C4996: 'AVCodecContext::channels': was declared deprecated
#elif defined(__clang__)
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#elif defined(__GNUC__)
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavcodec/version.h"
#include "libavformat/avformat.h"
#include "libavformat/version.h"
#include "libavutil/dict.h"
#include "libavutil/opt.h"
#include "libavutil/version.h"
#include "libavutil/channel_layout.h"
#include "libavutil/pixdesc.h"
#include "libswscale/swscale.h"
#include "libswscale/version.h"
#include "libswresample/swresample.h"
#include "libswresample/version.h"
}
// Compatibility with both ffmpeg 4.x and 5.x.
#if (LIBAVFORMAT_VERSION_MAJOR < 59)
#define ff_const59
#else
#define ff_const59 const
#endif
#define VISIT_AVCODEC_IMPORTS(X) \
X(avcodec_find_encoder_by_name) \
X(avcodec_find_encoder) \
X(avcodec_alloc_context3) \
X(avcodec_open2) \
X(avcodec_free_context) \
X(avcodec_send_frame) \
X(avcodec_receive_packet) \
X(avcodec_parameters_from_context) \
X(avcodec_get_hw_config) \
X(av_codec_iterate) \
X(av_packet_alloc) \
X(av_packet_free) \
X(av_packet_rescale_ts) \
X(av_packet_unref)
#define VISIT_AVFORMAT_IMPORTS(X) \
X(avformat_alloc_output_context2) \
X(avformat_new_stream) \
X(avformat_write_header) \
X(av_guess_format) \
X(av_interleaved_write_frame) \
X(av_write_trailer) \
X(avformat_free_context) \
X(avformat_query_codec) \
X(avio_open) \
X(avio_closep)
#if LIBAVUTIL_VERSION_MAJOR < 57
#define AVUTIL_57_IMPORTS(X)
#else
#define AVUTIL_57_IMPORTS(X) \
X(av_channel_layout_default) \
X(av_channel_layout_copy) \
X(av_opt_set_chlayout)
#endif
#define VISIT_AVUTIL_IMPORTS(X) \
AVUTIL_57_IMPORTS(X) \
X(av_frame_alloc) \
X(av_frame_get_buffer) \
X(av_frame_free) \
X(av_frame_make_writable) \
X(av_strerror) \
X(av_reduce) \
X(av_dict_parse_string) \
X(av_dict_get) \
X(av_dict_free) \
X(av_opt_set_int) \
X(av_opt_set_sample_fmt) \
X(av_compare_ts) \
X(av_get_bytes_per_sample) \
X(av_sample_fmt_is_planar) \
X(av_d2q) \
X(av_hwdevice_get_type_name) \
X(av_hwdevice_ctx_create) \
X(av_hwframe_ctx_alloc) \
X(av_hwframe_ctx_init) \
X(av_hwframe_transfer_data) \
X(av_hwframe_get_buffer) \
X(av_buffer_ref) \
X(av_buffer_unref) \
X(av_get_pix_fmt_name)
#define VISIT_SWSCALE_IMPORTS(X) \
X(sws_getCachedContext) \
X(sws_scale) \
X(sws_freeContext)
#define VISIT_SWRESAMPLE_IMPORTS(X) \
X(swr_alloc) \
X(swr_init) \
X(swr_free) \
X(swr_convert) \
X(swr_next_pts)
namespace GSCapture
{
static constexpr u32 NUM_FRAMES_IN_FLIGHT = 3;
static constexpr u32 MAX_PENDING_FRAMES = NUM_FRAMES_IN_FLIGHT * 2;
static constexpr u32 AUDIO_BUFFER_SIZE = Common::AlignUpPow2((MAX_PENDING_FRAMES * 48000) / 60, AudioStream::CHUNK_SIZE);
static constexpr u32 AUDIO_CHANNELS = 2;
struct PendingFrame
{
enum class State
{
Unused,
NeedsMap,
NeedsEncoding
};
std::unique_ptr<GSDownloadTexture> tex;
s64 pts;
State state;
};
static void LogAVError(int errnum, const char* format, ...);
static bool LoadFFmpeg(bool report_errors);
static void UnloadFFmpeg();
static std::string GetCaptureTypeForMessage(bool capture_video, bool capture_audio);
static bool IsUsingHardwareVideoEncoding();
static void ProcessFramePendingMap(std::unique_lock<std::mutex>& lock);
static void ProcessAllInFlightFrames(std::unique_lock<std::mutex>& lock);
static void EncoderThreadEntryPoint();
static void StartEncoderThread();
static void StopEncoderThread(std::unique_lock<std::mutex>& lock);
static bool SendFrame(const PendingFrame& pf);
static bool ReceivePackets(AVCodecContext* codec_context, AVStream* stream, AVPacket* packet);
static bool ProcessAudioPackets(s64 video_pts);
static void InternalEndCapture(std::unique_lock<std::mutex>& lock);
static CodecList GetCodecListForContainer(const char* container, AVMediaType type);
static std::mutex s_lock;
static GSVector2i s_size{};
static std::string s_filename;
static std::atomic_bool s_capturing{false};
static std::atomic_bool s_encoding_error{false};
static AVFormatContext* s_format_context = nullptr;
static AVCodecContext* s_video_codec_context = nullptr;
static AVStream* s_video_stream = nullptr;
static AVFrame* s_converted_video_frame = nullptr; // YUV
static AVFrame* s_hw_video_frame = nullptr;
static AVPacket* s_video_packet = nullptr;
static SwsContext* s_sws_context = nullptr;
static AVDictionary* s_video_codec_arguments = nullptr;
static AVBufferRef* s_video_hw_context = nullptr;
static AVBufferRef* s_video_hw_frames = nullptr;
static s64 s_next_video_pts = 0;
static AVCodecContext* s_audio_codec_context = nullptr;
static AVStream* s_audio_stream = nullptr;
static AVFrame* s_converted_audio_frame = nullptr;
static AVPacket* s_audio_packet = nullptr;
static SwrContext* s_swr_context = nullptr;
static AVDictionary* s_audio_codec_arguments = nullptr;
static s64 s_next_audio_pts = 0;
static u32 s_audio_frame_bps = 0;
static u32 s_audio_frame_size = 0;
static u32 s_audio_frame_pos = 0;
static bool s_audio_frame_planar = false;
static Threading::Thread s_encoder_thread;
static std::condition_variable s_frame_ready_cv;
static std::condition_variable s_frame_encoded_cv;
static std::array<PendingFrame, MAX_PENDING_FRAMES> s_pending_frames = {};
static u32 s_pending_frames_pos = 0;
static u32 s_frames_pending_map = 0;
static u32 s_frames_map_consume_pos = 0;
static u32 s_frames_pending_encode = 0;
static u32 s_frames_encode_consume_pos = 0;
// NOTE: So this doesn't need locking, we allocate it once, and leave it.
static std::unique_ptr<s16[]> s_audio_buffer;
static std::atomic<u32> s_audio_buffer_size{0};
static u32 s_audio_buffer_write_pos = 0;
alignas(__cachelinesize) static u32 s_audio_buffer_read_pos = 0;
} // namespace GSCapture
#ifndef USE_LINKED_FFMPEG
#define DECLARE_IMPORT(X) static decltype(X)* wrap_##X;
#else
#define DECLARE_IMPORT(X) static constexpr decltype(X)* wrap_##X = X;
#endif
VISIT_AVCODEC_IMPORTS(DECLARE_IMPORT);
VISIT_AVFORMAT_IMPORTS(DECLARE_IMPORT);
VISIT_AVUTIL_IMPORTS(DECLARE_IMPORT);
VISIT_SWSCALE_IMPORTS(DECLARE_IMPORT);
VISIT_SWRESAMPLE_IMPORTS(DECLARE_IMPORT);
#undef DECLARE_IMPORT
// We could refcount this, but really, may as well just load it and pay the cost once.
// Not like we need to save a few megabytes of memory...
#ifndef USE_LINKED_FFMPEG
static void UnloadFFmpegFunctions(std::unique_lock<std::mutex>& lock);
static DynamicLibrary s_avcodec_library;
static DynamicLibrary s_avformat_library;
static DynamicLibrary s_avutil_library;
static DynamicLibrary s_swscale_library;
static DynamicLibrary s_swresample_library;
static bool s_library_loaded = false;
static std::mutex s_load_mutex;
bool GSCapture::LoadFFmpeg(bool report_errors)
{
std::unique_lock lock(s_load_mutex);
if (s_library_loaded)
return true;
const auto open_dynlib = [](DynamicLibrary& lib, const char* name, int major_version) {
std::string full_name(DynamicLibrary::GetVersionedFilename(name, major_version));
return lib.Open(full_name.c_str(), nullptr);
};
bool result = true;
result = result && open_dynlib(s_avutil_library, "avutil", LIBAVUTIL_VERSION_MAJOR);
result = result && open_dynlib(s_avcodec_library, "avcodec", LIBAVCODEC_VERSION_MAJOR);
result = result && open_dynlib(s_avformat_library, "avformat", LIBAVFORMAT_VERSION_MAJOR);
result = result && open_dynlib(s_swscale_library, "swscale", LIBSWSCALE_VERSION_MAJOR);
result = result && open_dynlib(s_swresample_library, "swresample", LIBSWRESAMPLE_VERSION_MAJOR);
#define RESOLVE_IMPORT(X) result = result && s_avcodec_library.GetSymbol(#X, &wrap_##X);
VISIT_AVCODEC_IMPORTS(RESOLVE_IMPORT);
#undef RESOLVE_IMPORT
#define RESOLVE_IMPORT(X) result = result && s_avformat_library.GetSymbol(#X, &wrap_##X);
VISIT_AVFORMAT_IMPORTS(RESOLVE_IMPORT);
#undef RESOLVE_IMPORT
#define RESOLVE_IMPORT(X) result = result && s_avutil_library.GetSymbol(#X, &wrap_##X);
VISIT_AVUTIL_IMPORTS(RESOLVE_IMPORT);
#undef RESOLVE_IMPORT
#define RESOLVE_IMPORT(X) result = result && s_swscale_library.GetSymbol(#X, &wrap_##X);
VISIT_SWSCALE_IMPORTS(RESOLVE_IMPORT);
#undef RESOLVE_IMPORT
#define RESOLVE_IMPORT(X) result = result && s_swresample_library.GetSymbol(#X, &wrap_##X);
VISIT_SWRESAMPLE_IMPORTS(RESOLVE_IMPORT);
#undef RESOLVE_IMPORT
if (result)
{
s_library_loaded = true;
return true;
}
UnloadFFmpegFunctions(lock);
lock.unlock();
if (report_errors)
{
Host::ReportErrorAsync(TRANSLATE_SV("GSCapture", "Failed to load FFmpeg"),
fmt::format(TRANSLATE_FS("GSCapture", "You may be missing one or more files, or are using the incorrect version. This build of PCSX2 requires:\n"
" libavcodec: {}\n"
" libavformat: {}\n"
" libavutil: {}\n"
" libswscale: {}\n"
" libswresample: {}\n\n"
"Please see our official documentation for more information."),
LIBAVCODEC_VERSION_MAJOR, LIBAVFORMAT_VERSION_MAJOR, LIBAVUTIL_VERSION_MAJOR,
LIBSWSCALE_VERSION_MAJOR, LIBSWRESAMPLE_VERSION_MAJOR));
}
return false;
}
void UnloadFFmpegFunctions(std::unique_lock<std::mutex>& lock)
{
#define CLEAR_IMPORT(X) wrap_##X = nullptr;
VISIT_AVCODEC_IMPORTS(CLEAR_IMPORT);
VISIT_AVFORMAT_IMPORTS(CLEAR_IMPORT);
VISIT_AVUTIL_IMPORTS(CLEAR_IMPORT);
VISIT_SWSCALE_IMPORTS(CLEAR_IMPORT);
VISIT_SWRESAMPLE_IMPORTS(CLEAR_IMPORT);
#undef CLEAR_IMPORT
s_swresample_library.Close();
s_swscale_library.Close();
s_avutil_library.Close();
s_avformat_library.Close();
s_avcodec_library.Close();
}
void GSCapture::UnloadFFmpeg()
{
std::unique_lock lock(s_load_mutex);
if (!s_library_loaded)
return;
s_library_loaded = false;
UnloadFFmpegFunctions(lock);
}
#else
bool GSCapture::LoadFFmpeg(bool report_errors)
{
return true;
}
void GSCapture::UnloadFFmpeg()
{
}
#endif
void GSCapture::LogAVError(int errnum, const char* format, ...)
{
va_list ap;
va_start(ap, format);
std::string msg(StringUtil::StdStringFromFormatV(format, ap));
va_end(ap);
char errbuf[128];
wrap_av_strerror(errnum, errbuf, sizeof(errbuf));
Host::AddIconOSDMessage("GSCaptureError", ICON_FA_CAMERA, fmt::format("{}{} ({})", msg, errbuf, errnum), Host::OSD_ERROR_DURATION);
}
std::string GSCapture::GetCaptureTypeForMessage(bool capture_video, bool capture_audio)
{
return capture_video ? capture_audio ? TRANSLATE("GSCapture", "capturing audio and video") : TRANSLATE("GSCapture", "capturing video") : TRANSLATE("GSCapture", "capturing audio");
}
bool GSCapture::IsUsingHardwareVideoEncoding()
{
return (s_video_hw_context != nullptr);
}
bool GSCapture::BeginCapture(float fps, GSVector2i recommendedResolution, float aspect, std::string filename)
{
const bool capture_video = GSConfig.EnableVideoCapture;
const bool capture_audio = GSConfig.EnableAudioCapture;
Console.WriteLn("Recommended resolution: %d x %d, DAR for muxing: %.4f", recommendedResolution.x, recommendedResolution.y, aspect);
if (filename.empty() || !LoadFFmpeg(true))
return false;
std::unique_lock<std::mutex> lock(s_lock);
pxAssert(fps != 0);
InternalEndCapture(lock);
s_size = GSVector2i(Common::AlignUpPow2(recommendedResolution.x, 8), Common::AlignUpPow2(recommendedResolution.y, 8));
s_filename = std::move(filename);
ff_const59 AVOutputFormat* output_format = wrap_av_guess_format(nullptr, s_filename.c_str(), nullptr);
if (!output_format)
{
Console.Error(fmt::format("Failed to get output format for '{}'", s_filename));
InternalEndCapture(lock);
return false;
}
int res = wrap_avformat_alloc_output_context2(&s_format_context, output_format, nullptr, s_filename.c_str());
if (res < 0)
{
LogAVError(res, "avformat_alloc_output_context2() failed: ");
InternalEndCapture(lock);
return false;
}
// find the codec id
if (capture_video)
{
const float sample_aspect_ratio = aspect / (static_cast<float>(s_size.x) / static_cast<float>(s_size.y));
const AVCodec* vcodec = nullptr;
if (!GSConfig.VideoCaptureCodec.empty())
{
vcodec = wrap_avcodec_find_encoder_by_name(GSConfig.VideoCaptureCodec.c_str());
if (!vcodec)
{
Host::AddIconOSDMessage("GSCaptureCodecNotFound", ICON_FA_CAMERA,
fmt::format("Video codec {} not found, using default.", GSConfig.VideoCaptureCodec), Host::OSD_ERROR_DURATION);
}
}
// FFmpeg decides whether mp4, mkv, etc should use h264 or mpeg4 as their default codec by whether x264 was enabled
// But there's a lot of other h264 encoders (e.g. hardware encoders) we may want to use instead
if (!vcodec && wrap_avformat_query_codec(output_format, AV_CODEC_ID_H264, FF_COMPLIANCE_NORMAL))
vcodec = wrap_avcodec_find_encoder(AV_CODEC_ID_H264);
if (!vcodec)
vcodec = wrap_avcodec_find_encoder(output_format->video_codec);
if (!vcodec)
{
Host::AddIconOSDMessage("GSCaptureError", ICON_FA_CAMERA, "Failed to find video encoder.", Host::OSD_ERROR_DURATION);
InternalEndCapture(lock);
return false;
}
s_video_codec_context = wrap_avcodec_alloc_context3(vcodec);
if (!s_video_codec_context)
{
Host::AddIconOSDMessage("GSCaptureError", ICON_FA_CAMERA, "Failed to allocate video codec context.", Host::OSD_ERROR_DURATION);
InternalEndCapture(lock);
return false;
}
s_video_codec_context->codec_type = AVMEDIA_TYPE_VIDEO;
s_video_codec_context->bit_rate = GSConfig.VideoCaptureBitrate * 1000;
s_video_codec_context->width = s_size.x;
s_video_codec_context->height = s_size.y;
s_video_codec_context->sample_aspect_ratio = wrap_av_d2q(sample_aspect_ratio, 100000);
wrap_av_reduce(&s_video_codec_context->time_base.num, &s_video_codec_context->time_base.den, 10000,
static_cast<s64>(static_cast<double>(fps) * 10000.0), std::numeric_limits<s32>::max());
// Default to NV12 if not overridden by the user
const AVPixelFormat preferred_sw_pix_fmt = GSConfig.VideoCaptureFormat.empty() ? AV_PIX_FMT_NV12 : static_cast<AVPixelFormat>(std::stoi(GSConfig.VideoCaptureFormat));
AVPixelFormat sw_pix_fmt = preferred_sw_pix_fmt;
if (vcodec->pix_fmts)
{
sw_pix_fmt = vcodec->pix_fmts[0];
for (u32 i = 0; vcodec->pix_fmts[i] != AV_PIX_FMT_NONE; i++)
{
if (vcodec->pix_fmts[i] == preferred_sw_pix_fmt)
{
sw_pix_fmt = vcodec->pix_fmts[i];
break;
}
}
}
if (sw_pix_fmt == AV_PIX_FMT_VAAPI)
sw_pix_fmt = AV_PIX_FMT_NV12;
s_video_codec_context->pix_fmt = sw_pix_fmt;
if (preferred_sw_pix_fmt != sw_pix_fmt)
Console.Warning("GSCapture: preferred pixel format (%d) was unsupported by the codec. Using (%d) instead.", preferred_sw_pix_fmt, sw_pix_fmt);
// Can we use hardware encoding?
const AVCodecHWConfig* hwconfig = wrap_avcodec_get_hw_config(vcodec, 0);
if (hwconfig && hwconfig->pix_fmt != AV_PIX_FMT_NONE && hwconfig->pix_fmt != sw_pix_fmt)
{
// First index isn't our preferred pixel format, try the others, but fall back if one doesn't exist.
int index = 1;
while (const AVCodecHWConfig* next_hwconfig = wrap_avcodec_get_hw_config(vcodec, index++))
{
if (next_hwconfig->pix_fmt == sw_pix_fmt)
{
hwconfig = next_hwconfig;
break;
}
}
}
if (hwconfig)
{
Console.WriteLn(Color_StrongGreen, fmt::format("Trying to use {} hardware device for video encoding.",
wrap_av_hwdevice_get_type_name(hwconfig->device_type)));
res = wrap_av_hwdevice_ctx_create(&s_video_hw_context, hwconfig->device_type, nullptr, nullptr, 0);
if (res < 0)
{
LogAVError(res, "av_hwdevice_ctx_create() failed: ");
}
else
{
s_video_hw_frames = wrap_av_hwframe_ctx_alloc(s_video_hw_context);
if (!s_video_hw_frames)
{
Console.Error("s_video_hw_frames() failed");
wrap_av_buffer_unref(&s_video_hw_context);
}
else
{
AVHWFramesContext* frames_ctx = reinterpret_cast<AVHWFramesContext*>(s_video_hw_frames->data);
frames_ctx->format = (hwconfig->pix_fmt != AV_PIX_FMT_NONE) ? hwconfig->pix_fmt : sw_pix_fmt;
frames_ctx->sw_format = sw_pix_fmt;
frames_ctx->width = s_video_codec_context->width;
frames_ctx->height = s_video_codec_context->height;
res = wrap_av_hwframe_ctx_init(s_video_hw_frames);
if (res < 0)
{
LogAVError(res, "av_hwframe_ctx_init() failed: ");
wrap_av_buffer_unref(&s_video_hw_frames);
wrap_av_buffer_unref(&s_video_hw_context);
}
else
{
s_video_codec_context->hw_frames_ctx = wrap_av_buffer_ref(s_video_hw_frames);
if (hwconfig->pix_fmt != AV_PIX_FMT_NONE)
s_video_codec_context->pix_fmt = hwconfig->pix_fmt;
}
}
}
if (!s_video_hw_context)
{
Host::AddIconOSDMessage("GSCaptureHWError", ICON_FA_CAMERA,
"Failed to create hardware encoder, using software encoding.", Host::OSD_ERROR_DURATION);
hwconfig = nullptr;
}
}
if (GSConfig.EnableVideoCaptureParameters)
{
res = wrap_av_dict_parse_string(&s_video_codec_arguments, GSConfig.VideoCaptureParameters.c_str(), "=", ":", 0);
if (res < 0)
{
LogAVError(res, "av_dict_parse_string() for video failed: ");
InternalEndCapture(lock);
return false;
}
}
if (output_format->flags & AVFMT_GLOBALHEADER)
s_video_codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
bool has_pixel_format_override = wrap_av_dict_get(s_video_codec_arguments, "pixel_format", nullptr, 0);
res = wrap_avcodec_open2(s_video_codec_context, vcodec, &s_video_codec_arguments);
if (res < 0)
{
LogAVError(res, "avcodec_open2() for video failed: ");
InternalEndCapture(lock);
return false;
}
// If the user overrode the pixel format, get that now
if (has_pixel_format_override)
sw_pix_fmt = s_video_codec_context->pix_fmt;
s_converted_video_frame = wrap_av_frame_alloc();
s_hw_video_frame = IsUsingHardwareVideoEncoding() ? wrap_av_frame_alloc() : nullptr;
if (!s_converted_video_frame || (IsUsingHardwareVideoEncoding() && !s_hw_video_frame))
{
LogAVError(AVERROR(ENOMEM), "Failed to allocate frame: ");
InternalEndCapture(lock);
return false;
}
s_converted_video_frame->format = sw_pix_fmt;
s_converted_video_frame->width = s_video_codec_context->width;
s_converted_video_frame->height = s_video_codec_context->height;
res = wrap_av_frame_get_buffer(s_converted_video_frame, 0);
if (res < 0)
{
LogAVError(res, "av_frame_get_buffer() for converted frame failed: ");
InternalEndCapture(lock);
return false;
}
if (IsUsingHardwareVideoEncoding())
{
s_hw_video_frame->format = s_video_codec_context->pix_fmt;
s_hw_video_frame->width = s_video_codec_context->width;
s_hw_video_frame->height = s_video_codec_context->height;
res = wrap_av_hwframe_get_buffer(s_video_hw_frames, s_hw_video_frame, 0);
if (res < 0)
{
LogAVError(res, "av_frame_get_buffer() for HW frame failed: ");
InternalEndCapture(lock);
return false;
}
}
s_video_stream = wrap_avformat_new_stream(s_format_context, vcodec);
if (!s_video_stream)
{
LogAVError(AVERROR(ENOMEM), "avformat_new_stream() for video failed: ");
InternalEndCapture(lock);
return false;
}
res = wrap_avcodec_parameters_from_context(s_video_stream->codecpar, s_video_codec_context);
if (res < 0)
{
LogAVError(AVERROR(ENOMEM), "avcodec_parameters_from_context() for video failed: ");
InternalEndCapture(lock);
return false;
}
s_video_stream->time_base = s_video_codec_context->time_base;
s_video_stream->sample_aspect_ratio = s_video_codec_context->sample_aspect_ratio;
s_video_packet = wrap_av_packet_alloc();
if (!s_video_packet)
{
LogAVError(AVERROR(ENOMEM), "av_packet_alloc() for video failed: ");
InternalEndCapture(lock);
return false;
}
s_next_video_pts = 0;
}
if (capture_audio)
{
// The CPU thread might have dumped some frames in here from the last capture, so clear it out.
s_audio_buffer_read_pos = 0;
s_audio_buffer_write_pos = 0;
s_audio_buffer_size.store(0, std::memory_order_release);
if (!s_audio_buffer)
s_audio_buffer = std::make_unique<s16[]>(AUDIO_BUFFER_SIZE * AUDIO_CHANNELS);
const AVCodec* acodec = nullptr;
if (!GSConfig.AudioCaptureCodec.empty())
{
acodec = wrap_avcodec_find_encoder_by_name(GSConfig.AudioCaptureCodec.c_str());
if (!acodec)
{
Host::AddIconOSDMessage("GSCaptureCodecNotFound", ICON_FA_CAMERA,
fmt::format("Audio codec {} not found, using default.", GSConfig.VideoCaptureCodec), Host::OSD_ERROR_DURATION);
}
}
if (!acodec)
acodec = wrap_avcodec_find_encoder(output_format->audio_codec);
if (!acodec)
{
Host::AddIconOSDMessage("GSCaptureError", ICON_FA_CAMERA, "Failed to find audio encoder.", Host::OSD_ERROR_DURATION);
InternalEndCapture(lock);
return false;
}
s_audio_codec_context = wrap_avcodec_alloc_context3(acodec);
if (!s_audio_codec_context)
{
Host::AddIconOSDMessage("GSCaptureError", ICON_FA_CAMERA, "Failed to allocate audio codec context.", Host::OSD_ERROR_DURATION);
InternalEndCapture(lock);
return false;
}
const s32 sample_rate = SPU2::GetConsoleSampleRate();
s_audio_codec_context->codec_type = AVMEDIA_TYPE_AUDIO;
s_audio_codec_context->bit_rate = GSConfig.AudioCaptureBitrate * 1000;
s_audio_codec_context->sample_fmt = AV_SAMPLE_FMT_S16;
s_audio_codec_context->sample_rate = sample_rate;
s_audio_codec_context->time_base = {1, sample_rate};
#if LIBAVUTIL_VERSION_MAJOR < 57
s_audio_codec_context->channels = AUDIO_CHANNELS;
s_audio_codec_context->channel_layout = AV_CH_LAYOUT_STEREO;
#else
wrap_av_channel_layout_default(&s_audio_codec_context->ch_layout, AUDIO_CHANNELS);
#endif
bool supports_format = false;
for (const AVSampleFormat* p = acodec->sample_fmts; *p != AV_SAMPLE_FMT_NONE; p++)
{
if (*p == s_audio_codec_context->sample_fmt)
{
supports_format = true;
break;
}
}
if (!supports_format)
{
Console.WriteLn(fmt::format("Audio codec '{}' does not support S16 samples, using default.", acodec->name));
s_audio_codec_context->sample_fmt = acodec->sample_fmts[0];
s_swr_context = wrap_swr_alloc();
if (!s_swr_context)
{
LogAVError(AVERROR(ENOMEM), "swr_alloc() failed: ");
InternalEndCapture(lock);
return false;
}
#if LIBAVUTIL_VERSION_MAJOR >= 57
const AVChannelLayout layout = AV_CHANNEL_LAYOUT_STEREO;
wrap_av_opt_set_chlayout(s_swr_context, "in_chlayout", &layout, 0);
wrap_av_opt_set_chlayout(s_swr_context, "out_chlayout", &layout, 0);
#endif
wrap_av_opt_set_int(s_swr_context, "in_channel_count", AUDIO_CHANNELS, 0);
wrap_av_opt_set_int(s_swr_context, "in_sample_rate", sample_rate, 0);
wrap_av_opt_set_sample_fmt(s_swr_context, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);
wrap_av_opt_set_int(s_swr_context, "out_channel_count", AUDIO_CHANNELS, 0);
wrap_av_opt_set_int(s_swr_context, "out_sample_rate", sample_rate, 0);
wrap_av_opt_set_sample_fmt(s_swr_context, "out_sample_fmt", s_audio_codec_context->sample_fmt, 0);
res = wrap_swr_init(s_swr_context);
if (res < 0)
{
LogAVError(res, "swr_init() failed: ");
InternalEndCapture(lock);
return false;
}
}
// TODO: Check channel layout support
if (GSConfig.EnableAudioCaptureParameters)
{
res = wrap_av_dict_parse_string(&s_audio_codec_arguments, GSConfig.AudioCaptureParameters.c_str(), "=", ":", 0);
if (res < 0)
{
LogAVError(res, "av_dict_parse_string() for audio failed: ");
InternalEndCapture(lock);
return false;
}
}
if (output_format->flags & AVFMT_GLOBALHEADER)
s_audio_codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
res = wrap_avcodec_open2(s_audio_codec_context, acodec, &s_audio_codec_arguments);
if (res < 0)
{
LogAVError(res, "avcodec_open2() for audio failed: ");
InternalEndCapture(lock);
return false;
}
// Use packet size for frame if it supports it... but most don't.
if (acodec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE)
s_audio_frame_size = AudioStream::CHUNK_SIZE;
else
s_audio_frame_size = s_audio_codec_context->frame_size;
if (s_audio_frame_size >= AUDIO_BUFFER_SIZE)
{
LogAVError(AVERROR(EINVAL), "Audio frame size %u exceeds buffer size %u", s_audio_frame_size, AUDIO_BUFFER_SIZE);
InternalEndCapture(lock);
return false;
}
s_audio_frame_bps = wrap_av_get_bytes_per_sample(s_audio_codec_context->sample_fmt);
s_audio_frame_planar = (wrap_av_sample_fmt_is_planar(s_audio_codec_context->sample_fmt) != 0);
s_converted_audio_frame = wrap_av_frame_alloc();
if (!s_converted_audio_frame)
{
LogAVError(AVERROR(ENOMEM), "Failed to allocate audio frame: ");
InternalEndCapture(lock);
return false;
}
s_converted_audio_frame->format = s_audio_codec_context->sample_fmt;
s_converted_audio_frame->nb_samples = s_audio_frame_size;
#if LIBAVUTIL_VERSION_MAJOR < 57
s_converted_audio_frame->channels = AUDIO_CHANNELS;
s_converted_audio_frame->channel_layout = s_audio_codec_context->channel_layout;
#else
wrap_av_channel_layout_copy(&s_converted_audio_frame->ch_layout, &s_audio_codec_context->ch_layout);
#endif
res = wrap_av_frame_get_buffer(s_converted_audio_frame, 0);
if (res < 0)
{
LogAVError(res, "av_frame_get_buffer() for audio frame failed: ");
InternalEndCapture(lock);
return false;
}
s_audio_stream = wrap_avformat_new_stream(s_format_context, acodec);
if (!s_audio_stream)
{
LogAVError(AVERROR(ENOMEM), "avformat_new_stream() for audio failed: ");
InternalEndCapture(lock);
return false;
}
res = wrap_avcodec_parameters_from_context(s_audio_stream->codecpar, s_audio_codec_context);
if (res < 0)
{
LogAVError(res, "avcodec_parameters_from_context() for audio failed: ");
InternalEndCapture(lock);
return false;
}
s_audio_stream->time_base = s_audio_codec_context->time_base;
s_audio_packet = wrap_av_packet_alloc();
if (!s_audio_packet)
{
LogAVError(AVERROR(ENOMEM), "av_packet_alloc() for audio failed: ");
InternalEndCapture(lock);
return false;
}
s_next_audio_pts = 0;
}
res = wrap_avio_open(&s_format_context->pb, s_filename.c_str(), AVIO_FLAG_WRITE);
if (res < 0)
{
LogAVError(res, "avio_open() failed: ");
InternalEndCapture(lock);
return false;
}
res = wrap_avformat_write_header(s_format_context, nullptr);
if (res < 0)
{
LogAVError(res, "avformat_write_header() failed: ");
InternalEndCapture(lock);
return false;
}
Host::AddIconOSDMessage("GSCapture", ICON_FA_VIDEO,
fmt::format(TRANSLATE_FS("GSCapture", "Starting {} to '{}'."), GetCaptureTypeForMessage(capture_video, capture_audio), Path::GetFileName(s_filename)),
Host::OSD_INFO_DURATION);
if (capture_audio)
SPU2::SetAudioCaptureActive(true);
s_capturing.store(true, std::memory_order_release);
StartEncoderThread();
lock.unlock();
Host::OnCaptureStarted(s_filename);
return true;
}
bool GSCapture::DeliverVideoFrame(GSTexture* stex)
{
std::unique_lock<std::mutex> lock(s_lock);
// If the encoder thread reported an error, stop the capture.
if (s_encoding_error)
{
InternalEndCapture(lock);
return false;
}
if (s_frames_pending_map >= NUM_FRAMES_IN_FLIGHT)
ProcessFramePendingMap(lock);
PendingFrame& pf = s_pending_frames[s_pending_frames_pos];
// It shouldn't be pending map, but the encode thread might be lagging.
pxAssert(pf.state != PendingFrame::State::NeedsMap);
if (pf.state == PendingFrame::State::NeedsEncoding)
{
s_frame_encoded_cv.wait(lock, [&pf]() { return pf.state == PendingFrame::State::Unused; });
}
if (!pf.tex || pf.tex->GetWidth() != static_cast<u32>(stex->GetWidth()) || pf.tex->GetHeight() != static_cast<u32>(stex->GetHeight()))
{
pf.tex.reset();
pf.tex = g_gs_device->CreateDownloadTexture(stex->GetWidth(), stex->GetHeight(), stex->GetFormat());
if (!pf.tex)
{
Console.Error("GSCapture: Failed to create %x%d download texture", stex->GetWidth(), stex->GetHeight());
return false;
}
#ifdef PCSX2_DEVBUILD
pf.tex->SetDebugName(TinyString::from_format("GSCapture {}x{} Download Texture", stex->GetWidth(), stex->GetHeight()));
#endif
}
const GSVector4i rc(0, 0, stex->GetWidth(), stex->GetHeight());
pf.tex->CopyFromTexture(rc, stex, rc, 0);
pf.pts = s_next_video_pts++;
pf.state = PendingFrame::State::NeedsMap;
s_pending_frames_pos = (s_pending_frames_pos + 1) % MAX_PENDING_FRAMES;
s_frames_pending_map++;
return true;
}
void GSCapture::ProcessFramePendingMap(std::unique_lock<std::mutex>& lock)
{
pxAssert(s_frames_pending_map > 0);
PendingFrame& pf = s_pending_frames[s_frames_map_consume_pos];
pxAssert(pf.state == PendingFrame::State::NeedsMap);
// Flushing is potentially expensive, so we leave it unlocked in case the encode thread
// needs to pick up another thread while we're waiting.
lock.unlock();
if (pf.tex->NeedsFlush())
pf.tex->Flush();
// Even if the map failed, we need to kick it to the encode thread anyway, because
// otherwise our queue indices will get desynchronized.
if (!pf.tex->Map(GSVector4i(0, 0, s_size.x, s_size.y)))
Console.Warning("GSCapture: Failed to map previously flushed frame.");
lock.lock();
// Kick to encoder thread!
pf.state = PendingFrame::State::NeedsEncoding;
s_frames_map_consume_pos = (s_frames_map_consume_pos + 1) % MAX_PENDING_FRAMES;
s_frames_pending_map--;
s_frames_pending_encode++;
s_frame_ready_cv.notify_one();
}
void GSCapture::EncoderThreadEntryPoint()
{
Threading::SetNameOfCurrentThread("GS Capture Encoding");
std::unique_lock<std::mutex> lock(s_lock);
for (;;)
{
s_frame_ready_cv.wait(lock, []() { return (s_frames_pending_encode > 0 || !s_capturing.load(std::memory_order_acquire)); });
if (s_frames_pending_encode == 0 && !s_capturing.load(std::memory_order_acquire))
break;
PendingFrame& pf = s_pending_frames[s_frames_encode_consume_pos];
pxAssert(pf.state == PendingFrame::State::NeedsEncoding);
lock.unlock();
bool okay = !s_encoding_error;
// If the frame failed to map, this will be false, and we'll just skip it.
if (okay && s_video_stream && pf.tex->IsMapped())
okay = SendFrame(pf);
// Encode as many audio frames while the video is ahead.
if (okay && s_audio_stream)
okay = ProcessAudioPackets(pf.pts);
lock.lock();
// If we had an encoding error, tell the GS thread to shut down the capture (later).
if (!okay)
s_encoding_error = true;
// Done with this frame! Wait for the next.
pf.state = PendingFrame::State::Unused;
s_frames_encode_consume_pos = (s_frames_encode_consume_pos + 1) % MAX_PENDING_FRAMES;
s_frames_pending_encode--;
s_frame_encoded_cv.notify_all();
}
}
void GSCapture::StartEncoderThread()
{
Console.WriteLn("GSCapture: Starting encoder thread.");
pxAssert(s_capturing.load(std::memory_order_acquire) && !s_encoder_thread.Joinable());
s_encoder_thread.Start(EncoderThreadEntryPoint);
}
void GSCapture::StopEncoderThread(std::unique_lock<std::mutex>& lock)
{
// Thread will exit when s_capturing is false.
pxAssert(!s_capturing.load(std::memory_order_acquire));
if (s_encoder_thread.Joinable())
{
Console.WriteLn("GSCapture: Stopping encoder thread.");
// Might be sleeping, so wake it before joining.
s_frame_ready_cv.notify_one();
lock.unlock();
s_encoder_thread.Join();
lock.lock();
}
}
bool GSCapture::SendFrame(const PendingFrame& pf)
{
const AVPixelFormat source_format = AV_PIX_FMT_RGBA;
const u8* source_ptr = pf.tex->GetMapPointer();
const int source_width = static_cast<int>(pf.tex->GetWidth());
const int source_height = static_cast<int>(pf.tex->GetHeight());
const int source_pitch = static_cast<int>(pf.tex->GetMapPitch());
// In case a previous frame is still using the frame.
wrap_av_frame_make_writable(s_converted_video_frame);
s_sws_context = wrap_sws_getCachedContext(s_sws_context, source_width, source_height, source_format, s_converted_video_frame->width,
s_converted_video_frame->height, static_cast<AVPixelFormat>(s_converted_video_frame->format), SWS_BICUBIC, nullptr, nullptr, nullptr);
if (!s_sws_context)
{
Console.Error("sws_getCachedContext() failed");
return false;
}
wrap_sws_scale(s_sws_context, reinterpret_cast<const u8**>(&source_ptr), &source_pitch, 0, source_height, s_converted_video_frame->data,
s_converted_video_frame->linesize);
AVFrame* frame_to_send = s_converted_video_frame;
if (IsUsingHardwareVideoEncoding())
{
// Need to transfer the frame to hardware.
const int res = wrap_av_hwframe_transfer_data(s_hw_video_frame, s_converted_video_frame, 0);
if (res < 0)
{
LogAVError(res, "av_hwframe_transfer_data() failed: ");
return false;
}
frame_to_send = s_hw_video_frame;
}
// Set the correct PTS before handing it off.
frame_to_send->pts = pf.pts;
const int res = wrap_avcodec_send_frame(s_video_codec_context, frame_to_send);
if (res < 0)
{
LogAVError(res, "avcodec_send_frame() failed: ");
return false;
}
return ReceivePackets(s_video_codec_context, s_video_stream, s_video_packet);
}
void GSCapture::ProcessAllInFlightFrames(std::unique_lock<std::mutex>& lock)
{
while (s_frames_pending_map > 0)
ProcessFramePendingMap(lock);
while (s_frames_pending_encode > 0)
{
s_frame_encoded_cv.wait(lock, []() { return (s_frames_pending_encode == 0 || s_encoding_error); });
}
}
bool GSCapture::ReceivePackets(AVCodecContext* codec_context, AVStream* stream, AVPacket* packet)
{
for (;;)
{
int res = wrap_avcodec_receive_packet(codec_context, packet);
if (res == AVERROR(EAGAIN) || res == AVERROR_EOF)
{
// no more data available
break;
}
else if (res < 0)
{
LogAVError(res, "avcodec_receive_packet() failed: ");
return false;
}
packet->stream_index = stream->index;
// in case the frame rate changed...
wrap_av_packet_rescale_ts(packet, codec_context->time_base, stream->time_base);
res = wrap_av_interleaved_write_frame(s_format_context, packet);
if (res < 0)
{
LogAVError(res, "av_interleaved_write_frame() failed: ");
return false;
}
wrap_av_packet_unref(packet);
}
return true;
}
void GSCapture::DeliverAudioPacket(const s16* frames)
{
// Since this gets called from the EE thread, we might race here after capture stops, and send a few too many samples
// late. We don't really want to be grabbing the lock on the EE thread, so instead, we'll just YOLO push the frames
// through and clear them out for the next capture. If we happen to fill the buffer, *then* we'll lock, and check if
// the capture has stopped.
static constexpr u32 num_frames = AudioStream::CHUNK_SIZE;
if ((AUDIO_BUFFER_SIZE - s_audio_buffer_size.load(std::memory_order_acquire)) < num_frames)
{
// Need to wait for it to drain a bit.
std::unique_lock<std::mutex> lock(s_lock);
s_frame_encoded_cv.wait(lock, []() {
return (!s_capturing.load(std::memory_order_acquire) ||
((AUDIO_BUFFER_SIZE - s_audio_buffer_size.load(std::memory_order_acquire)) >= num_frames));
});
if (!s_capturing.load(std::memory_order_acquire))
return;
}
// Since the buffer size is aligned to the SndOut packet size, we should always have space for at least one full packet.
pxAssert((AUDIO_BUFFER_SIZE - s_audio_buffer_write_pos) >= num_frames);
std::memcpy(s_audio_buffer.get() + (s_audio_buffer_write_pos * AUDIO_CHANNELS), frames, sizeof(s16) * AUDIO_CHANNELS * num_frames);
s_audio_buffer_write_pos = (s_audio_buffer_write_pos + num_frames) % AUDIO_BUFFER_SIZE;
const u32 buffer_size = s_audio_buffer_size.fetch_add(num_frames, std::memory_order_release) + num_frames;
if (!s_video_stream && buffer_size >= s_audio_frame_size)
{
// If we're not capturing video, push "frames" when we hit the audio packet size.
std::unique_lock<std::mutex> lock(s_lock);
if (!s_capturing.load(std::memory_order_acquire))
return;
PendingFrame& pf = s_pending_frames[s_pending_frames_pos];
pf.state = PendingFrame::State::NeedsEncoding;
s_pending_frames_pos = (s_pending_frames_pos + 1) % MAX_PENDING_FRAMES;
s_frames_pending_encode++;
s_frame_ready_cv.notify_one();
}
}
bool GSCapture::ProcessAudioPackets(s64 video_pts)
{
u32 pending_frames = s_audio_buffer_size.load(std::memory_order_acquire);
while (pending_frames > 0 && (!s_video_codec_context || wrap_av_compare_ts(video_pts, s_video_codec_context->time_base,
s_next_audio_pts, s_audio_codec_context->time_base) > 0))
{
pxAssert(pending_frames >= AudioStream::CHUNK_SIZE);
// In case the encoder is still using it.
if (s_audio_frame_pos == 0)
wrap_av_frame_make_writable(s_converted_audio_frame);
// Grab as many source frames as we can.
const u32 contig_frames = std::min(pending_frames, AUDIO_BUFFER_SIZE - s_audio_buffer_read_pos);
const u32 this_batch = std::min(s_audio_frame_size - s_audio_frame_pos, contig_frames);
// Do we need to convert the sample format?
if (!s_swr_context)
{
// No, just copy frames out of staging buffer.
if (s_audio_frame_planar)
{
// This is slow. Hopefully doesn't happen in too many configurations.
for (u32 i = 0; i < AUDIO_CHANNELS; i++)
{
u8* output = s_converted_audio_frame->data[i] + s_audio_frame_pos * s_audio_frame_bps;
const u8* input = reinterpret_cast<u8*>(&s_audio_buffer[s_audio_buffer_read_pos * AUDIO_CHANNELS + i]);
for (u32 j = 0; j < this_batch; j++)
{
std::memcpy(output, input, sizeof(s16));
input += sizeof(s16) * AUDIO_CHANNELS;
output += s_audio_frame_bps;
}
}
}
else
{
// Direct copy - optimal.
std::memcpy(s_converted_audio_frame->data[0] + s_audio_frame_pos * s_audio_frame_bps * AUDIO_CHANNELS,
&s_audio_buffer[s_audio_buffer_read_pos * AUDIO_CHANNELS], this_batch * sizeof(s16) * AUDIO_CHANNELS);
}
}
else
{
// Use swresample to convert.
const u8* input = reinterpret_cast<u8*>(&s_audio_buffer[s_audio_buffer_read_pos * AUDIO_CHANNELS]);
// Might be planar, so offset both buffers.
u8* output[AUDIO_CHANNELS];
if (s_audio_frame_planar)
{
for (u32 i = 0; i < AUDIO_CHANNELS; i++)
output[i] = s_converted_audio_frame->data[i] + (s_audio_frame_pos * s_audio_frame_bps);
}
else
{
output[0] = s_converted_audio_frame->data[0] + (s_audio_frame_pos * s_audio_frame_bps * AUDIO_CHANNELS);
}
const int res = wrap_swr_convert(s_swr_context, output, this_batch, &input, this_batch);
if (res < 0)
{
LogAVError(res, "swr_convert() failed: ");
return false;
}
}
s_audio_buffer_read_pos = (s_audio_buffer_read_pos + this_batch) % AUDIO_BUFFER_SIZE;
s_audio_buffer_size.fetch_sub(this_batch);
s_audio_frame_pos += this_batch;
pending_frames -= this_batch;
// Do we have a complete frame?
if (s_audio_frame_pos == s_audio_frame_size)
{
s_audio_frame_pos = 0;
if (!s_swr_context)
{
// PTS is simply frames.
s_converted_audio_frame->pts = s_next_audio_pts;
}
else
{
s_converted_audio_frame->pts = wrap_swr_next_pts(s_swr_context, s_next_audio_pts);
}
// Increment PTS.
s_next_audio_pts += s_audio_frame_size;
// Send off for encoding.
int res = wrap_avcodec_send_frame(s_audio_codec_context, s_converted_audio_frame);
if (res < 0)
{
LogAVError(res, "avcodec_send_frame() for audio failed: ");
return false;
}
// Write any packets back to the output file.
if (!ReceivePackets(s_audio_codec_context, s_audio_stream, s_audio_packet))
return false;
}
}
return true;
}
void GSCapture::InternalEndCapture(std::unique_lock<std::mutex>& lock)
{
int res;
const bool was_capturing = s_capturing.load(std::memory_order_acquire);
if (was_capturing)
{
SPU2::SetAudioCaptureActive(false);
if (!s_encoding_error)
{
ProcessAllInFlightFrames(lock);
Host::AddIconOSDMessage("GSCapture", ICON_FA_VIDEO_SLASH,
fmt::format(
TRANSLATE_FS("GSCapture", "Stopped {} to '{}'."), GetCaptureTypeForMessage(IsCapturingVideo(), IsCapturingAudio()), Path::GetFileName(s_filename)),
Host::OSD_INFO_DURATION);
}
else
{
Host::AddIconOSDMessage("GSCapture", ICON_FA_VIDEO_SLASH,
fmt::format(TRANSLATE_FS("GSCapture", "Aborted {} due to encoding error in '{}'."), GetCaptureTypeForMessage(IsCapturingVideo(), IsCapturingAudio()),
Path::GetFileName(s_filename)),
Host::OSD_INFO_DURATION);
}
s_capturing.store(false, std::memory_order_release);
StopEncoderThread(lock);
s_pending_frames = {};
s_pending_frames_pos = 0;
s_frames_pending_map = 0;
s_frames_map_consume_pos = 0;
s_frames_pending_encode = 0;
s_frames_encode_consume_pos = 0;
s_audio_buffer_read_pos = 0;
s_audio_buffer_write_pos = 0;
s_audio_buffer_size.store(0, std::memory_order_release);
s_audio_frame_pos = 0;
s_filename = {};
s_encoding_error = false;
// end of stream
if (s_video_stream)
{
res = wrap_avcodec_send_frame(s_video_codec_context, nullptr);
if (res < 0)
LogAVError(res, "avcodec_send_frame() for video EOS failed: ");
else
ReceivePackets(s_video_codec_context, s_video_stream, s_video_packet);
}
if (s_audio_stream)
{
res = wrap_avcodec_send_frame(s_audio_codec_context, nullptr);
if (res < 0)
LogAVError(res, "avcodec_send_frame() for audio EOS failed: ");
else
ReceivePackets(s_audio_codec_context, s_audio_stream, s_audio_packet);
}
// end of file!
res = wrap_av_write_trailer(s_format_context);
if (res < 0)
LogAVError(res, "av_write_trailer() failed: ");
}
if (s_format_context)
{
res = wrap_avio_closep(&s_format_context->pb);
if (res < 0)
LogAVError(res, "avio_closep() failed: ");
}
if (s_sws_context)
{
wrap_sws_freeContext(s_sws_context);
s_sws_context = nullptr;
}
if (s_video_packet)
wrap_av_packet_free(&s_video_packet);
if (s_converted_video_frame)
wrap_av_frame_free(&s_converted_video_frame);
if (s_hw_video_frame)
wrap_av_frame_free(&s_hw_video_frame);
if (s_video_hw_frames)
wrap_av_buffer_unref(&s_video_hw_frames);
if (s_video_hw_context)
wrap_av_buffer_unref(&s_video_hw_context);
if (s_video_codec_context)
wrap_avcodec_free_context(&s_video_codec_context);
s_video_stream = nullptr;
if (s_swr_context)
wrap_swr_free(&s_swr_context);
if (s_audio_packet)
wrap_av_packet_free(&s_audio_packet);
if (s_converted_audio_frame)
wrap_av_frame_free(&s_converted_audio_frame);
if (s_audio_codec_context)
wrap_avcodec_free_context(&s_audio_codec_context);
s_audio_stream = nullptr;
if (s_format_context)
{
wrap_avformat_free_context(s_format_context);
s_format_context = nullptr;
}
if (s_video_codec_arguments)
wrap_av_dict_free(&s_video_codec_arguments);
if (s_audio_codec_arguments)
wrap_av_dict_free(&s_audio_codec_arguments);
if (was_capturing)
UnloadFFmpeg();
}
void GSCapture::EndCapture()
{
{
std::unique_lock<std::mutex> lock(s_lock);
InternalEndCapture(lock);
}
Host::OnCaptureStopped();
}
bool GSCapture::IsCapturing()
{
return s_capturing.load(std::memory_order_acquire);
}
bool GSCapture::IsCapturingVideo()
{
return (s_video_stream != nullptr);
}
bool GSCapture::IsCapturingAudio()
{
return (s_audio_stream != nullptr);
}
TinyString GSCapture::GetElapsedTime()
{
std::unique_lock<std::mutex> lock(s_lock);
s64 seconds;
if (s_video_stream)
{
seconds = (s_next_video_pts * static_cast<s64>(s_video_codec_context->time_base.num)) /
static_cast<s64>(s_video_codec_context->time_base.den);
}
else if (s_audio_stream)
{
seconds = (s_next_audio_pts * static_cast<s64>(s_audio_codec_context->time_base.num)) /
static_cast<s64>(s_audio_codec_context->time_base.den);
}
else
{
seconds = -1;
}
TinyString ret;
if (seconds >= 0)
ret.format("{:02d}:{:02d}:{:02d}", seconds / 3600, (seconds % 3600) / 60, seconds % 60);
return ret;
}
const Threading::ThreadHandle& GSCapture::GetEncoderThreadHandle()
{
return s_encoder_thread;
}
GSVector2i GSCapture::GetSize()
{
return s_size;
}
std::string GSCapture::GetNextCaptureFileName()
{
std::string ret;
if (!IsCapturing())
return ret;
const std::string_view ext = Path::GetExtension(s_filename);
std::string_view name = Path::GetFileTitle(s_filename);
// Should end with a number.
int partnum = 2;
std::string_view::size_type pos = name.rfind("_part");
if (pos != std::string_view::npos)
{
std::string_view::size_type cpos = pos + 5;
for (; cpos < name.length(); cpos++)
{
if (name[cpos] < '0' || name[cpos] > '9')
break;
}
if (cpos == name.length())
{
// Has existing part number, so add to it.
partnum = StringUtil::FromChars<int>(name.substr(pos + 5)).value_or(1) + 1;
name = name.substr(0, pos);
}
}
// If we haven't started a new file previously, add "_part2".
ret = Path::BuildRelativePath(s_filename, fmt::format("{}_part{:03d}.{}", name, partnum, ext));
return ret;
}
void GSCapture::Flush()
{
std::unique_lock<std::mutex> lock(s_lock);
if (s_encoding_error)
return;
ProcessAllInFlightFrames(lock);
if (IsCapturingAudio())
{
// Clear any buffered audio frames out, we don't want to delay the CPU thread.
const u32 audio_frames = s_audio_buffer_size.load(std::memory_order_acquire);
if (audio_frames > 0)
Console.Warning("Dropping %u audio frames on for buffer clear.", audio_frames);
s_audio_buffer_read_pos = 0;
s_audio_buffer_write_pos = 0;
s_audio_buffer_size.store(0, std::memory_order_release);
}
}
GSCapture::CodecList GSCapture::GetCodecListForContainer(const char* container, AVMediaType type)
{
CodecList ret;
if (!LoadFFmpeg(false))
return ret;
const AVOutputFormat* output_format =
wrap_av_guess_format(nullptr, fmt::format("video.{}", container ? container : "mp4").c_str(), nullptr);
if (!output_format)
{
Console.Error("(GetCodecListForContainer) av_guess_format() failed");
return ret;
}
void* iter = nullptr;
const AVCodec* codec;
while ((codec = wrap_av_codec_iterate(&iter)) != nullptr)
{
// only get audio codecs
if (codec->type != type || !wrap_avcodec_find_encoder(codec->id) || !wrap_avcodec_find_encoder_by_name(codec->name))
continue;
if (!wrap_avformat_query_codec(output_format, codec->id, FF_COMPLIANCE_NORMAL))
continue;
if (std::find_if(ret.begin(), ret.end(), [codec](const auto& it) { return it.first == codec->name; }) != ret.end())
continue;
ret.emplace_back(codec->name, codec->long_name ? codec->long_name : codec->name);
}
return ret;
}
GSCapture::CodecList GSCapture::GetVideoCodecList(const char* container)
{
return GetCodecListForContainer(container, AVMEDIA_TYPE_VIDEO);
}
GSCapture::CodecList GSCapture::GetAudioCodecList(const char* container)
{
return GetCodecListForContainer(container, AVMEDIA_TYPE_AUDIO);
}
GSCapture::FormatList GSCapture::GetVideoFormatList(const char* codec)
{
FormatList ret;
if (!LoadFFmpeg(false))
return ret;
const AVCodec* v_codec = wrap_avcodec_find_encoder_by_name(codec);
if (!v_codec)
{
Console.Error("(GetVideoFormatList) avcodec_find_encoder_by_name() failed");
return ret;
}
// rawvideo doesn't have a list of formats.
if (v_codec->pix_fmts == nullptr)
{
Console.Error("(GetVideoFormatList) v_codec->pix_fmts is null.");
return ret;
}
for (int i = 0; v_codec->pix_fmts[i] != AVPixelFormat::AV_PIX_FMT_NONE; i++)
{
ret.emplace_back(v_codec->pix_fmts[i], wrap_av_get_pix_fmt_name(v_codec->pix_fmts[i]));
}
return ret;
}