// SPDX-FileCopyrightText: 2002-2026 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 "IconsFontAwesome.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 #include #include #include // 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 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& lock); static void ProcessAllInFlightFrames(std::unique_lock& lock); static void EncoderThreadEntryPoint(); static void StartEncoderThread(); static void StopEncoderThread(std::unique_lock& 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& 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 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 s_audio_buffer; static std::atomic 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& 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& 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 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(s_size.x) / static_cast(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(static_cast(fps) * 10000.0), std::numeric_limits::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(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 (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 (hwconfig) { const char* device = nullptr; if (AVDictionaryEntry* dev = wrap_av_dict_get(s_video_codec_arguments, "vaapi_device", nullptr, 0)) device = dev->value; if (AVDictionaryEntry* dev = wrap_av_dict_get(s_video_codec_arguments, "hwaccel_device", nullptr, 0)) device = dev->value; Console.WriteLnFmt(Color_StrongGreen, "Trying to use {}:{} for video encoding.", wrap_av_hwdevice_get_type_name(hwconfig->device_type), device ? device : "default"); res = wrap_av_hwdevice_ctx_create(&s_video_hw_context, hwconfig->device_type, device, 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(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 (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(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_FLT; 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 float 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_FLT, 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 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(stex->GetWidth()) || pf.tex->GetHeight() != static_cast(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& 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 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& 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(pf.tex->GetWidth()); const int source_height = static_cast(pf.tex->GetHeight()); const int source_pitch = static_cast(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(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(&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& 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 float* 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 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(float) * 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 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(&s_audio_buffer[s_audio_buffer_read_pos * AUDIO_CHANNELS + i]); for (u32 j = 0; j < this_batch; j++) { std::memcpy(output, input, sizeof(float)); input += sizeof(float) * 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(float) * AUDIO_CHANNELS); } } else { // Use swresample to convert. const u8* input = reinterpret_cast(&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& 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 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 lock(s_lock); s64 seconds; if (s_video_stream) { seconds = (s_next_video_pts * static_cast(s_video_codec_context->time_base.num)) / static_cast(s_video_codec_context->time_base.den); } else if (s_audio_stream) { seconds = (s_next_audio_pts * static_cast(s_audio_codec_context->time_base.num)) / static_cast(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(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 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; }