1
0
mirror of https://github.com/RPCS3/rpcs3.git synced 2024-11-25 20:22:30 +01:00

recording: initial audio recording

This commit is contained in:
Megamouse 2022-11-06 11:19:24 +01:00
parent ebf48800e6
commit 70c35642a4
13 changed files with 1036 additions and 474 deletions

View File

@ -33,8 +33,7 @@ void audio_resampler::put_samples(const f32* buf, u32 sample_cnt)
std::pair<f32* /* buffer */, u32 /* samples */> audio_resampler::get_samples(u32 sample_cnt)
{
f32 *const buf = resampler.bufBegin();
return std::make_pair(buf, resampler.receiveSamples(sample_cnt));
return std::make_pair(resampler.bufBegin(), resampler.receiveSamples(sample_cnt));
}
u32 audio_resampler::samples_available() const

View File

@ -5,6 +5,7 @@
#include "Emu/Cell/lv2/sys_process.h"
#include "Emu/Cell/lv2/sys_event.h"
#include "cellAudio.h"
#include "util/video_provider.h"
#include <cmath>
@ -69,7 +70,7 @@ void cell_audio_config::reset(bool backend_changed)
const AudioFreq freq = AudioFreq::FREQ_48K;
const AudioSampleSize sample_size = raw.convert_to_s16 ? AudioSampleSize::S16 : AudioSampleSize::FLOAT;
const auto [req_ch_cnt, downmix] = AudioBackend::get_channel_count_and_downmixer(0); // CELL_AUDIO_OUT_PRIMARY
const auto& [req_ch_cnt, downmix] = AudioBackend::get_channel_count_and_downmixer(0); // CELL_AUDIO_OUT_PRIMARY
f64 cb_frame_len = 0.0;
u32 ch_cnt = 2;
@ -276,16 +277,23 @@ void audio_ringbuffer::process_resampled_data()
{
if (!cfg.time_stretching_enabled) return;
const auto [buffer, samples] = resampler.get_samples(static_cast<u32>(cb_ringbuf.get_free_size() / (cfg.audio_sample_size * static_cast<u32>(cfg.backend_ch_cnt))));
const auto& [buffer, samples] = resampler.get_samples(static_cast<u32>(cb_ringbuf.get_free_size() / (cfg.audio_sample_size * static_cast<u32>(cfg.backend_ch_cnt))));
commit_data(buffer, samples);
}
void audio_ringbuffer::commit_data(f32* buf, u32 sample_cnt)
{
sample_cnt *= cfg.audio_channels;
const u32 sample_cnt_in = sample_cnt * cfg.audio_channels;
const u32 sample_cnt_out = sample_cnt * static_cast<u32>(cfg.backend_ch_cnt);
// Dump audio if enabled
m_dump.WriteData(buf, sample_cnt * static_cast<u32>(AudioSampleSize::FLOAT));
m_dump.WriteData(buf, sample_cnt_in * static_cast<u32>(AudioSampleSize::FLOAT));
// Record audio if enabled
if (utils::video_provider& provider = g_fxo->get<utils::video_provider>(); provider.can_consume_sample())
{
provider.present_samples(reinterpret_cast<u8*>(buf), sample_cnt, static_cast<u32>(cfg.audio_channels));
}
if (cfg.backend_ch_cnt < AudioChannelCnt{cfg.audio_channels})
{
@ -293,11 +301,11 @@ void audio_ringbuffer::commit_data(f32* buf, u32 sample_cnt)
{
if (cfg.backend_ch_cnt == AudioChannelCnt::SURROUND_5_1)
{
AudioBackend::downmix<AudioChannelCnt::SURROUND_7_1, AudioChannelCnt::SURROUND_5_1>(sample_cnt, buf, buf);
AudioBackend::downmix<AudioChannelCnt::SURROUND_7_1, AudioChannelCnt::SURROUND_5_1>(sample_cnt_in, buf, buf);
}
else if (cfg.backend_ch_cnt == AudioChannelCnt::STEREO)
{
AudioBackend::downmix<AudioChannelCnt::SURROUND_7_1, AudioChannelCnt::STEREO>(sample_cnt, buf, buf);
AudioBackend::downmix<AudioChannelCnt::SURROUND_7_1, AudioChannelCnt::STEREO>(sample_cnt_in, buf, buf);
}
else
{
@ -308,7 +316,7 @@ void audio_ringbuffer::commit_data(f32* buf, u32 sample_cnt)
{
if (cfg.backend_ch_cnt == AudioChannelCnt::STEREO)
{
AudioBackend::downmix<AudioChannelCnt::SURROUND_5_1, AudioChannelCnt::STEREO>(sample_cnt, buf, buf);
AudioBackend::downmix<AudioChannelCnt::SURROUND_5_1, AudioChannelCnt::STEREO>(sample_cnt_in, buf, buf);
}
else
{
@ -321,8 +329,6 @@ void audio_ringbuffer::commit_data(f32* buf, u32 sample_cnt)
}
}
const u32 sample_cnt_out = sample_cnt / cfg.audio_channels * static_cast<u32>(cfg.backend_ch_cnt);
if (cfg.backend->get_convert_to_s16())
{
AudioBackend::convert_to_s16(sample_cnt_out, buf, buf);

View File

@ -140,34 +140,26 @@ struct rec_param
constexpr u32 rec_framerate = 30; // Always 30 fps
class rec_image_sink : public utils::image_sink
class rec_video_sink : public utils::video_sink
{
public:
rec_image_sink() : utils::image_sink()
rec_video_sink() : utils::video_sink()
{
m_framerate = rec_framerate;
m_sample_rate = 44100; // TODO
}
void stop(bool flush = true) override
{
cellRec.notice("Stopping image sink. flush=%d", flush);
cellRec.notice("Stopping video sink. flush=%d", flush);
std::lock_guard lock(m_mtx);
m_flush = flush;
m_frames_to_encode.clear();
m_samples_to_encode.clear();
has_error = false;
}
void add_frame(std::vector<u8>& frame, u32 pitch, u32 width, u32 height, s32 pixel_format, usz timestamp_ms) override
{
std::lock_guard lock(m_mtx);
if (m_flush)
return;
m_frames_to_encode.emplace_back(timestamp_ms, pitch, width, height, pixel_format, std::move(frame));
}
encoder_frame get_frame()
{
std::lock_guard lock(m_mtx);
@ -196,7 +188,7 @@ struct rec_info
vm::bptr<u8> video_input_buffer{}; // Used by the game to inject a frame right before it would render a frame to the screen.
vm::bptr<u8> audio_input_buffer{}; // Used by the game to inject audio: 2-channel interleaved (left-right) * 256 samples * sizeof(f32) at 48000 kHz
std::vector<utils::image_sink::encoder_frame> video_ringbuffer;
std::vector<utils::video_sink::encoder_frame> video_ringbuffer;
std::vector<u8> audio_ringbuffer;
usz video_ring_pos = 0;
usz video_ring_frame_count = 0;
@ -209,9 +201,9 @@ struct rec_info
return pos;
}
std::shared_ptr<rec_image_sink> image_sink;
std::shared_ptr<rec_video_sink> video_sink;
std::shared_ptr<utils::video_encoder> encoder;
std::unique_ptr<named_thread<std::function<void()>>> image_provider_thread;
std::unique_ptr<named_thread<std::function<void()>>> video_provider_thread;
atomic_t<bool> paused = false;
s64 last_pts = -1;
@ -240,9 +232,9 @@ struct rec_info
void set_video_params(s32 video_format);
void set_audio_params(s32 audio_format);
void start_image_provider();
void pause_image_provider();
void stop_image_provider(bool flush);
void start_video_provider();
void pause_video_provider();
void stop_video_provider(bool flush);
};
void rec_info::set_video_params(s32 video_format)
@ -507,29 +499,29 @@ void rec_info::set_audio_params(s32 audio_format)
cellRec.notice("set_audio_params: audio_format=0x%x, audio_codec_id=%d, sample_rate=%d, audio_bps=%d", audio_format, audio_codec_id, sample_rate, audio_bps);
}
void rec_info::start_image_provider()
void rec_info::start_video_provider()
{
const bool was_paused = paused.exchange(false);
utils::video_provider& video_provider = g_fxo->get<utils::video_provider>();
if (image_provider_thread && was_paused)
if (video_provider_thread && was_paused)
{
// Resume
const u64 pause_time_end = get_system_time();
ensure(pause_time_end > pause_time_start);
pause_time_total += (pause_time_end - pause_time_start);
video_provider.set_pause_time(pause_time_total / 1000);
cellRec.notice("Resuming image provider.");
cellRec.notice("Resuming video provider.");
return;
}
cellRec.notice("Starting image provider.");
cellRec.notice("Starting video provider.");
recording_time_start = get_system_time();
pause_time_total = 0;
video_provider.set_pause_time(0);
image_provider_thread = std::make_unique<named_thread<std::function<void()>>>("cellRec Image Provider", [this]()
video_provider_thread = std::make_unique<named_thread<std::function<void()>>>("cellRec video provider", [this]()
{
const bool use_internal_audio = param.audio_input == CELL_REC_PARAM_AUDIO_INPUT_DISABLE || param.audio_input_mix_vol < 100;
const bool use_external_audio = param.audio_input != CELL_REC_PARAM_AUDIO_INPUT_DISABLE && param.audio_input_mix_vol > 0;
@ -537,7 +529,7 @@ void rec_info::start_image_provider()
const bool use_ring_buffer = param.ring_sec > 0;
const usz frame_size = input_format.pitch * input_format.height;
cellRec.notice("image_provider_thread: use_ring_buffer=%d, video_ringbuffer_size=%d, audio_ringbuffer_size=%d, ring_sec=%d, frame_size=%d, use_external_video=%d, use_external_audio=%d, use_internal_audio=%d", use_ring_buffer, video_ringbuffer.size(), audio_ringbuffer.size(), param.ring_sec, frame_size, use_external_video, use_external_audio, use_internal_audio);
cellRec.notice("video_provider_thread: use_ring_buffer=%d, video_ringbuffer_size=%d, audio_ringbuffer_size=%d, ring_sec=%d, frame_size=%d, use_external_video=%d, use_external_audio=%d, use_internal_audio=%d", use_ring_buffer, video_ringbuffer.size(), audio_ringbuffer.size(), param.ring_sec, frame_size, use_external_video, use_external_audio, use_internal_audio);
while (thread_ctrl::state() != thread_state::aborting && encoder)
{
@ -575,7 +567,7 @@ void rec_info::start_image_provider()
{
if (use_ring_buffer)
{
utils::image_sink::encoder_frame& frame_data = video_ringbuffer[next_video_ring_pos()];
utils::video_sink::encoder_frame& frame_data = video_ringbuffer[next_video_ring_pos()];
frame_data.pts = pts;
frame_data.width = input_format.width;
frame_data.height = input_format.height;
@ -595,14 +587,14 @@ void rec_info::start_image_provider()
last_pts = pts;
}
}
else if (use_ring_buffer && image_sink)
else if (use_ring_buffer && video_sink)
{
utils::image_sink::encoder_frame frame = image_sink->get_frame();
utils::video_sink::encoder_frame frame = video_sink->get_frame();
if (const s64 pts = encoder->get_pts(frame.timestamp_ms); pts > last_pts && frame.data.size() > 0)
{
ensure(frame.data.size() == frame_size);
utils::image_sink::encoder_frame& frame_data = video_ringbuffer[next_video_ring_pos()];
utils::video_sink::encoder_frame& frame_data = video_ringbuffer[next_video_ring_pos()];
frame_data = std::move(frame);
frame_data.pts = pts;
last_pts = pts;
@ -635,34 +627,34 @@ void rec_info::start_image_provider()
}
// Update recording time
recording_time_total = encoder->get_timestamp_ms(encoder->last_pts());
recording_time_total = encoder->get_timestamp_ms(encoder->last_video_pts());
thread_ctrl::wait_for(100);
}
});
}
void rec_info::pause_image_provider()
void rec_info::pause_video_provider()
{
cellRec.notice("Pausing image provider.");
if (image_provider_thread)
if (video_provider_thread)
{
paused = true;
pause_time_start = get_system_time();
}
}
void rec_info::stop_image_provider(bool flush)
void rec_info::stop_video_provider(bool flush)
{
cellRec.notice("Stopping image provider.");
cellRec.notice("Stopping video provider.");
if (image_provider_thread)
if (video_provider_thread)
{
auto& thread = *image_provider_thread;
auto& thread = *video_provider_thread;
thread = thread_state::aborting;
thread();
image_provider_thread.reset();
video_provider_thread.reset();
}
if (flush && param.ring_sec > 0 && !video_ringbuffer.empty())
@ -680,7 +672,7 @@ void rec_info::stop_image_provider(bool flush)
for (usz i = 0; i < frame_count; i++)
{
const usz pos = (start_offset + i) % video_ringbuffer.size();
utils::image_sink::encoder_frame& frame_data = video_ringbuffer[pos];
utils::video_sink::encoder_frame& frame_data = video_ringbuffer[pos];
encoder->add_frame(frame_data.data, frame_data.pitch, frame_data.width, frame_data.height, frame_data.av_pixel_format, encoder->get_timestamp_ms(frame_data.pts - start_pts));
// TODO: add audio data to encoder
@ -1073,7 +1065,7 @@ error_code cellRecOpen(vm::cptr<char> pDirName, vm::cptr<char> pFileName, vm::cp
rec.audio_ringbuffer.resize(audio_ring_buffer_size);
rec.audio_ring_step = audio_size_per_sample;
rec.video_ringbuffer.resize(video_ring_buffer_size, {});
rec.image_sink = std::make_shared<rec_image_sink>();
rec.video_sink = std::make_shared<rec_video_sink>();
}
rec.encoder = std::make_shared<utils::video_encoder>();
@ -1082,6 +1074,7 @@ error_code cellRecOpen(vm::cptr<char> pDirName, vm::cptr<char> pFileName, vm::cp
rec.encoder->set_video_bitrate(rec.video_bps);
rec.encoder->set_video_codec(rec.video_codec_id);
rec.encoder->set_sample_rate(rec.sample_rate);
rec.encoder->set_audio_channels(rec.channels);
rec.encoder->set_audio_bitrate(rec.audio_bps);
rec.encoder->set_audio_codec(rec.audio_codec_id);
rec.encoder->set_output_format(rec.output_format);
@ -1114,12 +1107,12 @@ error_code cellRecClose(s32 isDiscard)
if (isDiscard)
{
// No need to flush
rec.stop_image_provider(false);
rec.stop_video_provider(false);
rec.encoder->stop(false);
if (rec.image_sink)
if (rec.video_sink)
{
rec.image_sink->stop(false);
rec.video_sink->stop(false);
}
if (fs::is_file(rec.param.filename))
@ -1135,18 +1128,18 @@ error_code cellRecClose(s32 isDiscard)
else
{
// Flush to make sure we encode all remaining frames
rec.stop_image_provider(true);
rec.stop_video_provider(true);
rec.encoder->stop(true);
rec.recording_time_total = rec.encoder->get_timestamp_ms(rec.encoder->last_pts());
rec.recording_time_total = rec.encoder->get_timestamp_ms(rec.encoder->last_video_pts());
if (rec.image_sink)
if (rec.video_sink)
{
rec.image_sink->stop(true);
rec.video_sink->stop(true);
}
const s64 start_pts = rec.encoder->get_pts(rec.param.scene_metadata.start_time);
const s64 end_pts = rec.encoder->get_pts(rec.param.scene_metadata.end_time);
const s64 last_pts = rec.encoder->last_pts();
const s64 last_pts = rec.encoder->last_video_pts();
is_valid_range = start_pts >= 0 && end_pts <= last_pts;
}
@ -1157,7 +1150,7 @@ error_code cellRecClose(s32 isDiscard)
g_fxo->need<utils::video_provider>();
utils::video_provider& video_provider = g_fxo->get<utils::video_provider>();
// Release the image sink if it was used
// Release the video sink if it was used
if (rec.param.video_input == CELL_REC_PARAM_VIDEO_INPUT_DISABLE)
{
const recording_mode old_mode = g_recording_mode.exchange(recording_mode::stopped);
@ -1167,15 +1160,15 @@ error_code cellRecClose(s32 isDiscard)
cellRec.error("cellRecClose: Unexpected recording mode %s found while stopping video capture.", old_mode);
}
if (!video_provider.set_image_sink(nullptr, recording_mode::cell))
if (!video_provider.set_video_sink(nullptr, recording_mode::cell))
{
cellRec.error("cellRecClose failed to release image sink");
cellRec.error("cellRecClose failed to release video sink");
}
}
rec.param = {};
rec.encoder.reset();
rec.image_sink.reset();
rec.video_sink.reset();
rec.audio_ringbuffer.clear();
rec.video_ringbuffer.clear();
rec.state = rec_state::closed;
@ -1207,7 +1200,7 @@ error_code cellRecStop()
sysutil_register_cb([&rec](ppu_thread& ppu) -> s32
{
// Disable image sink if it was used
// Disable video sink if it was used
if (rec.param.video_input == CELL_REC_PARAM_VIDEO_INPUT_DISABLE)
{
const recording_mode old_mode = g_recording_mode.exchange(recording_mode::stopped);
@ -1219,12 +1212,12 @@ error_code cellRecStop()
}
// cellRecStop actually just pauses the recording
rec.pause_image_provider();
rec.pause_video_provider();
ensure(!!rec.encoder);
rec.encoder->pause(true);
rec.recording_time_total = rec.encoder->get_timestamp_ms(rec.encoder->last_pts());
rec.recording_time_total = rec.encoder->get_timestamp_ms(rec.encoder->last_video_pts());
rec.state = rec_state::stopped;
rec.cb(ppu, CELL_REC_STATUS_STOP, CELL_OK, rec.cbUserData);
@ -1254,15 +1247,15 @@ error_code cellRecStart()
g_fxo->need<utils::video_provider>();
utils::video_provider& video_provider = g_fxo->get<utils::video_provider>();
// Setup an image sink if it is needed
// Setup an video sink if it is needed
if (rec.param.video_input == CELL_REC_PARAM_VIDEO_INPUT_DISABLE)
{
if (rec.param.ring_sec <= 0)
{
// Regular recording
if (!video_provider.set_image_sink(rec.encoder, recording_mode::cell))
if (!video_provider.set_video_sink(rec.encoder, recording_mode::cell))
{
cellRec.error("Failed to set image sink");
cellRec.error("Failed to set video sink");
rec.cb(ppu, CELL_REC_STATUS_ERR, CELL_REC_ERROR_FATAL, rec.cbUserData);
return CELL_OK;
}
@ -1270,9 +1263,9 @@ error_code cellRecStart()
else
{
// Ringbuffer recording
if (!video_provider.set_image_sink(rec.image_sink, recording_mode::cell))
if (!video_provider.set_video_sink(rec.video_sink, recording_mode::cell))
{
cellRec.error("Failed to set image sink");
cellRec.error("Failed to set video sink");
rec.cb(ppu, CELL_REC_STATUS_ERR, CELL_REC_ERROR_FATAL, rec.cbUserData);
return CELL_OK;
}
@ -1287,7 +1280,7 @@ error_code cellRecStart()
g_recording_mode = recording_mode::stopped;
}
rec.start_image_provider();
rec.start_video_provider();
if (rec.encoder->has_error)
{

View File

@ -8,15 +8,32 @@ struct cfg_recording final : cfg::node
bool load();
void save() const;
struct node_video : cfg::node
{
node_video(cfg::node* _this) : cfg::node(_this, "Video") {}
cfg::uint<0, 60> framerate{this, "Framerate", 30};
cfg::uint<0, 7680> width{this, "Width", 1280};
cfg::uint<0, 4320> height{this, "Height", 720};
cfg::uint<0, 192> pixel_format{this, "AVPixelFormat", 0}; // AVPixelFormat::AV_PIX_FMT_YUV420P
cfg::uint<0, 32813> video_codec{this, "AVCodecID", 12}; // AVCodecID::AV_CODEC_ID_MPEG4
cfg::uint<0, 0xFFFF> video_codec{this, "AVCodecID", 12}; // AVCodecID::AV_CODEC_ID_MPEG4
cfg::uint<0, 25000000> video_bps{this, "Video Bitrate", 4000000};
cfg::uint<0, 5> max_b_frames{this, "Max B-Frames", 2};
cfg::uint<0, 20> gop_size{this, "Group of Pictures Size", 12};
} video{ this };
struct node_audio : cfg::node
{
node_audio(cfg::node* _this) : cfg::node(_this, "Audio") {}
cfg::uint<0x10000, 0x17000> audio_codec{this, "AVCodecID", 86019}; // AVCodecID::AV_CODEC_ID_AC3
cfg::uint<0, 8> channels{this, "Channels", 2};
cfg::uint<0, 25000000> audio_bps{this, "Audio Bitrate", 320000};
cfg::uint<0, 25000000> sample_rate{this, "Sample Rate", 48000};
} audio{ this };
const std::string path;
};

View File

@ -618,7 +618,7 @@
<ClInclude Include="Loader\mself.hpp" />
<ClInclude Include="util\atomic.hpp" />
<ClInclude Include="util\bless.hpp" />
<ClInclude Include="util\image_sink.h" />
<ClInclude Include="util\video_sink.h" />
<ClInclude Include="util\video_provider.h" />
<ClInclude Include="util\media_utils.h" />
<ClInclude Include="util\serialization.hpp" />

View File

@ -2275,7 +2275,7 @@
<ClInclude Include="util\video_provider.h">
<Filter>Utilities</Filter>
</ClInclude>
<ClInclude Include="util\image_sink.h">
<ClInclude Include="util\video_sink.h">
<Filter>Utilities</Filter>
</ClInclude>
<ClInclude Include="Emu\Io\recording_config.h">

View File

@ -12,6 +12,7 @@
#include "Emu/IdManager.h"
#include "Emu/Cell/Modules/cellScreenshot.h"
#include "Emu/Cell/Modules/cellVideoOut.h"
#include "Emu/Cell/Modules/cellAudio.h"
#include "Emu/RSX/rsx_utils.h"
#include "Emu/RSX/Overlays/overlay_message.h"
#include "Emu/Io/recording_config.h"
@ -445,9 +446,9 @@ void gs_frame::toggle_recording()
{
m_video_encoder->stop();
if (!video_provider.set_image_sink(nullptr, recording_mode::rpcs3))
if (!video_provider.set_video_sink(nullptr, recording_mode::rpcs3))
{
gui_log.warning("The video provider could not release the image sink. A sink with higher priority must have been set.");
gui_log.warning("The video provider could not release the video sink. A sink with higher priority must have been set.");
}
// Play a sound
@ -489,21 +490,23 @@ void gs_frame::toggle_recording()
video_path += "recording_" + date_time::current_time_narrow<'_'>() + ".mp4";
utils::video_encoder::frame_format output_format{};
output_format.av_pixel_format = static_cast<AVPixelFormat>(g_cfg_recording.pixel_format.get());
output_format.width = g_cfg_recording.width;
output_format.height = g_cfg_recording.height;
output_format.pitch = g_cfg_recording.width * 4;
output_format.av_pixel_format = static_cast<AVPixelFormat>(g_cfg_recording.video.pixel_format.get());
output_format.width = g_cfg_recording.video.width;
output_format.height = g_cfg_recording.video.height;
output_format.pitch = g_cfg_recording.video.width * 4;
m_video_encoder->set_path(video_path);
m_video_encoder->set_framerate(g_cfg_recording.framerate);
m_video_encoder->set_video_bitrate(g_cfg_recording.video_bps);
m_video_encoder->set_video_codec(g_cfg_recording.video_codec);
m_video_encoder->set_max_b_frames(g_cfg_recording.max_b_frames);
m_video_encoder->set_gop_size(g_cfg_recording.gop_size);
m_video_encoder->set_framerate(g_cfg_recording.video.framerate);
m_video_encoder->set_video_bitrate(g_cfg_recording.video.video_bps);
m_video_encoder->set_video_codec(g_cfg_recording.video.video_codec);
m_video_encoder->set_max_b_frames(g_cfg_recording.video.max_b_frames);
m_video_encoder->set_gop_size(g_cfg_recording.video.gop_size);
m_video_encoder->set_output_format(output_format);
m_video_encoder->set_sample_rate(0); // TODO
m_video_encoder->set_audio_bitrate(0); // TODO
m_video_encoder->set_audio_codec(0); // TODO
m_video_encoder->set_sample_rate(g_cfg_recording.audio.sample_rate);
//m_video_encoder->set_audio_channels(static_cast<u32>(g_fxo->get<cell_audio>().cfg.backend_ch_cnt));
m_video_encoder->set_audio_channels(static_cast<u32>(g_fxo->get<cell_audio>().cfg.audio_channels));
m_video_encoder->set_audio_bitrate(g_cfg_recording.audio.audio_bps);
m_video_encoder->set_audio_codec(g_cfg_recording.audio.audio_codec);
m_video_encoder->encode();
if (m_video_encoder->has_error)
@ -513,9 +516,9 @@ void gs_frame::toggle_recording()
return;
}
if (!video_provider.set_image_sink(m_video_encoder, recording_mode::rpcs3))
if (!video_provider.set_video_sink(m_video_encoder, recording_mode::rpcs3))
{
gui_log.warning("The video provider could not set the image sink. A sink with higher priority must have been set.");
gui_log.warning("The video provider could not set the video sink. A sink with higher priority must have been set.");
rsx::overlays::queue_message(tr("Recording not possible").toStdString());
m_video_encoder->stop();
return;

View File

@ -1,54 +0,0 @@
#pragma once
#include "util/types.hpp"
#include "util/atomic.hpp"
#include "Utilities/mutex.h"
#include <deque>
#include <cmath>
namespace utils
{
class image_sink
{
public:
image_sink() = default;
virtual void stop(bool flush = true) = 0;
virtual void add_frame(std::vector<u8>& frame, u32 pitch, u32 width, u32 height, s32 pixel_format, usz timestamp_ms) = 0;
s64 get_pts(usz timestamp_ms) const
{
return static_cast<s64>(std::round((timestamp_ms * m_framerate) / 1000.f));
}
usz get_timestamp_ms(s64 pts) const
{
return static_cast<usz>(std::round((pts * 1000) / static_cast<float>(m_framerate)));
}
atomic_t<bool> has_error{false};
struct encoder_frame
{
encoder_frame() = default;
encoder_frame(usz timestamp_ms, u32 pitch, u32 width, u32 height, s32 av_pixel_format, std::vector<u8>&& data)
: timestamp_ms(timestamp_ms), pitch(pitch), width(width), height(height), av_pixel_format(av_pixel_format), data(std::move(data))
{}
s64 pts = -1; // Optional
usz timestamp_ms = 0;
u32 pitch = 0;
u32 width = 0;
u32 height = 0;
s32 av_pixel_format = 0; // NOTE: Make sure this is a valid AVPixelFormat
std::vector<u8> data;
};
protected:
shared_mutex m_mtx;
std::deque<encoder_frame> m_frames_to_encode;
atomic_t<bool> m_flush = false;
u32 m_framerate = 0;
};
}

File diff suppressed because it is too large Load Diff

View File

@ -88,7 +88,7 @@ namespace utils
std::unique_ptr<named_thread<std::function<void()>>> m_thread;
};
class video_encoder : public utils::image_sink
class video_encoder : public utils::video_sink
{
public:
video_encoder();
@ -108,7 +108,7 @@ namespace utils
};
std::string path() const;
s64 last_pts() const;
s64 last_video_pts() const;
void set_path(const std::string& path);
void set_framerate(u32 framerate);
@ -118,16 +118,17 @@ namespace utils
void set_max_b_frames(s32 max_b_frames);
void set_gop_size(s32 gop_size);
void set_sample_rate(u32 sample_rate);
void set_audio_channels(u32 channels);
void set_audio_bitrate(u32 bitrate);
void set_audio_codec(s32 codec_id);
void add_frame(std::vector<u8>& frame, u32 pitch, u32 width, u32 height, s32 pixel_format, usz timestamp_ms) override;
void pause(bool flush = true);
void stop(bool flush = true) override;
void encode();
private:
std::string m_path;
s64 m_last_pts = 0;
s64 m_last_audio_pts = 0;
s64 m_last_video_pts = 0;
// Thread control
std::unique_ptr<named_thread<std::function<void()>>> m_thread;
@ -136,14 +137,14 @@ namespace utils
// Video parameters
u32 m_video_bitrate_bps = 0;
s32 m_video_codec_id = 12; // AV_CODEC_ID_MPEG4;
s32 m_video_codec_id = 12; // AV_CODEC_ID_MPEG4
s32 m_max_b_frames = 2;
s32 m_gop_size = 12;
frame_format m_out_format{};
// Audio parameters
u32 m_sample_rate = 48000;
u32 m_audio_bitrate_bps = 96000;
s32 m_audio_codec_id = 86018; // AV_CODEC_ID_AAC
u32 m_channels = 2;
u32 m_audio_bitrate_bps = 320000;
s32 m_audio_codec_id = 86019; // AV_CODEC_ID_AC3
};
}

View File

@ -34,37 +34,37 @@ namespace utils
g_recording_mode = recording_mode::stopped;
}
bool video_provider::set_image_sink(std::shared_ptr<image_sink> sink, recording_mode type)
bool video_provider::set_video_sink(std::shared_ptr<video_sink> sink, recording_mode type)
{
media_log.notice("video_provider: setting new image sink. sink=%d, type=%s", !!sink, type);
media_log.notice("video_provider: setting new video sink. sink=%d, type=%s", !!sink, type);
if (type == recording_mode::stopped)
{
// Prevent misuse. type is supposed to be a valid state.
media_log.error("video_provider: cannot set image sink with type %s", type);
media_log.error("video_provider: cannot set video sink with type %s", type);
return false;
}
std::lock_guard lock(m_mutex);
if (m_image_sink)
if (m_video_sink)
{
// cell has preference
if (m_type == recording_mode::cell && m_type != type)
{
media_log.warning("video_provider: cannot set image sink with type %s if type %s is active", type, m_type);
media_log.warning("video_provider: cannot set video sink with type %s if type %s is active", type, m_type);
return false;
}
if (m_type != type || m_image_sink != sink)
if (m_type != type || m_video_sink != sink)
{
media_log.warning("video_provider: stopping current image sink of type %s", m_type);
m_image_sink->stop();
media_log.warning("video_provider: stopping current video sink of type %s", m_type);
m_video_sink->stop();
}
}
m_type = sink ? type : recording_mode::stopped;
m_image_sink = sink;
m_video_sink = sink;
if (m_type == recording_mode::stopped)
{
@ -84,19 +84,17 @@ namespace utils
{
std::lock_guard lock(m_mutex);
if (!m_image_sink)
if (!m_video_sink)
return false;
const usz timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(steady_clock::now() - m_encoder_start).count() - m_pause_time_ms;
const s64 pts = m_image_sink->get_pts(timestamp_ms);
return pts > m_last_pts_incoming;
const s64 pts = m_video_sink->get_pts(timestamp_ms);
return pts > m_last_video_pts_incoming;
}
void video_provider::present_frame(std::vector<u8>& data, u32 pitch, u32 width, u32 height, bool is_bgra)
recording_mode video_provider::check_state()
{
std::lock_guard lock(m_mutex);
if (!m_image_sink || m_image_sink->has_error)
if (!m_video_sink || m_video_sink->has_error)
{
g_recording_mode = recording_mode::stopped;
rsx::overlays::queue_message(localized_string_id::RECORDING_ABORTED);
@ -105,33 +103,86 @@ namespace utils
if (g_recording_mode == recording_mode::stopped)
{
m_active = false;
return;
return g_recording_mode;
}
if (!m_active.exchange(true))
{
m_current_encoder_frame = 0;
m_last_pts_incoming = -1;
m_last_video_pts_incoming = -1;
m_last_audio_pts_incoming = -1;
}
if (m_current_encoder_frame == 0)
if (m_current_encoder_frame == 0 && m_current_encoder_sample == 0)
{
m_encoder_start = steady_clock::now();
}
// Calculate presentation timestamp.
const usz timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(steady_clock::now() - m_encoder_start).count() - m_pause_time_ms;
const s64 pts = m_image_sink->get_pts(timestamp_ms);
return g_recording_mode;
}
// We can just skip this frame if it has the same timestamp.
if (pts <= m_last_pts_incoming)
void video_provider::present_frame(std::vector<u8>& data, u32 pitch, u32 width, u32 height, bool is_bgra)
{
std::lock_guard lock(m_mutex);
if (check_state() == recording_mode::stopped)
{
return;
}
m_last_pts_incoming = pts;
// Calculate presentation timestamp.
const usz timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(steady_clock::now() - m_encoder_start).count() - m_pause_time_ms;
const s64 pts = m_video_sink->get_pts(timestamp_ms);
// We can just skip this frame if it has the same timestamp.
if (pts <= m_last_video_pts_incoming)
{
return;
}
m_last_video_pts_incoming = pts;
m_current_encoder_frame++;
m_image_sink->add_frame(data, pitch, width, height, is_bgra ? AVPixelFormat::AV_PIX_FMT_BGRA : AVPixelFormat::AV_PIX_FMT_RGBA, timestamp_ms);
m_video_sink->add_frame(data, pitch, width, height, is_bgra ? AVPixelFormat::AV_PIX_FMT_BGRA : AVPixelFormat::AV_PIX_FMT_RGBA, timestamp_ms);
}
bool video_provider::can_consume_sample()
{
std::lock_guard lock(m_mutex);
if (!m_video_sink)
return false;
const usz timestamp_us = std::chrono::duration_cast<std::chrono::microseconds>(steady_clock::now() - m_encoder_start).count() - (m_pause_time_ms * 1000ull);
const s64 pts = m_video_sink->get_audio_pts(timestamp_us);
return pts > m_last_audio_pts_incoming;
}
void video_provider::present_samples(u8* buf, u32 sample_count, u16 channels)
{
if (!buf || !sample_count || !channels)
{
return;
}
std::lock_guard lock(m_mutex);
if (check_state() == recording_mode::stopped)
{
return;
}
// Calculate presentation timestamp.
const usz timestamp_us = std::chrono::duration_cast<std::chrono::microseconds>(steady_clock::now() - m_encoder_start).count() - (m_pause_time_ms * 1000ull);
const s64 pts = m_video_sink->get_audio_pts(timestamp_us);
// We can just skip this sample if it has the same timestamp.
if (pts <= m_last_audio_pts_incoming)
{
return;
}
m_last_audio_pts_incoming = pts;
m_current_encoder_sample += sample_count;
m_video_sink->add_audio_samples(buf, sample_count, channels, timestamp_us);
}
}

View File

@ -1,6 +1,6 @@
#pragma once
#include "image_sink.h"
#include "video_sink.h"
enum class recording_mode
{
@ -17,19 +17,27 @@ namespace utils
video_provider() = default;
~video_provider();
bool set_image_sink(std::shared_ptr<image_sink> sink, recording_mode type);
bool set_video_sink(std::shared_ptr<video_sink> sink, recording_mode type);
void set_pause_time(usz pause_time_ms);
bool can_consume_frame();
void present_frame(std::vector<u8>& data, u32 pitch, u32 width, u32 height, bool is_bgra);
bool can_consume_sample();
void present_samples(u8* buf, u32 sample_count, u16 channels);
private:
recording_mode check_state();
recording_mode m_type = recording_mode::stopped;
std::shared_ptr<image_sink> m_image_sink;
std::shared_ptr<video_sink> m_video_sink;
shared_mutex m_mutex{};
atomic_t<bool> m_active{false};
atomic_t<usz> m_current_encoder_frame{0};
atomic_t<usz> m_current_encoder_sample{0};
steady_clock::time_point m_encoder_start{};
s64 m_last_pts_incoming = -1;
s64 m_last_video_pts_incoming = -1;
s64 m_last_audio_pts_incoming = -1;
usz m_pause_time_ms = 0;
};

104
rpcs3/util/video_sink.h Normal file
View File

@ -0,0 +1,104 @@
#pragma once
#include "util/types.hpp"
#include "util/atomic.hpp"
#include "Utilities/mutex.h"
#include <deque>
#include <cmath>
namespace utils
{
class video_sink
{
public:
video_sink() = default;
virtual void stop(bool flush = true) = 0;
void add_frame(std::vector<u8>& frame, u32 pitch, u32 width, u32 height, s32 pixel_format, usz timestamp_ms)
{
// Do not allow new frames while flushing
if (m_flush)
return;
std::lock_guard lock(m_mtx);
m_frames_to_encode.emplace_back(timestamp_ms, pitch, width, height, pixel_format, std::move(frame));
}
void add_audio_samples(u8* buf, u32 sample_count, u16 channels, usz timestamp_us)
{
// Do not allow new samples while flushing
if (m_flush || !buf || !sample_count || !channels)
return;
std::vector<u8> sample(buf, buf + sample_count * channels * sizeof(f32));
std::lock_guard lock(m_audio_mtx);
m_samples_to_encode.emplace_back(timestamp_us, sample_count, channels, std::move(sample));
}
s64 get_pts(usz timestamp_ms) const
{
return static_cast<s64>(std::round((timestamp_ms * m_framerate) / 1000.f));
}
s64 get_audio_pts(usz timestamp_us) const
{
static constexpr f32 us_per_sec = 1000000.0f;
const f32 us_per_block = us_per_sec / (m_sample_rate / static_cast<f32>(m_samples_per_block));
return static_cast<s64>(std::ceil(timestamp_us / us_per_block));
}
usz get_timestamp_ms(s64 pts) const
{
return static_cast<usz>(std::round((pts * 1000) / static_cast<float>(m_framerate)));
}
usz get_audio_timestamp_us(s64 pts) const
{
return static_cast<usz>(std::round((pts * 1000) / static_cast<float>(m_sample_rate)));
}
atomic_t<bool> has_error{false};
struct encoder_frame
{
encoder_frame() = default;
encoder_frame(usz timestamp_ms, u32 pitch, u32 width, u32 height, s32 av_pixel_format, std::vector<u8>&& data)
: timestamp_ms(timestamp_ms), pitch(pitch), width(width), height(height), av_pixel_format(av_pixel_format), data(std::move(data))
{}
s64 pts = -1; // Optional
usz timestamp_ms = 0;
u32 pitch = 0;
u32 width = 0;
u32 height = 0;
s32 av_pixel_format = 0; // NOTE: Make sure this is a valid AVPixelFormat
std::vector<u8> data;
};
struct encoder_sample
{
encoder_sample() = default;
encoder_sample(usz timestamp_us, u32 sample_count, u16 channels, std::vector<u8>&& data)
: timestamp_us(timestamp_us), sample_count(sample_count), channels(channels), data(std::move(data))
{
}
usz timestamp_us = 0;
u32 sample_count = 0;
u16 channels = 0;
std::vector<u8> data;
};
protected:
shared_mutex m_mtx;
std::deque<encoder_frame> m_frames_to_encode;
shared_mutex m_audio_mtx;
std::deque<encoder_sample> m_samples_to_encode;
atomic_t<bool> m_flush = false;
u32 m_framerate = 30;
u32 m_sample_rate = 48000;
static constexpr u32 m_samples_per_block = 256;
};
}