MayaFlux 0.4.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
7
10
12
15
18
20
21extern "C" {
22#include <libavutil/pixfmt.h>
23}
24
25namespace MayaFlux::IO {
26
27namespace {
28
29 AVPixelFormat vk_format_to_avpixfmt(uint32_t vk_fmt_uint)
30 {
31 switch (static_cast<vk::Format>(vk_fmt_uint)) {
32 case vk::Format::eR8G8B8A8Unorm:
33 case vk::Format::eR8G8B8A8Srgb:
34 return AV_PIX_FMT_RGBA;
35 case vk::Format::eB8G8R8A8Unorm:
36 case vk::Format::eB8G8R8A8Srgb:
37 return AV_PIX_FMT_BGRA;
38 case vk::Format::eR16G16B16A16Sfloat:
39 return AV_PIX_FMT_RGBA64LE;
40 default:
41 return AV_PIX_FMT_BGRA;
42 }
43 }
44
45 AVPixelFormat image_format_to_avpixfmt(Portal::Graphics::ImageFormat fmt)
46 {
48 switch (fmt) {
49 case F::RGBA8:
50 case F::RGBA8_SRGB:
51 return AV_PIX_FMT_RGBA;
52 case F::BGRA8:
53 case F::BGRA8_SRGB:
54 return AV_PIX_FMT_BGRA;
55 case F::RGB8:
56 return AV_PIX_FMT_RGB24;
57 case F::RGBA16F:
58 case F::RGBA16:
59 return AV_PIX_FMT_RGBA64LE;
60 case F::RGBA32F:
61 return AV_PIX_FMT_RGBAF32LE;
62 default:
63 return AV_PIX_FMT_NONE;
64 }
65 }
66
67} // namespace
68
69// =========================================================================
70// Constructor / destructor
71// =========================================================================
72
74 : m_queue(std::make_unique<Memory::LockFreeQueue<WorkItem, k_queue_capacity>>())
75{
76}
77
79{
80 if (m_observer_id.load(std::memory_order_acquire) != 0) {
81 stop_recording().get();
82 } else if (m_open.load(std::memory_order_acquire) && !m_closing.exchange(true)) {
83 auto fut = close();
84 if (fut.wait_for(std::chrono::seconds(5)) == std::future_status::timeout) {
86 "VideoFileWriter destructor timed out; worker detached, file may be incomplete");
87 m_worker.detach();
88 return;
89 }
90 }
91 if (m_worker.joinable())
92 m_worker.join();
93}
94
95// =========================================================================
96// Screen capture — untouched, works
97// =========================================================================
98
99bool VideoFileWriter::record(const std::shared_ptr<Core::Window>& window,
100 const std::string& filepath,
101 double frame_rate,
102 AVCodecID codec_id)
103{
104 if (!window) {
105 set_error("record: null window");
106 return false;
107 }
108
111 if (!svc || !svc->register_frame_observer) {
112 set_error("record: DisplayService unavailable");
113 return false;
114 }
115
116 if (m_observer_id.load(std::memory_order_acquire) != 0)
117 stop_recording().get();
118
119 m_capture_filepath = filepath;
120 m_capture_frame_rate = frame_rate;
121 m_capture_codec_id = codec_id;
122 m_capture_window = window;
123 m_capture_opened.store(false, std::memory_order_release);
124
125 if (!window->is_capture_enabled()) {
126 window->set_capture_enabled(true);
128 }
129
130 auto handle = std::static_pointer_cast<void>(window);
131
132 uint32_t obs_id = svc->register_frame_observer(handle,
133 [this](const std::shared_ptr<std::vector<uint8_t>>& buf,
134 uint32_t w, uint32_t h, uint32_t vk_fmt) {
135 if (!buf || buf->empty())
136 return;
137
138 if (!m_capture_opened.exchange(true, std::memory_order_acq_rel)) {
139 const AVPixelFormat av_fmt = vk_format_to_avpixfmt(vk_fmt);
140 if (!open(m_capture_filepath, w, h,
143 "[VideoFileWriter] record: failed to open encoder for "
144 "'{}': {}",
146 m_capture_opened.store(false, std::memory_order_release);
147 return;
148 }
149 }
150
151 if (!m_open.load(std::memory_order_acquire))
152 return;
153
154 post(RawFrame {
155 .pixels = std::vector<uint8_t>(buf->begin(), buf->end()),
156 .width = w,
157 .height = h });
158 });
159
160 if (obs_id == 0) {
161 set_error("record: register_frame_observer returned 0 — "
162 "capture not yet active for this window");
164 window->set_capture_enabled(false);
165 m_capture_did_enable = false;
166 }
167 m_capture_window.reset();
168 return false;
169 }
170
171 m_observer_id.store(obs_id, std::memory_order_release);
172
174 "[VideoFileWriter] record: observer {} registered for '{}' -> '{}'",
175 obs_id, window->get_create_info().title, filepath);
176
177 return true;
178}
179
181{
182 const uint32_t obs_id = m_observer_id.exchange(0, std::memory_order_acq_rel);
183
184 if (obs_id != 0) {
187 if (svc && svc->unregister_frame_observer && m_capture_window) {
189 std::static_pointer_cast<void>(m_capture_window), obs_id);
190 }
191
193 m_capture_window->set_capture_enabled(false);
194 m_capture_did_enable = false;
195 }
196
197 m_capture_window.reset();
198 }
199
200 if (m_open.load(std::memory_order_acquire))
201 return close();
202
203 std::promise<bool> p;
204 p.set_value(false);
205 return p.get_future();
206}
207
208// =========================================================================
209// Lifecycle
210// =========================================================================
211
212bool VideoFileWriter::open(const std::string& filepath,
213 uint32_t width,
214 uint32_t height,
215 double frame_rate,
216 AVPixelFormat src_pixel_format,
217 AVCodecID explicit_codec)
218{
219 if (m_open.load(std::memory_order_acquire)) {
220 set_error("open() called while already open");
221 return false;
222 }
223
224 m_width = width;
225 m_height = height;
226 m_src_fmt = src_pixel_format;
227 m_close_promise = std::promise<bool> {};
228 m_closing.store(false, std::memory_order_release);
229
230 m_worker = std::thread(&VideoFileWriter::worker_loop, this,
231 filepath, width, height, frame_rate, src_pixel_format, explicit_codec);
232
233 constexpr int k_spin_ms = 500;
234 constexpr int k_sleep_us = 500;
235 for (int i = 0; i < (k_spin_ms * 1000 / k_sleep_us); ++i) {
236 if (m_open.load(std::memory_order_acquire))
237 return true;
238 std::this_thread::sleep_for(std::chrono::microseconds(k_sleep_us));
239 }
240
241 if (m_worker.joinable())
242 m_worker.join();
243 return false;
244}
245
246std::future<bool> VideoFileWriter::close()
247{
248 if (!m_closing.exchange(true))
249 post(CloseCmd {});
250 return m_close_promise.get_future();
251}
252
253// =========================================================================
254// Write — raw pixels (capture path lands here; m_width/m_height from open())
255// =========================================================================
256
257void VideoFileWriter::write(const uint8_t* pixels, size_t size)
258{
259 if (!m_open.load(std::memory_order_acquire) || !pixels || size == 0)
260 return;
261 post(RawFrame {
262 .pixels = std::vector<uint8_t>(pixels, pixels + size),
263 .width = m_width,
264 .height = m_height });
265}
266
267void VideoFileWriter::write(std::span<const uint8_t> pixels)
268{
269 if (!m_open.load(std::memory_order_acquire) || pixels.empty())
270 return;
271 post(RawFrame {
272 .pixels = std::vector<uint8_t>(pixels.begin(), pixels.end()),
273 .width = m_width,
274 .height = m_height });
275}
276
277// =========================================================================
278// Write — TextureContainer
279// =========================================================================
280
281void VideoFileWriter::write(const std::shared_ptr<Kakshya::TextureContainer>& container,
282 uint32_t layer)
283{
284 if (!m_open.load(std::memory_order_acquire) || !container)
285 return;
286
287 auto span = container->pixel_bytes(layer);
288 if (span.empty()) {
290 "VideoFileWriter::write(TextureContainer): pixel_bytes empty for layer {}",
291 layer);
292 return;
293 }
294
295 post(RawFrame {
296 .pixels = std::vector<uint8_t>(span.begin(), span.end()),
297 .width = container->get_width(),
298 .height = container->get_height() });
299}
300
301// =========================================================================
302// Write — VideoStreamContainer / CameraContainer
303// =========================================================================
304
305void VideoFileWriter::write(const std::shared_ptr<Kakshya::VideoStreamContainer>& container,
306 uint64_t frame_index)
307{
308 if (!m_open.load(std::memory_order_acquire) || !container)
309 return;
310
311 auto span = container->get_frame_pixels(frame_index);
312 if (span.empty()) {
314 "VideoFileWriter::write(VideoStreamContainer): get_frame_pixels({}) returned empty",
315 frame_index);
316 return;
317 }
318
319 post(RawFrame {
320 .pixels = std::vector<uint8_t>(span.begin(), span.end()),
321 .width = container->get_width(),
322 .height = container->get_height() });
323}
324
325// =========================================================================
326// Write — TextureBuffer
327// =========================================================================
328
329void VideoFileWriter::write(const std::shared_ptr<Buffers::TextureBuffer>& buffer)
330{
331 if (!m_open.load(std::memory_order_acquire) || !buffer)
332 return;
333
334 if (buffer->get_pixel_data().empty() && !buffer->has_texture()) {
336 "VideoFileWriter::write(TextureBuffer): no CPU pixels and no GPU texture");
337 return;
338 }
339
340 post(DownloadCmd { .buffer = buffer });
341}
342
343// =========================================================================
344// Error / post
345// =========================================================================
346
348{
349 std::lock_guard lock(m_error_mutex);
350 return m_last_error;
351}
352
353void VideoFileWriter::set_error(std::string msg)
354{
355 std::lock_guard lock(m_error_mutex);
356 m_last_error = std::move(msg);
357}
358
360{
361 return m_queue->push(item);
362}
363
364// =========================================================================
365// Worker loop
366// =========================================================================
367
368void VideoFileWriter::worker_loop(const std::string& filepath,
369 uint32_t width,
370 uint32_t height,
371 double frame_rate,
372 AVPixelFormat src_fmt,
373 AVCodecID codec_id)
374{
377
378 auto fail = [&](std::string msg) {
379 set_error(std::move(msg));
380 m_open.store(false, std::memory_order_release);
381 m_close_promise.set_value(false);
382 };
383
384 if (!mux.open(filepath)) {
385 fail(mux.last_error());
386 return;
387 }
388 if (!enc.open(mux, width, height, frame_rate, src_fmt, codec_id)) {
389 fail(enc.last_error());
390 return;
391 }
392 if (!mux.write_header()) {
393 fail(mux.last_error());
394 return;
395 }
396
397 m_open.store(true, std::memory_order_release);
398
400 "[VideoFileWriter] worker started: '{}' {}x{} @{:.3f}fps",
401 filepath, width, height, frame_rate);
402
403 while (true) {
404 auto item_opt = m_queue->pop();
405 if (!item_opt) {
406 std::this_thread::sleep_for(std::chrono::microseconds(100));
407 continue;
408 }
409
410 bool done = std::visit([&](auto& cmd) -> bool {
411 using T = std::decay_t<decltype(cmd)>;
412
413 if constexpr (std::is_same_v<T, RawFrame>) {
414 if (!enc.encode_frame(cmd.pixels.data(), cmd.pixels.size(),
415 cmd.width, cmd.height, mux)) {
416 set_error(enc.last_error());
418 "[VideoFileWriter] encode_frame failed: {}", enc.last_error());
419 }
420 return false;
421 }
422
423 if constexpr (std::is_same_v<T, DownloadCmd>) {
424 const auto img_fmt = cmd.buffer->get_format();
425 const AVPixelFormat av_fmt = image_format_to_avpixfmt(img_fmt);
426 if (av_fmt == AV_PIX_FMT_NONE) {
428 "[VideoFileWriter] DownloadCmd: unsupported ImageFormat {}",
429 static_cast<int>(img_fmt));
430 return false;
431 }
432
433 const auto& cpu = cmd.buffer->get_pixel_data();
434 if (!cpu.empty()) {
435 if (!enc.encode_frame(cpu.data(), cpu.size(),
436 cmd.buffer->get_width(), cmd.buffer->get_height(), mux)) {
437 set_error(enc.last_error());
439 "[VideoFileWriter] encode_frame (cpu) failed: {}", enc.last_error());
440 }
441 return false;
442 }
443
444 auto tex = cmd.buffer->get_texture();
445 if (!tex) {
447 "[VideoFileWriter] DownloadCmd: no CPU pixels and no GPU texture");
448 return false;
449 }
450
452 const size_t mip0_bytes = static_cast<size_t>(tex->get_width())
453 * tex->get_height()
454 * TextureLoom::get_bytes_per_pixel(img_fmt);
455
456 if (mip0_bytes == 0)
457 return false;
458
459 std::vector<uint8_t> pixels(mip0_bytes);
460 TextureLoom::instance().download_data_async(tex, pixels.data(), mip0_bytes);
461
462 if (!enc.encode_frame(pixels.data(), pixels.size(),
463 tex->get_width(), tex->get_height(), mux)) {
464 set_error(enc.last_error());
466 "[VideoFileWriter] encode_frame (gpu) failed: {}", enc.last_error());
467 }
468 return false;
469 }
470
471 return static_cast<bool>(std::is_same_v<T, CloseCmd>);
472 },
473 *item_opt);
474
475 if (done)
476 break;
477 }
478
479 bool ok = enc.drain(mux);
480 if (!ok) {
481 set_error(enc.last_error());
483 "[VideoFileWriter] drain failed: {}", enc.last_error());
484 }
485
486 mux.close();
487 m_open.store(false, std::memory_order_release);
488 m_close_promise.set_value(ok);
489
491 "[VideoFileWriter] worker finished: '{}' status={}",
492 filepath, ok ? "ok" : "error");
493}
494
495} // 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:59
const std::vector< float > * pixels
Definition Decoder.cpp:58
uint32_t h
Definition InkPress.cpp:28
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.
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.
@ 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.