Merge branch 'avrecorder' into 'master'

WebM Encoder

See merge request KartKrew/Kart!908
This commit is contained in:
Oni 2023-02-25 05:59:52 +00:00
commit dba41a6de8
43 changed files with 3382 additions and 3 deletions

View file

@ -32,6 +32,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
m_aatree.c
m_anigif.c
m_argv.c
m_avrecorder.cpp
m_bbox.c
m_cheat.c
m_cond.c
@ -549,6 +550,7 @@ if(SRB2_CONFIG_ENABLE_TESTS)
add_subdirectory(tests)
endif()
add_subdirectory(menus)
add_subdirectory(media)
# strip debug symbols into separate file when using gcc.
# to be consistent with Makefile, don't generate for OS X.

View file

@ -62,6 +62,7 @@
#include "deh_tables.h"
#include "m_perfstats.h"
#include "k_specialstage.h"
#include "m_avrecorder.h"
#ifdef HAVE_DISCORDRPC
#include "discord.h"
@ -903,6 +904,7 @@ void D_RegisterClientCommands(void)
CV_RegisterVar(&cv_moviemode);
CV_RegisterVar(&cv_movie_option);
CV_RegisterVar(&cv_movie_folder);
M_AVRecorder_AddCommands();
// PNG variables
CV_RegisterVar(&cv_zlib_level);
CV_RegisterVar(&cv_zlib_memory);

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

@ -55,6 +55,10 @@ void I_StartupSound(void);
*/
void I_ShutdownSound(void);
/** \brief Update instance of AVRecorder for audio capture.
*/
void I_UpdateAudioRecorder(void);
/// ------------------------
/// SFX I/O
/// ------------------------

252
src/m_avrecorder.cpp Normal file
View file

@ -0,0 +1,252 @@
// 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 <algorithm>
#include <chrono>
#include <cstdint>
#include <exception>
#include <memory>
#include <stdexcept>
#include <fmt/format.h>
#include <tcb/span.hpp>
#include "cxxutil.hpp"
#include "m_avrecorder.hpp"
#include "media/options.hpp"
#include "command.h"
#include "i_sound.h"
#include "m_avrecorder.h"
#include "m_fixed.h"
#include "screen.h" // vid global
#include "st_stuff.h" // st_palette
#include "v_video.h" // pLocalPalette
using namespace srb2::media;
namespace
{
namespace Res
{
// Using an unscoped enum here so it can implicitly cast to
// int (in CV_PossibleValue_t). Wrap this in a namespace so
// access is still scoped. E.g. Res::kGame
enum : int32_t
{
kGame, // user chosen resolution, vid.width
kBase, // smallest version maintaining aspect ratio, vid.width / vid.dupx
kBase2x,
kBase4x,
kWindow, // window size (monitor in fullscreen), vid.realwidth
kCustom, // movie_custom_resolution
};
}; // namespace Res
CV_PossibleValue_t movie_resolution_cons_t[] = {
{Res::kGame, "Native"},
{Res::kBase, "Small"},
{Res::kBase2x, "Medium"},
{Res::kBase4x, "Large"},
{Res::kWindow, "Window"},
{Res::kCustom, "Custom"},
{0, NULL}};
CV_PossibleValue_t movie_limit_cons_t[] = {{1, "MIN"}, {INT32_MAX, "MAX"}, {0, "Unlimited"}, {0, NULL}};
}; // namespace
consvar_t cv_movie_resolution = CVAR_INIT("movie_resolution", "Medium", CV_SAVE, movie_resolution_cons_t, NULL);
consvar_t cv_movie_custom_resolution = CVAR_INIT("movie_custom_resolution", "640x400", CV_SAVE, NULL, NULL);
consvar_t cv_movie_fps = CVAR_INIT("movie_fps", "60", CV_SAVE, CV_Natural, NULL);
consvar_t cv_movie_showfps = CVAR_INIT("movie_showfps", "Yes", CV_SAVE, CV_YesNo, NULL);
consvar_t cv_movie_sound = CVAR_INIT("movie_sound", "On", CV_SAVE, CV_OnOff, NULL);
consvar_t cv_movie_duration = CVAR_INIT("movie_duration", "Unlimited", CV_SAVE | CV_FLOAT, movie_limit_cons_t, NULL);
consvar_t cv_movie_size = CVAR_INIT("movie_size", "8.0", CV_SAVE | CV_FLOAT, movie_limit_cons_t, NULL);
std::shared_ptr<AVRecorder> g_av_recorder;
void M_AVRecorder_AddCommands(void)
{
CV_RegisterVar(&cv_movie_custom_resolution);
CV_RegisterVar(&cv_movie_duration);
CV_RegisterVar(&cv_movie_fps);
CV_RegisterVar(&cv_movie_resolution);
CV_RegisterVar(&cv_movie_showfps);
CV_RegisterVar(&cv_movie_size);
CV_RegisterVar(&cv_movie_sound);
srb2::media::Options::register_all();
}
static AVRecorder::Config configure()
{
AVRecorder::Config cfg {};
if (cv_movie_duration.value > 0)
{
cfg.max_duration = std::chrono::duration<float>(FixedToFloat(cv_movie_duration.value));
}
if (cv_movie_size.value > 0)
{
cfg.max_size = FixedToFloat(cv_movie_size.value) * 1024 * 1024;
}
if (sound_started && cv_movie_sound.value)
{
cfg.audio = {
.sample_rate = 44100,
};
}
cfg.video = {
.frame_rate = cv_movie_fps.value,
};
AVRecorder::Config::Video& v = *cfg.video;
auto basex = [&v](int scale)
{
v.width = vid.width / vid.dupx * scale;
v.height = vid.height / vid.dupy * scale;
};
switch (cv_movie_resolution.value)
{
case Res::kGame:
v.width = vid.width;
v.height = vid.height;
break;
case Res::kBase:
basex(1);
break;
case Res::kBase2x:
basex(2);
break;
case Res::kBase4x:
basex(4);
break;
case Res::kWindow:
v.width = vid.realwidth;
v.height = vid.realheight;
break;
case Res::kCustom:
if (sscanf(cv_movie_custom_resolution.string, "%dx%d", &v.width, &v.height) != 2)
{
throw std::invalid_argument(fmt::format(
"Bad movie_custom_resolution '{}', should be <width>x<height> (e.g. 640x400)",
cv_movie_custom_resolution.string
));
}
break;
default:
SRB2_ASSERT(false);
}
return cfg;
}
boolean M_AVRecorder_Open(const char* filename)
{
try
{
AVRecorder::Config cfg = configure();
cfg.file_name = filename;
g_av_recorder = std::make_shared<AVRecorder>(cfg);
I_UpdateAudioRecorder();
return true;
}
catch (const std::exception& ex)
{
CONS_Alert(CONS_ERROR, "Exception starting video recorder: %s\n", ex.what());
return false;
}
}
void M_AVRecorder_Close(void)
{
g_av_recorder.reset();
I_UpdateAudioRecorder();
}
const char* M_AVRecorder_GetFileExtension(void)
{
return AVRecorder::file_extension();
}
const char* M_AVRecorder_GetCurrentFormat(void)
{
SRB2_ASSERT(g_av_recorder != nullptr);
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);
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)
{
SRB2_ASSERT(g_av_recorder != nullptr);
auto frame = g_av_recorder->new_indexed_video_frame(vid.width, vid.height);
if (!frame)
{
return;
}
tcb::span<RGBA_t> pal(&pLocalPalette[std::max(st_palette, 0) * 256], 256);
tcb::span<uint8_t> scr(screens[0], vid.width * vid.height);
std::copy(pal.begin(), pal.end(), frame->palette.begin());
std::copy(scr.begin(), scr.end(), frame->screen.begin());
g_av_recorder->push_indexed_video_frame(std::move(frame));
}

53
src/m_avrecorder.h Normal file
View file

@ -0,0 +1,53 @@
// 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 M_AVRECORDER_H
#define M_AVRECORDER_H
#include "typedef.h" // consvar_t
#ifdef __cplusplus
extern "C" {
#endif
void M_AVRecorder_AddCommands(void);
const char *M_AVRecorder_GetFileExtension(void);
// True if successully opened.
boolean M_AVRecorder_Open(const char *filename);
void M_AVRecorder_Close(void);
// Check whether AVRecorder is still valid. Call M_AVRecorder_Close if expired.
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);
extern consvar_t
cv_movie_custom_resolution,
cv_movie_duration,
cv_movie_fps,
cv_movie_resolution,
cv_movie_showfps,
cv_movie_size,
cv_movie_sound;
#ifdef __cplusplus
}; // extern "C"
#endif
#endif/*M_AVRECORDER_H*/

19
src/m_avrecorder.hpp Normal file
View file

@ -0,0 +1,19 @@
// 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 __M_AVRECORDER_HPP__
#define __M_AVRECORDER_HPP__
#include <memory> // shared_ptr
#include "media/avrecorder.hpp"
extern std::shared_ptr<srb2::media::AVRecorder> g_av_recorder;
#endif // __M_AVRECORDER_HPP__

View file

