Add user feedback for AVRecorder

- real time monitoring of duration and esimated file size
- estimated frame rate of final video (encoder speed)
- print brief details of audio and video tracks upon
  starting recording
- print exact file size and duration once recording is
  finished
This commit is contained in:
James R 2023-02-12 02:29:47 -08:00
parent 840c11577e
commit 304e57cbf0
10 changed files with 206 additions and 0 deletions

View file

@ -10,6 +10,7 @@
#include "../discord.h"
#endif
#include "../doomstat.h"
#include "../m_avrecorder.h"
#include "../st_stuff.h"
#include "../s_sound.h"
#include "../v_video.h"
@ -121,6 +122,8 @@ static void temp_legacy_finishupdate_draws()
}
if (cv_mindelay.value && consoleplayer == serverplayer && Playing())
SCR_DisplayLocalPing();
M_AVRecorder_DrawFrameRate();
}
if (marathonmode)

View file

@ -206,6 +206,13 @@ const char* M_AVRecorder_GetCurrentFormat(void)
return g_av_recorder->format_name();
}
void M_AVRecorder_PrintCurrentConfiguration(void)
{
SRB2_ASSERT(g_av_recorder != nullptr);
g_av_recorder->print_configuration();
}
boolean M_AVRecorder_IsExpired(void)
{
SRB2_ASSERT(g_av_recorder != nullptr);
@ -213,6 +220,16 @@ boolean M_AVRecorder_IsExpired(void)
return g_av_recorder->invalid();
}
void M_AVRecorder_DrawFrameRate(void)
{
if (!cv_movie_showfps.value || !g_av_recorder)
{
return;
}
g_av_recorder->draw_statistics();
}
// TODO: remove once hwr2 twodee is finished
void M_AVRecorder_CopySoftwareScreen(void)
{

View file

@ -30,6 +30,10 @@ boolean M_AVRecorder_IsExpired(void);
const char *M_AVRecorder_GetCurrentFormat(void);
void M_AVRecorder_PrintCurrentConfiguration(void);
void M_AVRecorder_DrawFrameRate(void);
// TODO: remove once hwr2 twodee is finished
void M_AVRecorder_CopySoftwareScreen(void);

View file

@ -1366,7 +1366,10 @@ void M_StartMovie(void)
else if (moviemode == MM_SCREENSHOT)
CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), "screenshots");
else if (moviemode == MM_AVRECORDER)
{
CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), M_AVRecorder_GetCurrentFormat());
M_AVRecorder_PrintCurrentConfiguration();
}
//singletics = (moviemode != MM_OFF);
#endif

View file

