MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VideoFileWriter.cpp
Go to the documentation of this file.
1#include "VideoFileWriter.hpp"
2
5
8
11
13
16
19
21
22extern "C" {
23#include <libavutil/pixfmt.h>
24}
25
26namespace MayaFlux::IO {
27
28namespace {
29
30 AVPixelFormat vk_format_to_avpixfmt(uint32_t vk_fmt_uint)
31 {
32 switch (static_cast<vk::Format>(vk_fmt_uint)) {
33 case vk::Format::eR8G8B8A8Unorm:
34 case vk::Format::eR8G8B8A8Srgb:
35 return AV_PIX_FMT_RGBA;
36 case vk::Format::eB8G8R8A8Unorm:
37 case vk::Format::eB8G8R8A8Srgb:
38 return AV_PIX_FMT_BGRA;
39 case vk::Format::eR16G16B16A16Sfloat:
40 return AV_PIX_FMT_RGBA64LE;
41 default:
42 return AV_PIX_FMT_BGRA;
43 }
44 }
45
46 AVPixelFormat image_format_to_avpixfmt(Portal::Graphics::ImageFormat fmt)
47 {
49 switch (fmt) {
50 case F::RGBA8:
51 case F::RGBA8_SRGB:
52 return AV_PIX_FMT_RGBA;
53 case F::BGRA8:
54 case F::BGRA8_SRGB:
55 return AV_PIX_FMT_BGRA;
56 case F::RGB8:
57 return AV_PIX_FMT_RGB24;
58 case F::RGBA16F:
59 case F::RGBA16:
60 return AV_PIX_FMT_RGBA64LE;
61 case F::RGBA32F:
62 return AV_PIX_FMT_RGBAF32LE;
63 default:
64 return AV_PIX_FMT_NONE;
65 }
66 }
67
68} // namespace
69
70// =========================================================================
71// Constructor / destructor
72// =========================================================================
73
75 : m_queue(std::make_unique<Memory::LockFreeQueue<WorkItem, k_queue_capacity>>())
76{
77}
78
80{
81 if (m_observer_id.load(std::memory_order_acquire) != 0) {
82 stop_recording().get();
83 } else if (m_open.load(std::memory_order_acquire) && !m_closing.exchange(true)) {
84 auto fut = close();
85 if (fut.wait_for(std::chrono::seconds(5)) == std::future_status::timeout) {
87 "VideoFileWriter destructor timed out; worker detached, file may be incomplete");
88 m_worker.detach();
89 return;
90 }
91 }
92 if (m_worker.joinable())
93 m_worker.join();
94}
95
96// =========================================================================
97// Screen capture — untouched, works
98// =========================================================================
99
100bool VideoFileWriter::record(const std::shared_ptr<Core::Window>& window,
101 const std::string& filepath,
102 double frame_rate,
103 AVCodecID codec_id)
104{
105 if (!window) {
106 set_error("record: null window");
107 return false;
108 }
109
112 if (!svc || !svc->register_frame_observer) {
113 set_error("record: DisplayService unavailable");
114 return false;
115 }
116
117 if (m_observer_id.load(std::memory_order_acquire) != 0)
118 stop_recording().get();
119
120 m_capture_filepath = filepath;
121 m_capture_frame_rate = frame_rate;
122 m_capture_codec_id = codec_id;
123 m_capture_window = window;
124 m_capture_opened.store(false, std::memory_order_release);
125
126 if (!window->is_capture_enabled()) {
127 window->set_capture_enabled(true);
129 }
130
131 auto handle = std::static_pointer_cast<void>(window);
132
133 uint32_t obs_id = svc->register_frame_observer(handle,
134 [this](const std::shared_ptr<std::vector<uint8_t>>& buf,
135 uint32_t w, uint32_t h, uint32_t vk_fmt) {
136 if (!buf || buf->empty())
137 return;
138
139 if (!m_capture_opened.exchange(true, std::memory_order_acq_rel)) {
140 const AVPixelFormat av_fmt = vk_format_to_avpixfmt(vk_fmt);
141 if (!open(m_capture_filepath, w, h,
144 "[VideoFileWriter] record: failed to open encoder for "
145 "'{}': {}",
147 m_capture_opened.store(false, std::memory_order_release);
148 return;
149 }
150 }
151
152 if (!m_open.load(std::memory_order_acquire))
153 return;
154
155 post(RawFrame {
156 .pixels = std::vector<uint8_t>(buf->begin(), buf->end()),
157 .width = w,
158 .height = h });
159 });
160
161 if (obs_id == 0) {
162 set_error("record: register_frame_observer returned 0 — "
163 "capture not yet active for this window");
165 window->set_capture_enabled(false);
166 m_capture_did_enable = false;
167 }
168 m_capture_window.reset();
169 return false;
170 }
171
172 m_observer_id.store(obs_id, std::memory_order_release);
173
175 "[VideoFileWriter] record: observer {} registered for '{}' -> '{}'",
176 obs_id, window->get_create_info().title, filepath);
177
178 return true;
179}
180
182{
183 const uint32_t obs_id = m_observer_id.exchange(0, std::memory_order_acq_rel);
184
185 if (obs_id != 0) {
188 if (svc && svc->unregister_frame_observer && m_capture_window) {
190 std::static_pointer_cast<void>(m_capture_window), obs_id);
191 }
192
194 m_capture_window->set_capture_enabled(false);
195 m_capture_did_enable = false;
196 }
197
198 m_capture_window.reset();
199 }
200
201 if (m_open.load(std::memory_order_acquire))
202 return close();
203
204 std::promise<bool> p;
205 p.set_value(false);
206 return p.get_future();
207}
208
209// =========================================================================
210// Lifecycle
211// =========================================================================
212
213bool VideoFileWriter::open(const std::string& filepath,
214 uint32_t width,
215 uint32_t height,
216 double frame_rate,
217 AVPixelFormat src_pixel_format,
218 AVCodecID explicit_codec)
219{
220 if (m_open.load(std::memory_order_acquire)) {
221 set_error("open() called while already open");
222 return false;
223 }
224
225 m_width = width;
227 m_src_fmt = src_pixel_format;
228 m_close_promise = std::promise<bool> {};
229 m_closing.store(false, std::memory_order_release);
230
231 m_worker = std::thread(&VideoFileWriter::worker_loop, this,
232 filepath, width, height, frame_rate, src_pixel_format, explicit_codec);
233
234 constexpr int k_spin_ms = 500;
235 constexpr int k_sleep_us = 500;
236 for (int i = 0; i < (k_spin_ms * 1000 / k_sleep_us); ++i) {
237 if (m_open.load(std::memory_order_acquire))
238 return true;
239 std::this_thread::sleep_for(std::chrono::microseconds(k_sleep_us));
240 }
241
242 if (m_worker.joinable())
243 m_worker.join();
244 return false;
245}
246
247std::future<bool> VideoFileWriter::close()
248{
249 if (!m_closing.exchange(true))
250 post(CloseCmd {});
251 return m_close_promise.get_future();
252}
253
254// =========================================================================
255// Write — raw pixels (capture path lands here; m_width/m_height from open())
256// =========================================================================
257
258void VideoFileWriter::write(const uint8_t* pixels, size_t size)
259{
260 if (!m_open.load(std::memory_order_acquire) || !pixels || size == 0)
261 return;
262 post(RawFrame {
263 .pixels = std::vector<uint8_t>(pixels, pixels + size),
264 .width = m_width,
265 .height = m_height });
266}
267
268void VideoFileWriter::write(std::span<const uint8_t> pixels)
269{
270 if (!m_open.load(std::memory_order_acquire) || pixels.empty())
271 return;
272 post(RawFrame {
273 .pixels = std::vector<uint8_t>(pixels.begin(), pixels.end()),
274 .width = m_width,
275 .height = m_height });
276}
277
278// =========================================================================
279// Write — TextureContainer
280// =========================================================================
281
282void VideoFileWriter::write(const std::shared_ptr<Kakshya::TextureContainer>& container,
283 uint32_t layer)
284{
285 if (!m_open.load(std::memory_order_acquire) || !container)
286 return;
287
288 auto span = container->pixel_bytes(layer);
289 if (span.empty()) {
291 "VideoFileWriter::write(TextureContainer): pixel_bytes empty for layer {}",
292 layer);
293 return;
294 }
295
296 post(RawFrame {
297 .pixels = std::vector<uint8_t>(span.begin(), span.end()),
298 .width = container->get_width(),
299 .height = container->get_height() });
300}
301
302// =========================================================================
303// Write — VideoStreamContainer / CameraContainer
304// =========================================================================
305
306void VideoFileWriter::write(const std::shared_ptr<Kakshya::VideoStreamContainer>& container,
307 uint64_t frame_index)
308{
309 if (!m_open.load(std::memory_order_acquire) || !container)
310 return;
311
312 auto span = container->get_frame_pixels(frame_index);
313 if (span.empty()) {
315 "VideoFileWriter::write(VideoStreamContainer): get_frame_pixels({}) returned empty",
316 frame_index);
317 return;
318 }
319
320 post(RawFrame {
321 .pixels = std::vector<uint8_t>(span.begin(), span.end()),
322 .width = container->get_width(),
323 .height = container->get_height() });
324}
325
326// =========================================================================
327// Write — TextureBuffer
328// =========================================================================
329
330void VideoFileWriter::write(const std::shared_ptr<Buffers::TextureBuffer>& buffer)
331{
332 if (!m_open.load(std::memory_order_acquire) || !buffer)
333 return;
334
335 if (buffer->get_pixel_data().empty() && !buffer->has_texture()) {
337 "VideoFileWriter::write(TextureBuffer): no CPU pixels and no GPU texture");
338 return;
339 }
340
341 post(DownloadCmd { .buffer = buffer });
342}
343
344// =========================================================================
345// Error / post
346// =========================================================================
347
349{
350 std::lock_guard lock(m_error_mutex);
351 return m_last_error;
352}
353
354void VideoFileWriter::set_error(std::string msg)
355{
356 std::lock_guard lock(m_error_mutex);
357 m_last_error = std::move(msg);
358}
359
361{
362 return m_queue->push(item);
363}
364
365// =========================================================================
366// Worker loop
367// =========================================================================
368
369void VideoFileWriter::worker_loop(const std::string& filepath,
370 uint32_t width,
371 uint32_t height,
372 double frame_rate,
373 AVPixelFormat src_fmt,
374 AVCodecID codec_id)
375{
378
379 auto fail = [&](std::string msg) {
380 set_error(std::move(msg));
381 m_open.store(false, std::memory_order_release);
382 m_close_promise.set_value(false);
383 };
384
385 if (!mux.open(filepath)) {
386 fail(mux.last_error());
387 return;
388 }
389 if (!enc.open(mux, width, height, frame_rate, src_fmt, codec_id)) {
390 fail(enc.last_error());
391 return;
392 }
393 if (!mux.write_header()) {
394 fail(mux.last_error());
395 return;
396 }
397
398 m_open.store(true, std::memory_order_release);
399
401 "[VideoFileWriter] worker started: '{}' {}x{} @{:.3f}fps",
402 filepath, width, height, frame_rate);
403
404 while (true) {
405 auto item_opt = m_queue->pop();
406 if (!item_opt) {
407 std::this_thread::sleep_for(std::chrono::microseconds(100));
408 continue;
409 }
410
411 bool done = std::visit([&](auto& cmd) -> bool {
412 using T = std::decay_t<decltype(cmd)>;
413
414 if constexpr (std::is_same_v<T, RawFrame>) {
415 if (!enc.encode_frame(cmd.pixels.data(), cmd.pixels.size(),
416 cmd.width, cmd.height, mux)) {
417 set_error(enc.last_error());
419 "[VideoFileWriter] encode_frame failed: {}", enc.last_error());
420 }
421 return false;
422 }
423
424 if constexpr (std::is_same_v<T, DownloadCmd>) {
425 const auto img_fmt = cmd.buffer->get_format();
426 const AVPixelFormat av_fmt = image_format_to_avpixfmt(img_fmt);
427 if (av_fmt == AV_PIX_FMT_NONE) {
429 "[VideoFileWriter] DownloadCmd: unsupported ImageFormat {}",
430 static_cast<int>(img_fmt));
431 return false;
432 }
433
434 const auto& cpu = cmd.buffer->get_pixel_data();
435 if (!cpu.empty()) {
436 if (!enc.encode_frame(cpu.data(), cpu.size(),
437 cmd.buffer->get_width(), cmd.buffer->get_height(), mux)) {
438 set_error(enc.last_error());
440 "[VideoFileWriter] encode_frame (cpu) failed: {}", enc.last_error());
441 }
442 return false;
443 }
444
445 auto tex = cmd.buffer->get_texture();
446 if (!tex) {
448 "[VideoFileWriter] DownloadCmd: no CPU pixels and no GPU texture");
449 return false;
450 }
451
453 const size_t mip0_bytes = static_cast<size_t>(tex->get_width())
454 * tex->get_height()
455 * TextureLoom::get_bytes_per_pixel(img_fmt);
456
457 if (mip0_bytes == 0)
458 return false;
459
460 std::vector<uint8_t> pixels(mip0_bytes);
461
462 if (!m_staging_buffer) {
464 }
465
466 TextureLoom::instance().download_data(tex, pixels.data(), mip0_bytes, m_staging_buffer, true);
467
468 if (!enc.encode_frame(pixels.data(), pixels.size(),
469 tex->get_width(), tex->get_height(), mux)) {
470 set_error(enc.last_error());
472 "[VideoFileWriter] encode_frame (gpu) failed: {}", enc.last_error());
473 }
474 return false;
475 }
476
477 return static_cast<bool>(std::is_same_v<T, CloseCmd>);
478 },
479 *item_opt);
480
481 if (done)
482 break;
483 }
484
485 bool ok = enc.drain(mux);
486 if (!ok) {
487 set_error(enc.last_error());
489 "[VideoFileWriter] drain failed: {}", enc.last_error());
490 }
491
492 mux.close();
493 m_open.store(false, std::memory_order_release);
494 m_close_promise.set_value(ok);
495
497 "[VideoFileWriter] worker finished: '{}' status={}",
498 filepath, ok ? "ok" : "error");
499}
500
501} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
vk::CommandBuffer cmd
uint32_t width
Definition Decoder.cpp:66
const std::vector< float > * pixels
Definition Decoder.cpp:65
uint32_t h
Definition InkPress.cpp:28
uint32_t height
bool open(const std::string &filepath, const std::string &explicit_format={})
Allocate an output context and open the avio layer for writing.
bool write_header()
Write the container header to the output file.
const std::string & last_error() const
void close()
Write the container trailer, flush avio, and release all resources.
RAII owner of a single AVFormatContext on the write path.
const std::string & last_error() const
bool open(FFmpegMuxContext &mux, uint32_t width, uint32_t height, double frame_rate, AVPixelFormat src_pixel_format, AVCodecID codec_id)
Open the encoder and register a video stream in the mux context.
bool drain(FFmpegMuxContext &mux)
Flush all buffered frames from the encoder to the mux.
bool encode_frame(const uint8_t *src_data, size_t src_size, uint32_t src_width, uint32_t src_height, FFmpegMuxContext &mux)
Encode one raw pixel frame into the mux context.
RAII owner of one video stream's encoder and pixel-format converter on the write path.
std::shared_ptr< Buffers::VKBuffer > m_staging_buffer
void write(const uint8_t *pixels, size_t size)
bool open(const std::string &filepath, uint32_t width, uint32_t height, double frame_rate, AVPixelFormat src_pixel_format, AVCodecID explicit_codec=AV_CODEC_ID_NONE)
bool post(const WorkItem &item)
std::future< bool > stop_recording()
bool record(const std::shared_ptr< Core::Window > &window, const std::string &filepath, double frame_rate, AVCodecID codec_id=AV_CODEC_ID_NONE)
void worker_loop(const std::string &filepath, uint32_t width, uint32_t height, double frame_rate, AVPixelFormat src_fmt, AVCodecID codec_id)
std::atomic< uint32_t > m_observer_id
std::atomic< bool > m_capture_opened
std::shared_ptr< Core::Window > m_capture_window
std::variant< RawFrame, DownloadCmd, CloseCmd > WorkItem
void set_error(std::string msg)
std::unique_ptr< Memory::LockFreeQueue< WorkItem, k_queue_capacity > > m_queue
std::promise< bool > m_close_promise
Portal-level texture creation and management.
Interface * get_service()
Query for a backend service.
static BackendRegistry & instance()
Get the global registry instance.
std::shared_ptr< VKBuffer > create_image_staging_buffer(size_t size)
Allocate a persistent host-visible staging buffer sized for repeated streaming uploads to an image of...
@ FileIO
Filesystem I/O operations.
@ IO
Networking, file handling, streaming.
ImageFormat
User-friendly image format enum.
std::shared_ptr< Buffers::TextureBuffer > buffer
std::function< void(const std::shared_ptr< void > &, uint32_t)> unregister_frame_observer
Unregister a previously registered per-frame observer.
Backend display and presentation service interface.