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