@ -2,6 +2,7 @@ target_sources(SRB2SDL2 PRIVATE
audio_encoder.hpp
avrecorder.cpp
avrecorder.hpp
avrecorder_feedback.cpp
avrecorder_impl.hpp
avrecorder_indexed.cpp
avrecorder_queue.cpp

View file

@ -39,6 +39,7 @@ Impl::Impl(Config cfg) :
container_(std::make_unique<WebmContainer>(MediaContainer::Config {
.file_name = cfg.file_name,
.destructor_callback = [this](const MediaContainer& container) { container_dtor_handler(container); },
})),
audio_encoder_(make_audio_encoder(cfg)),

View file

@ -80,6 +80,9 @@ public:
AVRecorder(Config config);
~AVRecorder();
void print_configuration() const;
void draw_statistics() const;
void push_audio_samples(audio_buffer_t buffer);
// May return nullptr in case called between units of

View file

@ -0,0 +1,149 @@
// RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2023 by James Robert Roman
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include <filesystem>
#include <sstream>
#include <string>
#include <fmt/format.h>
#include "../cxxutil.hpp"
#include "avrecorder_impl.hpp"
#include "../v_video.h"
using namespace srb2::media;
using Impl = AVRecorder::Impl;
namespace
{
constexpr float kMb = 1024.f * 1024.f;
}; // namespace
void Impl::container_dtor_handler(const MediaContainer& container) const
{
// Note that because this method is called from
// container_'s destructor, any member variables declared
// after Impl::container_ should not be accessed by now
// (since they would have already destructed).
if (max_size_ && container.size() > *max_size_)
{
const std::string line = fmt::format(
"Video size has exceeded limit {} > {} ({}%)."
" This should not happen, please report this bug.\n",
container.size(),
*max_size_,
100.f * (*max_size_ / static_cast<float>(container.size()))
);
CONS_Alert(CONS_WARNING, "%s\n", line.c_str());
}
std::ostringstream msg;
msg << "Video saved: " << std::filesystem::path(container.file_name()).filename().string()
<< fmt::format(" ({:.2f}", container.size() / kMb);
if (max_size_)
{
msg << fmt::format("/{:.2f}", *max_size_ / kMb);
}
msg << fmt::format(" MB, {:.1f}", container.duration().count());
if (max_duration_config_)
{
msg << fmt::format("/{:.1f}", max_duration_config_->count());
}
msg << " seconds)";
CONS_Printf("%s\n", msg.str().c_str());
}
void AVRecorder::print_configuration() const
{
if (impl_->audio_encoder_)
{
const auto& a = *impl_->audio_encoder_;
CONS_Printf("Audio: %s %dch %d Hz\n", a.name(), a.channels(), a.sample_rate());
}
if (impl_->video_encoder_)
{
const auto& v = *impl_->video_encoder_;
CONS_Printf(
"Video: %s %dx%d %d fps %d threads\n",
v.name(),
v.width(),
v.height(),
v.frame_rate(),
v.thread_count()
);
}
}
void AVRecorder::draw_statistics() const
{
SRB2_ASSERT(impl_->video_encoder_ != nullptr);
auto draw = [](int x, std::string text, int32_t flags = 0)
{
V_DrawThinString(
x,
190,
(V_6WIDTHSPACE | V_ALLOWLOWERCASE | V_SNAPTOBOTTOM | V_SNAPTORIGHT) | flags,
text.c_str()
);
};
const float fps = impl_->video_frame_rate_avg_;
const float size = impl_->container_->size();
const int32_t fps_color = [&]
{
const int cap = impl_->video_encoder_->frame_rate();
// red when dropped below 60% of the target
if (fps > 0.f && fps < (0.6f * cap))
{
return V_REDMAP;
}
return 0;
}();
const int32_t mb_color = [&]
{
if (!impl_->max_size_)
{
return 0;
}
const std::size_t cap = *impl_->max_size_;
// yellow when within 1 MB of the limit
if (size >= (cap - kMb))
{
return V_YELLOWMAP;
}
return 0;
}();
draw(200, fmt::format("{:.0f}", fps), fps_color);
draw(230, fmt::format("{:.1f}s", impl_->container_->duration().count()));
draw(260, fmt::format("{:.1f} MB", size / kMb), mb_color);
}

View file

@ -110,6 +110,9 @@ public:
// 2) the object has begun destructing
std::atomic<bool> valid_ = true;
// Average number of frames actually encoded per second.
std::atomic<float> video_frame_rate_avg_ = 0.f;
Impl(Config config);
~Impl();
@ -132,6 +135,8 @@ private:
const tic_t epoch_;
VideoEncoder::FrameCount video_frame_count_reference_ = {};
std::thread thread_;
mutable std::recursive_mutex queue_mutex_; // guards audio and video queues
std::condition_variable_any queue_cond_;
@ -140,9 +145,12 @@ private:
std::unique_ptr<VideoEncoder> make_video_encoder(const Config cfg) const;
QueueState encode_queues();
void update_video_frame_rate_avg();
void worker();
void container_dtor_handler(const MediaContainer& container) const;
// TODO: remove once hwr2 twodee is finished
VideoFrame::instance_t convert_indexed_video_frame(const IndexedVideoFrame& indexed);
};

View file

@ -127,6 +127,8 @@ Impl::QueueState Impl::encode_queues()
video_encoder_->encode(std::move(frame));
}
update_video_frame_rate_avg();
};
check(audio_queue_, encode_audio);
@ -146,5 +148,20 @@ Impl::QueueState Impl::encode_queues()
}
}
void Impl::update_video_frame_rate_avg()
{
constexpr auto period = std::chrono::duration<float>(1.f);
auto& ref = video_frame_count_reference_;
const auto count = video_encoder_->frame_count();
const auto t = (count.duration - ref.duration);
if (t >= period)
{
video_frame_rate_avg_ = (count.frames - ref.frames) * (period / t);
ref = count;
}
}
template class Impl::Queue<AudioEncoder>;
template class Impl::Queue<VideoEncoder>;