@ -44,6 +44,7 @@
#include "command.h" // cv_execversion
#include "m_anigif.h"
#include "m_avrecorder.h"
// So that the screenshot menu auto-updates...
#include "k_menu.h"
@ -113,8 +114,8 @@ consvar_t cv_screenshot_folder = CVAR_INIT ("screenshot_folder", "", CV_SAVE, NU
consvar_t cv_screenshot_colorprofile = CVAR_INIT ("screenshot_colorprofile", "Yes", CV_SAVE, CV_YesNo, NULL);
static CV_PossibleValue_t moviemode_cons_t[] = {{MM_GIF, "GIF"}, {MM_APNG, "aPNG"}, {MM_SCREENSHOT, "Screenshots"}, {0, NULL}};
consvar_t cv_moviemode = CVAR_INIT ("moviemode_mode", "GIF", CV_SAVE|CV_CALL, moviemode_cons_t, Moviemode_mode_Onchange);
static CV_PossibleValue_t moviemode_cons_t[] = {{MM_GIF, "GIF"}, {MM_APNG, "aPNG"}, {MM_SCREENSHOT, "Screenshots"}, {MM_AVRECORDER, "WebM"}, {0, NULL}};
consvar_t cv_moviemode = CVAR_INIT ("moviemode_mode", "WebM", CV_SAVE|CV_CALL, moviemode_cons_t, Moviemode_mode_Onchange);
consvar_t cv_movie_option = CVAR_INIT ("movie_option", "Default", CV_SAVE|CV_CALL, screenshot_cons_t, Moviemode_option_Onchange);
consvar_t cv_movie_folder = CVAR_INIT ("movie_folder", "", CV_SAVE, NULL, NULL);
@ -1295,6 +1296,25 @@ static inline moviemode_t M_StartMovieGIF(const char *pathname)
}
#endif
static inline moviemode_t M_StartMovieAVRecorder(const char *pathname)
{
const char *ext = M_AVRecorder_GetFileExtension();
const char *freename;
if (!(freename = Newsnapshotfile(pathname, ext)))
{
CONS_Alert(CONS_ERROR, "Couldn't create %s file: no slots open in %s\n", ext, pathname);
return MM_OFF;
}
if (!M_AVRecorder_Open(va(pandf,pathname,freename)))
{
return MM_OFF;
}
return MM_AVRECORDER;
}
void M_StartMovie(void)
{
#if NUMSCREENS > 2
@ -1332,6 +1352,9 @@ void M_StartMovie(void)
case MM_SCREENSHOT:
moviemode = MM_SCREENSHOT;
break;
case MM_AVRECORDER:
moviemode = M_StartMovieAVRecorder(pathname);
break;
default: //???
return;
}
@ -1342,6 +1365,11 @@ void M_StartMovie(void)
CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), "GIF");
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
@ -1353,6 +1381,22 @@ void M_SaveFrame(void)
// paranoia: should be unnecessary without singletics
static tic_t oldtic = 0;
if (moviemode == MM_AVRECORDER)
{
// TODO: replace once hwr2 twodee is finished
if (rendermode == render_soft)
{
M_AVRecorder_CopySoftwareScreen();
}
if (M_AVRecorder_IsExpired())
{
M_StopMovie();
}
return;
}
// skip interpolated frames for other modes
if (oldtic == I_GetTime())
return;
else
@ -1440,6 +1484,9 @@ void M_StopMovie(void)
#endif
case MM_SCREENSHOT:
break;
case MM_AVRECORDER:
M_AVRecorder_Close();
break;
default:
return;
}

View file

@ -29,7 +29,8 @@ typedef enum {
MM_OFF = 0,
MM_APNG,
MM_GIF,
MM_SCREENSHOT
MM_SCREENSHOT,
MM_AVRECORDER,
} moviemode_t;
extern moviemode_t moviemode;

34
src/media/CMakeLists.txt Normal file
View file

@ -0,0 +1,34 @@
target_sources(SRB2SDL2 PRIVATE
audio_encoder.hpp
avrecorder.cpp
avrecorder.hpp
avrecorder_feedback.cpp
avrecorder_impl.hpp
avrecorder_indexed.cpp
avrecorder_queue.cpp
cfile.cpp
cfile.hpp
container.hpp
encoder.hpp
options.cpp
options.hpp
options_values.cpp
video_encoder.hpp
video_frame.hpp
vorbis.cpp
vorbis.hpp
vorbis_error.hpp
vp8.cpp
vp8.hpp
vpx_error.hpp
webm.hpp
webm_encoder.hpp
webm_container.cpp
webm_container.hpp
webm_vorbis.hpp
webm_vorbis_lace.cpp
webm_vp8.hpp
webm_writer.hpp
yuv420p.cpp
yuv420p.hpp
)

View file

