MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VideoStreamContainer.hpp
Go to the documentation of this file.
1#pragma once
2
4
6
9
11struct IOService;
12}
13
14namespace MayaFlux::Kakshya {
15
16/**
17 * @class VideoStreamContainer
18 * @brief Concrete base implementation for streaming video containers.
19 *
20 * VideoStreamContainer provides a complete, concrete implementation of all
21 * StreamContainer functionality for decoded video frame data. It serves as:
22 * 1. A standalone streaming container for real-time video processing
23 * 2. A base class for specialized containers like VideoFileContainer
24 *
25 * Data is stored in the DataVariant alternative matching the container's
26 * ImageFormat: uint8_t for 8-bit formats, uint16_t for 16-bit, float for
27 * 32-bit. Each frame is width * height * bytes_per_pixel bytes. All frames
28 * are stored contiguously in a single DataVariant.
29 *
30 * Dimensions follow the format's modality:
31 * dims[0] → TIME (frame count)
32 * dims[1] → SPATIAL_Y (height)
33 * dims[2] → SPATIAL_X (width)
34 * dims[3] → CHANNEL, or DEPTH for range formats (component count)
35 *
36 * Reader model follows WindowContainer's pattern: a simple atomic reader
37 * count rather than per-dimension/per-channel tracking. Video frames are
38 * atomic spatial units — channel-level access is a processor concern,
39 * not a container concern.
40 *
41 * Uses virtual inheritance to support diamond inheritance when used as a
42 * base for FileContainer-derived classes.
43 */
44class MAYAFLUX_API VideoStreamContainer : public virtual StreamContainer {
45public:
46 /**
47 * @brief Construct a VideoStreamContainer with specified parameters.
48 * @param width Frame width in pixels.
49 * @param height Frame height in pixels.
50 * @param format Pixel format; determines channel count, bytes per
51 * pixel, the DataVariant element type, and whether
52 * the structure is VIDEO_COLOR or VIDEO_DEPTH.
53 * @param frame_rate Temporal rate in frames per second.
54 */
55 VideoStreamContainer(uint32_t width = 0,
56 uint32_t height = 0,
57 Portal::Graphics::ImageFormat format = Portal::Graphics::ImageFormat::RGBA8,
58 double frame_rate = 0.0);
59
60 ~VideoStreamContainer() override = default;
61
62 // =========================================================================
63 // NDDimensionalContainer
64 // =========================================================================
65
66 [[nodiscard]] std::vector<DataDimension> get_dimensions() const override;
67 [[nodiscard]] uint64_t get_total_elements() const override;
68 [[nodiscard]] MemoryLayout get_memory_layout() const override { return m_structure.memory_layout; }
69 void set_memory_layout(MemoryLayout layout) override;
70
71 [[nodiscard]] uint64_t get_frame_size() const override;
72 [[nodiscard]] uint64_t get_num_frames() const override;
73
74 std::vector<DataVariant> get_region_data(const Region& region) const override;
75 void set_region_data(const Region& region, const std::vector<DataVariant>& data) override;
76
77 std::vector<DataVariant> get_region_group_data(const RegionGroup& group) const override;
78 std::vector<DataVariant> get_segments_data(const std::vector<RegionSegment>& segment) const override;
79
80 [[nodiscard]] std::type_index value_element_type() const override;
81
82 [[nodiscard]] uint64_t coordinates_to_linear_index(const std::vector<uint64_t>& coordinates) const override;
83 [[nodiscard]] std::vector<uint64_t> linear_index_to_coordinates(uint64_t linear_index) const override;
84
85 void clear() override;
86
87 [[nodiscard]] const void* get_raw_data() const override;
88 [[nodiscard]] bool has_data() const override;
89
90 ContainerDataStructure& get_structure() override { return m_structure; }
91 const ContainerDataStructure& get_structure() const override { return m_structure; }
92 void set_structure(ContainerDataStructure structure) override { m_structure = structure; }
93
94 // =========================================================================
95 // Ring buffer streaming API
96 // =========================================================================
97
98 /**
99 * @brief Allocate m_data[0] as a ring of ring_capacity frames.
100 *
101 * Switches the container from flat mode to ring mode. m_data[0] is
102 * resized to ring_capacity x frame_byte_size. m_num_frames is set to
103 * total_frames so processors see the full temporal extent. Pixel data
104 * is indexed by frame_index % ring_capacity.
105 *
106 * @param total_frames Total frames in the source (file, stream, etc).
107 * @param ring_capacity Number of frame slots (must be power of 2).
108 * @param width Frame width in pixels.
109 * @param height Frame height in pixels.
110 * @param format Pixel format for the ring's frames.
111 * @param frame_rate Frame rate in fps.
112 * @param refill_threshold Frames of look-ahead below which refill callback fires.
113 * @param reader_id The current class ID registered at the stream/file-read source
114 */
115 void setup_ring(uint64_t total_frames,
116 uint32_t ring_capacity,
117 uint32_t width,
118 uint32_t height,
120 double frame_rate,
121 uint32_t refill_threshold,
122 uint64_t reader_id = 0);
123
124 /**
125 * @brief Mutable pointer into m_data[0] for the decode thread to write into.
126 * @param frame_index Absolute frame index; mapped to slot via modulo.
127 * @return Pointer into the pixel vector, or nullptr if not in ring mode.
128 */
129 [[nodiscard]] uint8_t* mutable_slot_ptr(uint64_t frame_index);
130
131 /**
132 * @brief Publish a decoded frame. Sets validity, pushes to ready queue,
133 * notifies any thread blocked in get_frame_pixels().
134 * @param frame_index Absolute frame index just written.
135 */
136 void commit_frame(uint64_t frame_index);
137
138 /**
139 * @brief Invalidate all ring slots. Called before seek.
140 */
141 void invalidate_ring();
142
143 /**
144 * @brief Check if a frame is currently valid in the ring.
145 * @param frame_index Absolute frame index.
146 */
147 [[nodiscard]] bool is_frame_available(uint64_t frame_index) const;
148
149 /**
150 * @brief True if the container is operating in ring mode.
151 */
152 [[nodiscard]] bool is_ring_mode() const { return m_ring_capacity > 0; }
153
154 [[nodiscard]] uint32_t get_ring_capacity() const { return m_ring_capacity; }
155 [[nodiscard]] uint64_t get_total_source_frames() const { return m_total_source_frames; }
156
157 /**
158 * @brief Set the number of frames below which the refill callback fires.
159 * Called by the reader before or immediately after setup_ring().
160 * @param threshold Frames of look-ahead below which notification fires.
161 */
163 {
164 m_refill_threshold = threshold;
165 }
166
167 /**
168 * @brief Advance the container's view of how many frames have been decoded.
169 * Called by the decode thread (via VideoFileReader) after commit_frame().
170 * Monotonically increasing; never decremented (seek resets via setup_ring).
171 * @param frame_index The highest frame index just committed.
172 */
173 void advance_cache_head(uint64_t frame_index)
174 {
175 uint64_t prev = m_cache_head.load(std::memory_order_relaxed);
176 while (frame_index > prev
177 && !m_cache_head.compare_exchange_weak(prev, frame_index,
178 std::memory_order_release, std::memory_order_relaxed)) { }
179 }
180
181 /**
182 * @brief Total frame count known at construction / setup_ring() time.
183 * Non-zero even before any frames are decoded.
184 */
185 [[nodiscard]] uint64_t get_cache_head() const
186 {
187 return m_cache_head.load(std::memory_order_acquire);
188 }
189
190 /**
191 * @brief Processed frame at @p frame_index as a normalised float span.
192 *
193 * Valid after FrameAccessProcessor has written processed_data.
194 * uint8_t source values are divided by 255.0f. float source is
195 * zero-copy. Returns empty span if frame_index is out of range or
196 * the variant holds a non-pixel type.
197 *
198 * The cache covers the last requested frame_index only. A call with
199 * a different index invalidates and recomputes.
200 *
201 * @param frame_index Zero-based index into processed_data. Defaults to 0.
202 * @return Normalised float span, w * h * channels elements.
203 */
204 [[nodiscard]] std::span<const float> processed_frame_as_float(
205 uint64_t frame_index = 0) const;
206
207 // =========================================================================
208 // RegionGroup management
209 // =========================================================================
210
211 void add_region_group(const RegionGroup& group) override;
212 RegionGroup get_region_group(const std::string& name) const override;
213 std::unordered_map<std::string, RegionGroup> get_all_region_groups() const override;
214 void remove_region_group(const std::string& name) override;
215
216 bool is_region_loaded(const Region& region) const override;
217 void load_region(const Region& region) override;
218 void unload_region(const Region& region) override;
219
220 // =========================================================================
221 // Read position and looping
222 // =========================================================================
223
224 void set_read_position(const std::vector<uint64_t>& position) override;
225 void update_read_position_for_channel(size_t channel, uint64_t frame) override;
226 [[nodiscard]] const std::vector<uint64_t>& get_read_position() const override;
227 void advance_read_position(const std::vector<uint64_t>& frames) override;
228 [[nodiscard]] bool is_at_end() const override;
229 void reset_read_position() override;
230
231 [[nodiscard]] uint64_t get_temporal_rate() const override;
232 [[nodiscard]] uint64_t time_to_position(double time) const override;
233 [[nodiscard]] double position_to_time(uint64_t position) const override;
234
235 void set_looping(bool enable) override;
236 [[nodiscard]] bool is_looping() const override { return m_looping_enabled; }
237 void set_loop_region(const Region& region) override;
238 [[nodiscard]] Region get_loop_region() const override;
239
240 [[nodiscard]] bool is_ready() const override;
241 [[nodiscard]] std::vector<uint64_t> get_remaining_frames() const override;
242 uint64_t read_sequential(std::span<double> output, uint64_t count) override;
243 uint64_t peek_sequential(std::span<double> output, uint64_t count, uint64_t offset) const override;
244
245 // =========================================================================
246 // Processing state
247 // =========================================================================
248
249 [[nodiscard]] ProcessingState get_processing_state() const override { return m_processing_state.load(); }
250 void update_processing_state(ProcessingState new_state) override;
251
252 void register_state_change_callback(
253 std::function<void(const std::shared_ptr<SignalSourceContainer>&, ProcessingState)> callback) override;
254 void unregister_state_change_callback() override;
255
256 [[nodiscard]] bool is_ready_for_processing() const override;
257 void mark_ready_for_processing(bool ready) override;
258
259 void create_default_processor() override;
260 void process_default() override;
261 void set_default_processor(const std::shared_ptr<DataProcessor>& processor) override;
262 [[nodiscard]] std::shared_ptr<DataProcessor> get_default_processor() const override;
263
264 std::shared_ptr<DataProcessingChain> get_processing_chain() override;
265 void set_processing_chain(const std::shared_ptr<DataProcessingChain>& chain) override { m_processing_chain = chain; }
266
267 // =========================================================================
268 // Reader tracking (WindowContainer-style atomic counting)
269 // =========================================================================
270
271 uint32_t register_dimension_reader(uint32_t dimension_index) override;
272 void unregister_dimension_reader(uint32_t dimension_index) override;
273 [[nodiscard]] bool has_active_readers() const override;
274 void mark_dimension_consumed(uint32_t dimension_index, uint32_t reader_id) override;
275 [[nodiscard]] bool all_dimensions_consumed() const override;
276
277 // =========================================================================
278 // Processing token
279 // =========================================================================
280
281 void reset_processing_token() override { m_processing_token_channel.store(-1); }
282
283 bool try_acquire_processing_token(int channel) override
284 {
285 int expected = -1;
286 return m_processing_token_channel.compare_exchange_strong(expected, channel);
287 }
288
289 [[nodiscard]] bool has_processing_token(int channel) const override
290 {
291 return m_processing_token_channel.load() == channel;
292 }
293
294 void invalidate_float_frame_cache(uint32_t slot_index = 0);
295
296 // =========================================================================
297 // Data access
298 // =========================================================================
299
300 const std::vector<DataVariant>& get_data() override { return m_data; }
301
302 DataAccess channel_data(size_t channel) override;
303 std::vector<DataAccess> all_channel_data() override;
304
305 std::vector<DataVariant>& get_processed_data() override { return m_processed_data; }
306 const std::vector<DataVariant>& get_processed_data() const override { return m_processed_data; }
307
308 void mark_buffers_for_processing(bool) override { }
309 void mark_buffers_for_removal() override { }
310
311 // =========================================================================
312 // Video-specific accessors
313 // =========================================================================
314
315 [[nodiscard]] uint32_t get_width() const { return m_width; }
316 [[nodiscard]] uint32_t get_height() const { return m_height; }
317 [[nodiscard]] double get_frame_rate() const { return m_frame_rate; }
318
319 /** @brief Pixel format governing storage type, channel count, and modality. */
320 [[nodiscard]] Portal::Graphics::ImageFormat get_format() const { return m_format; }
321
322 /** @brief Component channels per pixel. Not bytes per pixel. */
323 [[nodiscard]] uint32_t get_channels() const { return m_channels; }
324
325 /** @brief Bytes occupied by one pixel across all channels. */
326 [[nodiscard]] size_t get_bytes_per_pixel() const { return m_bpp; }
327
328 /** @brief Elements in one frame: width * height * channels. */
329 [[nodiscard]] size_t get_frame_element_count() const;
330
331 /**
332 * @brief Get raw pixel data for a single frame as a byte span.
333 * @param frame_index Zero-based frame index.
334 * @return Span of pixel bytes for the frame, empty if out of range.
335 */
336 [[nodiscard]] std::span<const uint8_t> get_frame_pixels(uint64_t frame_index) const;
337
338 /**
339 * @brief Total byte size of one frame: width * height * bytes_per_pixel.
340 *
341 * Not width * height * channels. Those agree only for formats with one
342 * byte per component. Use get_frame_element_count() for element counts.
343 */
344 [[nodiscard]] size_t get_frame_byte_size() const;
345
346protected:
347 void setup_dimensions();
348 void notify_state_change(ProcessingState new_state);
349
350 uint32_t m_width = 0;
351 uint32_t m_height = 0;
352 uint32_t m_channels = 4;
353 size_t m_bpp = 4;
354 Portal::Graphics::ImageFormat m_format = Portal::Graphics::ImageFormat::RGBA8;
355 double m_frame_rate = 0.0;
356 uint64_t m_num_frames = 0;
357
359
360 std::vector<DataVariant> m_data;
361 std::vector<DataVariant> m_processed_data;
362
366
367 std::atomic<ProcessingState> m_processing_state { ProcessingState::IDLE };
368 std::atomic<int> m_processing_token_channel { -1 };
369
370 std::function<void(const std::shared_ptr<SignalSourceContainer>&, ProcessingState)> m_state_callback;
371 std::shared_ptr<DataProcessor> m_default_processor;
372 std::shared_ptr<DataProcessingChain> m_processing_chain;
373
374 std::unordered_map<std::string, RegionGroup> m_region_groups;
375
376 std::atomic<uint64_t> m_read_position { 0 };
377 bool m_looping_enabled {};
379
380 std::atomic<uint32_t> m_registered_readers { 0 };
381 std::atomic<uint32_t> m_consumed_readers { 0 };
382
383 // =========================================================================
384 // Ring buffer state (inactive when m_ring_capacity == 0)
385 // =========================================================================
386
387 uint32_t m_ring_capacity { 0 };
388 uint64_t m_total_source_frames { 0 };
389
390 std::vector<std::atomic<uint64_t>> m_slot_frame;
391
392 static constexpr uint32_t READY_QUEUE_CAPACITY = 256;
394
395 /**
396 * @brief Highest frame index committed by the decode thread.
397 * Written by the decode thread via commit_frame(); read by
398 * update_read_position_for_channel() to compute buffered-ahead count.
399 */
400 std::atomic<uint64_t> m_cache_head { 0 };
401
402 /**
403 * @brief Trigger refill when (m_cache_head - read_position) drops below this.
404 * Set by the reader at load_into_container() time.
405 * A value of 0 disables threshold notification.
406 */
407 uint32_t m_refill_threshold { 0 };
408
409 Registry::Service::IOService* m_io_service { nullptr }; // non-owning; owned by registry
410 uint64_t m_io_reader_id { 0 };
411
412 [[nodiscard]] uint32_t slot_for(uint64_t frame_index) const
413 {
414 return static_cast<uint32_t>(frame_index % m_ring_capacity);
415 }
416
417 [[nodiscard]] DataSpanVariant get_frame_span_impl(uint64_t frame_index) const override
418 {
419 return get_frame_typed(frame_index);
420 }
421
422 void get_frames_impl(
423 void* output,
424 size_t count,
425 uint64_t start_frame,
426 uint64_t num_frames,
427 const std::type_info& type) const override;
428
429 void get_value_impl(const std::vector<uint64_t>& coords,
430 void* out, const std::type_info& type) const override;
431
432 void set_value_impl(const std::vector<uint64_t>& coords,
433 const void* in, const std::type_info& type) override;
434
435private:
436 [[nodiscard]] DataSpanVariant get_frame_typed(uint64_t frame_index) const;
437
438 template <typename T>
439 void get_frames_typed_as(std::span<T> output, uint64_t start_frame, uint64_t num_frames) const;
440
441 /**
442 * @brief Value range declared on the component dimension, if any.
443 *
444 * Resolves DEPTH first, then CHANNEL, matching the two roles
445 * setup_dimensions() emits for the component axis.
446 */
447 [[nodiscard]] std::optional<DataDimension::ValueRange> component_range() const;
448
449 void get_frames_typed(std::span<uint8_t> output, uint64_t start_frame, uint64_t num_frames) const;
450
451 mutable std::vector<std::vector<float>> m_float_frame_cache;
452 mutable std::vector<std::atomic<bool>> m_float_frame_dirty;
453
454 void reset_float_frame_cache();
455};
456
457} // namespace MayaFlux::Kakshya
vk::PhysicalDeviceType type
Definition VKDevice.cpp:146
std::string name
Definition VKDevice.cpp:143
size_t count
std::shared_ptr< Core::VKImage > output
float threshold
float offset
uint32_t width
uint32_t height
Type-erased accessor for NDData with semantic view construction.
Data-driven interface for temporal stream containers with navigable read position.
std::shared_ptr< DataProcessor > m_default_processor
const ContainerDataStructure & get_structure() const override
uint64_t get_cache_head() const
Total frame count known at construction / setup_ring() time.
ContainerDataStructure & get_structure() override
Get the data structure defining this container's layout.
void set_refill_threshold(uint32_t threshold)
Set the number of frames below which the refill callback fires.
std::shared_ptr< DataProcessingChain > m_processing_chain
std::function< void(const std::shared_ptr< SignalSourceContainer > &, ProcessingState)> m_state_callback
std::vector< DataVariant > & get_processed_data() override
Get a mutable reference to the processed data buffer.
std::vector< std::atomic< uint64_t > > m_slot_frame
void get_frames_typed(std::span< uint8_t > output, uint64_t start_frame, uint64_t num_frames) const
ProcessingState get_processing_state() const override
Get the current processing state of the container.
MemoryLayout get_memory_layout() const override
Get the memory layout used by this container.
uint32_t get_channels() const
Component channels per pixel.
DataSpanVariant get_frame_span_impl(uint64_t frame_index) const override
Implementation-specific method to retrieve a frame span.
Memory::LockFreeQueue< uint64_t, READY_QUEUE_CAPACITY > m_ready_queue
bool is_ring_mode() const
True if the container is operating in ring mode.
size_t get_bytes_per_pixel() const
Bytes occupied by one pixel across all channels.
std::vector< std::atomic< bool > > m_float_frame_dirty
bool try_acquire_processing_token(int channel) override
std::vector< std::vector< float > > m_float_frame_cache
const std::vector< DataVariant > & get_data() override
Get a reference to the raw data stored in the container.
void mark_buffers_for_removal() override
Mark associated buffers for removal from the system.
std::unordered_map< std::string, RegionGroup > m_region_groups
void advance_cache_head(uint64_t frame_index)
Advance the container's view of how many frames have been decoded.
bool has_processing_token(int channel) const override
Portal::Graphics::ImageFormat get_format() const
Pixel format governing storage type, channel count, and modality.
void set_processing_chain(const std::shared_ptr< DataProcessingChain > &chain) override
Set the processing chain for this container.
const std::vector< DataVariant > & get_processed_data() const override
Get a const reference to the processed data buffer.
void mark_buffers_for_processing(bool) override
Mark associated buffers for processing in the next cycle.
uint32_t slot_for(uint64_t frame_index) const
void set_structure(ContainerDataStructure structure) override
Set the data structure for this container.
bool is_looping() const override
Check if looping is enabled for the stream.
Concrete base implementation for streaming video containers.
Policy-driven unified circular buffer implementation.
Single-writer multiple-reader sequence lock for fixed-size data regions.
Definition SeqLock.hpp:44
ProcessingState
Represents the current processing lifecycle state of a container.
typename detail::span_const_from_vector_variant< DataVariant >::type DataSpanVariant
Definition NDData.hpp:657
std::optional< RegionGroup > get_region_group(const std::unordered_map< std::string, RegionGroup > &groups, const std::string &name)
Get a RegionGroup by name from a group map.
void add_region_group(std::unordered_map< std::string, RegionGroup > &groups, const RegionGroup &group)
Add a RegionGroup to a group map.
MemoryLayout
Memory layout for multi-dimensional data.
Definition NDData.hpp:65
double position_to_time(uint64_t position, double sample_rate)
Convert position (samples/frames) to time (seconds) given a sample rate.
void remove_region_group(std::unordered_map< std::string, RegionGroup > &groups, const std::string &name)
Remove a RegionGroup by name from a group map.
uint64_t time_to_position(double time, double sample_rate)
Convert time (seconds) to position (samples/frames) given a sample rate.
ImageFormat
User-friendly image format enum.
Container structure for consistent dimension ordering.
Organizes related signal regions into a categorized collection.
Represents a point or span in N-dimensional space.
Definition Region.hpp:73
Backend IO streaming service interface.
Definition IOService.hpp:18