mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2025-10-30 08:01:28 +00:00
Merge branch 'avrecorder' into 'master'
WebM Encoder See merge request KartKrew/Kart!908
This commit is contained in:
commit
dba41a6de8
43 changed files with 3382 additions and 3 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
252
src/m_avrecorder.cpp
Normal 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
53
src/m_avrecorder.h
Normal 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
19
src/m_avrecorder.hpp
Normal 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__
|
||||
51
src/m_misc.c
51
src/m_misc.c
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
34
src/media/CMakeLists.txt
Normal 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
|
||||
)
|
||||
39
src/media/audio_encoder.hpp
Normal file
39
src/media/audio_encoder.hpp
Normal 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
214
src/media/avrecorder.cpp
Normal 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
108
src/media/avrecorder.hpp
Normal 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__
|
||||
149
src/media/avrecorder_feedback.cpp
Normal file
149
src/media/avrecorder_feedback.cpp
Normal 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);
|
||||
}
|
||||
175
src/media/avrecorder_impl.hpp
Normal file
175
src/media/avrecorder_impl.hpp
Normal 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__
|
||||
69
src/media/avrecorder_indexed.cpp
Normal file
69
src/media/avrecorder_indexed.cpp
Normal 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();
|
||||
}
|
||||
169
src/media/avrecorder_queue.cpp
Normal file
169
src/media/avrecorder_queue.cpp
Normal 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
34
src/media/cfile.cpp
Normal 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
36
src/media/cfile.hpp
Normal 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
53
src/media/container.hpp
Normal 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
51
src/media/encoder.hpp
Normal 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
117
src/media/options.cpp
Normal 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
58
src/media/options.hpp
Normal 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__
|
||||
58
src/media/options_values.cpp
Normal file
58
src/media/options_values.cpp
Normal 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
|
||||
58
src/media/video_encoder.hpp
Normal file
58
src/media/video_encoder.hpp
Normal 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
63
src/media/video_frame.hpp
Normal 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
142
src/media/vorbis.cpp
Normal 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
54
src/media/vorbis.hpp
Normal 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__
|
||||
54
src/media/vorbis_error.hpp
Normal file
54
src/media/vorbis_error.hpp
Normal 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
241
src/media/vp8.cpp
Normal 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
112
src/media/vp8.hpp
Normal 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
45
src/media/vpx_error.hpp
Normal 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
26
src/media/webm.hpp
Normal 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__
|
||||
241
src/media/webm_container.cpp
Normal file
241
src/media/webm_container.cpp
Normal 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;
|
||||
}
|
||||
112
src/media/webm_container.hpp
Normal file
112
src/media/webm_container.hpp
Normal 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__
|
||||
51
src/media/webm_encoder.hpp
Normal file
51
src/media/webm_encoder.hpp
Normal 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
65
src/media/webm_vorbis.hpp
Normal 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__
|
||||
79
src/media/webm_vorbis_lace.cpp
Normal file
79
src/media/webm_vorbis_lace.cpp
Normal 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
44
src/media/webm_vp8.hpp
Normal 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
32
src/media/webm_writer.hpp
Normal 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
125
src/media/yuv420p.cpp
Normal 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
74
src/media/yuv420p.hpp
Normal 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__
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue