#include "H264_Encoder.hpp" #include #include #include #include #include extern "C" { #include #include #include #include } namespace aethera::web { namespace { std::runtime_error ffmpeg_failure(std::string operation, int code) { std::array 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(width); codec_context->height = static_cast(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( static_cast(width) * height * 8, 1'000'000, 24'000'000); codec_context->gop_size = std::max(1, static_cast(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(frame_rate)) {} H264_Encoder::~H264_Encoder() = default; void H264_Encoder::request_key_frame() { d->key_frame_requested = true; } std::shared_ptr H264_Encoder::encode( std::span 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(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(pixels.data())}; const int source_stride[]{static_cast(width * 4U)}; if (sws_scale(d->scaler, source, source_stride, 0, static_cast(height), d->yuv_frame->data, d->yuv_frame->linesize) != static_cast(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(); output->annex_b.assign(reinterpret_cast(d->packet->data), reinterpret_cast(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; } }