MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VideoFileReader.cpp
Go to the documentation of this file.
1#include "VideoFileReader.hpp"
3
6
9
10extern "C" {
11#include <cstddef>
12#include <libavcodec/avcodec.h>
13#include <libavformat/avformat.h>
14#include <libswscale/swscale.h>
15}
16
17namespace MayaFlux::IO {
18
19// =========================================================================
20// Construction / destruction
21// =========================================================================
22
24
25void VideoFileReader::setup_io_service(uint64_t reader_id)
26{
27 m_reader_id = reader_id;
28
30 .get_service<Registry::Service::IOService>()) {
31
32 m_io_service = std::make_shared<Registry::Service::IOService>();
33 m_io_service->request_decode = [this](uint64_t reader_id) {
34 if (reader_id == m_reader_id)
36 };
37
40 [this]() -> void* { return m_io_service.get(); });
41
42 m_owns_io_service = true;
43 }
44}
45
46void VideoFileReader::setup_io_service(const std::shared_ptr<Registry::Service::IOService>& io_service, uint64_t reader_id)
47{
48 m_io_service = io_service;
49 m_reader_id = reader_id;
50 m_owns_io_service = false;
51}
52
57
58bool VideoFileReader::can_read(const std::string& filepath) const
59{
60 static const std::vector<std::string> exts = {
61 "mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v", "ts", "mts"
62 };
63 auto dot = filepath.rfind('.');
64 if (dot == std::string::npos)
65 return false;
66 std::string ext = filepath.substr(dot + 1);
67 std::ranges::transform(ext, ext.begin(), ::tolower);
68 return std::ranges::find(exts, ext) != exts.end();
69}
70
71bool VideoFileReader::open(const std::string& filepath, FileReadOptions options)
72{
73 close();
74
75 auto resolved = resolve_path(filepath);
76 m_filepath = resolved;
77 m_options = options;
78
79 auto demux = std::make_shared<FFmpegDemuxContext>();
80 if (!demux->open(resolved)) {
81 set_error(demux->last_error());
82 return false;
83 }
84
85 auto video = std::make_shared<VideoStreamContext>();
86 if (!video->open(*demux, m_target_width, m_target_height, m_target_format)) {
87 set_error(video->last_error());
88 return false;
89 }
90
91 std::shared_ptr<AudioStreamContext> audio;
94
95 audio = std::make_shared<AudioStreamContext>();
96 if (!audio->open(*demux, planar, m_target_sample_rate)) {
98 "VideoFileReader: no audio stream found or audio open failed");
99 audio.reset();
100 }
101 }
102
103 {
104 std::unique_lock lock(m_context_mutex);
105 m_demux = std::move(demux);
106 m_video = std::move(video);
107 m_audio = std::move(audio);
108 }
109
114
115 return true;
116}
117
119{
121 m_container_ref.reset();
122
123 std::unique_lock ctx_lock(m_context_mutex);
124
125 if (m_audio) {
126 m_audio->close();
127 m_audio.reset();
128 }
129 if (m_video) {
130 m_video->close();
131 m_video.reset();
132 }
133 if (m_demux) {
134 m_demux->close();
135 m_demux.reset();
136 }
137
138 m_audio_container.reset();
139 m_sws_buf.clear();
140 m_sws_buf.shrink_to_fit();
141
142 {
143 std::lock_guard lock(m_metadata_mutex);
144 m_cached_metadata.reset();
145 m_cached_regions.clear();
146 }
147
148 m_decode_head.store(0);
149 clear_error();
150
151 if (m_owns_io_service) {
154 m_io_service.reset();
155 m_owns_io_service = false;
156 }
157}
158
160{
161 std::shared_lock lock(m_context_mutex);
162 return m_demux && m_video && m_video->is_valid();
163}
164
165// =========================================================================
166// Metadata / regions
167// =========================================================================
168
169std::optional<FileMetadata> VideoFileReader::get_metadata() const
170{
171 std::lock_guard lock(m_metadata_mutex);
172 return m_cached_metadata;
173}
174
175std::vector<FileRegion> VideoFileReader::get_regions() const
176{
177 std::lock_guard lock(m_metadata_mutex);
178 return m_cached_regions;
179}
180
182 const std::shared_ptr<FFmpegDemuxContext>& demux,
183 const std::shared_ptr<VideoStreamContext>& video) const
184{
185 FileMetadata meta;
186 meta.mime_type = "video";
187 demux->extract_container_metadata(meta);
188 video->extract_stream_metadata(*demux, meta);
189
190 std::lock_guard lock(m_metadata_mutex);
191 m_cached_metadata = std::move(meta);
192}
193
195 const std::shared_ptr<FFmpegDemuxContext>& demux,
196 const std::shared_ptr<VideoStreamContext>& video) const
197{
198 std::vector<FileRegion> regions;
199
200 auto chapters = demux->extract_chapter_regions();
201 regions.insert(regions.end(),
202 std::make_move_iterator(chapters.begin()),
203 std::make_move_iterator(chapters.end()));
204
205 auto keyframes = video->extract_keyframe_regions(*demux);
206 regions.insert(regions.end(),
207 std::make_move_iterator(keyframes.begin()),
208 std::make_move_iterator(keyframes.end()));
209
210 std::lock_guard lock(m_metadata_mutex);
211 m_cached_regions = std::move(regions);
212}
213
215{
216 return typeid(Kakshya::VideoFileContainer);
217}
218
219// =========================================================================
220// FileReader interface
221// =========================================================================
222
223std::vector<Kakshya::DataVariant> VideoFileReader::read_all()
224{
226 "VideoFileReader::read_all() is not supported; "
227 "use create_container() + load_into_container()");
228 return {};
229}
230
231std::vector<Kakshya::DataVariant> VideoFileReader::read_region(const FileRegion& /*region*/)
232{
234 "VideoFileReader::read_region() is not supported; "
235 "use the container API to access regions");
236 return {};
237}
238
239// =========================================================================
240// Container operations
241// =========================================================================
242
243std::shared_ptr<Kakshya::SignalSourceContainer> VideoFileReader::create_container()
244{
245 std::shared_lock lock(m_context_mutex);
246 if (!m_demux || !m_video) {
247 set_error("File not open");
248 return nullptr;
249 }
250 return std::make_shared<Kakshya::VideoFileContainer>();
251}
252
254 std::shared_ptr<Kakshya::SignalSourceContainer> container)
255{
256 if (!container) {
257 set_error("Invalid container");
258 return false;
259 }
260
261 auto vc = std::dynamic_pointer_cast<Kakshya::VideoFileContainer>(container);
262 if (!vc) {
263 set_error("Container is not a VideoFileContainer");
264 return false;
265 }
266
267 std::shared_ptr<VideoStreamContext> video;
268 std::shared_ptr<AudioStreamContext> audio;
269 std::shared_ptr<FFmpegDemuxContext> demux;
270 {
271 std::shared_lock lock(m_context_mutex);
272 if (!m_demux || !m_video) {
273 set_error("File not open");
274 return false;
275 }
276 video = m_video;
277 audio = m_audio;
278 demux = m_demux;
279 }
280
281 vc->set_source_path(m_filepath);
282 if (m_demux && m_demux->format_context)
283 vc->set_source_format(m_demux->format_context->iformat->name);
284
285 const uint64_t total = video->total_frames;
286 if (total == 0) {
287 set_error("Video stream reports 0 frames");
288 return false;
289 }
290
291 const uint32_t ring_cap = std::min(
293 static_cast<uint32_t>(total));
294
295 const uint32_t threshold = (m_refill_threshold > 0)
297 : ring_cap / 4;
298
299 const auto fmt = to_image_format(video->out_pixel_format);
300 if (!fmt) {
301 set_error("Video output pixel format has no ImageFormat equivalent");
302 return false;
303 }
304
305 vc->setup_ring(total, ring_cap,
306 video->out_width, video->out_height,
307 *fmt, video->frame_rate,
309
310 m_sws_buf.resize(
311 static_cast<size_t>(video->out_linesize) * video->out_height);
312
315 && demux->find_best_stream(AVMEDIA_TYPE_AUDIO) >= 0;
316
317 if (want_audio && audio && audio->is_valid()) {
318 {
319 std::unique_lock lock(m_context_mutex);
320 demux->seek(audio->stream_index, 0);
321 audio->flush_codec();
322 audio->drain_resampler_init();
323 }
324
325 SoundFileReader audio_reader;
328
329 if (audio_reader.open_from_demux(demux, audio, m_filepath, m_options)) {
330 auto sc = audio_reader.create_container();
331 if (audio_reader.load_into_container(sc)) {
332 m_audio_container = std::dynamic_pointer_cast<Kakshya::SoundFileContainer>(sc);
333 } else {
335 "VideoFileReader: audio load failed: {}",
336 audio_reader.get_last_error());
337 }
338 } else {
340 "VideoFileReader: open_from_demux failed: {}",
341 audio_reader.get_last_error());
342 }
343
344 {
345 std::unique_lock lock(m_context_mutex);
346 demux->seek(video->stream_index, 0);
347 video->flush_codec();
348 }
349 }
350
351 m_decode_head.store(0);
352 m_container_ref = vc;
353
354 const uint64_t preload = std::min(
355 static_cast<uint64_t>(ring_cap),
356 total);
357
358 uint64_t decoded = decode_batch(*vc, preload);
359
360 if (decoded == 0) {
361 set_error("Failed to decode any frames during preload");
362 return false;
363 }
364
366 "VideoFileReader: preloaded {}/{} frames ({}x{}, {:.1f} fps, ring={})",
367 decoded, total,
368 video->out_width, video->out_height,
369 video->frame_rate, ring_cap);
370
371 auto regions = get_regions();
372 auto region_groups = regions_to_groups(regions);
373 for (const auto& [name, group] : region_groups)
374 vc->add_region_group(group);
375
376 vc->create_default_processor();
377 vc->mark_ready_for_processing(true);
378
379 if (decoded < total)
381
382 return true;
383}
384
386 Kakshya::VideoFileContainer& vc, uint64_t batch_size)
387{
388 std::shared_lock ctx_lock(m_context_mutex);
389 if (!m_demux || !m_video || !m_video->is_valid())
390 return 0;
391
392 const size_t required = static_cast<size_t>(m_video->out_linesize) * m_video->out_height;
393 if (m_sws_buf.size() < required)
394 m_sws_buf.resize(required);
395
396 const size_t frame_bytes = vc.get_frame_byte_size();
397 const int packed_stride = static_cast<int>(
398 m_video->out_width * m_video->out_bytes_per_pixel);
399
400 uint64_t decoded = 0;
401
402 AVPacket* pkt = av_packet_alloc();
403 AVFrame* frame = av_frame_alloc();
404 if (!pkt || !frame) {
405 av_packet_free(&pkt);
406 av_frame_free(&frame);
407 return 0;
408 }
409
410 uint8_t* sws_dst[1] = { m_sws_buf.data() };
411 int sws_stride[1] = { m_video->out_linesize };
412
413 auto write_frame_to_ring = [&]() -> bool {
414 uint64_t idx = m_decode_head.load();
415 if (idx >= vc.get_total_source_frames())
416 return false;
417
418 uint8_t* dest = vc.mutable_slot_ptr(idx);
419 if (!dest)
420 return false;
421
422 sws_scale(m_video->sws_context,
423 frame->data, frame->linesize,
424 0, static_cast<int>(m_video->height),
425 sws_dst, sws_stride);
426
427 if (m_video->out_linesize == packed_stride) {
428 std::memcpy(dest, m_sws_buf.data(), frame_bytes);
429 } else {
430 for (uint32_t row = 0; row < m_video->out_height; ++row) {
431 std::memcpy(
432 dest + static_cast<size_t>(row) * packed_stride,
433 m_sws_buf.data() + static_cast<size_t>(row) * m_video->out_linesize,
434 static_cast<size_t>(packed_stride));
435 }
436 }
437
438 vc.commit_frame(idx);
439 m_decode_head.fetch_add(1);
440 ++decoded;
441
442 av_frame_unref(frame);
443 return true;
444 };
445
446 while (decoded < batch_size) {
447 int ret = av_read_frame(m_demux->format_context, pkt);
448
449 if (ret < 0) {
450 if (ret == AVERROR_EOF) {
451 avcodec_send_packet(m_video->codec_context, nullptr);
452 } else {
453 break;
454 }
455 } else if (pkt->stream_index != m_video->stream_index) {
456 av_packet_unref(pkt);
457 continue;
458 } else {
459 ret = avcodec_send_packet(m_video->codec_context, pkt);
460 av_packet_unref(pkt);
461 if (ret < 0 && ret != AVERROR(EAGAIN))
462 continue;
463 }
464
465 while (decoded < batch_size) {
466 ret = avcodec_receive_frame(m_video->codec_context, frame);
467 if (ret == AVERROR(EAGAIN))
468 break;
469 if (ret == AVERROR_EOF)
470 goto done;
471 if (ret < 0) {
472 av_frame_unref(frame);
473 break;
474 }
475
476 if (!write_frame_to_ring())
477 goto done;
478 }
479
480 if (ret == AVERROR_EOF)
481 break;
482 }
483
484done:
485 av_packet_free(&pkt);
486 av_frame_free(&frame);
487 return decoded;
488}
489
490// =========================================================================
491// Background decode thread
492// =========================================================================
493
495{
497
498 m_decode_stop.store(false);
499 m_decode_active.store(true);
501}
502
504{
505 if (!m_decode_active.load())
506 return;
507
508 m_decode_stop.store(true);
509 m_decode_cv.notify_all();
510
511 if (m_decode_thread.joinable())
512 m_decode_thread.join();
513
514 m_decode_active.store(false);
515}
516
518{
519 auto vc = m_container_ref.lock();
520 if (!vc) {
522 "VideoFileReader: decode thread — container expired");
523 m_decode_active.store(false);
524 return;
525 }
526
527 const uint64_t total = vc->get_total_source_frames();
528 const uint32_t ring_cap = vc->get_ring_capacity();
529 const uint32_t threshold = (m_refill_threshold > 0)
531 : ring_cap / 4;
532
533 while (!m_decode_stop.load()) {
534 uint64_t head = m_decode_head.load();
535 const uint64_t read_pos = vc->get_read_position()[0];
536
537 if (head >= total)
538 break;
539
540 const uint64_t buffered = (head > read_pos) ? (head - read_pos) : 0;
541
542 if (buffered >= static_cast<uint64_t>(ring_cap)) {
543 std::unique_lock lock(m_decode_mutex);
544 m_decode_cv.wait_for(lock, std::chrono::milliseconds(50), [&] {
545 if (m_decode_stop.load())
546 return true;
547 const uint64_t h = m_decode_head.load(std::memory_order_acquire);
548 const uint64_t rp = vc->get_read_position()[0];
549 const uint64_t ahead = (h > rp) ? (h - rp) : 0;
550 return ahead <= static_cast<uint64_t>(ring_cap - threshold);
551 });
552 continue;
553 }
554
555 const uint64_t want = static_cast<uint64_t>(ring_cap) - buffered;
556 const uint64_t capped = std::min(want, total - head);
557 const uint64_t batch = std::min(capped,
558 static_cast<uint64_t>(m_decode_batch_size));
559
560 uint64_t decoded = decode_batch(*vc, batch);
561
562 if (decoded == 0)
563 break;
564 }
565
566 m_decode_active.store(false);
567}
568
569// =========================================================================
570// Seeking
571// =========================================================================
572
573std::vector<uint64_t> VideoFileReader::get_read_position() const
574{
575 return { m_decode_head.load() };
576}
577
578bool VideoFileReader::seek(const std::vector<uint64_t>& position)
579{
580 if (position.empty())
581 return false;
582
583 const uint64_t target_frame = position[0];
584
586
587 std::shared_ptr<VideoStreamContext> video;
588 std::shared_ptr<FFmpegDemuxContext> demux;
589 {
590 std::shared_lock lock(m_context_mutex);
591 if (!m_demux || !m_video || !m_video->is_valid()) {
592 set_error("Cannot seek: reader not open");
593 return false;
594 }
595 video = m_video;
596 demux = m_demux;
597 }
598
599 if (!seek_internal(demux, video, target_frame))
600 return false;
601
602 m_decode_head.store(target_frame);
603
604 auto vc = m_container_ref.lock();
605 if (!vc)
606 return true;
607
608 vc->invalidate_ring();
609 vc->set_read_position({ target_frame });
610
611 const uint64_t total = vc->get_total_source_frames();
612 const uint64_t batch = std::min(
613 static_cast<uint64_t>(m_decode_batch_size),
614 total > target_frame ? total - target_frame : 0UL);
615
616 decode_batch(*vc, batch);
617
618 if (m_decode_head.load() < total)
620
621 return true;
622}
623
625 const std::shared_ptr<FFmpegDemuxContext>& demux,
626 const std::shared_ptr<VideoStreamContext>& video,
627 uint64_t frame_position)
628{
629 if (frame_position > video->total_frames)
630 frame_position = video->total_frames;
631
632 if (video->frame_rate <= 0.0) {
633 set_error("Invalid frame rate for seeking");
634 return false;
635 }
636
637 AVStream* stream = demux->get_stream(video->stream_index);
638 if (!stream) {
639 set_error("Invalid stream index");
640 return false;
641 }
642
643 double target_seconds = static_cast<double>(frame_position) / video->frame_rate;
644 auto ts = static_cast<int64_t>(target_seconds / av_q2d(stream->time_base));
645
646 if (!demux->seek(video->stream_index, ts)) {
647 set_error(demux->last_error());
648 return false;
649 }
650
651 video->flush_codec();
652 return true;
653}
654
656{
657 m_decode_cv.notify_one();
658}
659
660// =========================================================================
661// Dimension queries
662// =========================================================================
663
665{
666 return 4;
667}
668
669std::vector<uint64_t> VideoFileReader::get_dimension_sizes() const
670{
671 std::shared_lock lock(m_context_mutex);
672 if (!m_video)
673 return { 0, 0, 0, 0 };
674 return {
675 m_video->total_frames,
676 m_video->out_height,
677 m_video->out_width,
678 m_video->out_bytes_per_pixel
679 };
680}
681
682std::vector<std::string> VideoFileReader::get_supported_extensions() const
683{
684 return { "mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v", "ts", "mts" };
685}
686
687// =========================================================================
688// Error
689// =========================================================================
690
692{
693 std::lock_guard lock(m_error_mutex);
694 return m_last_error;
695}
696
697void VideoFileReader::set_error(const std::string& msg) const
698{
699 std::lock_guard lock(m_error_mutex);
700 m_last_error = msg;
702 "VideoFileReader: {}", msg);
703}
704
706{
707 std::lock_guard lock(m_error_mutex);
708 m_last_error.clear();
709}
710
711} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
Core::GlobalStreamInfo stream
Definition Config.cpp:36
uint32_t h
Definition InkPress.cpp:28
std::string name
Definition VKDevice.cpp:143
float threshold
static std::string resolve_path(const std::string &filepath)
Resolve a filepath against the project source root if not found as-is.
static std::unordered_map< std::string, Kakshya::RegionGroup > regions_to_groups(const std::vector< FileRegion > &regions)
Convert file regions to region groups.
std::string get_last_error() const override
Get the last error message encountered by the reader.
bool load_into_container(std::shared_ptr< Kakshya::SignalSourceContainer > container) override
Load file data into an existing SignalSourceContainer.
bool open_from_demux(std::shared_ptr< FFmpegDemuxContext > demux, std::shared_ptr< AudioStreamContext > audio, const std::string &filepath, FileReadOptions options=FileReadOptions::ALL)
Open an audio stream from an already-constructed demux and stream context.
void set_audio_options(AudioReadOptions options)
Set audio-specific read options.
std::shared_ptr< Kakshya::SignalSourceContainer > create_container() override
Create a SignalSourceContainer for this file.
void set_target_sample_rate(uint32_t sample_rate)
Set the target sample rate for resampling.
FFmpeg-based audio file reader for MayaFlux.
std::condition_variable m_decode_cv
uint64_t decode_batch(Kakshya::VideoFileContainer &vc, uint64_t batch_size)
Decode up to batch_size frames starting at m_decode_head.
void set_error(const std::string &msg) const
std::vector< FileRegion > m_cached_regions
std::vector< FileRegion > get_regions() const override
Get semantic regions from the file.
void build_metadata(const std::shared_ptr< FFmpegDemuxContext > &demux, const std::shared_ptr< VideoStreamContext > &video) const
std::vector< uint8_t > m_sws_buf
One-frame sws scratch buffer (padded linesize, reused by decode thread).
void build_regions(const std::shared_ptr< FFmpegDemuxContext > &demux, const std::shared_ptr< VideoStreamContext > &video) const
bool open(const std::string &filepath, FileReadOptions options=FileReadOptions::ALL) override
Open a file for reading.
std::vector< Kakshya::DataVariant > read_all() override
Read all data from the file into memory.
std::weak_ptr< Kakshya::VideoFileContainer > m_container_ref
std::shared_ptr< FFmpegDemuxContext > m_demux
std::shared_ptr< Registry::Service::IOService > m_io_service
bool seek(const std::vector< uint64_t > &position) override
Seek to a specific position in the file.
std::vector< std::string > get_supported_extensions() const override
Get supported file extensions for this reader.
std::atomic< bool > m_decode_active
bool seek_internal(const std::shared_ptr< FFmpegDemuxContext > &demux, const std::shared_ptr< VideoStreamContext > &video, uint64_t frame_position)
bool load_into_container(std::shared_ptr< Kakshya::SignalSourceContainer > container) override
Load file data into an existing container.
bool is_open() const override
Check if a file is currently open.
std::string get_last_error() const override
Get the last error message.
std::vector< uint64_t > get_read_position() const override
Get current read position in primary dimension.
std::shared_ptr< VideoStreamContext > m_video
std::optional< FileMetadata > get_metadata() const override
Get metadata from the open file.
std::shared_ptr< AudioStreamContext > m_audio
void close() override
Close the currently open file.
std::optional< FileMetadata > m_cached_metadata
std::shared_ptr< Kakshya::SignalSourceContainer > create_container() override
Create and initialize a container from the file.
std::shared_ptr< Kakshya::SoundFileContainer > m_audio_container
void setup_io_service(uint64_t reader_id=0)
Internal setup for IOService integration.
std::atomic< uint64_t > m_decode_head
bool can_read(const std::string &filepath) const override
Check if a file can be read by this reader.
std::type_index get_container_type() const override
Get the container type this reader creates.
void signal_decode()
Non-blocking signal to the background decode thread.
size_t get_num_dimensions() const override
Get the dimensionality of the file data.
std::vector< Kakshya::DataVariant > read_region(const FileRegion &region) override
Read a specific region of data.
std::vector< uint64_t > get_dimension_sizes() const override
Get size of each dimension in the file data.
File-backed video container — semantic marker over VideoStreamContainer.
uint8_t * mutable_slot_ptr(uint64_t frame_index)
Mutable pointer into m_data[0] for the decode thread to write into.
size_t get_frame_byte_size() const
Total byte size of one frame: width * height * bytes_per_pixel.
void commit_frame(uint64_t frame_index)
Publish a decoded frame.
void register_service(ServiceFactory factory)
Register a backend service capability.
static BackendRegistry & instance()
Get the global registry instance.
void unregister_service()
Unregister a service.
@ DEINTERLEAVE
Output planar (per-channel) doubles instead of interleaved.
FileReadOptions
Generic options for file reading behavior.
@ EXTRACT_METADATA
Extract file metadata.
@ EXTRACT_REGIONS
Extract semantic regions (format-specific)
@ NONE
No special options.
std::optional< Portal::Graphics::ImageFormat > to_image_format(int av_pixel_format)
Map an AVPixelFormat to the Portal ImageFormat backing it.
@ FileIO
Filesystem I/O operations.
@ IO
Networking, file handling, streaming.
std::string mime_type
MIME type if applicable (e.g., "audio/wav")
Generic metadata structure for any file type.
Generic region descriptor for any file type.
Backend IO streaming service interface.
Definition IOService.hpp:18