mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2025-10-30 08:01:28 +00:00
media: add libvpx VP8 encoder
This commit is contained in:
parent
1415254131
commit
b8015b4ad2
4 changed files with 443 additions and 0 deletions
|
|
@ -9,6 +9,9 @@ target_sources(SRB2SDL2 PRIVATE
|
||||||
vorbis.cpp
|
vorbis.cpp
|
||||||
vorbis.hpp
|
vorbis.hpp
|
||||||
vorbis_error.hpp
|
vorbis_error.hpp
|
||||||
|
vp8.cpp
|
||||||
|
vp8.hpp
|
||||||
|
vpx_error.hpp
|
||||||
yuv420p.cpp
|
yuv420p.cpp
|
||||||
yuv420p.hpp
|
yuv420p.hpp
|
||||||
)
|
)
|
||||||
|
|
|
||||||
293
src/media/vp8.cpp
Normal file
293
src/media/vp8.cpp
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace KeyFrameOption
|
||||||
|
{
|
||||||
|
|
||||||
|
enum : int
|
||||||
|
{
|
||||||
|
kAuto = -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
}; // namespace KeyFrameOption
|
||||||
|
|
||||||
|
namespace DeadlineOption
|
||||||
|
{
|
||||||
|
|
||||||
|
enum : int
|
||||||
|
{
|
||||||
|
kInfinite = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
}; // namespace DeadlineOption
|
||||||
|
|
||||||
|
}; // namespace
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
const Options VP8Encoder::options_("vp8", {
|
||||||
|
{"quality_mode", Options::value_map<int>("q", {
|
||||||
|
{"vbr", VPX_VBR},
|
||||||
|
{"cbr", VPX_CBR},
|
||||||
|
{"cq", VPX_CQ},
|
||||||
|
{"q", VPX_Q},
|
||||||
|
})},
|
||||||
|
{"target_bitrate", Options::range_min<int>("800", 1)},
|
||||||
|
{"min_q", Options::range<int>("4", 4, 63)},
|
||||||
|
{"max_q", Options::range<int>("55", 4, 63)},
|
||||||
|
{"kf_min", Options::range_min<int>("0", 0)},
|
||||||
|
{"kf_max", Options::value_map<int>("auto", {
|
||||||
|
{"auto", KeyFrameOption::kAuto},
|
||||||
|
{"MIN", 0},
|
||||||
|
{"MAX", INT32_MAX},
|
||||||
|
})},
|
||||||
|
{"cpu_used", Options::range<int>("0", -16, 16)},
|
||||||
|
{"cq_level", Options::range<int>("10", 0, 63)},
|
||||||
|
{"deadline", Options::value_map<int>("10", {
|
||||||
|
{"infinite", DeadlineOption::kInfinite},
|
||||||
|
{"MIN", 1},
|
||||||
|
{"MAX", INT32_MAX},
|
||||||
|
})},
|
||||||
|
{"sharpness", Options::range<int>("7", 0, 7)},
|
||||||
|
{"token_parts", Options::range<int>("0", 0, 3)},
|
||||||
|
{"threads", Options::range_min<int>("1", 1)},
|
||||||
|
});
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
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 == 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)
|
||||||
|
{
|
||||||
|
SRB2_ASSERT(vpx_img_alloc(&img_, VPX_IMG_FMT_I420, width, height, YUV420pFrame::kAlignment) != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()))};
|
||||||
|
}
|
||||||
102
src/media/vp8.hpp
Normal file
102
src/media/vp8.hpp
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
// 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_;
|
||||||
|
};
|
||||||
|
|
||||||
|
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__
|
||||||
Loading…
Add table
Reference in a new issue