diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index 8282d847f..ec387a428 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -1,5 +1,9 @@ target_sources(SRB2SDL2 PRIVATE audio_encoder.hpp + avrecorder.cpp + avrecorder.hpp + avrecorder_impl.hpp + avrecorder_queue.cpp cfile.cpp cfile.hpp container.hpp diff --git a/src/media/avrecorder.cpp b/src/media/avrecorder.cpp new file mode 100644 index 000000000..ef1962fed --- /dev/null +++ b/src/media/avrecorder.cpp @@ -0,0 +1,211 @@ +// 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 +#include +#include +#include + +#include "../cxxutil.hpp" +#include "../i_time.h" +#include "../m_fixed.h" +#include "avrecorder_impl.hpp" +#include "webm_container.hpp" + +using namespace srb2::media; + +using Impl = AVRecorder::Impl; + +namespace +{ + +constexpr auto kBufferMethod = VideoFrame::BufferMethod::kEncoderAllocatedRGBA8888; + +}; // namespace + +Impl::Impl(Config cfg) : + max_size_(cfg.max_size), + max_duration_(cfg.max_duration), + + container_(std::make_unique(MediaContainer::Config { + .file_name = cfg.file_name, + })), + + audio_encoder_(make_audio_encoder(cfg)), + video_encoder_(make_video_encoder(cfg)), + + epoch_(I_GetTime()), + + thread_([this] { worker(); }) +{ +} + +std::unique_ptr Impl::make_audio_encoder(const Config cfg) const +{ + if (!cfg.audio) + { + return nullptr; + } + + const Config::Audio& a = *cfg.audio; + + return container_->make_audio_encoder({ + .channels = 2, + .sample_rate = a.sample_rate, + }); +} + +std::unique_ptr Impl::make_video_encoder(const Config cfg) const +{ + if (!cfg.video) + { + return nullptr; + } + + const Config::Video& v = *cfg.video; + + return container_->make_video_encoder({ + .width = v.width, + .height = v.height, + .frame_rate = v.frame_rate, + .buffer_method = kBufferMethod, + }); +} + +Impl::~Impl() +{ + valid_ = false; + wake_up_worker(); + thread_.join(); + + try + { + // Finally flush encoders, unless queues were finished + // already due to time or size constraints. + + if (!audio_queue_.finished()) + { + audio_encoder_->flush(); + } + + if (!video_queue_.finished()) + { + video_encoder_->flush(); + } + } + catch (const std::exception& ex) + { + CONS_Alert(CONS_ERROR, "AVRecorder::Impl::~Impl: %s\n", ex.what()); + return; + } +} + +std::optional Impl::advance_video_pts() +{ + auto _ = queue_guard(); + + // Don't let this queue grow out of hand. It's normal + // for encoding time to vary by a small margin and + // spend longer than one frame rate on a single + // frame. It should normalize though. + + if (video_queue_.vec_.size() >= 3) + { + return {}; + } + + SRB2_ASSERT(video_encoder_ != nullptr); + + const float tic_pts = video_encoder_->frame_rate() / static_cast(TICRATE); + const int pts = ((I_GetTime() - epoch_) + FixedToFloat(g_time.timefrac)) * tic_pts; + + if (!video_queue_.advance(pts, 1)) + { + return {}; + } + + return pts; +} + +void Impl::worker() +{ + for (;;) + { + QueueState qs; + + try + { + while ((qs = encode_queues()) == QueueState::kFlushed) + ; + } + catch (const std::exception& ex) + { + CONS_Alert(CONS_ERROR, "AVRecorder::Impl::worker: %s\n", ex.what()); + break; + } + + if (qs != QueueState::kFinished && valid_) + { + std::unique_lock lock(queue_mutex_); + + queue_cond_.wait(lock); + } + else + { + break; + } + } + + // Breaking out of the loop ensures invalidation! + valid_ = false; +} + +AVRecorder::AVRecorder(const Config config) : impl_(std::make_unique(config)) +{ +} + +AVRecorder::~AVRecorder() +{ + // impl_ is destroyed in a background thread so it doesn't + // block the thread AVRecorder was destroyed in. + // + // TODO: Save into a thread pool instead of detaching so + // the thread could be joined at program exit and + // not possibly terminate before fully destroyed? + + std::thread([_ = std::move(impl_)] {}).detach(); +} + +void AVRecorder::push_audio_samples(audio_buffer_t buffer) +{ + const auto _ = impl_->queue_guard(); + + auto& q = impl_->audio_queue_; + + if (!q.advance(q.pts(), buffer.size())) + { + return; + } + + using T = const float; + tcb::span p(reinterpret_cast(buffer.data()), buffer.size() * 2); // 2 channels + + std::copy(p.begin(), p.end(), std::back_inserter(q.vec_)); + + impl_->wake_up_worker(); +} + +bool AVRecorder::invalid() const +{ + return !impl_->valid_; +} diff --git a/src/media/avrecorder.hpp b/src/media/avrecorder.hpp new file mode 100644 index 000000000..45834ec08 --- /dev/null +++ b/src/media/avrecorder.hpp @@ -0,0 +1,76 @@ +// 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. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_AVRECORDER_HPP__ +#define __SRB2_MEDIA_AVRECORDER_HPP__ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../audio/sample.hpp" + +namespace srb2::media +{ + +class AVRecorder +{ +public: + using audio_sample_t = srb2::audio::Sample<2>; + using audio_buffer_t = tcb::span; + + class Impl; + + struct Config + { + struct Audio + { + int sample_rate; + }; + + struct Video + { + int width; + int height; + int frame_rate; + }; + + std::string file_name; + + std::optional max_size; // file size limit + std::optional> max_duration; + + std::optional