From 304e57cbf089bb503203ea592b3923067ed06a84 Mon Sep 17 00:00:00 2001 From: James R Date: Sun, 12 Feb 2023 02:29:47 -0800 Subject: [PATCH] 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 --- src/hwr2/pass_software.cpp | 3 + src/m_avrecorder.cpp | 17 ++++ src/m_avrecorder.h | 4 + src/m_misc.c | 3 + src/media/CMakeLists.txt | 1 + src/media/avrecorder.cpp | 1 + src/media/avrecorder.hpp | 3 + src/media/avrecorder_feedback.cpp | 149 ++++++++++++++++++++++++++++++ src/media/avrecorder_impl.hpp | 8 ++ src/media/avrecorder_queue.cpp | 17 ++++ 10 files changed, 206 insertions(+) create mode 100644 src/media/avrecorder_feedback.cpp diff --git a/src/hwr2/pass_software.cpp b/src/hwr2/pass_software.cpp index 4876a52cd..f3bfd69ef 100644 --- a/src/hwr2/pass_software.cpp +++ b/src/hwr2/pass_software.cpp @@ -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) diff --git a/src/m_avrecorder.cpp b/src/m_avrecorder.cpp index 3f16470c2..cfdd22454 100644 --- a/src/m_avrecorder.cpp +++ b/src/m_avrecorder.cpp @@ -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) { diff --git a/src/m_avrecorder.h b/src/m_avrecorder.h index ccc63dc14..9cce8349c 100644 --- a/src/m_avrecorder.h +++ b/src/m_avrecorder.h @@ -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); diff --git a/src/m_misc.c b/src/m_misc.c index 952b93f4e..53e3534ac 100644 --- a/src/m_misc.c +++ b/src/m_misc.c @@ -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 diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index 6ffa6dd4a..daa0e721a 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -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 diff --git a/src/media/avrecorder.cpp b/src/media/avrecorder.cpp index d6ce488dc..ca3ecf681 100644 --- a/src/media/avrecorder.cpp +++ b/src/media/avrecorder.cpp @@ -39,6 +39,7 @@ Impl::Impl(Config cfg) : container_(std::make_unique(MediaContainer::Config { .file_name = cfg.file_name, + .destructor_callback = [this](const MediaContainer& container) { container_dtor_handler(container); }, })), audio_encoder_(make_audio_encoder(cfg)), diff --git a/src/media/avrecorder.hpp b/src/media/avrecorder.hpp index fa000afbf..fdee91b18 100644 --- a/src/media/avrecorder.hpp +++ b/src/media/avrecorder.hpp @@ -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 diff --git a/src/media/avrecorder_feedback.cpp b/src/media/avrecorder_feedback.cpp new file mode 100644 index 000000000..4dac2eaf6 --- /dev/null +++ b/src/media/avrecorder_feedback.cpp @@ -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 +#include +#include + +#include + +#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(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); +} diff --git a/src/media/avrecorder_impl.hpp b/src/media/avrecorder_impl.hpp index f4204f717..3fab89df8 100644 --- a/src/media/avrecorder_impl.hpp +++ b/src/media/avrecorder_impl.hpp @@ -110,6 +110,9 @@ public: // 2) the object has begun destructing std::atomic valid_ = true; + // Average number of frames actually encoded per second. + std::atomic 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 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); }; diff --git a/src/media/avrecorder_queue.cpp b/src/media/avrecorder_queue.cpp index 710aa702e..d08fa49ba 100644 --- a/src/media/avrecorder_queue.cpp +++ b/src/media/avrecorder_queue.cpp @@ -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(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; template class Impl::Queue;