From 60899133c1711d6d4dca6c6766a7b75f3d95fa3e Mon Sep 17 00:00:00 2001 From: James R Date: Sun, 12 Feb 2023 02:12:55 -0800 Subject: [PATCH] media: add libwebm container --- src/media/CMakeLists.txt | 6 + src/media/cfile.cpp | 34 ++++++ src/media/cfile.hpp | 36 ++++++ src/media/webm.hpp | 26 ++++ src/media/webm_container.cpp | 228 +++++++++++++++++++++++++++++++++++ src/media/webm_container.hpp | 112 +++++++++++++++++ src/media/webm_writer.hpp | 32 +++++ 7 files changed, 474 insertions(+) create mode 100644 src/media/cfile.cpp create mode 100644 src/media/cfile.hpp create mode 100644 src/media/webm.hpp create mode 100644 src/media/webm_container.cpp create mode 100644 src/media/webm_container.hpp create mode 100644 src/media/webm_writer.hpp diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index 3e748c08a..5025a7550 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -1,5 +1,7 @@ target_sources(SRB2SDL2 PRIVATE audio_encoder.hpp + cfile.cpp + cfile.hpp container.hpp encoder.hpp options.cpp @@ -12,6 +14,10 @@ target_sources(SRB2SDL2 PRIVATE vp8.cpp vp8.hpp vpx_error.hpp + webm.hpp + webm_container.cpp + webm_container.hpp + webm_writer.hpp yuv420p.cpp yuv420p.hpp ) diff --git a/src/media/cfile.cpp b/src/media/cfile.cpp new file mode 100644 index 000000000..25c8ba019 --- /dev/null +++ b/src/media/cfile.cpp @@ -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 +#include +#include +#include + +#include + +#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_); +} diff --git a/src/media/cfile.hpp b/src/media/cfile.hpp new file mode 100644 index 000000000..7d0e8595e --- /dev/null +++ b/src/media/cfile.hpp @@ -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 +#include + +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__ diff --git a/src/media/webm.hpp b/src/media/webm.hpp new file mode 100644 index 000000000..2031f58f0 --- /dev/null +++ b/src/media/webm.hpp @@ -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 +#include +#include + +namespace srb2::media::webm +{ + +using track = uint64_t; +using timestamp = uint64_t; +using duration = std::chrono::duration; + +}; // namespace srb2::media::webm + +#endif // __SRB2_MEDIA_WEBM_HPP__ diff --git a/src/media/webm_container.cpp b/src/media/webm_container.cpp new file mode 100644 index 000000000..3d2ffc5b9 --- /dev/null +++ b/src/media/webm_container.cpp @@ -0,0 +1,228 @@ +// 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 "../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) +{ + SRB2_ASSERT(segment_.Init(&writer_) == true); +} + +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 WebmContainer::make_audio_encoder(AudioEncoder::Config cfg) +{ + const uint64_t tid = segment_.AddAudioTrack(cfg.sample_rate, cfg.channels, 0); + + return std::make_unique(*this, tid, cfg); +} + +std::unique_ptr WebmContainer::make_video_encoder(VideoEncoder::Config cfg) +{ + const uint64_t tid = segment_.AddVideoTrack(cfg.width, cfg.height, 0); + + return std::make_unique(*this, tid, cfg); +} + +time_unit_t WebmContainer::duration() const +{ + if (finalized_) + { + const auto& si = *segment_.segment_info(); + + return webm::duration(static_cast(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 buffer, + webm::track trackid, + webm::timestamp timestamp, + bool is_key_frame +) +{ + SRB2_ASSERT( + segment_.AddFrame( + reinterpret_cast(buffer.data()), + buffer.size_bytes(), + trackid, + timestamp, + is_key_frame + ) == true + ); + + queue_[trackid].data_size += buffer.size_bytes(); +} + +void WebmContainer::queue_frame( + tcb::span 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; +} diff --git a/src/media/webm_container.hpp b/src/media/webm_container.hpp new file mode 100644 index 000000000..e763515c8 --- /dev/null +++ b/src/media/webm_container.hpp @@ -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 +#include +#include +#include + +#include + +#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 make_audio_encoder(AudioEncoder::Config config) override final; + virtual std::unique_ptr 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 + T* get_track(webm::track trackid) const + { + return reinterpret_cast(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 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 buffer; + webm::timestamp timestamp; + bool is_key_frame; + + Frame(tcb::span buffer_, webm::timestamp timestamp_, bool is_key_frame_) : + buffer(buffer_.begin(), buffer_.end()), timestamp(timestamp_), is_key_frame(is_key_frame_) + { + } + }; + + std::vector 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 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 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__ diff --git a/src/media/webm_writer.hpp b/src/media/webm_writer.hpp new file mode 100644 index 000000000..50e091baa --- /dev/null +++ b/src/media/webm_writer.hpp @@ -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 +#include + +#include + +#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(*this)) {} + ~WebmWriter() { MkvWriter::Close(); } +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_WEBM_WRITER_HPP__