Files
Aethera/web_server/src/H264_Encoder.cpp
T
2026-08-23 17:11:12 +08:00

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;
}
}