diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index d0ade1566..3e748c08a 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -9,6 +9,9 @@ target_sources(SRB2SDL2 PRIVATE vorbis.cpp vorbis.hpp vorbis_error.hpp + vp8.cpp + vp8.hpp + vpx_error.hpp yuv420p.cpp yuv420p.hpp ) diff --git a/src/media/vp8.cpp b/src/media/vp8.cpp new file mode 100644 index 000000000..341b1b932 --- /dev/null +++ b/src/media/vp8.cpp @@ -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 +#include +#include +#include +#include +#include + +#include +#include + +#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("q", { + {"vbr", VPX_VBR}, + {"cbr", VPX_CBR}, + {"cq", VPX_CQ}, + {"q", VPX_Q}, + })}, + {"target_bitrate", Options::range_min("800", 1)}, + {"min_q", Options::range("4", 4, 63)}, + {"max_q", Options::range("55", 4, 63)}, + {"kf_min", Options::range_min("0", 0)}, + {"kf_max", Options::value_map("auto", { + {"auto", KeyFrameOption::kAuto}, + {"MIN", 0}, + {"MAX", INT32_MAX}, + })}, + {"cpu_used", Options::range("0", -16, 16)}, + {"cq_level", Options::range("10", 0, 63)}, + {"deadline", Options::value_map("10", { + {"infinite", DeadlineOption::kInfinite}, + {"MIN", 1}, + {"MAX", INT32_MAX}, + })}, + {"sharpness", Options::range("7", 0, 7)}, + {"token_parts", Options::range("0", 0, 3)}, + {"threads", Options::range_min("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("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(options_.get("quality_mode")); + cfg.kf_mode = VPX_KF_AUTO; + + cfg.rc_target_bitrate = options_.get("target_bitrate"); + cfg.rc_min_quantizer = options_.get("min_q"); + cfg.rc_max_quantizer = options_.get("max_q"); + + // Keyframe spacing, in number of frames. + // kf_max_dist should be low enough to allow scrubbing. + + int kf_max = options_.get("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("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(VP8E_SET_CPUUSED, "cpu_used"); + control(VP8E_SET_CQ_LEVEL, "cq_level"); + control(VP8E_SET_SHARPNESS, "sharpness"); + control(VP8E_SET_TOKEN_PARTITIONS, "token_parts"); + + auto plane = [this](int k, int ycs = 0) + { + using T = uint8_t; + auto view = tcb::span(reinterpret_cast(img_->planes[k]), img_->stride[k] * (img_->h >> ycs)); + + return VideoFrame::Buffer {view, static_cast(img_->stride[k])}; + }; + + frame_ = std::make_unique( + 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(frame.get()) != nullptr); + + frame_ = std::unique_ptr(static_cast(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(frame_rate()); + + using T = const std::byte; + tcb::span p(reinterpret_cast(frame.buf), frame.sz); + + write_frame(p, std::chrono::duration(ts), (frame.flags & VPX_FRAME_IS_KEY)); + } + + return output; +} + +template +void VP8Encoder::control(vp8e_enc_control_id id, const char* option) +{ + auto value = options_.get(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(duration_ / static_cast(frame_rate()))}; +} diff --git a/src/media/vp8.hpp b/src/media/vp8.hpp new file mode 100644 index 000000000..b4c68f24d --- /dev/null +++ b/src/media/vp8.hpp @@ -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 + +#include + +#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("threads"); + const int deadline_ = options_.get("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 frame_; + + bool process(); + + template // T = option type + void control(vp8e_enc_control_id id, const char* option); +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_VP8_HPP__ diff --git a/src/media/vpx_error.hpp b/src/media/vpx_error.hpp new file mode 100644 index 000000000..06b502398 --- /dev/null +++ b/src/media/vpx_error.hpp @@ -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 + +#include +#include + +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 : formatter +{ + template + auto format(const VpxError& error, FormatContext& ctx) const + { + return formatter::format(error.description(), ctx); + } +}; + +#endif // __SRB2_MEDIA_VPX_ERROR_HPP__