MXVK Vulkan Framework 0.33.1
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
mxwrite.cpp
Go to the documentation of this file.
1#include "mxwrite.hpp"
2#include <algorithm>
3#include <cctype>
4#include <cmath>
5#include <cstdio>
6#include <cstring>
7#include <iomanip>
8#include <iostream>
9#include <numeric>
10#include <set>
11#include <sstream>
12#include <string>
13#include <thread>
14#include <utility>
15#ifdef MXWRITE_HAS_CUDA_COPY
16#include <cuda_runtime.h>
17#endif
18extern "C" {
19#include <libavcodec/avcodec.h>
20#include <libavformat/avformat.h>
21#include <libavutil/imgutils.h>
22#include <libavutil/mastering_display_metadata.h>
23#include <libavutil/mathematics.h>
24#include <libavutil/opt.h>
25#include <libavutil/pixdesc.h>
26#include <libswscale/swscale.h>
27}
28
29namespace {
30
31 // --- HDR helpers -----------------------------------------------------------
32 // SMPTE ST.2084 (PQ) constants.
33 constexpr float kPqM1 = 2610.0f / 16384.0f;
34 constexpr float kPqM2 = (2523.0f / 4096.0f) * 128.0f;
35 constexpr float kPqC1 = 3424.0f / 4096.0f;
36 constexpr float kPqC2 = (2413.0f / 4096.0f) * 32.0f;
37 constexpr float kPqC3 = (2392.0f / 4096.0f) * 32.0f;
38 // SDR reference white as a fraction of PQ peak (100 nits / 10000 nits).
39 constexpr float kSdrRefFraction = 100.0f / 10000.0f;
40
41 inline float srgbEotf(float v) {
42 // sRGB non-linear -> linear light.
43 return (v <= 0.04045f) ? (v / 12.92f)
44 : std::pow((v + 0.055f) / 1.055f, 2.4f);
45 }
46
47 inline float pqOetf(float L) {
48 // L in [0,1] where 1.0 == 10000 nits; returns PQ code value in [0,1].
49 const float Lm = std::pow(std::max(0.0f, L), kPqM1);
50 const float num = kPqC1 + kPqC2 * Lm;
51 const float den = 1.0f + kPqC3 * Lm;
52 return std::pow(num / den, kPqM2);
53 }
54
55 inline uint16_t clamp10(float v) {
56 if (v < 0.0f)
57 v = 0.0f;
58 if (v > 1023.0f)
59 v = 1023.0f;
60 return static_cast<uint16_t>(v + 0.5f);
61 }
62
63 // Convert one RGBA8 row pair + 2 UV rows into BT.2020 PQ YUV420P10LE.
64 // Assumes RGBA input is sRGB-gamma-encoded BT.709 SDR (which is what the
65 // shader pipeline produces for HDR inputs after the 8-bit swscale path).
66 // Output is limited-range 10-bit. Y: [64..940], UV: [64..960] centered at 512.
67 void convertRgbaToBt2020PqYuv420p10(const uint8_t *rgba,
68 int src_stride_bytes,
69 uint16_t *y_plane, int y_stride_shorts,
70 uint16_t *u_plane, int u_stride_shorts,
71 uint16_t *v_plane, int v_stride_shorts,
72 int width, int height) {
73 // BT.2020 non-constant luminance RGB->YUV (limited range).
74 // E'Y = 0.2627*R + 0.6780*G + 0.0593*B
75 // E'Pb = (B - Y) / 1.8814
76 // E'Pr = (R - Y) / 1.4746
77 // Limited 10-bit: Y: 0..1 -> 64..940 (range 876), UV: -0.5..0.5 -> 64..960 (range 896, center 512).
78 constexpr float kKr = 0.2627f;
79 constexpr float kKg = 0.6780f;
80 constexpr float kKb = 0.0593f;
81 constexpr float kPbDiv = 1.0f / 1.8814f;
82 constexpr float kPrDiv = 1.0f / 1.4746f;
83
84 for (int y = 0; y < height; y += 2) {
85 const int y1 = std::min(y + 1, height - 1);
86 const uint8_t *row0 = rgba + y * src_stride_bytes;
87 const uint8_t *row1 = rgba + y1 * src_stride_bytes;
88 uint16_t *yr0 = y_plane + y * y_stride_shorts;
89 uint16_t *yr1 = y_plane + y1 * y_stride_shorts;
90 uint16_t *ur = u_plane + (y / 2) * u_stride_shorts;
91 uint16_t *vr = v_plane + (y / 2) * v_stride_shorts;
92
93 for (int x = 0; x < width; x += 2) {
94 const int x1 = std::min(x + 1, width - 1);
95
96 // Load 2x2 block of sRGB 8-bit pixels.
97 auto loadPq = [](const uint8_t *px,
98 float &Y, float &U, float &V) {
99 // sRGB 8-bit -> linear [0,1]
100 const float r = srgbEotf(px[0] * (1.0f / 255.0f));
101 const float g = srgbEotf(px[1] * (1.0f / 255.0f));
102 const float b = srgbEotf(px[2] * (1.0f / 255.0f));
103 // Scale SDR linear [0,1] (reference 100 nits) to PQ fractional.
104 const float rL = r * kSdrRefFraction;
105 const float gL = g * kSdrRefFraction;
106 const float bL = b * kSdrRefFraction;
107 // PQ encode per channel (RGB PQ).
108 const float rp = pqOetf(rL);
109 const float gp = pqOetf(gL);
110 const float bp = pqOetf(bL);
111 // BT.2020 RGB' -> YUV'.
112 Y = kKr * rp + kKg * gp + kKb * bp;
113 U = (bp - Y) * kPbDiv;
114 V = (rp - Y) * kPrDiv;
115 };
116
117 float Y00, U00, V00;
118 float Y01, U01, V01;
119 float Y10, U10, V10;
120 float Y11, U11, V11;
121 loadPq(row0 + x * 4, Y00, U00, V00);
122 loadPq(row0 + x1 * 4, Y01, U01, V01);
123 loadPq(row1 + x * 4, Y10, U10, V10);
124 loadPq(row1 + x1 * 4, Y11, U11, V11);
125
126 // Y: per-pixel, limited-range 10-bit.
127 yr0[x] = clamp10(Y00 * 876.0f + 64.0f);
128 yr0[x1] = clamp10(Y01 * 876.0f + 64.0f);
129 yr1[x] = clamp10(Y10 * 876.0f + 64.0f);
130 yr1[x1] = clamp10(Y11 * 876.0f + 64.0f);
131
132 // UV: 4:2:0 average of 2x2 block.
133 const float Uavg = 0.25f * (U00 + U01 + U10 + U11);
134 const float Vavg = 0.25f * (V00 + V01 + V10 + V11);
135 ur[x / 2] = clamp10(Uavg * 896.0f + 512.0f);
136 vr[x / 2] = clamp10(Vavg * 896.0f + 512.0f);
137 }
138 }
139 }
140
141 // Convert 16-bit RGBA (already BT.2020-primaries, PQ- or HLG-encoded) into
142 // BT.2020 YUV420P10LE limited-range. No transfer conversion is applied here
143 // because the GPU HDR encode pass already produced the non-linear signal.
144 // @c rgba is tightly-packed 16-bit (8 bytes/pixel), @c src_stride_shorts is
145 // the row stride in 16-bit samples (i.e. bytes/2).
146 void convertBt2020Rgba16EncodedToYuv420p10(const uint16_t *rgba,
147 int src_stride_shorts,
148 uint16_t *y_plane, int y_stride_shorts,
149 uint16_t *u_plane, int u_stride_shorts,
150 uint16_t *v_plane, int v_stride_shorts,
151 int width, int height) {
152 constexpr float kKr = 0.2627f;
153 constexpr float kKg = 0.6780f;
154 constexpr float kKb = 0.0593f;
155 constexpr float kPbDiv = 1.0f / 1.8814f;
156 constexpr float kPrDiv = 1.0f / 1.4746f;
157 constexpr float kInv65535 = 1.0f / 65535.0f;
158
159 for (int y = 0; y < height; y += 2) {
160 const int y1 = std::min(y + 1, height - 1);
161 const uint16_t *row0 = rgba + y * src_stride_shorts;
162 const uint16_t *row1 = rgba + y1 * src_stride_shorts;
163 uint16_t *yr0 = y_plane + y * y_stride_shorts;
164 uint16_t *yr1 = y_plane + y1 * y_stride_shorts;
165 uint16_t *ur = u_plane + (y / 2) * u_stride_shorts;
166 uint16_t *vr = v_plane + (y / 2) * v_stride_shorts;
167
168 for (int x = 0; x < width; x += 2) {
169 const int x1 = std::min(x + 1, width - 1);
170
171 auto load = [&](const uint16_t *px, float &Y, float &U, float &V) {
172 const float rp = px[0] * kInv65535;
173 const float gp = px[1] * kInv65535;
174 const float bp = px[2] * kInv65535;
175 Y = kKr * rp + kKg * gp + kKb * bp;
176 U = (bp - Y) * kPbDiv;
177 V = (rp - Y) * kPrDiv;
178 };
179
180 float Y00, U00, V00;
181 float Y01, U01, V01;
182 float Y10, U10, V10;
183 float Y11, U11, V11;
184 load(row0 + x * 4, Y00, U00, V00);
185 load(row0 + x1 * 4, Y01, U01, V01);
186 load(row1 + x * 4, Y10, U10, V10);
187 load(row1 + x1 * 4, Y11, U11, V11);
188
189 yr0[x] = clamp10(Y00 * 876.0f + 64.0f);
190 yr0[x1] = clamp10(Y01 * 876.0f + 64.0f);
191 yr1[x] = clamp10(Y10 * 876.0f + 64.0f);
192 yr1[x1] = clamp10(Y11 * 876.0f + 64.0f);
193
194 const float Uavg = 0.25f * (U00 + U01 + U10 + U11);
195 const float Vavg = 0.25f * (V00 + V01 + V10 + V11);
196 ur[x / 2] = clamp10(Uavg * 896.0f + 512.0f);
197 vr[x / 2] = clamp10(Vavg * 896.0f + 512.0f);
198 }
199 }
200 }
201
202} // namespace
203
205
206bool is_format_supported(const char *filename) {
207 const char *ext = strrchr(filename, '.');
208 if (!ext)
209 return false;
210 // Lowercase the extension for case-insensitive comparison.
211 std::string lower_ext(ext);
212 std::transform(lower_ext.begin(), lower_ext.end(), lower_ext.begin(),
213 [](unsigned char c) { return std::tolower(c); });
214 static const char *kSupported[] = {
215 ".mp4", ".mkv", ".mov", ".avi", ".m4v",
216 ".ts", ".mts", ".m2ts", ".mpg", ".mpeg",
217 ".flv", ".f4v", ".3gp", ".3g2", ".wmv",
218 ".asf", ".vob"};
219 for (const char *s : kSupported) {
220 if (lower_ext == s)
221 return true;
222 }
223 return false;
224}
225
226void cleanup_contexts(AVFormatContext *source_ctx,
227 AVFormatContext *dest_ctx,
228 AVFormatContext *output_ctx) {
229 if (source_ctx)
230 avformat_close_input(&source_ctx);
231 if (dest_ctx)
232 avformat_close_input(&dest_ctx);
233 if (output_ctx) {
234 if (!(output_ctx->oformat->flags & AVFMT_NOFILE))
235 avio_closep(&output_ctx->pb);
236 avformat_free_context(output_ctx);
237 }
238}
239
240void transfer_audio(std::string_view sourceAudioFile, std::string_view destVideoFile) {
241 std::lock_guard<std::mutex> lock(transfer_audio_mutex);
242 if (!is_format_supported(destVideoFile.data())) {
243 std::cerr << "Unsupported output format. Supported formats: .mp4, .mkv, .avi, .mov\n";
244 return;
245 }
246
247 AVFormatContext *source_ctx = nullptr, *dest_ctx = nullptr, *output_ctx = nullptr;
248 int source_audio_idx = -1, dest_video_idx = -1, dest_audio_idx = -1;
249 std::string temp_output = std::string(destVideoFile) + ".tmp";
250
251 if (avformat_open_input(&source_ctx, sourceAudioFile.data(), nullptr, nullptr) != 0 ||
252 avformat_open_input(&dest_ctx, destVideoFile.data(), nullptr, nullptr) != 0) {
253 std::cerr << "Failed to open input files\n";
254 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
255 return;
256 }
257
258 if (avformat_find_stream_info(source_ctx, nullptr) < 0 ||
259 avformat_find_stream_info(dest_ctx, nullptr) < 0) {
260 std::cerr << "Failed to find stream info\n";
261 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
262 return;
263 }
264
265 for (unsigned i = 0; i < source_ctx->nb_streams; ++i) {
266 if (source_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
267 source_audio_idx = i;
268 break;
269 }
270 }
271
272 for (unsigned i = 0; i < dest_ctx->nb_streams; ++i) {
273 AVMediaType type = dest_ctx->streams[i]->codecpar->codec_type;
274 if (type == AVMEDIA_TYPE_VIDEO)
275 dest_video_idx = i;
276 else if (type == AVMEDIA_TYPE_AUDIO)
277 dest_audio_idx = i;
278 }
279
280 if (source_audio_idx == -1 || dest_video_idx == -1) {
281 std::cerr << "Required streams not found\n";
282 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
283 return;
284 }
285
286 const AVOutputFormat *output_fmt = av_guess_format(nullptr, destVideoFile.data(), nullptr);
287 if (!output_fmt) {
288 output_fmt = av_guess_format("mp4", nullptr, nullptr);
289 if (!output_fmt) {
290 std::cerr << "Failed to determine output format\n";
291 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
292 return;
293 }
294 }
295
296 if (avformat_alloc_output_context2(&output_ctx, output_fmt, nullptr, temp_output.c_str()) < 0) {
297 std::cerr << "Failed to create output context\n";
298 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
299 return;
300 }
301
302 const AVCodec *audio_codec = avcodec_find_decoder(source_ctx->streams[source_audio_idx]->codecpar->codec_id);
303 if (!audio_codec) {
304 std::cerr << "Failed to find audio decoder\n";
305 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
306 return;
307 }
308
309 for (unsigned i = 0; i < dest_ctx->nb_streams; ++i) {
310 if (dest_ctx->streams[i]->codecpar->codec_type != AVMEDIA_TYPE_VIDEO) {
311 continue;
312 }
313
314 AVStream *dest_stream = dest_ctx->streams[i];
315 AVStream *out_stream = avformat_new_stream(output_ctx, nullptr);
316 if (!out_stream) {
317 std::cerr << "Failed to create output stream\n";
318 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
319 return;
320 }
321
322 if (avcodec_parameters_copy(out_stream->codecpar, dest_stream->codecpar) < 0) {
323 std::cerr << "Failed to copy video parameters\n";
324 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
325 return;
326 }
327
328 out_stream->time_base = dest_stream->time_base;
329 out_stream->codecpar->codec_tag = 0;
330 }
331
332 AVStream *out_stream = avformat_new_stream(output_ctx, audio_codec);
333 if (!out_stream) {
334 std::cerr << "Failed to create audio stream\n";
335 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
336 return;
337 }
338
339 AVCodecParameters *source_params = source_ctx->streams[source_audio_idx]->codecpar;
340 if (avcodec_parameters_copy(out_stream->codecpar, source_params) < 0) {
341 std::cerr << "Failed to copy audio parameters\n";
342 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
343 return;
344 }
345
346 if (source_params->frame_size == 0) {
347 out_stream->codecpar->frame_size = 1024;
348 } else {
349 out_stream->codecpar->frame_size = source_params->frame_size;
350 }
351
352 out_stream->time_base = source_ctx->streams[source_audio_idx]->time_base;
353 out_stream->codecpar->codec_tag = 0;
354 dest_audio_idx = out_stream->index;
355
356 if (!(output_ctx->oformat->flags & AVFMT_NOFILE)) {
357 if (avio_open(&output_ctx->pb, temp_output.c_str(), AVIO_FLAG_WRITE) < 0) {
358 std::cerr << "Failed to open output file\n";
359 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
360 return;
361 }
362 }
363 if (avformat_write_header(output_ctx, nullptr) < 0) {
364 std::cerr << "Failed to write header\n";
365 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
366 return;
367 }
368 AVPacket packet;
369 while (av_read_frame(dest_ctx, &packet) >= 0) {
370 if (packet.stream_index == dest_audio_idx) {
371 av_packet_unref(&packet);
372 continue;
373 }
374
375 AVStream *in_stream = dest_ctx->streams[packet.stream_index];
376 AVStream *out_stream = output_ctx->streams[packet.stream_index];
377 av_packet_rescale_ts(&packet, in_stream->time_base, out_stream->time_base);
378
379 if (av_interleaved_write_frame(output_ctx, &packet) < 0) {
380 std::cerr << "Failed to write packet\n";
381 av_packet_unref(&packet);
382 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
383 return;
384 }
385 av_packet_unref(&packet);
386 }
387
388 int64_t video_duration_ts = 0;
389 {
390 AVStream *vid_stream = dest_ctx->streams[0];
391 if (vid_stream->duration > 0) {
392 video_duration_ts = av_rescale_q(vid_stream->duration, vid_stream->time_base, source_ctx->streams[source_audio_idx]->time_base);
393 } else if (dest_ctx->duration > 0) {
394 AVRational av_tb = {1, AV_TIME_BASE};
395 video_duration_ts = av_rescale_q(dest_ctx->duration, av_tb, source_ctx->streams[source_audio_idx]->time_base);
396 }
397 }
398
399 av_seek_frame(source_ctx, source_audio_idx, 0, AVSEEK_FLAG_BACKWARD);
400 while (av_read_frame(source_ctx, &packet) >= 0) {
401 if (packet.stream_index == source_audio_idx) {
402 if (video_duration_ts > 0 && packet.pts != AV_NOPTS_VALUE && packet.pts > video_duration_ts) {
403 av_packet_unref(&packet);
404 break;
405 }
406 AVStream *in_stream = source_ctx->streams[packet.stream_index];
407 AVStream *out_stream = output_ctx->streams[dest_audio_idx];
408 av_packet_rescale_ts(&packet, in_stream->time_base, out_stream->time_base);
409 packet.stream_index = dest_audio_idx;
410
411 if (av_interleaved_write_frame(output_ctx, &packet) < 0) {
412 std::cerr << "Failed to write audio packet\n";
413 av_packet_unref(&packet);
414 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
415 return;
416 }
417 }
418 av_packet_unref(&packet);
419 }
420 av_write_trailer(output_ctx);
421 cleanup_contexts(source_ctx, dest_ctx, output_ctx);
422 std::remove(destVideoFile.data());
423 std::rename(temp_output.c_str(), destVideoFile.data());
424}
425
426void Writer::calculateFPSFraction(float fps, int &fps_num, int &fps_den) {
427 const float epsilon = 0.001f;
428 fps_den = 1001;
429 if (std::fabs(fps - 29.97f) < epsilon) {
430 fps_num = 30000;
431 fps_den = 1001;
432 } else if (std::fabs(fps - 59.94f) < epsilon) {
433 fps_num = 60000;
434 fps_den = 1001;
435 } else {
436 float precision = 1000.0f;
437 fps_num = static_cast<int>(std::round(fps * precision));
438 fps_den = static_cast<int>(precision);
439 int gcd = std::gcd(fps_num, fps_den);
440 fps_num /= gcd;
441 fps_den /= gcd;
442 }
443}
444
445namespace {
446
447 // Map an x264-style preset name to an NVENC preset (p1..p7).
448 // p1 = fastest/lowest quality, p7 = slowest/highest quality.
449 const char *x264_preset_to_nvenc(const std::string &p) {
450 if (p == "ultrafast")
451 return "p1";
452 if (p == "superfast")
453 return "p2";
454 if (p == "veryfast" || p == "faster")
455 return "p3";
456 if (p == "fast")
457 return "p4";
458 if (p == "medium" || p.empty())
459 return "p5";
460 if (p == "slow" || p == "slower")
461 return "p6";
462 if (p == "veryslow")
463 return "p7";
464 // Allow passing NVENC preset names through directly.
465 return p.c_str();
466 }
467
468 bool is_valid_x264_preset(const std::string &p) {
469 static const char *presets[] = {
470 "ultrafast", "superfast", "veryfast", "faster", "fast",
471 "medium", "slow", "slower", "veryslow", "placebo"};
472 for (const char *n : presets)
473 if (p == n)
474 return true;
475 return false;
476 }
477
478 std::string lowercase_ascii(std::string text) {
479 std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) {
480 return static_cast<char>(std::tolower(ch));
481 });
482 return text;
483 }
484
486 std::string name;
487 std::string value;
488 };
489
490 int option_base_type(AVOptionType type) {
491#if LIBAVUTIL_VERSION_MAJOR >= 60
492 return static_cast<int>(type) & ~AV_OPT_TYPE_FLAG_ARRAY;
493#else
494 return static_cast<int>(type);
495#endif
496 }
497
498 bool looks_like_option(const std::string &token) {
499 if (token.size() < 2 || token.front() != '-') {
500 return false;
501 }
502 const unsigned char next = static_cast<unsigned char>(token[1]);
503 return !std::isdigit(next) && token[1] != '.';
504 }
505
506 bool tokenize_ffmpeg_options(const std::string &text, std::vector<std::string> &tokens,
507 std::string &error) {
508 std::string token;
509 bool token_started = false;
510 bool escaped = false;
511 char quote = '\0';
512
513 for (char ch : text) {
514 if (escaped) {
515 token.push_back(ch);
516 token_started = true;
517 escaped = false;
518 continue;
519 }
520 if (ch == '\\' && quote != '\'') {
521 escaped = true;
522 token_started = true;
523 continue;
524 }
525 if (quote != '\0') {
526 if (ch == quote) {
527 quote = '\0';
528 } else {
529 token.push_back(ch);
530 }
531 token_started = true;
532 continue;
533 }
534 if (ch == '\'' || ch == '"') {
535 quote = ch;
536 token_started = true;
537 } else if (std::isspace(static_cast<unsigned char>(ch))) {
538 if (token_started) {
539 tokens.push_back(token);
540 token.clear();
541 token_started = false;
542 }
543 } else {
544 token.push_back(ch);
545 token_started = true;
546 }
547 }
548
549 if (escaped) {
550 error = "trailing escape character";
551 return false;
552 }
553 if (quote != '\0') {
554 error = "unterminated quote";
555 return false;
556 }
557 if (token_started) {
558 tokens.push_back(token);
559 }
560 return true;
561 }
562
563 std::string normalize_ffmpeg_option_name(std::string name) {
564 while (!name.empty() && name.front() == '-') {
565 name.erase(name.begin());
566 }
567
568 const size_t stream_specifier = name.find(':');
569 if (stream_specifier != std::string::npos) {
570 const std::string suffix = name.substr(stream_specifier + 1);
571 if (suffix == "v" || suffix.starts_with("v:")) {
572 name.erase(stream_specifier);
573 }
574 }
575 return name;
576 }
577
578 bool is_codec_name(const std::string &value) {
579 const std::string codec = lowercase_ascii(value);
580 return codec == "h265_nvenc" || codec == "h264" || codec == "h265" ||
581 codec == "hevc" || codec == "nvenc" || codec == "software" || codec == "auto" ||
582 avcodec_find_encoder_by_name(codec.c_str()) != nullptr;
583 }
584
585 bool parse_ffmpeg_options(const std::string &text, std::vector<FfmpegOption> &options) {
586 std::vector<std::string> tokens;
587 std::string error;
588 if (!tokenize_ffmpeg_options(text, tokens, error)) {
589 std::cerr << "MXWrite: invalid extra FFmpeg parameters: " << error << ".\n";
590 return false;
591 }
592
593 for (size_t index = 0; index < tokens.size(); ++index) {
594 std::string token = tokens[index];
595 if (!looks_like_option(token)) {
596 if (index == 0 && is_codec_name(token)) {
597 options.push_back({"codec", token});
598 } else {
599 std::cerr << "MXWrite: ignoring non-option extra parameter '" << token
600 << "' (filenames are selected by Writer::open).\n";
601 }
602 continue;
603 }
604
605 while (!token.empty() && token.front() == '-') {
606 token.erase(token.begin());
607 }
608 const size_t equals = token.find('=');
609 std::string name = normalize_ffmpeg_option_name(token.substr(0, equals));
610 std::string value;
611 if (equals != std::string::npos) {
612 value = token.substr(equals + 1);
613 } else if (index + 1 < tokens.size() && !looks_like_option(tokens[index + 1])) {
614 value = tokens[++index];
615 } else {
616 value = "1";
617 }
618
619 if (name.empty()) {
620 std::cerr << "MXWrite: invalid empty FFmpeg option name.\n";
621 return false;
622 }
623 options.push_back({std::move(name), std::move(value)});
624 }
625 return true;
626 }
627
628 const std::string *find_ffmpeg_option(const std::vector<FfmpegOption> &options,
629 const std::string &name) {
630 for (auto option = options.rbegin(); option != options.rend(); ++option) {
631 if (option->name == name) {
632 return &option->value;
633 }
634 }
635 return nullptr;
636 }
637
638 bool is_reserved_ffmpeg_option(const std::string &name) {
639 return name == "c" || name == "codec" || name == "vcodec" || name == "pix_fmt" ||
640 name == "pixel_format";
641 }
642
643 bool apply_ffmpeg_options(const std::vector<FfmpegOption> &options, AVCodecContext *context,
644 AVFormatContext *output_context) {
645 for (const FfmpegOption &option : options) {
646 if (is_reserved_ffmpeg_option(option.name)) {
647 continue;
648 }
649
650 int result = AVERROR_OPTION_NOT_FOUND;
651 if (context->priv_data) {
652 result = av_opt_set(context->priv_data, option.name.c_str(), option.value.c_str(), 0);
653 }
654 if (result == AVERROR_OPTION_NOT_FOUND) {
655 result = av_opt_set(context, option.name.c_str(), option.value.c_str(), 0);
656 }
657 if (result == AVERROR_OPTION_NOT_FOUND && output_context->priv_data) {
658 result = av_opt_set(output_context->priv_data, option.name.c_str(), option.value.c_str(), 0);
659 }
660 if (result < 0) {
661 char error_text[AV_ERROR_MAX_STRING_SIZE] = {};
662 av_strerror(result, error_text, sizeof(error_text));
663 std::cerr << "MXWrite: FFmpeg option '-" << option.name << " " << option.value
664 << "' was rejected: " << error_text << ".\n";
665 return false;
666 }
667 }
668 return true;
669 }
670
671 bool set_named_encoder_option(void *object, const char *name, const std::string &value) {
672 if (!object || value.empty()) {
673 return false;
674 }
675 const AVOption *option = av_opt_find(object, name, nullptr, 0, 0);
676 if (!option) {
677 return false;
678 }
679 const int base_type = option_base_type(option->type);
680 const bool textual = base_type == AV_OPT_TYPE_STRING ||
681 base_type == AV_OPT_TYPE_DICT ||
682 base_type == AV_OPT_TYPE_BINARY;
683 const bool numeric_text = std::isdigit(static_cast<unsigned char>(value.front())) ||
684 value.front() == '-' || value.front() == '+' ||
685 value.front() == '.';
686 if (!textual && !numeric_text) {
687 const AVOption *named_value = av_opt_find(object, value.c_str(), option->unit, 0, 0);
688 if (!named_value || named_value->type != AV_OPT_TYPE_CONST) {
689 return false;
690 }
691 }
692 return av_opt_set(object, name, value.c_str(), 0) >= 0;
693 }
694
695 bool is_nvenc_tune(const std::string &tune) {
696 return tune == "hq" || tune == "uhq" || tune == "ll" || tune == "ull" ||
697 tune == "lossless";
698 }
699
700 std::string nvenc_preset_to_software(const std::string &preset) {
701 if (preset == "p1")
702 return "ultrafast";
703 if (preset == "p2")
704 return "superfast";
705 if (preset == "p3")
706 return "veryfast";
707 if (preset == "p4")
708 return "fast";
709 if (preset == "p5")
710 return "medium";
711 if (preset == "p6")
712 return "slow";
713 if (preset == "p7")
714 return "veryslow";
715 return preset;
716 }
717
718 std::vector<FfmpegOption> software_fallback_options(
719 const std::vector<FfmpegOption> &options, bool use_hevc_codec) {
720 std::vector<FfmpegOption> translated;
721 bool enable_lossless = false;
722
723 for (const FfmpegOption &option : options) {
724 FfmpegOption value = option;
725 if (value.name == "preset") {
727 } else if (value.name == "tune") {
728 const std::string tune = lowercase_ascii(value.value);
729 if (tune == "lossless") {
730 enable_lossless = true;
731 continue;
732 }
733 if (tune == "hq" || tune == "uhq") {
734 continue;
735 }
736 if (tune == "ll" || tune == "ull") {
737 value.value = "zerolatency";
738 }
739 } else if (value.name == "profile" && lowercase_ascii(value.value) == "rext") {
740 value.value = use_hevc_codec ? "main444-8" : "high444";
741 } else if (value.name == "rgb_mode" || value.name == "rc") {
742 continue;
743 } else if (value.name == "cq") {
744 value.name = "crf";
745 }
746 translated.push_back(std::move(value));
747 }
748
749 if (enable_lossless) {
750 if (use_hevc_codec) {
751 bool appended = false;
752 for (FfmpegOption &option : translated) {
753 if (option.name == "x265-params") {
754 if (!option.value.empty())
755 option.value += ':';
756 option.value += "lossless=1";
757 appended = true;
758 }
759 }
760 if (!appended) {
761 translated.push_back({"x265-params", "lossless=1"});
762 }
763 } else {
764 translated.push_back({"qp", "0"});
765 }
766 }
767 return translated;
768 }
769
770 std::vector<AVPixelFormat> supported_pixel_formats(const AVCodec *codec) {
771 std::vector<AVPixelFormat> formats;
772 if (!codec) {
773 return formats;
774 }
775
776#if LIBAVCODEC_VERSION_MAJOR >= 61
777 const void *configurations = nullptr;
778 int configuration_count = 0;
779 if (avcodec_get_supported_config(nullptr, codec, AV_CODEC_CONFIG_PIX_FORMAT, 0,
780 &configurations, &configuration_count) >= 0 &&
781 configurations) {
782 const auto *pixel_formats = static_cast<const AVPixelFormat *>(configurations);
783 formats.assign(pixel_formats, pixel_formats + configuration_count);
784 }
785#else
786 if (codec->pix_fmts) {
787 for (const AVPixelFormat *format = codec->pix_fmts;
788 *format != AV_PIX_FMT_NONE; ++format) {
789 formats.push_back(*format);
790 }
791 }
792#endif
793 return formats;
794 }
795
796 bool is_hardware_pixel_format(AVPixelFormat format) {
797 const AVPixFmtDescriptor *descriptor = av_pix_fmt_desc_get(format);
798 return descriptor && (descriptor->flags & AV_PIX_FMT_FLAG_HWACCEL) != 0;
799 }
800
801 AVPixelFormat choose_software_pixel_format(const AVCodec *codec,
802 AVPixelFormat requested_format) {
803 const std::vector<AVPixelFormat> formats = supported_pixel_formats(codec);
804 auto is_usable = [](AVPixelFormat format) {
805 return !is_hardware_pixel_format(format) && sws_isSupportedOutput(format);
806 };
807
808 if (requested_format != AV_PIX_FMT_NONE) {
809 if (!is_usable(requested_format)) {
810 return AV_PIX_FMT_NONE;
811 }
812 if (formats.empty() ||
813 std::find(formats.begin(), formats.end(), requested_format) != formats.end()) {
814 return requested_format;
815 }
816 return AV_PIX_FMT_NONE;
817 }
818
819 static constexpr AVPixelFormat preferred_formats[] = {
820 AV_PIX_FMT_YUV420P, AV_PIX_FMT_NV12, AV_PIX_FMT_YUV422P,
821 AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUV420P10LE, AV_PIX_FMT_YUV422P10LE,
822 AV_PIX_FMT_YUV444P10LE, AV_PIX_FMT_BGRA, AV_PIX_FMT_RGBA};
823 if (formats.empty()) {
824 return AV_PIX_FMT_YUV420P;
825 }
826 for (AVPixelFormat preferred : preferred_formats) {
827 if (std::find(formats.begin(), formats.end(), preferred) != formats.end() &&
828 is_usable(preferred)) {
829 return preferred;
830 }
831 }
832 for (AVPixelFormat format : formats) {
833 if (is_usable(format)) {
834 return format;
835 }
836 }
837 return AV_PIX_FMT_NONE;
838 }
839
840 bool codec_uses_hardware(const AVCodec *codec) {
841 if (!codec) {
842 return false;
843 }
844 if ((codec->capabilities & (AV_CODEC_CAP_HARDWARE | AV_CODEC_CAP_HYBRID)) != 0) {
845 return true;
846 }
847 for (int index = 0; avcodec_get_hw_config(codec, index); ++index) {
848 return true;
849 }
850 const std::string name = codec->name ? lowercase_ascii(codec->name) : std::string{};
851 static constexpr std::string_view hardware_markers[] = {
852 "_nvenc", "_qsv", "_vaapi", "_amf", "_v4l2m2m", "_videotoolbox",
853 "_mediafoundation", "_vulkan"};
854 return std::any_of(std::begin(hardware_markers), std::end(hardware_markers),
855 [&name](std::string_view marker) {
856 return name.find(marker) != std::string::npos;
857 });
858 }
859
860 std::string option_type_name(AVOptionType type) {
861 const int base_type = option_base_type(type);
862 switch (base_type) {
863 case AV_OPT_TYPE_FLAGS:
864 return "flags";
865 case AV_OPT_TYPE_INT:
866 return "integer";
867 case AV_OPT_TYPE_INT64:
868 return "integer64";
869 case AV_OPT_TYPE_DOUBLE:
870 return "double";
871 case AV_OPT_TYPE_FLOAT:
872 return "float";
873 case AV_OPT_TYPE_STRING:
874 return "string";
875 case AV_OPT_TYPE_RATIONAL:
876 return "rational";
877 case AV_OPT_TYPE_BINARY:
878 return "binary";
879 case AV_OPT_TYPE_DICT:
880 return "dictionary";
881 case AV_OPT_TYPE_UINT64:
882 return "unsigned64";
883 case AV_OPT_TYPE_IMAGE_SIZE:
884 return "size";
885 case AV_OPT_TYPE_PIXEL_FMT:
886 return "pixel-format";
887 case AV_OPT_TYPE_SAMPLE_FMT:
888 return "sample-format";
889 case AV_OPT_TYPE_VIDEO_RATE:
890 return "frame-rate";
891 case AV_OPT_TYPE_DURATION:
892 return "duration";
893 case AV_OPT_TYPE_COLOR:
894 return "color";
895 case AV_OPT_TYPE_BOOL:
896 return "boolean";
897#if LIBAVUTIL_VERSION_MAJOR >= 57
898 case AV_OPT_TYPE_CHLAYOUT:
899#else
900 case AV_OPT_TYPE_CHANNEL_LAYOUT:
901#endif
902 return "channel-layout";
903#if LIBAVUTIL_VERSION_MAJOR >= 60
904 case AV_OPT_TYPE_UINT:
905 return "unsigned";
906#endif
907 default:
908 return "value";
909 }
910 }
911
912 bool option_has_numeric_range(AVOptionType type) {
913 const int base_type = option_base_type(type);
914 return base_type == AV_OPT_TYPE_FLAGS || base_type == AV_OPT_TYPE_INT ||
915 base_type == AV_OPT_TYPE_INT64 || base_type == AV_OPT_TYPE_DOUBLE ||
916 base_type == AV_OPT_TYPE_FLOAT || base_type == AV_OPT_TYPE_UINT64 ||
917 base_type == AV_OPT_TYPE_BOOL
918#if LIBAVUTIL_VERSION_MAJOR >= 60
919 || base_type == AV_OPT_TYPE_UINT
920#endif
921 ;
922 }
923
924 std::string number_text(double value) {
925 std::ostringstream stream;
926 stream << std::setprecision(12) << value;
927 return stream.str();
928 }
929
930 void append_encoder_options(void *object, std::vector<EncoderOptionInfo> &result,
931 std::set<std::string> &seen) {
932 if (!object) {
933 return;
934 }
935 for (const AVOption *option = nullptr; (option = av_opt_next(object, option));) {
936 if (option->type == AV_OPT_TYPE_CONST ||
937 (option->flags & AV_OPT_FLAG_ENCODING_PARAM) == 0 ||
938 (option->flags & AV_OPT_FLAG_VIDEO_PARAM) == 0 ||
939 (option->flags & (AV_OPT_FLAG_READONLY | AV_OPT_FLAG_DEPRECATED)) != 0 ||
940 !option->name || !seen.insert(option->name).second) {
941 continue;
942 }
943
945 info.name = option->name;
946 info.type = option_type_name(option->type);
947 info.help = option->help ? option->help : "";
948 if (option_has_numeric_range(option->type)) {
949 info.minimum = number_text(option->min);
950 info.maximum = number_text(option->max);
951 }
952
953 uint8_t *value = nullptr;
954 if (av_opt_get(object, option->name, 0, &value) >= 0 && value) {
955 info.default_value = reinterpret_cast<char *>(value);
956 }
957 av_free(value);
958
959 if (option->unit) {
960 std::vector<std::string> choices;
961 for (const AVOption *choice = nullptr; (choice = av_opt_next(object, choice));) {
962 if (choice->type == AV_OPT_TYPE_CONST && choice->unit && choice->name &&
963 std::string_view(choice->unit) == option->unit) {
964 choices.emplace_back(choice->name);
965 }
966 }
967 for (size_t index = 0; index < choices.size(); ++index) {
968 if (index > 0) {
969 info.choices += ", ";
970 }
971 info.choices += choices[index];
972 }
973 }
974 result.push_back(std::move(info));
975 }
976 }
977
978} // namespace
979
980std::vector<EncoderInfo> available_video_encoders() {
981 std::vector<EncoderInfo> result;
982 void *iterator = nullptr;
983 while (const AVCodec *codec = av_codec_iterate(&iterator)) {
984 if (!av_codec_is_encoder(codec) || codec->type != AVMEDIA_TYPE_VIDEO || !codec->name) {
985 continue;
986 }
987 EncoderInfo info;
988 info.name = codec->name;
989 info.long_name = codec->long_name ? codec->long_name : codec->name;
990 info.codec_name = avcodec_get_name(codec->id);
991 info.hardware = codec_uses_hardware(codec);
992 info.experimental = (codec->capabilities & AV_CODEC_CAP_EXPERIMENTAL) != 0;
993 const std::vector<AVPixelFormat> formats = supported_pixel_formats(codec);
994 for (AVPixelFormat format : formats) {
995 const char *name = av_get_pix_fmt_name(format);
996 if (!name) {
997 continue;
998 }
999 if (!info.pixel_formats.empty()) {
1000 info.pixel_formats += ", ";
1001 }
1002 info.pixel_formats += name;
1003 }
1004 result.push_back(std::move(info));
1005 }
1006 std::sort(result.begin(), result.end(), [](const EncoderInfo &left, const EncoderInfo &right) {
1007 if (left.codec_name != right.codec_name) {
1008 return left.codec_name < right.codec_name;
1009 }
1010 if (left.hardware != right.hardware) {
1011 return left.hardware > right.hardware;
1012 }
1013 return left.name < right.name;
1014 });
1015 return result;
1016}
1017
1018std::vector<EncoderOptionInfo> video_encoder_options(std::string_view encoder_name) {
1019 const std::string name(encoder_name);
1020 const AVCodec *codec = avcodec_find_encoder_by_name(name.c_str());
1021 if (!codec || codec->type != AVMEDIA_TYPE_VIDEO) {
1022 return {};
1023 }
1024 AVCodecContext *context = avcodec_alloc_context3(codec);
1025 if (!context) {
1026 return {};
1027 }
1028 std::vector<EncoderOptionInfo> result;
1029 std::set<std::string> seen;
1030 append_encoder_options(context->priv_data, result, seen);
1031 append_encoder_options(context, result, seen);
1032 avcodec_free_context(&context);
1033 std::sort(result.begin(), result.end(), [](const EncoderOptionInfo &left, const EncoderOptionInfo &right) {
1034 return left.name < right.name;
1035 });
1036 return result;
1037}
1038
1039bool Writer::open(const std::string &filename, int w, int h, float fps, const char *crf) {
1040 std::lock_guard<std::mutex> lock(writer_mutex);
1041 EncodeOptions opts;
1042 if (crf && *crf) {
1043 try {
1044 opts.crf = std::stoi(crf);
1045 } catch (...) {
1046 }
1047 }
1048 // Preserve legacy low-latency behaviour for old callers.
1049 opts.preset = "ultrafast";
1050 opts.tune = "zerolatency";
1051 opts.realtime = true;
1052 return openInternal(filename, w, h, fps, opts, false);
1053}
1054
1055bool Writer::open(const std::string &filename, int w, int h, float fps, const EncodeOptions &opts) {
1056 std::lock_guard<std::mutex> lock(writer_mutex);
1057 return openInternal(filename, w, h, fps, opts, false);
1058}
1059
1060bool Writer::open_ts(const std::string &filename, int w, int h, float fps, const char *crf) {
1061 std::lock_guard<std::mutex> lock(writer_mutex);
1062 EncodeOptions opts;
1063 if (crf && *crf) {
1064 try {
1065 opts.crf = std::stoi(crf);
1066 } catch (...) {
1067 }
1068 }
1069 opts.preset = "ultrafast";
1070 opts.tune = "zerolatency";
1071 opts.realtime = true;
1072 return openInternal(filename, w, h, fps, opts, true);
1073}
1074
1075bool Writer::open_ts(const std::string &filename, int w, int h, float fps, const EncodeOptions &opts) {
1076 std::lock_guard<std::mutex> lock(writer_mutex);
1077 return openInternal(filename, w, h, fps, opts, true);
1078}
1079
1080bool Writer::initHardwareEncoding(const AVCodec *codec, AVPixelFormat requested_format,
1081 bool prefer_cuda_rgba) {
1082 const AVCodecHWConfig *hardware_config = nullptr;
1083 for (int index = 0; const AVCodecHWConfig *config = avcodec_get_hw_config(codec, index);
1084 ++index) {
1085 if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX) == 0 ||
1086 config->device_type == AV_HWDEVICE_TYPE_NONE || config->pix_fmt == AV_PIX_FMT_NONE) {
1087 continue;
1088 }
1089 if (!hardware_config ||
1090 (prefer_cuda_rgba && config->device_type == AV_HWDEVICE_TYPE_CUDA)) {
1091 hardware_config = config;
1092 }
1093 if (prefer_cuda_rgba && config->device_type == AV_HWDEVICE_TYPE_CUDA) {
1094 break;
1095 }
1096 }
1097 if (!hardware_config ||
1098 av_hwdevice_ctx_create(&hw_device_ctx, hardware_config->device_type,
1099 nullptr, nullptr, 0) < 0) {
1100 return false;
1101 }
1102
1103 codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
1104 codec_ctx->pix_fmt = hardware_config->pix_fmt;
1105
1106 hw_frames_ctx = av_hwframe_ctx_alloc(hw_device_ctx);
1107 if (!hw_frames_ctx) {
1108 return false;
1109 }
1110
1111 auto *frames_ctx = reinterpret_cast<AVHWFramesContext *>(hw_frames_ctx->data);
1112 frames_ctx->format = hardware_config->pix_fmt;
1113
1114 AVHWFramesConstraints *constraints =
1115 av_hwdevice_get_hwframe_constraints(hw_device_ctx, nullptr);
1116 auto valid_software_format = [constraints](AVPixelFormat format) {
1117 if (format == AV_PIX_FMT_NONE || is_hardware_pixel_format(format) ||
1118 !sws_isSupportedOutput(format)) {
1119 return false;
1120 }
1121 if (!constraints || !constraints->valid_sw_formats) {
1122 return true;
1123 }
1124 for (const AVPixelFormat *valid = constraints->valid_sw_formats;
1125 *valid != AV_PIX_FMT_NONE; ++valid) {
1126 if (*valid == format) {
1127 return true;
1128 }
1129 }
1130 return false;
1131 };
1132
1133 AVPixelFormat upload_format = AV_PIX_FMT_NONE;
1134 if (prefer_cuda_rgba && valid_software_format(AV_PIX_FMT_RGBA)) {
1135 upload_format = AV_PIX_FMT_RGBA;
1136 } else if (valid_software_format(requested_format)) {
1137 upload_format = requested_format;
1138 } else {
1139 static constexpr AVPixelFormat preferred_upload_formats[] = {
1140 AV_PIX_FMT_NV12, AV_PIX_FMT_YUV420P, AV_PIX_FMT_P010LE,
1141 AV_PIX_FMT_YUV420P10LE, AV_PIX_FMT_YUV422P, AV_PIX_FMT_BGRA,
1142 AV_PIX_FMT_RGBA};
1143 for (AVPixelFormat format : preferred_upload_formats) {
1144 if (valid_software_format(format)) {
1145 upload_format = format;
1146 break;
1147 }
1148 }
1149 if (upload_format == AV_PIX_FMT_NONE && constraints &&
1150 constraints->valid_sw_formats) {
1151 for (const AVPixelFormat *valid = constraints->valid_sw_formats;
1152 *valid != AV_PIX_FMT_NONE; ++valid) {
1153 if (valid_software_format(*valid)) {
1154 upload_format = *valid;
1155 break;
1156 }
1157 }
1158 }
1159 }
1160 av_hwframe_constraints_free(&constraints);
1161 if (upload_format == AV_PIX_FMT_NONE) {
1162 return false;
1163 }
1164
1165 frames_ctx->sw_format = upload_format;
1166 frames_ctx->width = width;
1167 frames_ctx->height = height;
1168 // Pool must comfortably exceed MAX_QUEUE_SIZE so av_hwframe_get_buffer()
1169 // on the producer thread never becomes the throttle. A few extra slots
1170 // cover frames currently in-flight inside the encoder.
1171 frames_ctx->initial_pool_size = static_cast<int>(MAX_QUEUE_SIZE) + 8;
1172
1173 if (av_hwframe_ctx_init(hw_frames_ctx) < 0) {
1174 return false;
1175 }
1176
1177 codec_ctx->hw_frames_ctx = av_buffer_ref(hw_frames_ctx);
1178 codec_ctx->sw_pix_fmt = upload_format;
1179
1180 upload_sw_frame = av_frame_alloc();
1181 if (!upload_sw_frame) {
1182 return false;
1183 }
1184 upload_sw_frame->format = upload_format;
1185 upload_sw_frame->width = width;
1186 upload_sw_frame->height = height;
1187 if (av_frame_get_buffer(upload_sw_frame, 32) < 0) {
1188 return false;
1189 }
1190
1191 if (upload_format != AV_PIX_FMT_RGBA) {
1192 sws_ctx = sws_getContext(width, height, AV_PIX_FMT_RGBA, width, height,
1193 upload_format, SWS_FAST_BILINEAR, nullptr,
1194 nullptr, nullptr);
1195 if (!sws_ctx) {
1196 return false;
1197 }
1198 }
1199
1200 direct_cuda_upload = hardware_config->device_type == AV_HWDEVICE_TYPE_CUDA &&
1201 upload_format == AV_PIX_FMT_RGBA;
1202
1203#ifdef MXWRITE_HAS_CUDA_COPY
1204 // Non-blocking stream so device-to-device uploads from write_cuda_rgba()
1205 // do not serialise against the renderer's CUDA work on the default stream.
1206 if (direct_cuda_upload && !cuda_upload_stream) {
1207 if (cudaStreamCreateWithFlags(&cuda_upload_stream, cudaStreamNonBlocking) != cudaSuccess) {
1208 cuda_upload_stream = nullptr;
1209 }
1210 }
1211#endif
1212
1213 return true;
1214}
1215
1216bool Writer::openInternal(const std::string &filename, int w, int h, float fps, const EncodeOptions &opts, bool ts_mode) {
1217 avformat_network_init();
1218 av_log_set_level(AV_LOG_ERROR);
1219 opened = false;
1220 active_encoder_hardware = false;
1221 stop_requested = false;
1222 frame_count = 0;
1223 last_duration = 0.0;
1224 bytes_written.store(0, std::memory_order_relaxed);
1225 block_when_full.store(opts.block_when_full, std::memory_order_relaxed);
1226
1227 while (!encode_queue.empty()) {
1228 releaseFrame(encode_queue.front());
1229 encode_queue.pop();
1230 }
1231
1232 std::vector<FfmpegOption> extra_options;
1233 if (!opts.ffmpeg_options.empty() &&
1234 !parse_ffmpeg_options(opts.ffmpeg_options, extra_options)) {
1235 return false;
1236 }
1237
1238 const std::string *codec_override = find_ffmpeg_option(extra_options, "c");
1239 if (!codec_override) {
1240 codec_override = find_ffmpeg_option(extra_options, "codec");
1241 }
1242 if (!codec_override) {
1243 codec_override = find_ffmpeg_option(extra_options, "vcodec");
1244 }
1245
1246 const std::string *pixel_format_option = find_ffmpeg_option(extra_options, "pix_fmt");
1247 if (!pixel_format_option) {
1248 pixel_format_option = find_ffmpeg_option(extra_options, "pixel_format");
1249 }
1250 AVPixelFormat requested_pixel_format = AV_PIX_FMT_NONE;
1251 if (pixel_format_option) {
1252 requested_pixel_format = av_get_pix_fmt(pixel_format_option->c_str());
1253 if (requested_pixel_format == AV_PIX_FMT_NONE) {
1254 std::cerr << "MXWrite: unknown pixel format '" << *pixel_format_option << "'.\n";
1255 return false;
1256 }
1257 }
1258
1259 // Pass nullptr for format_name so libavformat picks the container based
1260 // on the filename extension (mp4, mkv, mov, avi...).
1261 if (avformat_alloc_output_context2(&format_ctx, nullptr, nullptr, filename.c_str()) < 0) {
1262 std::cerr << "Could not allocate output context.\n";
1263 return false;
1264 }
1265
1266 width = w;
1267 height = h;
1268 hdr_output = opts.hdr.enabled;
1269 hdr_info = opts.hdr;
1270
1271 // ---- HDR (HEVC Main10 + BT.2020/PQ) path ------------------------------
1272 // Short-circuits the normal SDR codec selection when opts.hdr.enabled is
1273 // true. Forces software libx265 + YUV420P10LE + PQ metadata, writes the
1274 // color tags and mastering/content-light side data, and bypasses NVENC.
1275 if (hdr_output) {
1276 const AVCodec *hdr_codec = avcodec_find_encoder_by_name("libx265");
1277 if (!hdr_codec) {
1278 std::cerr << "MXWrite: HDR output requested but libx265 encoder not available.\n";
1279 avformat_free_context(format_ctx);
1280 format_ctx = nullptr;
1281 return false;
1282 }
1283
1284 stream = avformat_new_stream(format_ctx, hdr_codec);
1285 if (!stream) {
1286 std::cerr << "MXWrite: could not create HDR stream.\n";
1287 avformat_free_context(format_ctx);
1288 format_ctx = nullptr;
1289 return false;
1290 }
1291
1292 calculateFPSFraction(fps, fps_num, fps_den);
1293 AVRational tb_hdr = {fps_den, fps_num};
1294 stream->time_base = tb_hdr;
1295
1296 codec_ctx = avcodec_alloc_context3(hdr_codec);
1297 if (!codec_ctx) {
1298 std::cerr << "MXWrite: could not allocate HDR codec context.\n";
1299 avformat_free_context(format_ctx);
1300 format_ctx = nullptr;
1301 return false;
1302 }
1303
1304 codec_ctx->width = width;
1305 codec_ctx->height = height;
1306 codec_ctx->time_base = stream->time_base;
1307 codec_ctx->framerate = AVRational{fps_num, fps_den};
1308 codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P10LE;
1309 codec_ctx->profile = AV_PROFILE_HEVC_MAIN_10;
1310 codec_ctx->bits_per_raw_sample = 10;
1311 codec_ctx->gop_size = 30;
1312 codec_ctx->max_b_frames = 0;
1313 codec_ctx->thread_count = std::max(1u, std::thread::hardware_concurrency());
1314 codec_ctx->thread_type = FF_THREAD_SLICE;
1315 codec_ctx->delay = 0;
1316
1317 // Tag the stream with BT.2020 + PQ (or whatever the input used).
1318 codec_ctx->color_primaries = static_cast<AVColorPrimaries>(
1319 hdr_info.color_primaries ? hdr_info.color_primaries : AVCOL_PRI_BT2020);
1320 codec_ctx->color_trc = static_cast<AVColorTransferCharacteristic>(
1321 hdr_info.color_trc ? hdr_info.color_trc : AVCOL_TRC_SMPTE2084);
1322 codec_ctx->colorspace = static_cast<AVColorSpace>(
1323 hdr_info.color_space ? hdr_info.color_space : AVCOL_SPC_BT2020_NCL);
1324 codec_ctx->color_range = static_cast<AVColorRange>(
1325 hdr_info.color_range ? hdr_info.color_range : AVCOL_RANGE_MPEG);
1326 codec_ctx->chroma_sample_location = AVCHROMA_LOC_LEFT;
1327
1328 // Encoder options: Main10, matching x265-params for color volume.
1329 std::string preset_hdr = opts.preset.empty() ? std::string("medium") : opts.preset;
1330 av_opt_set(codec_ctx->priv_data, "preset", preset_hdr.c_str(), 0);
1331 int crf_val_hdr = opts.crf;
1332 if (crf_val_hdr < 0)
1333 crf_val_hdr = 0;
1334 if (crf_val_hdr > 51)
1335 crf_val_hdr = 51;
1336 const std::string crf_hdr = std::to_string(crf_val_hdr);
1337 av_opt_set(codec_ctx->priv_data, "crf", crf_hdr.c_str(), 0);
1338
1339 // x265 params: colorprim, transfer, colormatrix, range, hdr flag.
1340 // These drive the stream VUI + SEI so players recognise the file as HDR.
1341 std::string x265_params = "profile=main10:colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:range=limited:repeat-headers=1";
1342 // When HLG transfer is requested, swap transfer + mark hlg.
1343 if (codec_ctx->color_trc == AVCOL_TRC_ARIB_STD_B67) {
1344 x265_params = "profile=main10:colorprim=bt2020:transfer=arib-std-b67:colormatrix=bt2020nc:range=limited:repeat-headers=1";
1345 }
1346 av_opt_set(codec_ctx->priv_data, "x265-params", x265_params.c_str(), 0);
1347
1348 if (pixel_format_option && requested_pixel_format != AV_PIX_FMT_YUV420P10LE) {
1349 std::cerr << "MXWrite: HDR output forces yuv420p10le; ignoring requested pixel format '"
1350 << *pixel_format_option << "'.\n";
1351 }
1352 if (!apply_ffmpeg_options(extra_options, codec_ctx, format_ctx)) {
1353 avcodec_free_context(&codec_ctx);
1354 avformat_free_context(format_ctx);
1355 format_ctx = nullptr;
1356 return false;
1357 }
1358
1359 time_base = tb_hdr;
1360 if (format_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
1361 codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
1362 }
1363 if (avcodec_open2(codec_ctx, hdr_codec, nullptr) < 0) {
1364 std::cerr << "MXWrite: could not open libx265 for HDR output.\n";
1365 avcodec_free_context(&codec_ctx);
1366 avformat_free_context(format_ctx);
1367 format_ctx = nullptr;
1368 return false;
1369 }
1370 if (avcodec_parameters_from_context(stream->codecpar, codec_ctx) < 0) {
1371 std::cerr << "MXWrite: could not copy HDR codec parameters.\n";
1372 avcodec_free_context(&codec_ctx);
1373 avformat_free_context(format_ctx);
1374 format_ctx = nullptr;
1375 return false;
1376 }
1377
1378 // Attach mastering-display / content-light side data to the stream
1379 // codec parameters. Uses the modern AVCodecParameters coded_side_data
1380 // API. Failures are logged but non-fatal.
1381 auto attach_side = [&](AVPacketSideDataType type,
1382 const std::vector<uint8_t> &payload) {
1383 if (payload.empty())
1384 return;
1385 uint8_t *buf = static_cast<uint8_t *>(av_malloc(payload.size()));
1386 if (!buf)
1387 return;
1388 std::memcpy(buf, payload.data(), payload.size());
1389 const AVPacketSideData *added = av_packet_side_data_add(
1390 &stream->codecpar->coded_side_data,
1391 &stream->codecpar->nb_coded_side_data,
1392 type,
1393 buf,
1394 payload.size(),
1395 0);
1396 if (!added) {
1397 av_free(buf);
1398 std::cerr << "MXWrite: failed to attach HDR side data (type " << (int)type << ").\n";
1399 }
1400 };
1401 attach_side(AV_PKT_DATA_MASTERING_DISPLAY_METADATA, hdr_info.mastering_display);
1402 attach_side(AV_PKT_DATA_CONTENT_LIGHT_LEVEL, hdr_info.content_light);
1403
1404 if (!(format_ctx->oformat->flags & AVFMT_NOFILE)) {
1405 if (avio_open(&format_ctx->pb, filename.c_str(), AVIO_FLAG_WRITE) < 0) {
1406 std::cerr << "MXWrite: could not open HDR output file: " << filename << "\n";
1407 avcodec_free_context(&codec_ctx);
1408 avformat_free_context(format_ctx);
1409 format_ctx = nullptr;
1410 return false;
1411 }
1412 }
1413 if (avformat_write_header(format_ctx, nullptr) < 0) {
1414 std::cerr << "MXWrite: error writing HDR MP4 header.\n";
1415 avio_closep(&format_ctx->pb);
1416 avcodec_free_context(&codec_ctx);
1417 avformat_free_context(format_ctx);
1418 format_ctx = nullptr;
1419 return false;
1420 }
1421 updateBytesWritten();
1422
1423 // Allocate the 10-bit YUV staging frame used by encodeAndWriteFrame.
1424 frame10 = av_frame_alloc();
1425 if (!frame10) {
1426 std::cerr << "MXWrite: could not allocate YUV420P10LE frame.\n";
1427 avio_closep(&format_ctx->pb);
1428 avcodec_free_context(&codec_ctx);
1429 avformat_free_context(format_ctx);
1430 format_ctx = nullptr;
1431 return false;
1432 }
1433 frame10->format = AV_PIX_FMT_YUV420P10LE;
1434 frame10->width = width;
1435 frame10->height = height;
1436 if (av_frame_get_buffer(frame10, 32) < 0) {
1437 std::cerr << "MXWrite: could not allocate YUV420P10LE buffer.\n";
1438 av_frame_free(&frame10);
1439 avio_closep(&format_ctx->pb);
1440 avcodec_free_context(&codec_ctx);
1441 avformat_free_context(format_ctx);
1442 format_ctx = nullptr;
1443 return false;
1444 }
1445
1446 opened = true;
1447 use_hw_encode = false;
1448 recordingStart = std::chrono::steady_clock::now();
1449 startEncoderThread();
1450 std::cout << "MXWrite: HDR output active (libx265 Main10, BT.2020, "
1451 << (codec_ctx->color_trc == AVCOL_TRC_ARIB_STD_B67 ? "HLG" : "PQ")
1452 << ")\n";
1453 return true;
1454 }
1455 // ---- End HDR path -----------------------------------------------------
1456
1457 const bool is_high_res = (width > 3840 || height > 2160);
1458 std::string codec_pref = lowercase_ascii(codec_override ? *codec_override : opts.codec);
1459 if (codec_pref.empty()) {
1460 codec_pref = "auto";
1461 }
1462 const bool explicit_hevc_nvenc = (codec_pref == "hevc_nvenc" || codec_pref == "h265_nvenc");
1463 const bool explicit_h264_nvenc = (codec_pref == "h264_nvenc");
1464 const bool explicit_hevc_software =
1465 (codec_pref == "hevc" || codec_pref == "h265" || codec_pref == "libx265");
1466 const bool explicit_h264_software = (codec_pref == "h264" || codec_pref == "libx264");
1467 const bool use_hevc_codec = explicit_hevc_nvenc || explicit_hevc_software ||
1468 (!explicit_h264_nvenc && !explicit_h264_software && is_high_res);
1469 std::string hw_codec_name = use_hevc_codec ? "hevc_nvenc" : "h264_nvenc";
1470 AVCodecID sw_codec_id = use_hevc_codec ? AV_CODEC_ID_HEVC : AV_CODEC_ID_H264;
1471
1472 // Codec selection based on user preference.
1473 const AVCodec *codec = nullptr;
1474 bool wants_hw = false;
1475 if (codec_pref == "software" || codec_pref == "x264" || codec_pref == "cpu" ||
1476 explicit_hevc_software || explicit_h264_software) {
1477 if (codec_pref == "libx264" || codec_pref == "libx265") {
1478 codec = avcodec_find_encoder_by_name(codec_pref.c_str());
1479 } else {
1480 codec = avcodec_find_encoder(sw_codec_id);
1481 }
1482 wants_hw = false;
1483 } else if (codec_pref == "auto" || codec_pref == "nvenc" ||
1484 explicit_hevc_nvenc || explicit_h264_nvenc) {
1485 // "auto" or "nvenc" keeps the resolution-based default; concrete
1486 // names like "hevc_nvenc" and "h264_nvenc" select that NVENC codec.
1487 codec = avcodec_find_encoder_by_name(hw_codec_name.c_str());
1488 wants_hw = (codec != nullptr);
1489 if (!codec) {
1490 if (codec_pref == "nvenc" || explicit_hevc_nvenc || explicit_h264_nvenc) {
1491 std::cerr << "MXWrite: NVENC requested but " << hw_codec_name
1492 << " not available; falling back to software.\n";
1493 }
1494 codec = avcodec_find_encoder(sw_codec_id);
1495 }
1496 } else {
1497 // Concrete FFmpeg encoder names are kept distinct. This permits all
1498 // linked encoders that accept system-memory frames (for example AV1,
1499 // VP9, ProRes, FFV1, QSV, AMF, VideoToolbox, and V4L2 M2M) instead of
1500 // collapsing the request to the default H.264 encoder.
1501 codec = avcodec_find_encoder_by_name(codec_pref.c_str());
1502 wants_hw = codec && (codec_pref.ends_with("_nvenc"));
1503 if (wants_hw) {
1504 hw_codec_name = codec_pref;
1505 sw_codec_id = codec->id;
1506 }
1507 }
1508
1509 if (!codec) {
1510 std::cerr << "MXWrite: could not find requested video encoder '"
1511 << codec_pref << "'. Use acmx2 --list-encoders to see this FFmpeg build.\n";
1512 avformat_free_context(format_ctx);
1513 format_ctx = nullptr;
1514 return false;
1515 }
1516
1517 // Validate / sanitise preset and CRF.
1518 std::string preset = opts.preset.empty() ? std::string("medium") : opts.preset;
1519 if (!is_valid_x264_preset(preset)) {
1520 // Accept unknown names; forward as-is. If empty, medium.
1521 }
1522 int crf_val = opts.crf;
1523 if (crf_val < 0)
1524 crf_val = 0;
1525 if (crf_val > 51)
1526 crf_val = 51;
1527 const std::string crf_str = std::to_string(crf_val);
1528
1529 stream = avformat_new_stream(format_ctx, codec);
1530 if (!stream) {
1531 std::cerr << "Could not create new stream.\n";
1532 avformat_free_context(format_ctx);
1533 format_ctx = nullptr;
1534 return false;
1535 }
1536
1537 calculateFPSFraction(fps, fps_num, fps_den);
1538
1539 AVRational tb = {fps_den, fps_num};
1540 stream->time_base = tb;
1541
1542 codec_ctx = avcodec_alloc_context3(codec);
1543 if (!codec_ctx) {
1544 std::cerr << "Could not allocate codec context.\n";
1545 avformat_free_context(format_ctx);
1546 format_ctx = nullptr;
1547 return false;
1548 }
1549
1550 codec_ctx->width = width;
1551 codec_ctx->height = height;
1552 codec_ctx->time_base = stream->time_base;
1553 codec_ctx->framerate = AVRational{fps_num, fps_den};
1554 AVPixelFormat software_pixel_format = choose_software_pixel_format(
1555 wants_hw ? avcodec_find_encoder(sw_codec_id) : codec, requested_pixel_format);
1556 const bool requires_hardware_frames = !wants_hw &&
1557 software_pixel_format == AV_PIX_FMT_NONE &&
1558 codec_uses_hardware(codec);
1559 if (software_pixel_format == AV_PIX_FMT_NONE && !requires_hardware_frames) {
1560 const char *requested_name = requested_pixel_format == AV_PIX_FMT_NONE
1561 ? "an automatic system-memory format"
1562 : av_get_pix_fmt_name(requested_pixel_format);
1563 std::cerr << "MXWrite: encoder '" << codec->name << "' does not accept "
1564 << (requested_name ? requested_name : "the requested pixel format")
1565 << " that MXWrite can convert from RGBA. Choose a supported -pix_fmt; "
1566 "hardware-frame-only encoders require a device-specific upload path.\n";
1567 avcodec_free_context(&codec_ctx);
1568 avformat_free_context(format_ctx);
1569 format_ctx = nullptr;
1570 return false;
1571 }
1572 codec_ctx->pix_fmt = wants_hw ? AV_PIX_FMT_CUDA : software_pixel_format;
1573 codec_ctx->gop_size = 30;
1574 codec_ctx->max_b_frames = 0;
1575 codec_ctx->thread_count = std::max(1u, std::thread::hardware_concurrency());
1576 // Frame threading scales much better than slice threading for x264 when
1577 // latency is not a concern; switch only to slice threading in realtime/ts.
1578 if (ts_mode || opts.realtime) {
1579 codec_ctx->thread_type = FF_THREAD_SLICE;
1580 codec_ctx->slices = 4;
1581 } else {
1582 codec_ctx->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
1583 }
1584 codec_ctx->delay = 0;
1585
1586 if (ts_mode || opts.realtime) {
1587 codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
1588 }
1589
1590 bool extra_options_applied = false;
1591 bool fell_back_from_hardware = false;
1592 if (requires_hardware_frames) {
1593 set_named_encoder_option(codec_ctx->priv_data, "preset", preset);
1594 if (!opts.tune.empty() && opts.tune != "none") {
1595 set_named_encoder_option(codec_ctx->priv_data, "tune", opts.tune);
1596 }
1597 set_named_encoder_option(codec_ctx->priv_data, "crf", crf_str);
1598 if (!apply_ffmpeg_options(extra_options, codec_ctx, format_ctx)) {
1599 avcodec_free_context(&codec_ctx);
1600 avformat_free_context(format_ctx);
1601 format_ctx = nullptr;
1602 return false;
1603 }
1604 extra_options_applied = true;
1605 if (!initHardwareEncoding(codec, requested_pixel_format, false)) {
1606 std::cerr << "MXWrite: encoder '" << codec->name
1607 << "' requires hardware frames, but its FFmpeg hardware device "
1608 "could not be initialized.\n";
1609 av_buffer_unref(&hw_frames_ctx);
1610 av_buffer_unref(&hw_device_ctx);
1611 av_frame_free(&upload_sw_frame);
1612 sws_freeContext(sws_ctx);
1613 sws_ctx = nullptr;
1614 avcodec_free_context(&codec_ctx);
1615 avformat_free_context(format_ctx);
1616 format_ctx = nullptr;
1617 return false;
1618 }
1619 use_hw_encode = true;
1620 std::cout << "MXWrite: hardware encoder selected (" << codec->name << ")\n";
1621 }
1622 if (wants_hw) {
1623 const char *nv_preset = x264_preset_to_nvenc(preset);
1624 av_opt_set(codec_ctx->priv_data, "preset", nv_preset, 0);
1625 // NVENC "tune": hq (high quality), ll (low latency), ull (ultra low latency), lossless.
1626 const std::string requested_tune = lowercase_ascii(opts.tune);
1627 const std::string nv_tune =
1628 opts.realtime ? std::string("ll")
1629 : (is_nvenc_tune(requested_tune) ? requested_tune : std::string("hq"));
1630 av_opt_set(codec_ctx->priv_data, "tune", nv_tune.c_str(), 0);
1631 const std::string *custom_tune = find_ffmpeg_option(extra_options, "tune");
1632 const bool lossless_tune = nv_tune == "lossless" ||
1633 (custom_tune && lowercase_ascii(*custom_tune) == "lossless");
1634 if (!lossless_tune) {
1635 av_opt_set(codec_ctx->priv_data, "rc", "vbr", 0);
1636 av_opt_set(codec_ctx->priv_data, "cq", crf_str.c_str(), 0);
1637 }
1638 if (opts.realtime) {
1639 av_opt_set(codec_ctx->priv_data, "zerolatency", "1", 0);
1640 }
1641 if (use_hevc_codec) {
1642 av_opt_set(codec_ctx->priv_data, "tier", "high", 0);
1643 }
1644
1645 if (requested_pixel_format == AV_PIX_FMT_YUV444P) {
1646 if (av_opt_set(codec_ctx->priv_data, "rgb_mode", "yuv444", 0) < 0) {
1647 std::cerr << "MXWrite: this NVENC build cannot convert RGBA input to yuv444p.\n";
1648 avcodec_free_context(&codec_ctx);
1649 avformat_free_context(format_ctx);
1650 format_ctx = nullptr;
1651 return false;
1652 }
1653 if (!find_ffmpeg_option(extra_options, "profile")) {
1654 const char *profile = use_hevc_codec ? "rext" : "high444p";
1655 av_opt_set(codec_ctx->priv_data, "profile", profile, 0);
1656 }
1657 } else if (requested_pixel_format != AV_PIX_FMT_NONE &&
1658 requested_pixel_format != AV_PIX_FMT_YUV420P &&
1659 requested_pixel_format != AV_PIX_FMT_RGBA) {
1660 std::cerr << "MXWrite: NVENC RGBA ingestion supports custom -pix_fmt yuv420p, "
1661 "yuv444p, or rgba; requested '"
1662 << av_get_pix_fmt_name(requested_pixel_format) << "'.\n";
1663 avcodec_free_context(&codec_ctx);
1664 avformat_free_context(format_ctx);
1665 format_ctx = nullptr;
1666 return false;
1667 }
1668
1669 if (!apply_ffmpeg_options(extra_options, codec_ctx, format_ctx)) {
1670 avcodec_free_context(&codec_ctx);
1671 avformat_free_context(format_ctx);
1672 format_ctx = nullptr;
1673 return false;
1674 }
1675 extra_options_applied = true;
1676
1677 if (initHardwareEncoding(codec, requested_pixel_format, true)) {
1678 use_hw_encode = true;
1679 std::cout << "MXWrite: hardware encoder selected (" << hw_codec_name << ")\n";
1680 } else {
1681 std::cerr << "MXWrite: " << hw_codec_name << " present but CUDA context failed, falling back to software encoder\n";
1682 fell_back_from_hardware = true;
1683 av_buffer_unref(&hw_frames_ctx);
1684 av_buffer_unref(&hw_device_ctx);
1685 av_frame_free(&upload_sw_frame);
1686 sws_freeContext(sws_ctx);
1687 sws_ctx = nullptr;
1688 direct_cuda_upload = false;
1689 avcodec_free_context(&codec_ctx);
1690
1691 codec = avcodec_find_encoder(sw_codec_id);
1692 if (!codec) {
1693 std::cerr << "Could not find software fallback encoder.\n";
1694 avformat_free_context(format_ctx);
1695 format_ctx = nullptr;
1696 return false;
1697 }
1698
1699 codec_ctx = avcodec_alloc_context3(codec);
1700 if (!codec_ctx) {
1701 std::cerr << "Could not allocate fallback codec context.\n";
1702 avformat_free_context(format_ctx);
1703 format_ctx = nullptr;
1704 return false;
1705 }
1706
1707 codec_ctx->width = width;
1708 codec_ctx->height = height;
1709 codec_ctx->time_base = stream->time_base;
1710 codec_ctx->framerate = AVRational{fps_num, fps_den};
1711 codec_ctx->pix_fmt = software_pixel_format;
1712 codec_ctx->gop_size = 30;
1713 codec_ctx->max_b_frames = 0;
1714 codec_ctx->thread_count = std::max(1u, std::thread::hardware_concurrency());
1715 if (ts_mode || opts.realtime) {
1716 codec_ctx->thread_type = FF_THREAD_SLICE;
1717 codec_ctx->slices = 4;
1718 } else {
1719 codec_ctx->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
1720 }
1721 codec_ctx->delay = 0;
1722
1723 if (ts_mode || opts.realtime) {
1724 codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
1725 }
1726 extra_options_applied = false;
1727 }
1728 }
1729
1730 if (!use_hw_encode) {
1731 const std::string software_preset =
1732 fell_back_from_hardware ? nvenc_preset_to_software(lowercase_ascii(preset)) : preset;
1733 set_named_encoder_option(codec_ctx->priv_data, "preset", software_preset);
1734 // Apply tune: realtime forces zerolatency; otherwise honour user value.
1735 std::string tune = opts.realtime ? std::string("zerolatency") : opts.tune;
1736 if (fell_back_from_hardware) {
1737 const std::string lowered_tune = lowercase_ascii(tune);
1738 if (lowered_tune == "hq" || lowered_tune == "uhq") {
1739 tune.clear();
1740 } else if (lowered_tune == "ll" || lowered_tune == "ull") {
1741 tune = "zerolatency";
1742 } else if (lowered_tune == "lossless") {
1743 tune.clear();
1744 if (use_hevc_codec) {
1745 av_opt_set(codec_ctx->priv_data, "x265-params", "lossless=1", 0);
1746 } else {
1747 av_opt_set(codec_ctx->priv_data, "qp", "0", 0);
1748 }
1749 }
1750 }
1751 if (!tune.empty() && tune != "none") {
1752 set_named_encoder_option(codec_ctx->priv_data, "tune", tune);
1753 }
1754 set_named_encoder_option(codec_ctx->priv_data, "crf", crf_str);
1755 if (opts.realtime && codec->id == AV_CODEC_ID_H264) {
1756 // Legacy low-latency parameters kept for realtime path to avoid
1757 // pipeline stalls during live capture.
1758 av_opt_set(codec_ctx->priv_data, "x264-params", "bframes=0:ref=1:me=dia:subme=0", 0);
1759 av_opt_set(codec_ctx->priv_data, "force_cfr", "1", 0);
1760 }
1761 }
1762
1763 const std::vector<FfmpegOption> translated_fallback_options =
1764 fell_back_from_hardware ? software_fallback_options(extra_options, use_hevc_codec)
1765 : std::vector<FfmpegOption>{};
1766 const std::vector<FfmpegOption> &options_to_apply =
1767 fell_back_from_hardware ? translated_fallback_options : extra_options;
1768 if (!extra_options_applied &&
1769 !apply_ffmpeg_options(options_to_apply, codec_ctx, format_ctx)) {
1770 avcodec_free_context(&codec_ctx);
1771 avformat_free_context(format_ctx);
1772 format_ctx = nullptr;
1773 return false;
1774 }
1775
1776 time_base = tb;
1777
1778 if (format_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
1779 codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
1780 }
1781 if (avcodec_open2(codec_ctx, codec, nullptr) < 0) {
1782 std::cerr << "MXWrite: could not open encoder '" << codec->name
1783 << "'. The encoder may require unavailable hardware or options.\n";
1784 avcodec_free_context(&codec_ctx);
1785 avformat_free_context(format_ctx);
1786 format_ctx = nullptr;
1787 return false;
1788 }
1789 active_encoder_hardware = codec_uses_hardware(codec);
1790 if (avcodec_parameters_from_context(stream->codecpar, codec_ctx) < 0) {
1791 std::cerr << "Could not copy codec parameters.\n";
1792 avcodec_free_context(&codec_ctx);
1793 avformat_free_context(format_ctx);
1794 format_ctx = nullptr;
1795 return false;
1796 }
1797 if (!(format_ctx->oformat->flags & AVFMT_NOFILE)) {
1798 if (avio_open(&format_ctx->pb, filename.c_str(), AVIO_FLAG_WRITE) < 0) {
1799 std::cerr << "Could not open output file: " << filename << "\n";
1800 avcodec_free_context(&codec_ctx);
1801 avformat_free_context(format_ctx);
1802 format_ctx = nullptr;
1803 return false;
1804 }
1805 }
1806 if (avformat_write_header(format_ctx, nullptr) < 0) {
1807 std::cerr << "Error writing MP4 header.\n";
1808 avio_closep(&format_ctx->pb);
1809 avcodec_free_context(&codec_ctx);
1810 avformat_free_context(format_ctx);
1811 format_ctx = nullptr;
1812 return false;
1813 }
1814 updateBytesWritten();
1815
1816 if (!use_hw_encode) {
1817 frameYUV = av_frame_alloc();
1818 if (!frameYUV) {
1819 std::cerr << "Could not allocate YUV frame.\n";
1820 avio_closep(&format_ctx->pb);
1821 avcodec_free_context(&codec_ctx);
1822 avformat_free_context(format_ctx);
1823 format_ctx = nullptr;
1824 return false;
1825 }
1826 frameYUV->format = software_pixel_format;
1827 frameYUV->width = width;
1828 frameYUV->height = height;
1829 if (av_frame_get_buffer(frameYUV, 32) < 0) {
1830 std::cerr << "Could not allocate frame buffer for YUV frame.\n";
1831 av_frame_free(&frameYUV);
1832 avio_closep(&format_ctx->pb);
1833 avcodec_free_context(&codec_ctx);
1834 avformat_free_context(format_ctx);
1835 format_ctx = nullptr;
1836 return false;
1837 }
1838
1839 sws_ctx = sws_getContext(width, height, AV_PIX_FMT_RGBA, width, height,
1840 software_pixel_format, SWS_FAST_BILINEAR, nullptr,
1841 nullptr, nullptr);
1842 if (!sws_ctx) {
1843 std::cerr << "Could not initialize conversion context.\n";
1844 av_frame_free(&frameYUV);
1845 avio_closep(&format_ctx->pb);
1846 avcodec_free_context(&codec_ctx);
1847 avformat_free_context(format_ctx);
1848 format_ctx = nullptr;
1849 return false;
1850 }
1851 }
1852
1853 opened = true;
1854 recordingStart = std::chrono::steady_clock::now();
1855 startEncoderThread();
1856 return true;
1857}
1858
1859void Writer::write(void *rgba_buffer) {
1860 write_at_pts(rgba_buffer, AV_NOPTS_VALUE);
1861}
1862
1863void Writer::write_at_pts(void *rgba_buffer, int64_t pts) {
1864 if (!rgba_buffer) {
1865 return;
1866 }
1867
1868 {
1869 std::lock_guard<std::mutex> lock(writer_mutex);
1870 if (!opened) {
1871 return;
1872 }
1873 }
1874
1875 AVFrame *queued_frame = av_frame_alloc();
1876 if (!queued_frame) {
1877 std::cerr << "Writer: failed to allocate queued frame\n";
1878 return;
1879 }
1880
1881 if (use_hw_encode && hdr_output) {
1882 queued_frame->format = codec_ctx->pix_fmt;
1883 queued_frame->width = width;
1884 queued_frame->height = height;
1885 if (av_hwframe_get_buffer(hw_frames_ctx, queued_frame, 0) < 0) {
1886 std::cerr << "Writer: failed to allocate frame from hardware pool\n";
1887 releaseFrame(queued_frame);
1888 return;
1889 }
1890
1891 if (av_frame_make_writable(upload_sw_frame) < 0) {
1892 std::cerr << "Writer: software upload frame not writable\n";
1893 releaseFrame(queued_frame);
1894 return;
1895 }
1896
1897 const auto *src = static_cast<const uint8_t *>(rgba_buffer);
1898 if (upload_sw_frame->format == AV_PIX_FMT_RGBA) {
1899 for (int y = 0; y < height; ++y) {
1900 std::memcpy(upload_sw_frame->data[0] +
1901 static_cast<size_t>(y) * upload_sw_frame->linesize[0],
1902 src + static_cast<size_t>(y) * static_cast<size_t>(width) * 4,
1903 static_cast<size_t>(width) * 4);
1904 }
1905 } else {
1906 const uint8_t *source_data[1] = {src};
1907 const int source_linesize[1] = {width * 4};
1908 sws_scale(sws_ctx, source_data, source_linesize, 0, height,
1909 upload_sw_frame->data, upload_sw_frame->linesize);
1910 }
1911
1912 if (av_hwframe_transfer_data(queued_frame, upload_sw_frame, 0) < 0) {
1913 std::cerr << "Writer: failed to transfer system frame to hardware frame\n";
1914 releaseFrame(queued_frame);
1915 return;
1916 }
1917 } else {
1918 queued_frame->format = AV_PIX_FMT_RGBA;
1919 queued_frame->width = width;
1920 queued_frame->height = height;
1921 if (av_frame_get_buffer(queued_frame, 32) < 0) {
1922 std::cerr << "Writer: failed to allocate queued RGBA frame buffer\n";
1923 releaseFrame(queued_frame);
1924 return;
1925 }
1926 if (av_frame_make_writable(queued_frame) < 0) {
1927 std::cerr << "Writer: queued RGBA frame not writable\n";
1928 releaseFrame(queued_frame);
1929 return;
1930 }
1931
1932 const auto *src = static_cast<const uint8_t *>(rgba_buffer);
1933 for (int y = 0; y < height; ++y) {
1934 std::memcpy(queued_frame->data[0] + static_cast<size_t>(y) * queued_frame->linesize[0],
1935 src + static_cast<size_t>(y) * static_cast<size_t>(width) * 4,
1936 static_cast<size_t>(width) * 4);
1937 }
1938 }
1939
1940 {
1941 std::unique_lock<std::mutex> lock(queue_mutex);
1942 if (block_when_full.load(std::memory_order_relaxed)) {
1943 if (encode_queue.size() >= NO_DROP_QUEUE_SIZE) {
1944 // Keep file processing paced with the encoder. Wake whenever
1945 // one queue slot becomes available rather than waiting for a
1946 // large queue to fill and then drain completely.
1947 queue_cv.wait(lock, [this] {
1948 return stop_requested || encode_queue.size() < NO_DROP_QUEUE_SIZE;
1949 });
1950 }
1951 if (stop_requested) {
1952 releaseFrame(queued_frame);
1953 return;
1954 }
1955 } else if (stop_requested || encode_queue.size() >= MAX_QUEUE_SIZE) {
1956 static int drop_counter = 0;
1957 if (++drop_counter % 30 == 0) {
1958 std::cerr << "Writer: dropped " << drop_counter << " SDR frames (encoder queue full)\n";
1959 }
1960 releaseFrame(queued_frame);
1961 return;
1962 }
1963 if (pts != AV_NOPTS_VALUE && pts < frame_count) {
1964 // A timestamped live frame landed in a nominal frame slot that
1965 // was already filled. Drop it instead of extending the video
1966 // timeline and allowing it to drift behind the capture clock.
1967 releaseFrame(queued_frame);
1968 return;
1969 }
1970 queued_frame->pts = pts == AV_NOPTS_VALUE ? frame_count : pts;
1971 frame_count = queued_frame->pts + 1;
1972 encode_queue.push(queued_frame);
1973 }
1974
1975 queue_cv.notify_one();
1976}
1977
1978void Writer::write_hdr_rgba16(void *rgba16_buffer) {
1979 write_hdr_rgba16_at_pts(rgba16_buffer, AV_NOPTS_VALUE);
1980}
1981
1982void Writer::write_hdr_rgba16_at_pts(void *rgba16_buffer, int64_t pts) {
1983 if (!rgba16_buffer) {
1984 return;
1985 }
1986
1987 {
1988 std::lock_guard<std::mutex> lock(writer_mutex);
1989 if (!opened) {
1990 return;
1991 }
1992 if (!hdr_output) {
1993 std::cerr << "Writer: write_hdr_rgba16 called but writer not in HDR mode\n";
1994 return;
1995 }
1996 }
1997
1998 AVFrame *queued_frame = av_frame_alloc();
1999 if (!queued_frame) {
2000 std::cerr << "Writer: failed to allocate queued HDR frame\n";
2001 return;
2002 }
2003 queued_frame->format = AV_PIX_FMT_YUV420P10LE;
2004 queued_frame->width = width;
2005 queued_frame->height = height;
2006 if (av_frame_get_buffer(queued_frame, 32) < 0) {
2007 std::cerr << "Writer: failed to allocate YUV420P10 buffer\n";
2008 releaseFrame(queued_frame);
2009 return;
2010 }
2011 if (av_frame_make_writable(queued_frame) < 0) {
2012 std::cerr << "Writer: queued HDR frame not writable\n";
2013 releaseFrame(queued_frame);
2014 return;
2015 }
2016
2017 // Convert the already-PQ/HLG-encoded 16-bit BT.2020 RGBA into the
2018 // 10-bit limited-range YUV420 plane that libx265 Main10 expects.
2019 convertBt2020Rgba16EncodedToYuv420p10(
2020 reinterpret_cast<const uint16_t *>(rgba16_buffer),
2021 width * 4,
2022 reinterpret_cast<uint16_t *>(queued_frame->data[0]),
2023 queued_frame->linesize[0] / 2,
2024 reinterpret_cast<uint16_t *>(queued_frame->data[1]),
2025 queued_frame->linesize[1] / 2,
2026 reinterpret_cast<uint16_t *>(queued_frame->data[2]),
2027 queued_frame->linesize[2] / 2,
2028 width,
2029 height);
2030
2031 queued_frame->color_primaries = static_cast<AVColorPrimaries>(
2032 hdr_info.color_primaries ? hdr_info.color_primaries : AVCOL_PRI_BT2020);
2033 queued_frame->color_trc = static_cast<AVColorTransferCharacteristic>(
2034 hdr_info.color_trc ? hdr_info.color_trc : AVCOL_TRC_SMPTE2084);
2035 queued_frame->colorspace = static_cast<AVColorSpace>(
2036 hdr_info.color_space ? hdr_info.color_space : AVCOL_SPC_BT2020_NCL);
2037 queued_frame->color_range = static_cast<AVColorRange>(
2038 hdr_info.color_range ? hdr_info.color_range : AVCOL_RANGE_MPEG);
2039
2040 {
2041 std::unique_lock<std::mutex> lock(queue_mutex);
2042 if (block_when_full.load(std::memory_order_relaxed)) {
2043 if (encode_queue.size() >= NO_DROP_QUEUE_SIZE) {
2044 queue_cv.wait(lock, [this] {
2045 return stop_requested || encode_queue.size() < NO_DROP_QUEUE_SIZE;
2046 });
2047 }
2048 if (stop_requested) {
2049 releaseFrame(queued_frame);
2050 return;
2051 }
2052 } else if (stop_requested || encode_queue.size() >= MAX_QUEUE_SIZE) {
2053 static int drop_counter = 0;
2054 if (++drop_counter % 30 == 0) {
2055 std::cerr << "Writer: dropped " << drop_counter << " HDR frames (encoder queue full)\n";
2056 }
2057 releaseFrame(queued_frame);
2058 return;
2059 }
2060 if (pts != AV_NOPTS_VALUE && pts < frame_count) {
2061 releaseFrame(queued_frame);
2062 return;
2063 }
2064 queued_frame->pts = pts == AV_NOPTS_VALUE ? frame_count : pts;
2065 frame_count = queued_frame->pts + 1;
2066 encode_queue.push(queued_frame);
2067 }
2068
2069 queue_cv.notify_one();
2070}
2071
2072bool Writer::write_cuda_rgba(void *cuda_rgba_buffer, int src_stride, [[maybe_unused]] bool bottom_up) {
2073 return write_cuda_rgba_at_pts(cuda_rgba_buffer, src_stride, AV_NOPTS_VALUE,
2074 bottom_up);
2075}
2076
2077bool Writer::write_cuda_rgba_at_pts(void *cuda_rgba_buffer, int src_stride,
2078 int64_t pts,
2079 [[maybe_unused]] bool bottom_up) {
2080 if (!cuda_rgba_buffer || src_stride <= 0) {
2081 return false;
2082 }
2083
2084 {
2085 std::lock_guard<std::mutex> lock(writer_mutex);
2086 if (!opened || !use_hw_encode || !direct_cuda_upload) {
2087 return false;
2088 }
2089 }
2090
2091 AVFrame *queued_frame = av_frame_alloc();
2092 if (!queued_frame) {
2093 std::cerr << "Writer: failed to allocate queued CUDA frame\n";
2094 return false;
2095 }
2096
2097 queued_frame->format = AV_PIX_FMT_CUDA;
2098 queued_frame->width = width;
2099 queued_frame->height = height;
2100
2101 if (av_hwframe_get_buffer(hw_frames_ctx, queued_frame, 0) < 0) {
2102 std::cerr << "Writer: failed to allocate CUDA frame from hardware pool\n";
2103 releaseFrame(queued_frame);
2104 return false;
2105 }
2106
2107#ifdef MXWRITE_HAS_CUDA_COPY
2108 cudaStream_t stream = cuda_upload_stream;
2109 const bool use_async_stream = (stream != nullptr);
2110 if (!bottom_up) {
2111 const auto copy_err = use_async_stream
2112 ? cudaMemcpy2DAsync(
2113 queued_frame->data[0],
2114 static_cast<size_t>(queued_frame->linesize[0]),
2115 cuda_rgba_buffer,
2116 static_cast<size_t>(src_stride),
2117 static_cast<size_t>(width) * 4,
2118 static_cast<size_t>(height),
2119 cudaMemcpyDeviceToDevice,
2120 stream)
2121 : cudaMemcpy2D(
2122 queued_frame->data[0],
2123 static_cast<size_t>(queued_frame->linesize[0]),
2124 cuda_rgba_buffer,
2125 static_cast<size_t>(src_stride),
2126 static_cast<size_t>(width) * 4,
2127 static_cast<size_t>(height),
2128 cudaMemcpyDeviceToDevice);
2129
2130 if (copy_err != cudaSuccess) {
2131 std::cerr << "Writer: cudaMemcpy2D device upload failed: " << cudaGetErrorString(copy_err) << "\n";
2132 releaseFrame(queued_frame);
2133 return false;
2134 }
2135 } else {
2136 // Flip vertically by issuing one async row copy per destination row
2137 // onto a single stream — kept asynchronous so launch overhead overlaps
2138 // and the producer thread only blocks once at cudaStreamSynchronize.
2139 auto *src_base = static_cast<unsigned char *>(cuda_rgba_buffer);
2140 auto *dst_base = queued_frame->data[0];
2141 const size_t row_bytes = static_cast<size_t>(width) * 4;
2142
2143 for (int y = 0; y < height; ++y) {
2144 auto *src_row = src_base + static_cast<size_t>(height - 1 - y) * static_cast<size_t>(src_stride);
2145 auto *dst_row = dst_base + static_cast<size_t>(y) * static_cast<size_t>(queued_frame->linesize[0]);
2146 const auto row_copy_err = use_async_stream
2147 ? cudaMemcpyAsync(dst_row, src_row, row_bytes, cudaMemcpyDeviceToDevice, stream)
2148 : cudaMemcpy(dst_row, src_row, row_bytes, cudaMemcpyDeviceToDevice);
2149 if (row_copy_err != cudaSuccess) {
2150 std::cerr << "Writer: cudaMemcpy row upload failed: " << cudaGetErrorString(row_copy_err) << "\n";
2151 releaseFrame(queued_frame);
2152 return false;
2153 }
2154 }
2155 }
2156 // Single synchronisation point — NVENC requires the data to be ready when
2157 // avcodec_send_frame() reads it, and the encoder thread is decoupled by
2158 // the queue so this sync only serialises this one producer call.
2159 if (use_async_stream) {
2160 const auto sync_err = cudaStreamSynchronize(stream);
2161 if (sync_err != cudaSuccess) {
2162 std::cerr << "Writer: cudaStreamSynchronize failed: " << cudaGetErrorString(sync_err) << "\n";
2163 releaseFrame(queued_frame);
2164 return false;
2165 }
2166 }
2167#else
2168 std::cerr << "Writer: CUDA copy support disabled at build time\n";
2169 releaseFrame(queued_frame);
2170 return false;
2171#endif
2172
2173 {
2174 std::unique_lock<std::mutex> lock(queue_mutex);
2175 if (block_when_full.load(std::memory_order_relaxed)) {
2176 if (encode_queue.size() >= NO_DROP_QUEUE_SIZE) {
2177 queue_cv.wait(lock, [this] {
2178 return stop_requested || encode_queue.size() < NO_DROP_QUEUE_SIZE;
2179 });
2180 }
2181 if (stop_requested) {
2182 releaseFrame(queued_frame);
2183 return false;
2184 }
2185 } else if (stop_requested || encode_queue.size() >= MAX_QUEUE_SIZE) {
2186 static int drop_counter = 0;
2187 if (++drop_counter % 30 == 0) {
2188 std::cerr << "Writer: dropped " << drop_counter << " frames (encoder queue full)\n";
2189 }
2190 releaseFrame(queued_frame);
2191 // Return TRUE so the producer does NOT fall back to the slow CPU
2192 // write() path — we already "handled" the frame (by dropping it).
2193 // Falling back would double-process every frame and double the drops.
2194 queue_cv.notify_one();
2195 return true;
2196 }
2197 if (pts != AV_NOPTS_VALUE && pts < frame_count) {
2198 releaseFrame(queued_frame);
2199 queue_cv.notify_one();
2200 return true;
2201 }
2202 queued_frame->pts = pts == AV_NOPTS_VALUE ? frame_count : pts;
2203 frame_count = queued_frame->pts + 1;
2204 encode_queue.push(queued_frame);
2205 }
2206
2207 queue_cv.notify_one();
2208 return true;
2209}
2210
2211void Writer::write_ts(void *rgba_buffer) {
2212 write(rgba_buffer);
2213}
2214
2215void Writer::startEncoderThread() {
2216 stop_requested = false;
2217 encode_thread = std::jthread([this](std::stop_token st) {
2218 encodeLoop(st);
2219 });
2220}
2221
2222void Writer::stopEncoderThread() {
2223 {
2224 std::lock_guard<std::mutex> lock(queue_mutex);
2225 stop_requested = true;
2226 }
2227 queue_cv.notify_all();
2228
2229 if (encode_thread.joinable()) {
2230 encode_thread.request_stop();
2231 encode_thread.join();
2232 }
2233}
2234
2235void Writer::releaseFrame(AVFrame *f) {
2236 if (!f) {
2237 return;
2238 }
2239 av_frame_free(&f);
2240}
2241
2242void Writer::drainEncoderPackets() {
2243 AVPacket *pkt = av_packet_alloc();
2244 if (!pkt) {
2245 std::cerr << "Writer: failed to allocate packet\n";
2246 return;
2247 }
2248
2249 while (true) {
2250 int ret = avcodec_receive_packet(codec_ctx, pkt);
2251 if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
2252 break;
2253 }
2254 if (ret < 0) {
2255 std::cerr << "Writer: error receiving packet: " << ret << "\n";
2256 break;
2257 }
2258
2259 av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base);
2260 pkt->stream_index = stream->index;
2261
2262 if (av_interleaved_write_frame(format_ctx, pkt) < 0) {
2263 std::cerr << "Writer: error writing frame\n";
2264 av_packet_unref(pkt);
2265 break;
2266 }
2267 updateBytesWritten();
2268 av_packet_unref(pkt);
2269 }
2270
2271 av_packet_free(&pkt);
2272}
2273
2274void Writer::encodeAndWriteFrame(AVFrame *in_frame) {
2275 if (!in_frame) {
2276 return;
2277 }
2278
2279 AVFrame *encode_frame = in_frame;
2280 AVFrame *uploaded_frame = nullptr;
2281 if (hdr_output) {
2282 if (in_frame->format == AV_PIX_FMT_YUV420P10LE) {
2283 // Frame has already been converted to BT.2020 PQ YUV420P10LE
2284 // by write_hdr_rgba16(). Use directly.
2285 encode_frame = in_frame;
2286 } else {
2287 // in_frame is RGBA 8-bit from the shader pipeline. Convert to BT.2020
2288 // PQ YUV420P10LE in frame10 and submit that instead.
2289 if (av_frame_make_writable(frame10) < 0) {
2290 std::cerr << "Writer: HDR frame not writable\n";
2291 return;
2292 }
2294 in_frame->data[0],
2295 in_frame->linesize[0],
2296 reinterpret_cast<uint16_t *>(frame10->data[0]),
2297 frame10->linesize[0] / 2,
2298 reinterpret_cast<uint16_t *>(frame10->data[1]),
2299 frame10->linesize[1] / 2,
2300 reinterpret_cast<uint16_t *>(frame10->data[2]),
2301 frame10->linesize[2] / 2,
2302 width,
2303 height);
2304 frame10->pts = in_frame->pts;
2305 frame10->color_primaries = static_cast<AVColorPrimaries>(
2306 hdr_info.color_primaries ? hdr_info.color_primaries : AVCOL_PRI_BT2020);
2307 frame10->color_trc = static_cast<AVColorTransferCharacteristic>(
2308 hdr_info.color_trc ? hdr_info.color_trc : AVCOL_TRC_SMPTE2084);
2309 frame10->colorspace = static_cast<AVColorSpace>(
2310 hdr_info.color_space ? hdr_info.color_space : AVCOL_SPC_BT2020_NCL);
2311 frame10->color_range = static_cast<AVColorRange>(
2312 hdr_info.color_range ? hdr_info.color_range : AVCOL_RANGE_MPEG);
2313 encode_frame = frame10;
2314 }
2315 } else if (use_hw_encode && in_frame->format == AV_PIX_FMT_RGBA) {
2316 uploaded_frame = av_frame_alloc();
2317 if (!uploaded_frame) {
2318 std::cerr << "Writer: failed to allocate hardware upload frame\n";
2319 return;
2320 }
2321 uploaded_frame->format = codec_ctx->pix_fmt;
2322 uploaded_frame->width = width;
2323 uploaded_frame->height = height;
2324 if (av_hwframe_get_buffer(hw_frames_ctx, uploaded_frame, 0) < 0) {
2325 std::cerr << "Writer: failed to allocate frame from hardware pool\n";
2326 releaseFrame(uploaded_frame);
2327 return;
2328 }
2329 if (av_frame_make_writable(upload_sw_frame) < 0) {
2330 std::cerr << "Writer: software upload frame not writable\n";
2331 releaseFrame(uploaded_frame);
2332 return;
2333 }
2334
2335 if (upload_sw_frame->format == AV_PIX_FMT_RGBA) {
2336 for (int y = 0; y < height; ++y) {
2337 std::memcpy(
2338 upload_sw_frame->data[0] +
2339 static_cast<size_t>(y) * upload_sw_frame->linesize[0],
2340 in_frame->data[0] +
2341 static_cast<size_t>(y) * in_frame->linesize[0],
2342 static_cast<size_t>(width) * 4);
2343 }
2344 } else {
2345 const uint8_t *source_data[1] = {in_frame->data[0]};
2346 const int source_linesize[1] = {in_frame->linesize[0]};
2347 sws_scale(sws_ctx, source_data, source_linesize, 0, height,
2348 upload_sw_frame->data, upload_sw_frame->linesize);
2349 }
2350
2351 if (av_hwframe_transfer_data(uploaded_frame, upload_sw_frame, 0) < 0) {
2352 std::cerr << "Writer: failed to transfer system frame to hardware frame\n";
2353 releaseFrame(uploaded_frame);
2354 return;
2355 }
2356 uploaded_frame->pts = in_frame->pts;
2357 encode_frame = uploaded_frame;
2358 } else if (!use_hw_encode) {
2359 const uint8_t *src_data[1] = {in_frame->data[0]};
2360 int src_linesize[1] = {in_frame->linesize[0]};
2361 sws_scale(sws_ctx, src_data, src_linesize, 0, height, frameYUV->data, frameYUV->linesize);
2362 frameYUV->pts = in_frame->pts;
2363 encode_frame = frameYUV;
2364 }
2365
2366 int ret = avcodec_send_frame(codec_ctx, encode_frame);
2367 if (ret == AVERROR(EAGAIN)) {
2368 // Encoder output queue is full; drain and retry this frame once.
2369 drainEncoderPackets();
2370 ret = avcodec_send_frame(codec_ctx, encode_frame);
2371 }
2372 if (ret < 0) {
2373 std::cerr << "Writer: error sending frame to encoder: " << ret << "\n";
2374 releaseFrame(uploaded_frame);
2375 return;
2376 }
2377
2378 drainEncoderPackets();
2379 releaseFrame(uploaded_frame);
2380}
2381
2382void Writer::encodeLoop(std::stop_token stop_token) {
2383 while (true) {
2384 AVFrame *frame = nullptr;
2385 {
2386 std::unique_lock<std::mutex> lock(queue_mutex);
2387 queue_cv.wait(lock, [this, &stop_token]() {
2388 return stop_requested || stop_token.stop_requested() || !encode_queue.empty();
2389 });
2390
2391 if ((stop_requested || stop_token.stop_requested()) && encode_queue.empty()) {
2392 break;
2393 }
2394
2395 frame = encode_queue.front();
2396 encode_queue.pop();
2397 }
2398 // Wake any producer blocked in write() waiting for queue space.
2399 queue_cv.notify_one();
2400
2401 encodeAndWriteFrame(frame);
2402 releaseFrame(frame);
2403 };
2404
2405 if (codec_ctx) {
2406 const int flush_ret = avcodec_send_frame(codec_ctx, nullptr);
2407 if (flush_ret >= 0) {
2408 drainEncoderPackets();
2409 }
2410 }
2411}
2412
2413void Writer::updateBytesWritten() noexcept {
2414 if (!format_ctx || !format_ctx->pb) {
2415 return;
2416 }
2417
2418 const int64_t position = avio_tell(format_ctx->pb);
2419 if (position >= 0) {
2420 bytes_written.store(static_cast<std::uint64_t>(position),
2421 std::memory_order_relaxed);
2422 }
2423}
2424
2426 std::lock_guard<std::mutex> lock(writer_mutex);
2427 if (!opened) {
2428 return;
2429 }
2430
2431 stopEncoderThread();
2432
2433 if (stream && stream->duration > 0) {
2434 last_duration = static_cast<double>(stream->duration) * av_q2d(stream->time_base);
2435 } else if (fps_num > 0 && fps_den > 0) {
2436 last_duration = static_cast<double>(frame_count) * static_cast<double>(fps_den) / static_cast<double>(fps_num);
2437 }
2438
2439 av_write_trailer(format_ctx);
2440 updateBytesWritten();
2441
2442 if (!(format_ctx->oformat->flags & AVFMT_NOFILE)) {
2443 avio_closep(&format_ctx->pb);
2444 }
2445
2446 av_frame_free(&frameRGBA);
2447 av_frame_free(&frameYUV);
2448 av_frame_free(&frame10);
2449 sws_freeContext(sws_ctx);
2450 av_frame_free(&upload_sw_frame);
2451#ifdef MXWRITE_HAS_CUDA_COPY
2452 // Destroy stream before tearing down FFmpeg CUDA device/frames contexts.
2453 if (cuda_upload_stream) {
2454 cudaStreamSynchronize(cuda_upload_stream);
2455 cudaStreamDestroy(cuda_upload_stream);
2456 cuda_upload_stream = nullptr;
2457 }
2458#endif
2459 avcodec_free_context(&codec_ctx);
2460 av_buffer_unref(&hw_frames_ctx);
2461 av_buffer_unref(&hw_device_ctx);
2462 avformat_free_context(format_ctx);
2463
2464 while (!encode_queue.empty()) {
2465 releaseFrame(encode_queue.front());
2466 encode_queue.pop();
2467 }
2468 opened = false;
2469 format_ctx = nullptr;
2470 codec_ctx = nullptr;
2471 sws_ctx = nullptr;
2472 frameRGBA = nullptr;
2473 frameYUV = nullptr;
2474 frame10 = nullptr;
2475 upload_sw_frame = nullptr;
2476 use_hw_encode = false;
2477 active_encoder_hardware = false;
2478 direct_cuda_upload = false;
2479 hdr_output = false;
2480 stop_requested = false;
2481}
2482
2483double Writer::get_duration() const {
2484 if (!opened && last_duration > 0.0) {
2485 return last_duration;
2486 }
2487 if (stream && stream->duration > 0) {
2488 return static_cast<double>(stream->duration) * av_q2d(stream->time_base);
2489 }
2490 if (fps_num > 0 && fps_den > 0) {
2491 return static_cast<double>(frame_count) * static_cast<double>(fps_den) / static_cast<double>(fps_num);
2492 }
2493 return 0.0;
2494}
void write_hdr_rgba16_at_pts(void *rgba16_buffer, int64_t pts)
Queue a 16-bit HDR RGBA frame with an explicit presentation timestamp.
Definition mxwrite.cpp:1982
bool write_cuda_rgba(void *cuda_rgba_buffer, int src_stride, bool bottom_up=false)
Queue a CUDA RGBA frame for encoding.
Definition mxwrite.cpp:2072
void write_at_pts(void *rgba_buffer, int64_t pts)
Queue a host RGBA frame with an explicit presentation timestamp.
Definition mxwrite.cpp:1863
bool open(const std::string &filename, int width, int height, float fps, const char *crf)
Open an output file using the legacy CRF string interface.
Definition mxwrite.cpp:1039
void write(void *rgba_buffer)
Queue a host RGBA frame for immediate-mode encoding.
Definition mxwrite.cpp:1859
bool open_ts(const std::string &filename, int width, int height, float fps, const char *crf)
Open a timestamp-based output stream using the legacy CRF string interface.
Definition mxwrite.cpp:1060
double get_duration() const
Return the encoded duration in seconds.
Definition mxwrite.cpp:2483
bool write_cuda_rgba_at_pts(void *cuda_rgba_buffer, int src_stride, int64_t pts, bool bottom_up=false)
Queue a CUDA RGBA frame with an explicit presentation timestamp.
Definition mxwrite.cpp:2077
void close()
Close the writer and flush pending packets.
Definition mxwrite.cpp:2425
void write_hdr_rgba16(void *rgba16_buffer)
Write a 16-bit RGBA frame that is already PQ- or HLG-encoded in BT.2020 primaries (8 bytes/pixel: R16...
Definition mxwrite.cpp:1978
void write_ts(void *rgba_buffer)
Queue a host RGBA frame using capture timestamps.
Definition mxwrite.cpp:2211
std::mutex transfer_audio_mutex
Definition mxwrite.cpp:204
void transfer_audio(std::string_view sourceAudioFile, std::string_view destVideoFile)
Copy audio from one video file to another.
Definition mxwrite.cpp:240
bool is_format_supported(const char *filename)
Definition mxwrite.cpp:206
void cleanup_contexts(AVFormatContext *source_ctx, AVFormatContext *dest_ctx, AVFormatContext *output_ctx)
Free FFmpeg format contexts used during transfer operations.
Definition mxwrite.cpp:226
std::vector< EncoderInfo > available_video_encoders()
Definition mxwrite.cpp:980
std::vector< EncoderOptionInfo > video_encoder_options(std::string_view encoder_name)
Return the options exposed by one registered video encoder.
Definition mxwrite.cpp:1018
FFmpeg-based video writer used by MXWrite.
const std::string * find_ffmpeg_option(const std::vector< FfmpegOption > &options, const std::string &name)
Definition mxwrite.cpp:628
std::string nvenc_preset_to_software(const std::string &preset)
Definition mxwrite.cpp:700
std::string lowercase_ascii(std::string text)
Definition mxwrite.cpp:478
bool looks_like_option(const std::string &token)
Definition mxwrite.cpp:498
bool parse_ffmpeg_options(const std::string &text, std::vector< FfmpegOption > &options)
Definition mxwrite.cpp:585
std::vector< AVPixelFormat > supported_pixel_formats(const AVCodec *codec)
Definition mxwrite.cpp:770
bool is_codec_name(const std::string &value)
Definition mxwrite.cpp:578
void convertRgbaToBt2020PqYuv420p10(const uint8_t *rgba, int src_stride_bytes, uint16_t *y_plane, int y_stride_shorts, uint16_t *u_plane, int u_stride_shorts, uint16_t *v_plane, int v_stride_shorts, int width, int height)
Definition mxwrite.cpp:67
bool is_nvenc_tune(const std::string &tune)
Definition mxwrite.cpp:695
bool option_has_numeric_range(AVOptionType type)
Definition mxwrite.cpp:912
std::string normalize_ffmpeg_option_name(std::string name)
Definition mxwrite.cpp:563
std::string number_text(double value)
Definition mxwrite.cpp:924
bool is_valid_x264_preset(const std::string &p)
Definition mxwrite.cpp:468
int option_base_type(AVOptionType type)
Definition mxwrite.cpp:490
bool tokenize_ffmpeg_options(const std::string &text, std::vector< std::string > &tokens, std::string &error)
Definition mxwrite.cpp:506
void convertBt2020Rgba16EncodedToYuv420p10(const uint16_t *rgba, int src_stride_shorts, uint16_t *y_plane, int y_stride_shorts, uint16_t *u_plane, int u_stride_shorts, uint16_t *v_plane, int v_stride_shorts, int width, int height)
Definition mxwrite.cpp:146
bool set_named_encoder_option(void *object, const char *name, const std::string &value)
Definition mxwrite.cpp:671
constexpr float kSdrRefFraction
Definition mxwrite.cpp:39
bool is_reserved_ffmpeg_option(const std::string &name)
Definition mxwrite.cpp:638
void append_encoder_options(void *object, std::vector< EncoderOptionInfo > &result, std::set< std::string > &seen)
Definition mxwrite.cpp:930
const char * x264_preset_to_nvenc(const std::string &p)
Definition mxwrite.cpp:449
std::vector< FfmpegOption > software_fallback_options(const std::vector< FfmpegOption > &options, bool use_hevc_codec)
Definition mxwrite.cpp:718
bool apply_ffmpeg_options(const std::vector< FfmpegOption > &options, AVCodecContext *context, AVFormatContext *output_context)
Definition mxwrite.cpp:643
bool codec_uses_hardware(const AVCodec *codec)
Definition mxwrite.cpp:840
std::string option_type_name(AVOptionType type)
Definition mxwrite.cpp:860
AVPixelFormat choose_software_pixel_format(const AVCodec *codec, AVPixelFormat requested_format)
Definition mxwrite.cpp:801
bool is_hardware_pixel_format(AVPixelFormat format)
Definition mxwrite.cpp:796
bool enabled
Enables the HDR output path.
Definition mxwrite.hpp:120
User-configurable video encoder quality options.
Definition mxwrite.hpp:92
std::string codec
Encoder selection policy or exact FFmpeg encoder name.
Definition mxwrite.hpp:96
std::string tune
Optional tuning mode.
Definition mxwrite.hpp:94
bool realtime
Enable low-latency settings.
Definition mxwrite.hpp:98
std::string ffmpeg_options
Additional FFmpeg-style video encoder options.
Definition mxwrite.hpp:97
struct EncodeOptions::HdrInfo hdr
int crf
Constant Rate Factor.
Definition mxwrite.hpp:95
bool block_when_full
Pace producers to encoder throughput instead of dropping frames.
Definition mxwrite.hpp:99
std::string preset
Encoder preset name.
Definition mxwrite.hpp:93
A video encoder reported by the linked FFmpeg installation.
Definition mxwrite.hpp:39
std::string long_name
Human-readable encoder description.
Definition mxwrite.hpp:41
std::string pixel_formats
Comma-separated supported input pixel formats.
Definition mxwrite.hpp:43
std::string name
Exact libavcodec encoder name (for example, libx265).
Definition mxwrite.hpp:40
std::string codec_name
Encoded format name (for example, hevc or av1).
Definition mxwrite.hpp:42
bool experimental
True when FFmpeg marks the encoder experimental.
Definition mxwrite.hpp:45
bool hardware
True for hardware or hybrid encoders.
Definition mxwrite.hpp:44
One configurable AVOption exposed by a video encoder.
Definition mxwrite.hpp:49
std::string minimum
Minimum value for numeric options.
Definition mxwrite.hpp:53
std::string name
Option name accepted by EncodeOptions::ffmpeg_options.
Definition mxwrite.hpp:50
std::string help
Human-readable FFmpeg option description.
Definition mxwrite.hpp:56
std::string choices
Comma-separated named values for enum-like options.
Definition mxwrite.hpp:55
std::string type
FFmpeg option type.
Definition mxwrite.hpp:51
std::string maximum
Maximum value for numeric options.
Definition mxwrite.hpp:54
std::string default_value
Encoder default, when it can be represented as text.
Definition mxwrite.hpp:52