换ffmpeg
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
#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 first_frame{true}; /* 编码器重建后必须立即产生可独立解码的关键帧。 */
|
||||
|
||||
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;
|
||||
first_frame = 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;
|
||||
|
||||
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->first_frame ? 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->first_frame = false;
|
||||
av_packet_unref(d->packet);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user