@ -0,0 +1,39 @@
// 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_AUDIO_ENCODER_HPP__
#define __SRB2_MEDIA_AUDIO_ENCODER_HPP__
#include <tcb/span.hpp>
#include "encoder.hpp"
namespace srb2::media
{
class AudioEncoder : virtual public MediaEncoder
{
public:
using sample_buffer_t = tcb::span<const float>;
struct Config
{
int channels;
int sample_rate;
};
virtual void encode(sample_buffer_t samples) = 0;
virtual int channels() const = 0;
virtual int sample_rate() const = 0;
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_AUDIO_ENCODER_HPP__

214
src/media/avrecorder.cpp Normal file
View file

@ -0,0 +1,214 @@
// 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 <algorithm>
#include <chrono>
#include <exception>
#include <iterator>
#include <memory>
#include <mutex>
#include <thread>
#include <utility>
#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<WebmContainer>(MediaContainer::Config {
cfg.file_name,
[this](const MediaContainer& container) { container_dtor_handler(container); },
})),
audio_encoder_(make_audio_encoder(cfg)),
video_encoder_(make_video_encoder(cfg)),
epoch_(I_GetTime()),
thread_([this] { worker(); })
{
}
std::unique_ptr<AudioEncoder> Impl::make_audio_encoder(const Config cfg) const
{
if (!cfg.audio)
{
return nullptr;
}
const Config::Audio& a = *cfg.audio;
return container_->make_audio_encoder({2, a.sample_rate});
}
std::unique_ptr<VideoEncoder> Impl::make_video_encoder(const Config cfg) const
{
if (!cfg.video)
{
return nullptr;
}
const Config::Video& v = *cfg.video;
return container_->make_video_encoder({v.width, v.height, v.frame_rate, 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<int> 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<float>(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;
}
const char* AVRecorder::file_extension()
{
return "webm";
}
AVRecorder::AVRecorder(const Config config) : impl_(std::make_unique<Impl>(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();
}
const char* AVRecorder::format_name() const
{
return impl_->container_->name();
}
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<T> p(reinterpret_cast<T*>(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_;
}

108
src/media/avrecorder.hpp Normal file
View file

@ -0,0 +1,108 @@
// 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 <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <tcb/span.hpp>
#include "../audio/sample.hpp"
namespace srb2::media
{
class AVRecorder
{
public:
using audio_sample_t = srb2::audio::Sample<2>;
using audio_buffer_t = tcb::span<const audio_sample_t>;
class Impl;
struct Config
{
struct Audio
{
int sample_rate;
};
struct Video
{
int width;
int height;
int frame_rate;
};
std::string file_name;
std::optional<std::size_t> max_size; // file size limit
std::optional<std::chrono::duration<float>> max_duration;
std::optional<Audio> audio;
std::optional<Video> video;
};
// TODO: remove once hwr2 twodee is finished
struct IndexedVideoFrame
{
using instance_t = std::unique_ptr<IndexedVideoFrame>;
std::array<RGBA_t, 256> palette;
std::vector<uint8_t> screen;
uint32_t width, height;
int pts;
IndexedVideoFrame(uint32_t width_, uint32_t height_, int pts_) :
screen(width_ * height_), width(width_), height(height_), pts(pts_)
{
}
};
// Returns the canonical file extension minus the dot.
// E.g. "webm" (not ".webm").
static const char* file_extension();
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
// Config::frame_rate
IndexedVideoFrame::instance_t new_indexed_video_frame(uint32_t width, uint32_t height);
void push_indexed_video_frame(IndexedVideoFrame::instance_t frame);
// Proper name of the container format.
const char* format_name() const;
// True if this instance has terminated. Continuing to use
// this interface is useless and the object should be
// destructed immediately.
bool invalid() const;
private:
std::unique_ptr<Impl> impl_;
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_AVRECORDER_HPP__

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

@ -0,0 +1,175 @@
// 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_IMPL_HPP__
#define __SRB2_MEDIA_AVRECORDER_IMPL_HPP__
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <memory>
#include <mutex>
#include <optional>
#include <thread>
#include <vector>
#include "../i_time.h"
#include "avrecorder.hpp"
#include "container.hpp"
namespace srb2::media
{
class AVRecorder::Impl
{
public:
template <typename T>
class Queue
{
public:
// The use of typename = void is a GCC bug.
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85282
// Explicit specialization inside of a class still
// does not work as of 12.2.1.
template <typename, typename = void>
struct Traits
{
};
template <typename _>
struct Traits<AudioEncoder, _>
{
using frame_type = float;
};
template <typename _>
struct Traits<VideoEncoder, _>
{
using frame_type = IndexedVideoFrame::instance_t;
};
std::vector<typename Traits<T>::frame_type> vec_;
// This number only decrements once a frame has
// actually been written to container.
std::size_t queued_frames_ = 0;
Queue(const std::unique_ptr<T>& encoder, Impl& impl) : encoder_(encoder.get()), impl_(&impl) {}
// This method handles validation of the queue,
// finishing the queue and advancing PTS. Returns true
// if PTS was advanced.
bool advance(int pts, int duration);
// True if no more data may be queued.
bool finished() const { return finished_; }
// Presentation Time Stamp; one frame for video
// encoders, one sample for audio encoders.
int pts() const { return pts_; }
private:
using time_unit_t = std::chrono::duration<float>;
T* const encoder_;
Impl* const impl_;
bool finished_ = (encoder_ == nullptr);
int pts_ = -1; // valid pts starts at 0
// Actual duration of PTS unit.
time_unit_t time_scale() const;
};
const std::optional<std::size_t> max_size_;
std::optional<std::chrono::duration<float>> max_duration_;
// max_duration_ may be readjusted in case a queue
// finishes early for any reason. max_duration_config_ is
// the original, unmodified value.
const decltype(max_duration_) max_duration_config_ = max_duration_;
std::unique_ptr<MediaContainer> container_;
std::unique_ptr<AudioEncoder> audio_encoder_;
std::unique_ptr<VideoEncoder> video_encoder_;
Queue<AudioEncoder> audio_queue_ {audio_encoder_, *this};
Queue<VideoEncoder> video_queue_ {video_encoder_, *this};
// This class becomes invalid if:
//
// 1) an exception occurred
// 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();
// Returns valid PTS if enough time has passed.
std::optional<int> advance_video_pts();
// Use before accessing audio_queue_ or video_queue_.
auto queue_guard() { return std::lock_guard(queue_mutex_); }
// Use to notify worker thread if queues were modified.
void wake_up_worker() { queue_cond_.notify_one(); }
private:
enum class QueueState
{
kEmpty, // all queues are empty
kFlushed, // a queue was flushed but more data may be waiting
kFinished, // all queues are finished -- no more data may be queued
};
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_;
std::unique_ptr<AudioEncoder> make_audio_encoder(const Config cfg) const;
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);
};
template <>
inline AVRecorder::Impl::Queue<AudioEncoder>::time_unit_t AVRecorder::Impl::Queue<AudioEncoder>::time_scale() const
{
return time_unit_t(1.f / encoder_->sample_rate());
}
template <>
inline AVRecorder::Impl::Queue<VideoEncoder>::time_unit_t AVRecorder::Impl::Queue<VideoEncoder>::time_scale() const
{
return time_unit_t(1.f / encoder_->frame_rate());
}
extern template class AVRecorder::Impl::Queue<AudioEncoder>;
extern template class AVRecorder::Impl::Queue<VideoEncoder>;
}; // namespace srb2::media
#endif // __SRB2_MEDIA_AVRECORDER_IMPL_HPP__

View file

@ -0,0 +1,69 @@
// 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.
//-----------------------------------------------------------------------------
// TODO: remove this file once hwr2 twodee is finished
#include <cstdint>
#include <memory>
#include <optional>
#include <utility>
#include "../cxxutil.hpp"
#include "avrecorder_impl.hpp"
using namespace srb2::media;
using Impl = AVRecorder::Impl;
VideoFrame::instance_t Impl::convert_indexed_video_frame(const IndexedVideoFrame& indexed)
{
VideoFrame::instance_t frame = video_encoder_->new_frame(indexed.width, indexed.height, indexed.pts);
SRB2_ASSERT(frame != nullptr);
const VideoFrame::Buffer& buffer = frame->rgba_buffer();
const uint8_t* s = indexed.screen.data();
uint8_t* p = buffer.plane.data();
for (int y = 0; y < frame->height(); ++y)
{
for (int x = 0; x < frame->width(); ++x)
{
const RGBA_t& c = indexed.palette[s[x]];
reinterpret_cast<uint32_t*>(p)[x] = c.rgba;
}
s += indexed.width;
p += buffer.row_stride;
}
return frame;
}
AVRecorder::IndexedVideoFrame::instance_t AVRecorder::new_indexed_video_frame(uint32_t width, uint32_t height)
{
std::optional<int> pts = impl_->advance_video_pts();
if (!pts)
{
return nullptr;
}
return std::make_unique<IndexedVideoFrame>(width, height, *pts);
}
void AVRecorder::push_indexed_video_frame(IndexedVideoFrame::instance_t frame)
{
auto _ = impl_->queue_guard();
impl_->video_queue_.vec_.emplace_back(std::move(frame));
impl_->wake_up_worker();
}

View file

@ -0,0 +1,169 @@
// 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 <chrono>
#include <cstddef>
#include <mutex>
#include <utility>
#include "avrecorder_impl.hpp"
using namespace srb2::media;
using Impl = AVRecorder::Impl;
template <typename T>
bool Impl::Queue<T>::advance(int new_pts, int duration)
{
if (!impl_->valid_ || finished())
{
return false;
}
new_pts += duration;
// PTS must only advance.
if (new_pts <= pts())
{
return false;
}
auto finish = [this]
{
finished_ = true;
const auto t = impl_->container_->duration();
// Tracks are ultimately cut to the shortest among
// them, therefore it would be pointless for another
// queue to continue beyond this point.
//
// This is relevant if finishing due to size
// constraint; in that case, another queue might be
// far behind this one in terms of size and would
// continue in vain.
if (!impl_->max_duration_ || t < impl_->max_duration_)
{
impl_->max_duration_ = t;
}
impl_->wake_up_worker();
};
if (impl_->max_duration_)
{
const int final_pts = *impl_->max_duration_ / time_scale();
if (new_pts > final_pts)
{
return finish(), false;
}
}
if (impl_->max_size_)
{
constexpr float kError = 0.99f; // 1% muxing overhead
const MediaEncoder::BitRate est = encoder_->estimated_bit_rate();
const float br = est.bits / 8.f;
// count size of already queued frames too
const float t = ((duration + queued_frames_) * time_scale()) / est.period;
if ((impl_->container_->size() + (t * br)) > (*impl_->max_size_ * kError))
{
return finish(), false;
}
}
pts_ = new_pts;
queued_frames_ += duration;
return true;
}
Impl::QueueState Impl::encode_queues()
{
bool remain = false;
bool flushed = false;
auto check = [&, this](auto& q, auto encode)
{
std::unique_lock lock(queue_mutex_);
if (!q.finished())
{
remain = true;
}
if (!q.vec_.empty())
{
const std::size_t n = q.queued_frames_;
auto copy = std::move(q.vec_);
lock.unlock();
encode(std::move(copy));
lock.lock();
q.queued_frames_ -= n;
flushed = true;
}
};
auto encode_audio = [this](auto copy) { audio_encoder_->encode(copy); };
auto encode_video = [this](auto copy)
{
for (auto& p : copy)
{
auto frame = convert_indexed_video_frame(*p);
video_encoder_->encode(std::move(frame));
}
update_video_frame_rate_avg();
};
check(audio_queue_, encode_audio);
check(video_queue_, encode_video);
if (flushed)
{
return QueueState::kFlushed;
}
else if (remain)
{
return QueueState::kEmpty;
}
else
{
return QueueState::kFinished;
}
}
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>;

34
src/media/cfile.cpp Normal file
View file

@ -0,0 +1,34 @@
// 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 <cerrno>
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <fmt/format.h>
#include "cfile.hpp"
using namespace srb2::media;
CFile::CFile(const std::string file_name) : name_(file_name)
{
file_ = std::fopen(name(), "wb");
if (file_ == nullptr)
{
throw std::invalid_argument(fmt::format("{}: {}", name(), std::strerror(errno)));
}
}
CFile::~CFile()
{
std::fclose(file_);
}

36
src/media/cfile.hpp Normal file
View file

@ -0,0 +1,36 @@
// 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_CFILE_HPP__
#define __SRB2_MEDIA_CFILE_HPP__
#include <cstdio>
#include <string>
namespace srb2::media
{
class CFile
{
public:
CFile(const std::string file_name);
~CFile();
operator std::FILE*() const { return file_; }
const char* name() const { return name_.c_str(); }
private:
std::string name_;
std::FILE* file_;
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_CFILE_HPP__

53
src/media/container.hpp Normal file
View file

@ -0,0 +1,53 @@
// 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_CONTAINER_HPP__
#define __SRB2_MEDIA_CONTAINER_HPP__
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include "audio_encoder.hpp"
#include "video_encoder.hpp"
namespace srb2::media
{
class MediaContainer
{
public:
using dtor_cb_t = std::function<void(const MediaContainer&)>;
using time_unit_t = std::chrono::duration<float>;
struct Config
{
std::string file_name;
dtor_cb_t destructor_callback;
};
virtual ~MediaContainer() = default;
virtual std::unique_ptr<AudioEncoder> make_audio_encoder(AudioEncoder::Config config) = 0;
virtual std::unique_ptr<VideoEncoder> make_video_encoder(VideoEncoder::Config config) = 0;
virtual const char* name() const = 0;
virtual const char* file_name() const = 0;
// These are normally estimates. However, when called from
// Config::destructor_callback, these are the exact final
// values.
virtual time_unit_t duration() const = 0;
virtual std::size_t size() const = 0;
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_CONTAINER_HPP__

51
src/media/encoder.hpp Normal file
View file

@ -0,0 +1,51 @@
// 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_ENCODER_HPP__
#define __SRB2_MEDIA_ENCODER_HPP__
#include <chrono>
#include <cstddef>
#include <tcb/span.hpp>
namespace srb2::media
{
class MediaEncoder
{
public:
using time_unit_t = std::chrono::duration<float>;
struct BitRate
{
std::size_t bits; // 8 bits = 1 byte :)
time_unit_t period;
};
virtual ~MediaEncoder() = default;
// Should be called finally but it's optional.
virtual void flush() = 0;
virtual const char* name() const = 0;
// Returns an average bit rate over a constant period of
// time, assuming no frames drops.
virtual BitRate estimated_bit_rate() const = 0;
protected:
using frame_buffer_t = tcb::span<const std::byte>;
virtual void write_frame(frame_buffer_t frame, time_unit_t timestamp, bool is_key_frame) = 0;
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_ENCODER_HPP__

117
src/media/options.cpp Normal file
View file

@ -0,0 +1,117 @@
// 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 <cstddef>
#include <cstdint>
#include <type_traits>
#include <fmt/format.h>
#include "../cxxutil.hpp"
#include "../m_fixed.h"
#include "options.hpp"
using namespace srb2::media;
Options::Options(const char* prefix, map_t map) : prefix_(prefix), map_(map)
{
for (auto& [suffix, cvar] : map_)
{
cvar.name = strdup(fmt::format("{}_{}", prefix_, suffix).c_str());
cvars_.emplace_back(&cvar);
}
}
const consvar_t& Options::cvar(const char* option) const
{
const consvar_t& cvar = map_.at(option);
SRB2_ASSERT(cvar.string != nullptr);
return cvar;
}
template <>
int Options::get<int>(const char* option) const
{
return cvar(option).value;
}
template <>
float Options::get<float>(const char* option) const
{
return FixedToFloat(cvar(option).value);
}
template <typename T>
consvar_t Options::values(const char* default_value, const Range<T> range, std::map<std::string_view, T> list)
{
constexpr bool is_float = std::is_floating_point_v<T>;
const std::size_t min_max_size = (range.min || range.max) ? 2 : 0;
auto* arr = new CV_PossibleValue_t[list.size() + min_max_size + 1];
auto cast = [is_float](T n)
{
if constexpr (is_float)
{
return FloatToFixed(n);
}
else
{
return n;
}
};
if (min_max_size)
{
// Order is very important, MIN then MAX.
arr[0] = {range.min ? cast(*range.min) : INT32_MIN, "MIN"};
arr[1] = {range.max ? cast(*range.max) : INT32_MAX, "MAX"};
}
{
std::size_t i = min_max_size;
for (const auto& [k, v] : list)
{
arr[i].value = cast(v);
arr[i].strvalue = k.data();
i++;
}
arr[i].value = 0;
arr[i].strvalue = nullptr;
}
int32_t flags = CV_SAVE;
if constexpr (is_float)
{
flags |= CV_FLOAT;
}
return CVAR_INIT(nullptr, default_value, flags, arr, nullptr);
}
void Options::register_all()
{
for (auto cvar : cvars_)
{
CV_RegisterVar(cvar);
}
cvars_ = {};
}
// clang-format off
template consvar_t Options::values(const char* default_value, const Range<int> range, std::map<std::string_view, int> list);
template consvar_t Options::values(const char* default_value, const Range<float> range, std::map<std::string_view, float> list);
// clang-format on

58
src/media/options.hpp Normal file
View file

@ -0,0 +1,58 @@
// 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_OPTIONS_HPP__
#define __SRB2_MEDIA_OPTIONS_HPP__
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include "../command.h"
namespace srb2::media
{
class Options
{
public:
using map_t = std::unordered_map<std::string, consvar_t>;
template <typename T>
struct Range
{
std::optional<T> min, max;
};
// Registers all options as cvars.
static void register_all();
Options(const char* prefix, map_t map);
template <typename T>
T get(const char* option) const;
template <typename T>
static consvar_t values(const char* default_value, const Range<T> range, std::map<std::string_view, T> list = {});
private:
static std::vector<consvar_t*> cvars_;
const char* prefix_;
map_t map_;
const consvar_t& cvar(const char* option) const;
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_OPTIONS_HPP__

View file

@ -0,0 +1,58 @@
// 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 <cstdint>
#include <vpx/vpx_encoder.h>
#include "options.hpp"
#include "vorbis.hpp"
#include "vp8.hpp"
using namespace srb2::media;
// NOTE: Options::cvars_ MUST be initialized before any
// Options instances construct. For static objects, they have
// to be defined in the same translation unit as
// Options::cvars_ to guarantee initialization order.
std::vector<consvar_t*> Options::cvars_;
// clang-format off
const Options VorbisEncoder::options_("vorbis", {
{"quality", Options::values<float>("0", {-0.1f, 1.f})},
{"max_bitrate", Options::values<int>("-1", {-1})},
{"nominal_bitrate", Options::values<int>("-1", {-1})},
{"min_bitrate", Options::values<int>("-1", {-1})},
});
const Options VP8Encoder::options_("vp8", {
{"quality_mode", Options::values<int>("q", {}, {
{"vbr", VPX_VBR},
{"cbr", VPX_CBR},
{"cq", VPX_CQ},
{"q", VPX_Q},
})},
{"target_bitrate", Options::values<int>("800", {1})},
{"min_q", Options::values<int>("4", {4, 63})},
{"max_q", Options::values<int>("55", {4, 63})},
{"kf_min", Options::values<int>("0", {0})},
{"kf_max", Options::values<int>("auto", {0}, {
{"auto", static_cast<int>(KeyFrameOption::kAuto)},
})},
{"cpu_used", Options::values<int>("0", {-16, 16})},
{"cq_level", Options::values<int>("10", {0, 63})},
{"deadline", Options::values<int>("10", {1}, {
{"infinite", static_cast<int>(DeadlineOption::kInfinite)},
})},
{"sharpness", Options::values<int>("7", {0, 7})},
{"token_parts", Options::values<int>("0", {0, 3})},
{"threads", Options::values<int>("1", {1})},
});
// clang-format on

View file

@ -0,0 +1,58 @@
// 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_VIDEO_ENCODER_HPP__
#define __SRB2_MEDIA_VIDEO_ENCODER_HPP__
#include "encoder.hpp"
#include "video_frame.hpp"
namespace srb2::media
{
class VideoEncoder : virtual public MediaEncoder
{
public:
struct Config
{
int width;
int height;
int frame_rate;
VideoFrame::BufferMethod buffer_method;
};
struct FrameCount
{
// Number of real frames, not counting frame skips.
int frames;
time_unit_t duration;
};
// VideoFrame::width() and VideoFrame::height() should be
// used on the returned frame.
virtual VideoFrame::instance_t new_frame(int width, int height, int pts) = 0;
virtual void encode(VideoFrame::instance_t frame) = 0;
virtual int width() const = 0;
virtual int height() const = 0;
virtual int frame_rate() const = 0;
// Reports the number of threads used, if the encoder is
// multithreaded.
virtual int thread_count() const = 0;
// Number of frames fully encoded so far.
virtual FrameCount frame_count() const = 0;
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_VIDEO_ENCODER_HPP__

63
src/media/video_frame.hpp Normal file
View file

@ -0,0 +1,63 @@
// 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_VIDEO_FRAME_HPP__
#define __SRB2_MEDIA_VIDEO_FRAME_HPP__
#include <cstddef>
#include <cstdint>
#include <memory>
#include <tcb/span.hpp>
namespace srb2::media
{
class VideoFrame
{
public:
using instance_t = std::unique_ptr<VideoFrame>;
enum class BufferMethod
{
// Returns an already allocated buffer for each
// frame. See VideoFrame::rgba_buffer(). The encoder
// completely manages allocating this buffer.
kEncoderAllocatedRGBA8888,
};
struct Buffer
{
tcb::span<uint8_t> plane;
std::size_t row_stride; // size of each row
};
virtual int width() const = 0;
virtual int height() const = 0;
int pts() const { return pts_; }
// Returns a buffer that should be
// filled with RGBA pixels.
//
// This method may only be used if
// the encoder was configured with
// BufferMethod::kEncoderAllocatedRGBA8888.
virtual const Buffer& rgba_buffer() const = 0;
protected:
VideoFrame(int pts) : pts_(pts) {}
private:
int pts_;
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_VIDEO_FRAME_HPP__

142
src/media/vorbis.cpp Normal file
View file

@ -0,0 +1,142 @@
// 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 <chrono>
#include <cstddef>
#include <stdexcept>
#include <fmt/format.h>
#include <vorbis/vorbisenc.h>
#include "../cxxutil.hpp"
#include "vorbis.hpp"
#include "vorbis_error.hpp"
using namespace srb2::media;
namespace
{
void runtime_assert(VorbisError error, const char *what)
{
if (error != 0)
{
throw std::runtime_error(fmt::format("{}: {}", what, error));
}
}
}; // namespace
VorbisEncoder::VorbisEncoder(Config cfg)
{
const long max_bitrate = options_.get<int>("max_bitrate");
const long nominal_bitrate = options_.get<int>("nominal_bitrate");
const long min_bitrate = options_.get<int>("min_bitrate");
vorbis_info_init(&vi_);
if (max_bitrate != -1 || nominal_bitrate != -1 || min_bitrate != -1)
{
// managed bitrate mode
VorbisError error =
vorbis_encode_init(&vi_, cfg.channels, cfg.sample_rate, max_bitrate, nominal_bitrate, min_bitrate);
if (error != 0)
{
throw std::invalid_argument(fmt::format(
"vorbis_encode_init: {}, max_bitrate={}, nominal_bitrate={}, min_bitrate={}",
error,
max_bitrate,
nominal_bitrate,
min_bitrate
));
}
}
else
{
// variable bitrate mode
const float quality = options_.get<float>("quality");
VorbisError error = vorbis_encode_init_vbr(&vi_, cfg.channels, cfg.sample_rate, quality);
if (error != 0)
{
throw std::invalid_argument(fmt::format("vorbis_encode_init: {}, quality={}", error, quality));
}
}
runtime_assert(vorbis_analysis_init(&vd_, &vi_), "vorbis_analysis_init");
runtime_assert(vorbis_block_init(&vd_, &vb_), "vorbis_block_init");
}
VorbisEncoder::~VorbisEncoder()
{
vorbis_block_clear(&vb_);
vorbis_dsp_clear(&vd_);
vorbis_info_clear(&vi_);
}
VorbisEncoder::headers_t VorbisEncoder::generate_headers()
{
headers_t op;
vorbis_comment vc;
vorbis_comment_init(&vc);
VorbisError error = vorbis_analysis_headerout(&vd_, &vc, &op[0], &op[1], &op[2]);
if (error != 0)
{
throw std::invalid_argument(fmt::format("vorbis_analysis_headerout: {}", error));
}
vorbis_comment_clear(&vc);
return op;
}
void VorbisEncoder::analyse(sample_buffer_t in)
{
const int ch = channels();
const std::size_t n = in.size() / ch;
float** fv = vorbis_analysis_buffer(&vd_, n);
for (std::size_t i = 0; i < n; ++i)
{
auto s = in.subspan(i * ch, ch);
fv[0][i] = s[0];
fv[1][i] = s[1];
}
// automatically handles end of stream if n = 0
runtime_assert(vorbis_analysis_wrote(&vd_, n), "vorbis_analysis_wrote");
while (vorbis_analysis_blockout(&vd_, &vb_) > 0)
{
runtime_assert(vorbis_analysis(&vb_, nullptr), "vorbis_analysis");
runtime_assert(vorbis_bitrate_addblock(&vb_), "vorbis_bitrate_addblock");
ogg_packet op;
while (vorbis_bitrate_flushpacket(&vd_, &op) > 0)
{
write_packet(&op);
}
}
}
void VorbisEncoder::write_packet(ogg_packet* op)
{
using T = const std::byte;
tcb::span<T> p(reinterpret_cast<T*>(op->packet), static_cast<std::size_t>(op->bytes));
write_frame(p, std::chrono::duration<float>(vorbis_granule_time(&vd_, op->granulepos)), true);
}

54
src/media/vorbis.hpp Normal file
View file

@ -0,0 +1,54 @@
// 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_VORBIS_HPP__
#define __SRB2_MEDIA_VORBIS_HPP__
#include <array>
#include <vorbis/codec.h>
#include "audio_encoder.hpp"
#include "options.hpp"
namespace srb2::media
{
class VorbisEncoder : public AudioEncoder
{
public:
static const Options options_;
VorbisEncoder(Config config);
~VorbisEncoder();
virtual void encode(sample_buffer_t samples) override final { analyse(samples); }
virtual void flush() override final { analyse(); }
virtual const char* name() const override final { return "Vorbis"; }
virtual int channels() const override final { return vi_.channels; }
virtual int sample_rate() const override final { return vi_.rate; }
protected:
using headers_t = std::array<ogg_packet, 3>;
headers_t generate_headers();
private:
vorbis_info vi_;
vorbis_dsp_state vd_;
vorbis_block vb_;
void analyse(sample_buffer_t samples = {});
void write_packet(ogg_packet* op);
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_VORBIS_HPP__

View file

@ -0,0 +1,54 @@
// 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_VORBIS_ERROR_HPP__
#define __SRB2_MEDIA_VORBIS_ERROR_HPP__
#include <string>
#include <fmt/format.h>
#include <vorbis/codec.h>
class VorbisError
{
public:
VorbisError(int error) : error_(error) {}
operator int() const { return error_; }
std::string name() const
{
switch (error_)
{
case OV_EFAULT:
return "Internal error (OV_EFAULT)";
case OV_EINVAL:
return "Invalid settings (OV_EINVAL)";
case OV_EIMPL:
return "Invalid settings (OV_EIMPL)";
default:
return fmt::format("error {}", error_);
}
}
private:
int error_;
};
template <>
struct fmt::formatter<VorbisError> : formatter<std::string>
{
template <typename FormatContext>
auto format(const VorbisError& error, FormatContext& ctx) const
{
return formatter<std::string>::format(error.name(), ctx);
}
};
#endif // __SRB2_MEDIA_VORBIS_ERROR_HPP__

241
src/media/vp8.cpp Normal file
View file

@ -0,0 +1,241 @@
// 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 <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <fmt/format.h>
#include <tcb/span.hpp>
#include "../cxxutil.hpp"
#include "vp8.hpp"
#include "vpx_error.hpp"
#include "yuv420p.hpp"
using namespace srb2::media;
vpx_codec_iface_t* VP8Encoder::kCodec = vpx_codec_vp8_cx();
const vpx_codec_enc_cfg_t VP8Encoder::configure(const Config user)
{
vpx_codec_enc_cfg_t cfg;
vpx_codec_enc_config_default(kCodec, &cfg, 0);
cfg.g_threads = options_.get<int>("threads");
cfg.g_w = user.width;
cfg.g_h = user.height;
cfg.g_bit_depth = VPX_BITS_8;
cfg.g_input_bit_depth = 8;
cfg.g_timebase.num = 1;
cfg.g_timebase.den = user.frame_rate;
cfg.g_pass = VPX_RC_ONE_PASS;
cfg.rc_end_usage = static_cast<vpx_rc_mode>(options_.get<int>("quality_mode"));
cfg.kf_mode = VPX_KF_AUTO;
cfg.rc_target_bitrate = options_.get<int>("target_bitrate");
cfg.rc_min_quantizer = options_.get<int>("min_q");
cfg.rc_max_quantizer = options_.get<int>("max_q");
// Keyframe spacing, in number of frames.
// kf_max_dist should be low enough to allow scrubbing.
int kf_max = options_.get<int>("kf_max");
if (kf_max == static_cast<int>(KeyFrameOption::kAuto))
{
// Automatically pick a good rate
kf_max = (user.frame_rate / 2); // every .5s
}
cfg.kf_min_dist = options_.get<int>("kf_min");
cfg.kf_max_dist = kf_max;
return cfg;
}
VP8Encoder::VP8Encoder(Config config) : ctx_(config), img_(config.width, config.height), frame_rate_(config.frame_rate)
{
SRB2_ASSERT(config.buffer_method == VideoFrame::BufferMethod::kEncoderAllocatedRGBA8888);
control<int>(VP8E_SET_CPUUSED, "cpu_used");
control<int>(VP8E_SET_CQ_LEVEL, "cq_level");
control<int>(VP8E_SET_SHARPNESS, "sharpness");
control<int>(VP8E_SET_TOKEN_PARTITIONS, "token_parts");
auto plane = [this](int k, int ycs = 0)
{
using T = uint8_t;
auto view = tcb::span<T>(reinterpret_cast<T*>(img_->planes[k]), img_->stride[k] * (img_->h >> ycs));
return VideoFrame::Buffer {view, static_cast<std::size_t>(img_->stride[k])};
};
frame_ = std::make_unique<YUV420pFrame>(
0,
plane(VPX_PLANE_Y),
plane(VPX_PLANE_U, img_->y_chroma_shift),
plane(VPX_PLANE_V, img_->y_chroma_shift),
rgba_buffer_
);
}
VP8Encoder::CtxWrapper::CtxWrapper(const Config user)
{
const vpx_codec_enc_cfg_t cfg = configure(user);
if (vpx_codec_enc_init(&ctx_, kCodec, &cfg, 0) != VPX_CODEC_OK)
{
throw std::invalid_argument(fmt::format("vpx_codec_enc_init: {}", VpxError(ctx_)));
}
}
VP8Encoder::CtxWrapper::~CtxWrapper()
{
vpx_codec_destroy(&ctx_);
}
VP8Encoder::ImgWrapper::ImgWrapper(int width, int height)
{
if (vpx_img_alloc(&img_, VPX_IMG_FMT_I420, width, height, YUV420pFrame::kAlignment) == nullptr)
{
throw std::runtime_error("vpx_img_alloc");
}
}
VP8Encoder::ImgWrapper::~ImgWrapper()
{
vpx_img_free(&img_);
}
VideoFrame::instance_t VP8Encoder::new_frame(int width, int height, int pts)
{
SRB2_ASSERT(frame_ != nullptr);
if (rgba_buffer_.resize(width, height))
{
// If there was a resize, the aspect ratio may not
// match. When the frame is scaled later, it will be
// "fit" into the target aspect ratio, leaving some
// empty space around the scaled image. (See
// VP8Encoder::encode)
//
// Set whole scaled buffer to black now so the empty
// space appears as "black bars".
rgba_scaled_buffer_.erase();
}
frame_->reset(pts, rgba_buffer_);
return std::move(frame_);
}
void VP8Encoder::encode(VideoFrame::instance_t frame)
{
{
using T = YUV420pFrame;
SRB2_ASSERT(frame_ == nullptr);
SRB2_ASSERT(dynamic_cast<T*>(frame.get()) != nullptr);
frame_ = std::unique_ptr<T>(static_cast<T*>(frame.release()));
}
// This frame must be scaled to match encoder configuration
if (frame_->width() != width() || frame_->height() != height())
{
rgba_scaled_buffer_.resize(width(), height());
frame_->scale(rgba_scaled_buffer_);
}
else
{
rgba_scaled_buffer_.release();
}
frame_->convert();
if (vpx_codec_encode(ctx_, img_, frame_->pts(), 1, 0, deadline_) != VPX_CODEC_OK)
{
throw std::invalid_argument(fmt::format("VP8Encoder::encode: vpx_codec_encode: {}", VpxError(ctx_)));
}
process();
}
void VP8Encoder::flush()
{
do
{
if (vpx_codec_encode(ctx_, nullptr, 0, 0, 0, 0) != VPX_CODEC_OK)
{
throw std::invalid_argument(fmt::format("VP8Encoder::flush: vpx_codec_encode: {}", VpxError(ctx_)));
}
} while (process());
}
bool VP8Encoder::process()
{
bool output = false;
vpx_codec_iter_t iter = NULL;
const vpx_codec_cx_pkt_t* pkt;
while ((pkt = vpx_codec_get_cx_data(ctx_, &iter)))
{
output = true;
if (pkt->kind != VPX_CODEC_CX_FRAME_PKT)
{
continue;
}
auto& frame = pkt->data.frame;
{
const std::lock_guard _(frame_count_mutex_);
duration_ = frame.pts + frame.duration;
frame_count_++;
}
const float ts = frame.pts / static_cast<float>(frame_rate());
using T = const std::byte;
tcb::span<T> p(reinterpret_cast<T*>(frame.buf), frame.sz);
write_frame(p, std::chrono::duration<float>(ts), (frame.flags & VPX_FRAME_IS_KEY));
}
return output;
}
template <typename T>
void VP8Encoder::control(vp8e_enc_control_id id, const char* option)
{
auto value = options_.get<T>(option);
if (vpx_codec_control_(ctx_, id, value) != VPX_CODEC_OK)
{
throw std::invalid_argument(fmt::format("vpx_codec_control: {}, {}={}", VpxError(ctx_), option, value));
}
}
VideoEncoder::FrameCount VP8Encoder::frame_count() const
{
const std::lock_guard _(frame_count_mutex_);
return {frame_count_, std::chrono::duration<float>(duration_ / static_cast<float>(frame_rate()))};
}

112
src/media/vp8.hpp Normal file
View file

@ -0,0 +1,112 @@
// 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_VP8_HPP__
#define __SRB2_MEDIA_VP8_HPP__
#include <mutex>
#include <vpx/vp8cx.h>
#include "options.hpp"
#include "video_encoder.hpp"
#include "yuv420p.hpp"
namespace srb2::media
{
class VP8Encoder : public VideoEncoder
{
public:
static const Options options_;
VP8Encoder(VideoEncoder::Config config);
virtual VideoFrame::instance_t new_frame(int width, int height, int pts) override final;
virtual void encode(VideoFrame::instance_t frame) override final;
virtual void flush() override final;
virtual const char* name() const override final { return "VP8"; }
virtual int width() const override final { return img_->w; }
virtual int height() const override final { return img_->h; }
virtual int frame_rate() const override final { return frame_rate_; }
virtual int thread_count() const override final { return thread_count_; }
virtual FrameCount frame_count() const override final;
private:
class CtxWrapper
{
public:
CtxWrapper(const Config config);
~CtxWrapper();
operator vpx_codec_ctx_t*() { return &ctx_; }
operator vpx_codec_ctx_t&() { return ctx_; }
private:
vpx_codec_ctx_t ctx_;
};
class ImgWrapper
{
public:
ImgWrapper(int width, int height);
~ImgWrapper();
operator vpx_image_t*() { return &img_; }
vpx_image_t* operator->() { return &img_; }
const vpx_image_t* operator->() const { return &img_; }
private:
vpx_image_t img_;
};
enum class KeyFrameOption : int
{
kAuto = -1,
};
enum class DeadlineOption : int
{
kInfinite = 0,
};
static vpx_codec_iface_t* kCodec;
static const vpx_codec_enc_cfg_t configure(const Config config);
CtxWrapper ctx_;
ImgWrapper img_;
const int frame_rate_;
const int thread_count_ = options_.get<int>("threads");
const int deadline_ = options_.get<int>("deadline");
mutable std::recursive_mutex frame_count_mutex_;
int duration_ = 0;
int frame_count_ = 0;
YUV420pFrame::BufferRGBA //
rgba_buffer_,
rgba_scaled_buffer_; // only allocated if input NEEDS scaling
std::unique_ptr<YUV420pFrame> frame_;
bool process();
template <typename T> // T = option type
void control(vp8e_enc_control_id id, const char* option);
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_VP8_HPP__

45
src/media/vpx_error.hpp Normal file
View file

@ -0,0 +1,45 @@
// 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_VPX_ERROR_HPP__
#define __SRB2_MEDIA_VPX_ERROR_HPP__
#include <string>
#include <fmt/format.h>
#include <vpx/vpx_codec.h>
class VpxError
{
public:
VpxError(vpx_codec_ctx_t& ctx) : ctx_(&ctx) {}
std::string description() const
{
const char* error = vpx_codec_error(ctx_);
const char* detail = vpx_codec_error_detail(ctx_);
return detail ? fmt::format("{}: {}", error, detail) : error;
}
private:
vpx_codec_ctx_t* ctx_;
};
template <>
struct fmt::formatter<VpxError> : formatter<std::string>
{
template <typename FormatContext>
auto format(const VpxError& error, FormatContext& ctx) const
{
return formatter<std::string>::format(error.description(), ctx);
}
};
#endif // __SRB2_MEDIA_VPX_ERROR_HPP__

26
src/media/webm.hpp Normal file
View file

@ -0,0 +1,26 @@
// 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_WEBM_HPP__
#define __SRB2_MEDIA_WEBM_HPP__
#include <chrono>
#include <cstdint>
#include <ratio>
namespace srb2::media::webm
{
using track = uint64_t;
using timestamp = uint64_t;
using duration = std::chrono::duration<timestamp, std::nano>;
}; // namespace srb2::media::webm
#endif // __SRB2_MEDIA_WEBM_HPP__

View file

@ -0,0 +1,241 @@
// 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 <cstdint>
#include <memory>
#include <stdexcept>
#include <fmt/format.h>
#include "../cxxutil.hpp"
#include "webm_vorbis.hpp"
#include "webm_vp8.hpp"
using namespace srb2::media;
using time_unit_t = MediaEncoder::time_unit_t;
WebmContainer::WebmContainer(const Config cfg) : writer_(cfg.file_name), dtor_cb_(cfg.destructor_callback)
{
if (!segment_.Init(&writer_))
{
throw std::runtime_error("mkvmuxer::Segment::Init");
}
}
WebmContainer::~WebmContainer()
{
flush_queue();
if (!segment_.Finalize())
{
CONS_Alert(CONS_WARNING, "mkvmuxer::Segment::Finalize has failed\n");
}
finalized_ = true;
if (dtor_cb_)
{
dtor_cb_(*this);
}
}
std::unique_ptr<AudioEncoder> WebmContainer::make_audio_encoder(AudioEncoder::Config cfg)
{
const uint64_t tid = segment_.AddAudioTrack(cfg.sample_rate, cfg.channels, 0);
return std::make_unique<WebmVorbisEncoder>(*this, tid, cfg);
}
std::unique_ptr<VideoEncoder> WebmContainer::make_video_encoder(VideoEncoder::Config cfg)
{
const uint64_t tid = segment_.AddVideoTrack(cfg.width, cfg.height, 0);
return std::make_unique<WebmVP8Encoder>(*this, tid, cfg);
}
time_unit_t WebmContainer::duration() const
{
if (finalized_)
{
const auto& si = *segment_.segment_info();
return webm::duration(static_cast<uint64_t>(si.duration() * si.timecode_scale()));
}
auto _ = queue_guard();
return webm::duration(latest_timestamp_);
}
std::size_t WebmContainer::size() const
{
if (finalized_)
{
return writer_.Position();
}
auto _ = queue_guard();
return writer_.Position() + queue_size_;
}
std::size_t WebmContainer::track_size(webm::track trackid) const
{
auto _ = queue_guard();
return queue_.at(trackid).data_size;
}
time_unit_t WebmContainer::track_duration(webm::track trackid) const
{
auto _ = queue_guard();
return webm::duration(queue_.at(trackid).flushed_timestamp);
}
void WebmContainer::write_frame(
tcb::span<const std::byte> buffer,
webm::track trackid,
webm::timestamp timestamp,
bool is_key_frame
)
{
if (!segment_.AddFrame(
reinterpret_cast<const uint8_t*>(buffer.data()),
buffer.size_bytes(),
trackid,
timestamp,
is_key_frame
))
{
throw std::runtime_error(fmt::format(
"mkvmuxer::Segment::AddFrame, size={}, track={}, ts={}, key={}",
buffer.size_bytes(),
trackid,
timestamp,
is_key_frame
));
}
queue_[trackid].data_size += buffer.size_bytes();
}
void WebmContainer::queue_frame(
tcb::span<const std::byte> buffer,
webm::track trackid,
webm::timestamp timestamp,
bool is_key_frame
)
{
auto _ = queue_guard();
auto& q = queue_.at(trackid);
// If another track is behind this one, queue this
// frame until the other track catches up.
if (flush_queue() < timestamp)
{
q.frames.emplace_back(buffer, timestamp, is_key_frame);
queue_size_ += buffer.size_bytes();
}
else
{
// Nothing is waiting; this frame can be written
// immediately.
write_frame(buffer, trackid, timestamp, is_key_frame);
q.flushed_timestamp = timestamp;
}
q.queued_timestamp = timestamp;
latest_timestamp_ = timestamp;
}
webm::timestamp WebmContainer::flush_queue()
{
webm::timestamp goal = latest_timestamp_;
// Flush all tracks' queues, not beyond the end of the
// shortest track.
for (const auto& [_, q] : queue_)
{
if (q.queued_timestamp < goal)
{
goal = q.queued_timestamp;
}
}
webm::timestamp shortest;
do
{
shortest = goal;
for (const auto& [tid, q] : queue_)
{
const webm::timestamp flushed = flush_single_queue(tid, q.queued_timestamp);
if (flushed < shortest)
{
shortest = flushed;
}
}
} while (shortest < goal);
return shortest;
}
webm::timestamp WebmContainer::flush_single_queue(webm::track trackid, webm::timestamp flushed_timestamp)
{
webm::timestamp goal = flushed_timestamp;
// Find the lowest timestamp yet flushed from all other
// tracks. We cannot write a frame beyond this timestamp
// because PTS must only increase.
for (const auto& [tid, other] : queue_)
{
if (tid != trackid && other.flushed_timestamp < goal)
{
goal = other.flushed_timestamp;
}
}
auto& q = queue_.at(trackid);
auto it = q.frames.cbegin();
// Flush previously queued frames in this track.
for (; it != q.frames.cend(); ++it)
{
const auto& frame = *it;
if (frame.timestamp > goal)
{
q.flushed_timestamp = frame.timestamp;
break;
}
write_frame(frame.buffer, trackid, frame.timestamp, frame.is_key_frame);
queue_size_ -= frame.buffer.size();
}
q.frames.erase(q.frames.cbegin(), it);
if (q.frames.empty())
{
q.flushed_timestamp = flushed_timestamp;
}
return goal;
}

View file

@ -0,0 +1,112 @@
// 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_WEBM_CONTAINER_HPP__
#define __SRB2_MEDIA_WEBM_CONTAINER_HPP__
#include <cstddef>
#include <mutex>
#include <unordered_map>
#include <vector>
#include <mkvmuxer/mkvmuxer.h>
#include "container.hpp"
#include "webm.hpp"
#include "webm_writer.hpp"
namespace srb2::media
{
class WebmContainer : virtual public MediaContainer
{
public:
WebmContainer(Config cfg);
~WebmContainer();
virtual std::unique_ptr<AudioEncoder> make_audio_encoder(AudioEncoder::Config config) override final;
virtual std::unique_ptr<VideoEncoder> make_video_encoder(VideoEncoder::Config config) override final;
virtual const char* name() const override final { return "WebM"; }
virtual const char* file_name() const override final { return writer_.name(); }
virtual time_unit_t duration() const override final;
virtual std::size_t size() const override final;
std::size_t track_size(webm::track trackid) const;
time_unit_t track_duration(webm::track trackid) const;
template <typename T = mkvmuxer::Track>
T* get_track(webm::track trackid) const
{
return reinterpret_cast<T*>(segment_.GetTrackByNumber(trackid));
}
void init_queue(webm::track trackid) { queue_.try_emplace(trackid); }
// init_queue MUST be called before using this function.
void queue_frame(
tcb::span<const std::byte> buffer,
webm::track trackid,
webm::timestamp timestamp,
bool is_key_frame
);
auto queue_guard() const { return std::lock_guard(queue_mutex_); }
private:
struct FrameQueue
{
struct Frame
{
std::vector<std::byte> buffer;
webm::timestamp timestamp;
bool is_key_frame;
Frame(tcb::span<const std::byte> buffer_, webm::timestamp timestamp_, bool is_key_frame_) :
buffer(buffer_.begin(), buffer_.end()), timestamp(timestamp_), is_key_frame(is_key_frame_)
{
}
};
std::vector<Frame> frames;
std::size_t data_size = 0;
webm::timestamp flushed_timestamp = 0;
webm::timestamp queued_timestamp = 0;
};
mkvmuxer::Segment segment_;
WebmWriter writer_;
mutable std::recursive_mutex queue_mutex_;
std::unordered_map<webm::track, FrameQueue> queue_;
webm::timestamp latest_timestamp_ = 0;
std::size_t queue_size_ = 0;
bool finalized_ = false;
const dtor_cb_t dtor_cb_;
void write_frame(
tcb::span<const std::byte> buffer,
webm::track trackid,
webm::timestamp timestamp,
bool is_key_frame
);
// Returns the largest timestamp that can be written.
webm::timestamp flush_queue();
webm::timestamp flush_single_queue(webm::track trackid, webm::timestamp flushed_timestamp);
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_WEBM_CONTAINER_HPP__

View file

@ -0,0 +1,51 @@
// 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_WEBM_ENCODER_HPP__
#define __SRB2_MEDIA_WEBM_ENCODER_HPP__
#include <mkvmuxer/mkvmuxer.h>
#include "encoder.hpp"
#include "webm_container.hpp"
namespace srb2::media
{
template <typename T = mkvmuxer::Track>
class WebmEncoder : virtual public MediaEncoder
{
public:
WebmEncoder(WebmContainer& container, webm::track trackid) : container_(container), trackid_(trackid)
{
container_.init_queue(trackid_);
}
protected:
WebmContainer& container_;
webm::track trackid_;
std::size_t size() const { return container_.track_size(trackid_); }
time_unit_t duration() const { return container_.track_duration(trackid_); }
static T* get_track(const WebmContainer& container, webm::track trackid) { return container.get_track<T>(trackid); }
T* track() const { return get_track(container_, trackid_); }
virtual void write_frame(frame_buffer_t p, time_unit_t ts, bool is_key_frame) override final
{
const auto ts_nano = std::chrono::duration_cast<webm::duration>(ts);
container_.queue_frame(p, trackid_, ts_nano.count(), is_key_frame);
}
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_WEBM_ENCODER_HPP__

65
src/media/webm_vorbis.hpp Normal file
View file

@ -0,0 +1,65 @@
// 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_WEBM_VORBIS_HPP__
#define __SRB2_MEDIA_WEBM_VORBIS_HPP__
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <vector>
#include <fmt/format.h>
#include "../cxxutil.hpp"
#include "vorbis.hpp"
#include "webm_encoder.hpp"
namespace srb2::media
{
class WebmVorbisEncoder : public WebmEncoder<mkvmuxer::AudioTrack>, public VorbisEncoder
{
public:
WebmVorbisEncoder(WebmContainer& container, webm::track trackid, AudioEncoder::Config cfg) :
WebmEncoder(container, trackid), VorbisEncoder(cfg)
{
// write Vorbis extra data
const auto p = make_vorbis_private_data();
if (!track()->SetCodecPrivate(reinterpret_cast<const uint8_t*>(p.data()), p.size()))
{
throw std::runtime_error(fmt::format("mkvmuxer::AudioTrack::SetCodecPrivate, size={}", p.size()));
}
}
virtual BitRate estimated_bit_rate() const override final
{
auto _ = container_.queue_guard();
const std::chrono::duration<float> t = duration();
if (t <= t.zero())
{
return {};
}
using namespace std::chrono_literals;
return {static_cast<std::size_t>((size() * 8) / t.count()), 1s};
}
private:
std::vector<std::byte> make_vorbis_private_data();
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_WEBM_VORBIS_HPP__

View file

@ -0,0 +1,79 @@
// 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 <algorithm>
#include <cstddef>
#include <iterator>
#include <vector>
#include <tcb/span.hpp>
#include "webm_vorbis.hpp"
// https://www.matroska.org/technical/notes.html#xiph-lacing
// https://www.matroska.org/technical/codec_specs.html#a_vorbis
using namespace srb2::media;
static std::size_t lace_length(const ogg_packet& op)
{
return (op.bytes / 255) + 1;
}
static void lace(std::vector<std::byte>& v, const ogg_packet& op)
{
// The lacing size is encoded in at least one byte. If
// the value is 255, add the value of the next byte in
// sequence. This ends with a byte that is less than 255.
std::fill_n(std::back_inserter(v), lace_length(op) - 1, std::byte {255});
const unsigned char n = (op.bytes % 255);
v.emplace_back(std::byte {n});
}
std::vector<std::byte> WebmVorbisEncoder::make_vorbis_private_data()
{
const headers_t packets = generate_headers();
std::vector<std::byte> v;
// There are three Vorbis header packets. The lacing for
// these packets in Matroska does not count the final
// packet.
// clang-format off
v.reserve(
1
+ lace_length(packets[0])
+ lace_length(packets[1])
+ packets[0].bytes
+ packets[1].bytes
+ packets[2].bytes);
// clang-format on
// The first byte is the number of packets. Once again,
// the last packet is not counted.
v.emplace_back(std::byte {2});
// Then the laced sizes for each packet.
lace(v, packets[0]);
lace(v, packets[1]);
// Then each packet's data. The last packet's data
// actually is written here.
for (auto op : packets)
{
tcb::span<const std::byte> p(reinterpret_cast<const std::byte*>(op.packet), op.bytes);
std::copy(p.begin(), p.end(), std::back_inserter(v));
}
return v;
}

44
src/media/webm_vp8.hpp Normal file
View file

@ -0,0 +1,44 @@
// 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_WEBM_VP8_HPP__
#define __SRB2_MEDIA_WEBM_VP8_HPP__
#include "vp8.hpp"
#include "webm_encoder.hpp"
namespace srb2::media
{
class WebmVP8Encoder : public WebmEncoder<mkvmuxer::VideoTrack>, public VP8Encoder
{
public:
WebmVP8Encoder(WebmContainer& container, webm::track trackid, VideoEncoder::Config cfg) :
WebmEncoder(container, trackid), VP8Encoder(cfg)
{
}
virtual BitRate estimated_bit_rate() const override final
{
auto _ = container_.queue_guard();
const int frames = frame_count().frames;
if (frames <= 0)
{
return {};
}
return {(size() * 8) / frames, std::chrono::duration<float>(1.f / frame_rate())};
}
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_WEBM_VP8_HPP__

32
src/media/webm_writer.hpp Normal file
View file

@ -0,0 +1,32 @@
// 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_WEBM_WRITER_HPP__
#define __SRB2_MEDIA_WEBM_WRITER_HPP__
#include <cstdio>
#include <string>
#include <mkvmuxer/mkvwriter.h>
#include "cfile.hpp"
namespace srb2::media
{
class WebmWriter : public CFile, public mkvmuxer::MkvWriter
{
public:
WebmWriter(const std::string file_name) : CFile(file_name), MkvWriter(static_cast<std::FILE*>(*this)) {}
~WebmWriter() { MkvWriter::Close(); }
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_WEBM_WRITER_HPP__

125
src/media/yuv420p.cpp Normal file
View file

@ -0,0 +1,125 @@
// 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 <algorithm>
#include <cstdint>
#include <memory>
#include <libyuv/convert.h>
#include <libyuv/scale_argb.h>
#include <tcb/span.hpp>
#include "../cxxutil.hpp"
#include "yuv420p.hpp"
using namespace srb2::media;
bool YUV420pFrame::BufferRGBA::resize(int width, int height)
{
if (width == width_ && height == height_)
{
return false;
}
width_ = width;
height_ = height;
row_stride = width * 4;
const std::size_t new_size = row_stride * height;
// Overallocate since the vector's alignment can't be
// easily controlled. This is not a significant waste.
vec_.resize(new_size + (kAlignment - 1));
void* p = vec_.data();
std::size_t n = vec_.size();
p = std::align(kAlignment, 1, p, n);
SRB2_ASSERT(p != nullptr);
plane = tcb::span<uint8_t>(reinterpret_cast<uint8_t*>(p), new_size);
return true;
}
void YUV420pFrame::BufferRGBA::erase()
{
std::fill(vec_.begin(), vec_.end(), 0);
}
void YUV420pFrame::BufferRGBA::release()
{
if (!vec_.empty())
{
*this = {};
}
}
const VideoFrame::Buffer& YUV420pFrame::rgba_buffer() const
{
return *rgba_;
}
void YUV420pFrame::convert() const
{
// ABGR = RGBA in memory
libyuv::ABGRToI420(
rgba_->plane.data(),
rgba_->row_stride,
y_.plane.data(),
y_.row_stride,
u_.plane.data(),
u_.row_stride,
v_.plane.data(),
v_.row_stride,
width(),
height()
);
}
void YUV420pFrame::scale(const BufferRGBA& scaled_rgba)
{
int vw = scaled_rgba.width();
int vh = scaled_rgba.height();
uint8_t* p = scaled_rgba.plane.data();
const float ru = width() / static_cast<float>(height());
const float rs = vw / static_cast<float>(vh);
// Maintain aspect ratio of unscaled. Fit inside scaled
// aspect by centering image.
if (rs > ru) // scaled is wider
{
vw = vh * ru;
p += (scaled_rgba.width() - vw) / 2 * 4;
}
else
{
vh = vw / ru;
p += (scaled_rgba.height() - vh) / 2 * scaled_rgba.row_stride;
}
// Curiously, this function doesn't care about channel order.
libyuv::ARGBScale(
rgba_->plane.data(),
rgba_->row_stride,
width(),
height(),
p,
scaled_rgba.row_stride,
vw,
vh,
libyuv::FilterMode::kFilterNone
);
rgba_ = &scaled_rgba;
}

74
src/media/yuv420p.hpp Normal file
View file

@ -0,0 +1,74 @@
// 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_YUV420P_HPP__
#define __SRB2_MEDIA_YUV420P_HPP__
#include <cstdint>
#include <vector>
#include "video_frame.hpp"
namespace srb2::media
{
class YUV420pFrame : public VideoFrame
{
public:
// 32-byte aligned for AVX optimizations (see libyuv)
static constexpr int kAlignment = 32;
class BufferRGBA : public VideoFrame::Buffer
{
public:
bool resize(int width, int height); // true if resized
void erase(); // fills with black
void release();
int width() const { return width_; }
int height() const { return height_; }
private:
int width_, height_;
std::vector<uint8_t> vec_;
};
YUV420pFrame(int pts, Buffer y, Buffer u, Buffer v, const BufferRGBA& rgba) :
VideoFrame(pts), y_(y), u_(u), v_(v), rgba_(&rgba)
{
}
~YUV420pFrame() = default;
// Simply resets PTS and RGBA buffer while keeping YUV
// buffers intact.
void reset(int pts, const BufferRGBA& rgba) { *this = YUV420pFrame(pts, y_, u_, v_, rgba); }
// Converts RGBA buffer to YUV planes.
void convert() const;
// Scales the existing buffer into a new one. This new
// buffer replaces the existing one.
void scale(const BufferRGBA& rgba);
virtual int width() const override { return rgba_->width(); }
virtual int height() const override { return rgba_->height(); }
virtual const Buffer& rgba_buffer() const override;
private:
Buffer y_, u_, v_;
const BufferRGBA* rgba_;
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_YUV420P_HPP__

View file

@ -21,6 +21,7 @@
#include "../audio/sound_effect_player.hpp"
#include "../cxxutil.hpp"
#include "../io/streams.hpp"
#include "../m_avrecorder.hpp"
#include "../doomdef.h"
#include "../i_sound.h"
@ -57,6 +58,8 @@ static shared_ptr<Gain<2>> gain_music;
static vector<shared_ptr<SoundEffectPlayer>> sound_effect_channels;
static shared_ptr<srb2::media::AVRecorder> av_recorder;
static void (*music_fade_callback)();
void* I_GetSfx(sfxinfo_t* sfx)
@ -135,6 +138,9 @@ void audio_callback(void* userdata, Uint8* buffer, int len)
std::clamp(float_buffer[i].amplitudes[1], -1.f, 1.f),
};
}
if (av_recorder)
av_recorder->push_audio_samples(tcb::span {float_buffer, float_len});
}
catch (...)
{
@ -749,3 +755,11 @@ boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
else
return false;
}
void I_UpdateAudioRecorder(void)
{
// must be locked since av_recorder is used by audio_callback
SdlAudioLockHandle _;
av_recorder = g_av_recorder;
}