MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VideoEncodeContext.cpp
Go to the documentation of this file.
2
4
5extern "C" {
6#include <libavcodec/avcodec.h>
7#include <libavformat/avformat.h>
8#include <libavutil/imgutils.h>
9#include <libavutil/opt.h>
10#include <libavutil/pixdesc.h>
11#include <libswscale/swscale.h>
12}
13
14namespace MayaFlux::IO {
15
16namespace {
17
18 /**
19 * @brief Select encoder codec for a given container format.
20 *
21 * Falls back to H.264 for any unrecognised container. MP4 and MKV default
22 * to H.264 because it has the widest hardware decode support. WebM defaults
23 * to VP9. AVI defaults to MPEG-4 Part 2 for maximum compatibility.
24 */
25 AVCodecID infer_video_codec(AVFormatContext* fmt_ctx)
26 {
27 if (!fmt_ctx || !fmt_ctx->oformat)
28 return AV_CODEC_ID_H264;
29
30 const char* name = fmt_ctx->oformat->name;
31 if (!name)
32 return AV_CODEC_ID_H264;
33
34 if (std::string_view(name).find("webm") != std::string_view::npos)
35 return AV_CODEC_ID_VP9;
36 if (std::string_view(name).find("avi") != std::string_view::npos)
37 return AV_CODEC_ID_MPEG4;
38
39 return AV_CODEC_ID_H264;
40 }
41
42 /**
43 * @brief Resolve bytes-per-pixel for a packed AVPixelFormat.
44 *
45 * Only the formats the swapchain readback can deliver are handled here.
46 * Any unrecognised format falls back to 4 (BGRA/RGBA assumption).
47 */
48 int bpp_for_format(AVPixelFormat fmt)
49 {
50 switch (fmt) {
51 case AV_PIX_FMT_BGRA:
52 case AV_PIX_FMT_RGBA:
53 case AV_PIX_FMT_ARGB:
54 case AV_PIX_FMT_ABGR:
55 return 4;
56 case AV_PIX_FMT_BGR24:
57 case AV_PIX_FMT_RGB24:
58 return 3;
59 case AV_PIX_FMT_RGBA64LE:
60 case AV_PIX_FMT_RGBA64BE:
61 case AV_PIX_FMT_BGRA64LE:
62 case AV_PIX_FMT_BGRA64BE:
63 return 8;
64 default:
65 return 4;
66 }
67 }
68
69} // namespace
70
71// =========================================================================
72// Destructor
73// =========================================================================
74
79
81{
82 if (m_scratch_frame) {
83 av_frame_free(&m_scratch_frame);
84 m_scratch_frame = nullptr;
85 }
86 if (sws_context) {
87 sws_freeContext(sws_context);
88 sws_context = nullptr;
89 }
90 if (codec_context) {
91 avcodec_free_context(&codec_context);
92 codec_context = nullptr;
93 }
94 m_stream = nullptr;
95 m_stream_index = -1;
96 m_pts = 0;
97 m_width = 0;
98 m_height = 0;
99 m_last_error.clear();
100}
101
102// =========================================================================
103// Open
104// =========================================================================
105
107 uint32_t width,
108 uint32_t height,
109 double frame_rate,
110 AVPixelFormat src_pixel_format,
111 AVCodecID codec_id)
112{
113 close();
114
115 if (!mux.is_open()) {
116 m_last_error = "VideoEncodeContext::open: mux context is not open";
117 return false;
118 }
119
120 if (width == 0 || height == 0 || frame_rate <= 0.0) {
121 m_last_error = "VideoEncodeContext::open: invalid dimensions or frame rate";
122 return false;
123 }
124
125 m_width = width;
127 m_src_src_bpp = bpp_for_format(src_pixel_format);
128 m_src_pixel_fmt = src_pixel_format;
129 m_src_pixel_fmt = src_pixel_format;
130
131 if (codec_id == AV_CODEC_ID_NONE)
132 codec_id = infer_video_codec(mux.format_context);
133
134 const AVCodec* codec = avcodec_find_encoder(codec_id);
135 if (!codec) {
136 m_last_error = std::string("avcodec_find_encoder failed for codec_id=")
137 + std::to_string(static_cast<int>(codec_id));
138 return false;
139 }
140
141 codec_context = avcodec_alloc_context3(codec);
142 if (!codec_context) {
143 m_last_error = "avcodec_alloc_context3 failed";
144 return false;
145 }
146
147 ///< H.264/H.265 require dimensions divisible by 2
148 codec_context->width = static_cast<int>(width % 2 == 0 ? width : width - 1);
149 codec_context->height = static_cast<int>(height % 2 == 0 ? height : height - 1);
150
151 /*
152 * Convert double fps to AVRational. Use millisecond time_base so fractional
153 * rates (23.976, 29.97, 59.94) round-trip without drift.
154 */
155 const int fps_num = static_cast<int>(std::round(frame_rate * 1000.0));
156 codec_context->framerate = { .num = fps_num, .den = 1000 };
157 codec_context->time_base = { .num = 1000, .den = fps_num };
158
159 /*
160 * YUV420P is the universally supported pixel format for H.264/H.265.
161 * VP9 and MPEG4 also accept it. For codecs that require a different native
162 * format, callers must pass an appropriate codec_id and the caller-side
163 * src_pixel_format. The sws_scale below handles any conversion.
164 */
165 codec_context->pix_fmt = AV_PIX_FMT_YUV420P;
166
167 /*
168 *Reasonable quality defaults — not tunable here; callers set codec options
169 * via AVDictionary on codec_context before open() returns if needed.
170 */
171 codec_context->bit_rate = 0; ///< let encoder decide from crf
172 if (codec_id == AV_CODEC_ID_H264 || codec_id == AV_CODEC_ID_H265) {
173 av_opt_set(codec_context->priv_data, "preset", "fast", 0);
174 av_opt_set(codec_context->priv_data, "crf", "23", 0);
175 }
176
177 if (mux.format_context->oformat->flags & AVFMT_GLOBALHEADER)
178 codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
179
180 if (avcodec_open2(codec_context, codec, nullptr) < 0) {
181 m_last_error = "avcodec_open2 failed";
182 close();
183 return false;
184 }
185
186 m_stream = mux.add_stream();
187 if (!m_stream) {
188 m_last_error = "FFmpegMuxContext::add_stream returned nullptr";
189 close();
190 return false;
191 }
192
193 if (avcodec_parameters_from_context(m_stream->codecpar, codec_context) < 0) {
194 m_last_error = "avcodec_parameters_from_context failed";
195 close();
196 return false;
197 }
198
199 m_stream->time_base = codec_context->time_base;
200 m_stream_index = m_stream->index;
201
202 // -------------------------------------------------------------------------
203 // SwsContext: source format -> YUV420P
204 // -------------------------------------------------------------------------
205 sws_context = sws_getContext(
206 codec_context->width,
207 codec_context->height,
208 src_pixel_format,
209 codec_context->width,
210 codec_context->height,
211 codec_context->pix_fmt,
212 SWS_BILINEAR,
213 nullptr, nullptr, nullptr);
214
215 if (!sws_context) {
216 m_last_error = "sws_getContext failed";
217 close();
218 return false;
219 }
220
221 // -------------------------------------------------------------------------
222 // Scratch frame for sws_scale output
223 // -------------------------------------------------------------------------
224 m_scratch_frame = av_frame_alloc();
225 if (!m_scratch_frame) {
226 m_last_error = "av_frame_alloc failed";
227 close();
228 return false;
229 }
230
231 m_scratch_frame->format = codec_context->pix_fmt;
232 m_scratch_frame->width = codec_context->width;
233 m_scratch_frame->height = codec_context->height;
234
235 if (av_frame_get_buffer(m_scratch_frame, 32) < 0) {
236 m_last_error = "av_frame_get_buffer failed";
237 close();
238 return false;
239 }
240
242 "[VideoEncodeContext] open: {}x{} @ {:.3f} fps | src={} enc={} stream#{}",
243 codec_context->width, codec_context->height, frame_rate,
244 av_get_pix_fmt_name(src_pixel_format) ? av_get_pix_fmt_name(src_pixel_format) : "unknown",
245 avcodec_get_name(codec_id),
247
248 return true;
249}
250
251// =========================================================================
252// Encoding
253// =========================================================================
254
255bool VideoEncodeContext::encode_frame(const uint8_t* src_data,
256 size_t src_size,
257 uint32_t src_width,
258 uint32_t src_height,
259 FFmpegMuxContext& mux)
260{
261 if (!is_valid())
262 return false;
263
264 if (src_width == 0 || src_height == 0) {
265 m_last_error = "encode_frame: zero source dimensions";
266 return false;
267 }
268
269 const size_t expected = static_cast<size_t>(src_width)
270 * static_cast<size_t>(src_height)
271 * static_cast<size_t>(m_src_src_bpp);
272
273 if (src_size < expected) {
274 m_last_error = "encode_frame: src_size too small for declared dimensions";
275 return false;
276 }
277
278 if (av_frame_make_writable(m_scratch_frame) < 0) {
279 m_last_error = "av_frame_make_writable failed";
280 return false;
281 }
282
283 if (static_cast<int>(src_width) != m_cached_src_width
284 || static_cast<int>(src_height) != m_cached_src_height) {
285
286 sws_freeContext(sws_context);
287 sws_context = sws_getContext(
288 static_cast<int>(src_width),
289 static_cast<int>(src_height),
291 codec_context->width,
292 codec_context->height,
293 codec_context->pix_fmt,
294 SWS_BILINEAR,
295 nullptr, nullptr, nullptr);
296
297 if (!sws_context) {
298 m_last_error = "sws_getContext failed on dimension change";
299 return false;
300 }
301
302 m_cached_src_width = static_cast<int>(src_width);
303 m_cached_src_height = static_cast<int>(src_height);
304 }
305
306 const int src_stride = static_cast<int>(src_width) * m_src_src_bpp;
307 sws_scale(sws_context,
308 &src_data, &src_stride,
309 0, static_cast<int>(src_height),
310 m_scratch_frame->data, m_scratch_frame->linesize);
311
312 m_scratch_frame->pts = m_pts++;
313
314 if (avcodec_send_frame(codec_context, m_scratch_frame) < 0) {
315 m_last_error = "avcodec_send_frame failed";
316 return false;
317 }
318
319 return drain_packets(mux);
320}
321
322// =========================================================================
323// Drain
324// =========================================================================
325
327{
328 if (!is_valid())
329 return false;
330
331 if (avcodec_send_frame(codec_context, nullptr) < 0) {
332 m_last_error = "avcodec_send_frame(null) failed during drain";
333 return false;
334 }
335
336 return drain_packets(mux);
337}
338
339// =========================================================================
340// Private helpers
341// =========================================================================
342
344{
345 AVPacket* pkt = av_packet_alloc();
346 if (!pkt) {
347 m_last_error = "av_packet_alloc failed";
348 return false;
349 }
350
351 bool ok = true;
352 while (true) {
353 int ret = avcodec_receive_packet(codec_context, pkt);
354 if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
355 break;
356 if (ret < 0) {
357 char errbuf[AV_ERROR_MAX_STRING_SIZE];
358 av_strerror(ret, errbuf, sizeof(errbuf));
359 m_last_error = std::string("avcodec_receive_packet failed: ") + errbuf;
360 ok = false;
361 break;
362 }
363
364 pkt->stream_index = m_stream_index;
365 av_packet_rescale_ts(pkt,
366 codec_context->time_base,
367 m_stream->time_base);
368
369 if (!mux.write_packet(pkt)) {
370 m_last_error = mux.last_error();
371 ok = false;
372 break;
373 }
374
375 av_packet_unref(pkt);
376 }
377
378 av_packet_free(&pkt);
379 return ok;
380}
381
382} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
uint32_t width
Definition Decoder.cpp:59
bool is_open() const
True if the context is open and ready to accept streams / packets.
AVStream * add_stream()
Allocate a new AVStream inside this context.
const std::string & last_error() const
bool write_packet(AVPacket *pkt)
Submit one encoded packet for interleaved writing.
AVFormatContext * format_context
Owned; freed in close().
RAII owner of a single AVFormatContext on the write path.
AVStream * m_stream
Weak ref into FFmpegMuxContext; not owned.
AVFrame * m_scratch_frame
Owned scratch buffer for encoder input.
bool open(FFmpegMuxContext &mux, uint32_t width, uint32_t height, double frame_rate, AVPixelFormat src_pixel_format, AVCodecID codec_id)
Open the encoder and register a video stream in the mux context.
bool drain(FFmpegMuxContext &mux)
Flush all buffered frames from the encoder to the mux.
bool encode_frame(const uint8_t *src_data, size_t src_size, uint32_t src_width, uint32_t src_height, FFmpegMuxContext &mux)
Encode one raw pixel frame into the mux context.
bool is_valid() const
True if codec, scaler, and scratch frame are all ready.
bool drain_packets(FFmpegMuxContext &mux)
void close()
Release all owned resources.
SwsContext * sws_context
Owned; freed in destructor.
AVCodecContext * codec_context
Owned; freed in destructor.
@ FileIO
Filesystem I/O operations.
@ IO
Networking, file handling, streaming.