144 lines
7.4 KiB
C++
144 lines
7.4 KiB
C++
#include "H264_Encoder.hpp"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
extern "C" {
|
|
#include <libavcodec/avcodec.h>
|
|
#include <libavutil/error.h>
|
|
#include <libavutil/opt.h>
|
|
#include <libswscale/swscale.h>
|
|
}
|
|
|
|
namespace aethera::web {
|
|
namespace {
|
|
std::runtime_error ffmpeg_failure(std::string operation, int code) {
|
|
std::array<char, AV_ERROR_MAX_STRING_SIZE> description{};
|
|
av_strerror(code, description.data(), description.size());
|
|
return std::runtime_error(std::move(operation) + ": " + description.data());
|
|
}
|
|
void require_ffmpeg(int result, std::string operation) {
|
|
if (result < 0) throw ffmpeg_failure(std::move(operation), result);
|
|
}
|
|
}
|
|
|
|
struct H264_Encoder::Private {
|
|
double frame_rate{}; /* 编码时间基与 GOP 计算使用的服务端帧时钟频率。 */
|
|
AVCodecContext* codec_context{}; /* 当前尺寸对应的 libx264 低延迟编码上下文。 */
|
|
AVFrame* yuv_frame{}; /* swscale 写入并提交给编码器的复用 YUV420P 帧。 */
|
|
AVPacket* packet{}; /* 接收单个 H.264 access unit 的复用包。 */
|
|
SwsContext* scaler{}; /* RGBA/BGRA 到 YUV420P 的像素转换上下文。 */
|
|
std::uint32_t width{}; /* 当前编码尺寸;尺寸变化时整体重建编码器。 */
|
|
std::uint32_t height{}; /* 当前编码尺寸;H.264 4:2:0 要求偶数。 */
|
|
Video_Pixel_Layout layout{Video_Pixel_Layout::bgra}; /* scaler 当前输入格式。 */
|
|
bool key_frame_requested{true}; /* Track 新建或恢复时,下一 access unit 必须可独立解码。 */
|
|
|
|
explicit Private(double value_frame_rate) : frame_rate(value_frame_rate) {
|
|
if (!std::isfinite(frame_rate) || frame_rate <= 0.0)
|
|
throw std::invalid_argument("H.264 frame rate must be positive");
|
|
}
|
|
~Private() { reset(); }
|
|
void reset() noexcept {
|
|
sws_freeContext(scaler);
|
|
scaler = nullptr;
|
|
av_packet_free(&packet);
|
|
av_frame_free(&yuv_frame);
|
|
avcodec_free_context(&codec_context);
|
|
width = 0;
|
|
height = 0;
|
|
key_frame_requested = true;
|
|
}
|
|
void configure(std::uint32_t next_width, std::uint32_t next_height,
|
|
Video_Pixel_Layout next_layout) {
|
|
if ((next_width & 1U) != 0 || (next_height & 1U) != 0)
|
|
throw std::invalid_argument("H.264 YUV420P dimensions must be even");
|
|
if (codec_context && width == next_width && height == next_height &&
|
|
layout == next_layout) return;
|
|
reset();
|
|
const AVCodec* codec = avcodec_find_encoder_by_name("libx264");
|
|
if (!codec) throw std::runtime_error("FFmpeg libx264 encoder is unavailable");
|
|
codec_context = avcodec_alloc_context3(codec);
|
|
if (!codec_context) throw std::bad_alloc{};
|
|
width = next_width;
|
|
height = next_height;
|
|
layout = next_layout;
|
|
codec_context->width = static_cast<int>(width);
|
|
codec_context->height = static_cast<int>(height);
|
|
codec_context->pix_fmt = AV_PIX_FMT_YUV420P;
|
|
codec_context->time_base = AVRational{1, 1'000'000};
|
|
codec_context->framerate = av_d2q(frame_rate, 1000);
|
|
codec_context->bit_rate = std::clamp<std::int64_t>(
|
|
static_cast<std::int64_t>(width) * height * 8, 1'000'000, 24'000'000);
|
|
codec_context->gop_size = std::max(1, static_cast<int>(std::lround(frame_rate * 2.0)));
|
|
codec_context->max_b_frames = 0;
|
|
codec_context->thread_count = 1;
|
|
codec_context->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
|
require_ffmpeg(av_opt_set(codec_context->priv_data, "preset", "ultrafast", 0),
|
|
"setting x264 preset");
|
|
require_ffmpeg(av_opt_set(codec_context->priv_data, "tune", "zerolatency", 0),
|
|
"setting x264 tune");
|
|
require_ffmpeg(av_opt_set(codec_context->priv_data, "profile", "baseline", 0),
|
|
"setting H.264 profile");
|
|
require_ffmpeg(av_opt_set(codec_context->priv_data, "x264-params",
|
|
"annexb=1:repeat-headers=1:scenecut=0", 0), "setting x264 RTP parameters");
|
|
require_ffmpeg(avcodec_open2(codec_context, codec, nullptr), "opening libx264 encoder");
|
|
yuv_frame = av_frame_alloc();
|
|
packet = av_packet_alloc();
|
|
if (!yuv_frame || !packet) throw std::bad_alloc{};
|
|
yuv_frame->format = codec_context->pix_fmt;
|
|
yuv_frame->width = codec_context->width;
|
|
yuv_frame->height = codec_context->height;
|
|
require_ffmpeg(av_frame_get_buffer(yuv_frame, 32), "allocating H.264 YUV frame");
|
|
const AVPixelFormat source_format = layout == Video_Pixel_Layout::bgra
|
|
? AV_PIX_FMT_BGRA : AV_PIX_FMT_RGBA;
|
|
scaler = sws_getContext(codec_context->width, codec_context->height,
|
|
source_format, codec_context->width, codec_context->height,
|
|
codec_context->pix_fmt, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
|
|
if (!scaler) throw std::runtime_error("failed to create FFmpeg pixel converter");
|
|
}
|
|
};
|
|
|
|
H264_Encoder::H264_Encoder(double frame_rate)
|
|
: d(std::make_unique<Private>(frame_rate)) {}
|
|
H264_Encoder::~H264_Encoder() = default;
|
|
|
|
void H264_Encoder::request_key_frame() {
|
|
d->key_frame_requested = true;
|
|
}
|
|
|
|
std::shared_ptr<const Encoded_Video_Frame> H264_Encoder::encode(
|
|
std::span<const std::byte> pixels, std::uint32_t width,
|
|
std::uint32_t height, Video_Pixel_Layout layout,
|
|
std::uint64_t sequence, std::chrono::microseconds presentation_time) {
|
|
const std::size_t required = static_cast<std::size_t>(width) * height * 4U;
|
|
if (pixels.size() != required)
|
|
throw std::invalid_argument("H.264 source frame byte size does not match its dimensions");
|
|
d->configure(width, height, layout);
|
|
require_ffmpeg(av_frame_make_writable(d->yuv_frame), "making H.264 frame writable");
|
|
const std::uint8_t* source[]{reinterpret_cast<const std::uint8_t*>(pixels.data())};
|
|
const int source_stride[]{static_cast<int>(width * 4U)};
|
|
if (sws_scale(d->scaler, source, source_stride, 0, static_cast<int>(height),
|
|
d->yuv_frame->data, d->yuv_frame->linesize) != static_cast<int>(height))
|
|
throw std::runtime_error("FFmpeg did not convert the complete video frame");
|
|
d->yuv_frame->pts = presentation_time.count();
|
|
d->yuv_frame->pict_type = d->key_frame_requested
|
|
? AV_PICTURE_TYPE_I
|
|
: AV_PICTURE_TYPE_NONE;
|
|
require_ffmpeg(avcodec_send_frame(d->codec_context, d->yuv_frame),
|
|
"submitting frame to H.264 encoder");
|
|
const int received = avcodec_receive_packet(d->codec_context, d->packet);
|
|
if (received == AVERROR(EAGAIN)) return {};
|
|
require_ffmpeg(received, "receiving H.264 access unit");
|
|
auto output = std::make_shared<Encoded_Video_Frame>();
|
|
output->annex_b.assign(reinterpret_cast<const std::byte*>(d->packet->data),
|
|
reinterpret_cast<const std::byte*>(d->packet->data + d->packet->size));
|
|
output->presentation_time = presentation_time;
|
|
output->sequence = sequence;
|
|
output->key_frame = (d->packet->flags & AV_PKT_FLAG_KEY) != 0;
|
|
d->key_frame_requested = false;
|
|
av_packet_unref(d->packet);
|
|
return output;
|
|
}
|
|
}
|