Skip to main content

canvas_renderer/
video.rs

1//! Video texture management for streaming video content.
2//!
3//! This module provides types and utilities for managing video frame textures
4//! that can be uploaded to the GPU for rendering. It supports:
5//!
6//! - Per-frame RGBA data upload
7//! - Caching of video textures by stream ID
8//! - Graceful handling of missing video streams
9
10use std::collections::HashMap;
11
12use thiserror::Error;
13
14/// Errors that can occur during video texture operations.
15#[derive(Debug, Error)]
16pub enum VideoTextureError {
17    /// The video frame data has invalid dimensions or size.
18    #[error("Invalid video frame data: expected {expected} bytes, got {actual}")]
19    InvalidFrameData {
20        /// Expected byte count.
21        expected: usize,
22        /// Actual byte count.
23        actual: usize,
24    },
25
26    /// The requested stream was not found.
27    #[error("Video stream not found: {0}")]
28    StreamNotFound(String),
29
30    /// GPU texture creation failed.
31    #[error("Failed to create video texture: {0}")]
32    TextureCreation(String),
33
34    /// Frame dimensions would cause integer overflow.
35    #[error("Frame dimensions {width}x{height} would overflow")]
36    DimensionOverflow {
37        /// Width in pixels.
38        width: u32,
39        /// Height in pixels.
40        height: u32,
41    },
42}
43
44/// Result type for video texture operations.
45pub type VideoTextureResult<T> = Result<T, VideoTextureError>;
46
47/// Raw video frame data in RGBA format.
48///
49/// This struct holds a single frame of video data that can be uploaded
50/// to a GPU texture. The data is expected to be in RGBA format with
51/// 4 bytes per pixel.
52///
53/// # Note on Public Fields
54///
55/// Fields are public for zero-copy access to pixel data. Use the constructor
56/// [`VideoFrameData::new`] or [`VideoFrameData::placeholder`] to create
57/// validated instances. Direct modification of fields after construction
58/// may violate the `width*height*4 == data.len()` invariant.
59#[derive(Debug, Clone)]
60pub struct VideoFrameData {
61    /// Width of the frame in pixels.
62    pub width: u32,
63    /// Height of the frame in pixels.
64    pub height: u32,
65    /// RGBA pixel data (4 bytes per pixel, row-major order).
66    pub data: Vec<u8>,
67}
68
69impl VideoFrameData {
70    /// Calculate the expected byte size for a frame, with overflow checking.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if the dimensions would cause integer overflow.
75    fn checked_frame_size(width: u32, height: u32) -> VideoTextureResult<usize> {
76        (width as usize)
77            .checked_mul(height as usize)
78            .and_then(|pixels| pixels.checked_mul(4))
79            .ok_or(VideoTextureError::DimensionOverflow { width, height })
80    }
81
82    /// Create a new video frame from RGBA data.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error if:
87    /// - The dimensions would cause integer overflow
88    /// - The data length doesn't match width * height * 4
89    pub fn new(width: u32, height: u32, data: Vec<u8>) -> VideoTextureResult<Self> {
90        let expected = Self::checked_frame_size(width, height)?;
91        if data.len() != expected {
92            return Err(VideoTextureError::InvalidFrameData {
93                expected,
94                actual: data.len(),
95            });
96        }
97
98        Ok(Self {
99            width,
100            height,
101            data,
102        })
103    }
104
105    /// Create a placeholder frame with a solid color.
106    ///
107    /// Used when a video stream is not yet available.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if the dimensions would cause integer overflow.
112    pub fn placeholder(width: u32, height: u32) -> VideoTextureResult<Self> {
113        let byte_size = Self::checked_frame_size(width, height)?;
114        let pixel_count = byte_size / 4;
115        let mut data = Vec::with_capacity(byte_size);
116
117        // Create a dark gray placeholder (similar to video player background)
118        for _ in 0..pixel_count {
119            data.extend_from_slice(&[32, 32, 32, 255]); // Dark gray
120        }
121
122        Ok(Self {
123            width,
124            height,
125            data,
126        })
127    }
128
129    /// Check if the frame dimensions are valid (non-zero).
130    #[must_use]
131    pub fn is_valid(&self) -> bool {
132        self.width > 0 && self.height > 0 && !self.data.is_empty()
133    }
134
135    /// Get the frame width in pixels.
136    #[must_use]
137    pub fn width(&self) -> u32 {
138        self.width
139    }
140
141    /// Get the frame height in pixels.
142    #[must_use]
143    pub fn height(&self) -> u32 {
144        self.height
145    }
146
147    /// Get a reference to the RGBA pixel data.
148    #[must_use]
149    pub fn data(&self) -> &[u8] {
150        &self.data
151    }
152}
153
154/// Cached video texture entry metadata.
155///
156/// This struct tracks metadata about a video texture (dimensions and update time).
157/// Fields are public for simple read access in the render loop.
158#[derive(Debug)]
159pub struct VideoTextureEntry {
160    /// Width of the cached texture.
161    pub width: u32,
162    /// Height of the cached texture.
163    pub height: u32,
164    /// Last update timestamp (frame number or time).
165    pub last_updated: u64,
166}
167
168impl VideoTextureEntry {
169    /// Get the texture width in pixels.
170    #[must_use]
171    pub fn width(&self) -> u32 {
172        self.width
173    }
174
175    /// Get the texture height in pixels.
176    #[must_use]
177    pub fn height(&self) -> u32 {
178        self.height
179    }
180
181    /// Get the last update frame number.
182    #[must_use]
183    pub fn last_updated(&self) -> u64 {
184        self.last_updated
185    }
186}
187
188/// Manages video textures for multiple streams.
189///
190/// This manager tracks active video streams and their associated GPU textures.
191/// It provides methods to update textures with new frame data and retrieve
192/// cached textures for rendering.
193///
194/// # Example
195///
196/// ```
197/// use canvas_renderer::video::{VideoTextureManager, VideoFrameData};
198///
199/// let mut manager = VideoTextureManager::new();
200///
201/// // Update a video stream with new frame data
202/// let frame = VideoFrameData::placeholder(640, 480).expect("valid dimensions");
203/// manager.update_texture("stream-1", &frame);
204///
205/// // Check if texture exists
206/// if manager.has_texture("stream-1") {
207///     // Render the video element
208/// }
209/// ```
210#[derive(Debug, Default)]
211pub struct VideoTextureManager {
212    /// Texture metadata by stream ID.
213    entries: HashMap<String, VideoTextureEntry>,
214    /// Frame counter for tracking updates.
215    frame_counter: u64,
216}
217
218impl VideoTextureManager {
219    /// Create a new video texture manager.
220    #[must_use]
221    pub fn new() -> Self {
222        Self {
223            entries: HashMap::new(),
224            frame_counter: 0,
225        }
226    }
227
228    /// Update or create a video texture for a stream.
229    ///
230    /// This method records the texture metadata. The actual GPU texture
231    /// upload is handled by the wgpu backend using the frame data.
232    pub fn update_texture(&mut self, stream_id: &str, frame: &VideoFrameData) {
233        self.frame_counter += 1;
234
235        self.entries.insert(
236            stream_id.to_string(),
237            VideoTextureEntry {
238                width: frame.width,
239                height: frame.height,
240                last_updated: self.frame_counter,
241            },
242        );
243    }
244
245    /// Get texture metadata for a stream.
246    #[must_use]
247    pub fn get_texture(&self, stream_id: &str) -> Option<&VideoTextureEntry> {
248        self.entries.get(stream_id)
249    }
250
251    /// Check if a texture exists for a stream.
252    #[must_use]
253    pub fn has_texture(&self, stream_id: &str) -> bool {
254        self.entries.contains_key(stream_id)
255    }
256
257    /// Remove a video texture from the cache.
258    ///
259    /// Call this when a video stream ends or the element is removed.
260    pub fn remove_texture(&mut self, stream_id: &str) -> bool {
261        self.entries.remove(stream_id).is_some()
262    }
263
264    /// Clear all video textures.
265    pub fn clear(&mut self) {
266        self.entries.clear();
267    }
268
269    /// Get the number of cached video textures.
270    #[must_use]
271    pub fn texture_count(&self) -> usize {
272        self.entries.len()
273    }
274
275    /// Get an iterator over all stream IDs.
276    pub fn stream_ids(&self) -> impl Iterator<Item = &str> {
277        self.entries.keys().map(String::as_str)
278    }
279
280    /// Get the current frame counter.
281    #[must_use]
282    pub fn frame_counter(&self) -> u64 {
283        self.frame_counter
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_video_frame_data_new() {
293        // Valid frame
294        let data = vec![0u8; 4 * 4 * 4]; // 4x4 RGBA
295        let frame = VideoFrameData::new(4, 4, data);
296        assert!(frame.is_ok());
297
298        let frame = frame.expect("Frame should be valid");
299        assert_eq!(frame.width, 4);
300        assert_eq!(frame.height, 4);
301        assert!(frame.is_valid());
302    }
303
304    #[test]
305    fn test_video_frame_data_invalid() {
306        // Wrong size
307        let data = vec![0u8; 10]; // Not 4x4x4
308        let frame = VideoFrameData::new(4, 4, data);
309        assert!(frame.is_err());
310
311        match frame {
312            Err(VideoTextureError::InvalidFrameData { expected, actual }) => {
313                assert_eq!(expected, 64);
314                assert_eq!(actual, 10);
315            }
316            _ => panic!("Expected InvalidFrameData error"),
317        }
318    }
319
320    #[test]
321    fn test_video_frame_placeholder() {
322        let frame = VideoFrameData::placeholder(640, 480).expect("Should create placeholder");
323        assert_eq!(frame.width, 640);
324        assert_eq!(frame.height, 480);
325        assert_eq!(frame.data.len(), 640 * 480 * 4);
326        assert!(frame.is_valid());
327
328        // Check that it's dark gray
329        assert_eq!(&frame.data[0..4], &[32, 32, 32, 255]);
330    }
331
332    #[test]
333    fn test_video_frame_dimension_overflow() {
334        // Test that extremely large dimensions are rejected
335        let result = VideoFrameData::new(u32::MAX, u32::MAX, vec![]);
336        assert!(result.is_err());
337
338        match result {
339            Err(VideoTextureError::DimensionOverflow { width, height }) => {
340                assert_eq!(width, u32::MAX);
341                assert_eq!(height, u32::MAX);
342            }
343            _ => panic!("Expected DimensionOverflow error"),
344        }
345
346        // Placeholder should also reject overflow dimensions
347        let result = VideoFrameData::placeholder(u32::MAX, u32::MAX);
348        assert!(matches!(
349            result,
350            Err(VideoTextureError::DimensionOverflow { .. })
351        ));
352    }
353
354    #[test]
355    fn test_video_texture_manager() {
356        let mut manager = VideoTextureManager::new();
357
358        // Initially empty
359        assert_eq!(manager.texture_count(), 0);
360        assert!(!manager.has_texture("stream-1"));
361
362        // Add a texture
363        let frame = VideoFrameData::placeholder(320, 240).expect("Should create placeholder");
364        manager.update_texture("stream-1", &frame);
365
366        assert_eq!(manager.texture_count(), 1);
367        assert!(manager.has_texture("stream-1"));
368
369        // Get texture metadata
370        let entry = manager.get_texture("stream-1");
371        assert!(entry.is_some());
372        let entry = entry.expect("Entry should exist");
373        assert_eq!(entry.width, 320);
374        assert_eq!(entry.height, 240);
375        assert_eq!(entry.last_updated, 1);
376
377        // Update the same stream
378        let frame2 = VideoFrameData::placeholder(640, 480).expect("Should create placeholder");
379        manager.update_texture("stream-1", &frame2);
380
381        let entry = manager.get_texture("stream-1").expect("Entry should exist");
382        assert_eq!(entry.width, 640);
383        assert_eq!(entry.height, 480);
384        assert_eq!(entry.last_updated, 2);
385
386        // Add another stream
387        manager.update_texture("stream-2", &frame);
388        assert_eq!(manager.texture_count(), 2);
389
390        // Remove a texture
391        assert!(manager.remove_texture("stream-1"));
392        assert_eq!(manager.texture_count(), 1);
393        assert!(!manager.has_texture("stream-1"));
394        assert!(manager.has_texture("stream-2"));
395
396        // Remove non-existent
397        assert!(!manager.remove_texture("stream-1"));
398
399        // Clear all
400        manager.clear();
401        assert_eq!(manager.texture_count(), 0);
402    }
403
404    #[test]
405    fn test_video_texture_manager_stream_ids() {
406        let mut manager = VideoTextureManager::new();
407
408        let frame = VideoFrameData::placeholder(100, 100).expect("Should create placeholder");
409        manager.update_texture("a", &frame);
410        manager.update_texture("b", &frame);
411        manager.update_texture("c", &frame);
412
413        let mut ids: Vec<_> = manager.stream_ids().collect();
414        ids.sort_unstable();
415        assert_eq!(ids, vec!["a", "b", "c"]);
416    }
417
418    #[test]
419    fn test_video_frame_data_getters() {
420        let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
421        let frame = VideoFrameData::new(2, 2, data.clone()).expect("Should create frame");
422
423        assert_eq!(frame.width(), 2);
424        assert_eq!(frame.height(), 2);
425        assert_eq!(frame.data(), data.as_slice());
426    }
427
428    #[test]
429    fn test_video_texture_entry_getters() {
430        let mut manager = VideoTextureManager::new();
431        let frame = VideoFrameData::placeholder(800, 600).expect("Should create placeholder");
432        manager.update_texture("test-stream", &frame);
433
434        let entry = manager
435            .get_texture("test-stream")
436            .expect("Entry should exist");
437        assert_eq!(entry.width(), 800);
438        assert_eq!(entry.height(), 600);
439        assert_eq!(entry.last_updated(), 1);
440    }
441
442    #[test]
443    fn test_video_frame_zero_dimensions() {
444        // Zero width should produce empty data and be invalid
445        let result = VideoFrameData::new(0, 100, vec![]);
446        assert!(result.is_ok()); // Constructor succeeds but...
447
448        let frame = result.expect("Should create frame");
449        assert!(!frame.is_valid()); // ...frame is not valid
450
451        // Zero height same
452        let frame2 = VideoFrameData::new(100, 0, vec![]).expect("Should create frame");
453        assert!(!frame2.is_valid());
454    }
455
456    #[test]
457    fn test_video_texture_manager_frame_counter() {
458        let mut manager = VideoTextureManager::new();
459        assert_eq!(manager.frame_counter(), 0);
460
461        let frame = VideoFrameData::placeholder(100, 100).expect("Should create placeholder");
462
463        manager.update_texture("stream-1", &frame);
464        assert_eq!(manager.frame_counter(), 1);
465
466        manager.update_texture("stream-1", &frame);
467        assert_eq!(manager.frame_counter(), 2);
468
469        manager.update_texture("stream-2", &frame);
470        assert_eq!(manager.frame_counter(), 3);
471    }
472
473    #[test]
474    fn test_video_frame_missing_stream_behavior() {
475        let manager = VideoTextureManager::new();
476
477        // Accessing non-existent stream should return None, not panic
478        assert!(manager.get_texture("nonexistent").is_none());
479        assert!(!manager.has_texture("nonexistent"));
480    }
481}