Skip to main content

memvid_rs/video/
decoder.rs

1//! Video decoding functionality
2//!
3//! This module provides video decoding using static FFmpeg for extracting frames from memvid videos.
4
5use crate::error::{MemvidError, Result};
6use image::DynamicImage;
7use std::path::Path;
8
9/// Video decoder with static FFmpeg support for frame extraction
10pub struct VideoDecoder {}
11
12impl VideoDecoder {
13    /// Create a new video decoder
14    pub fn new() -> Result<Self> {
15        // Initialize FFmpeg
16        ffmpeg_next::init()
17            .map_err(|e| MemvidError::Video(format!("FFmpeg init failed: {}", e)))?;
18
19        Ok(Self {})
20    }
21
22    /// Extract all frames from video file
23    pub async fn extract_frames(&self, video_path: &str) -> Result<Vec<DynamicImage>> {
24        let path = Path::new(video_path);
25        if !path.exists() {
26            return Err(MemvidError::Video(format!(
27                "Video file not found: {}",
28                video_path
29            )));
30        }
31
32        log::info!("Extracting frames from video: {}", video_path);
33
34        // Open input video
35        let mut input_ctx = ffmpeg_next::format::input(&path)
36            .map_err(|e| MemvidError::Video(format!("Failed to open video file: {}", e)))?;
37
38        // Find video stream
39        let video_stream_index = input_ctx
40            .streams()
41            .best(ffmpeg_next::media::Type::Video)
42            .ok_or_else(|| MemvidError::Video("No video stream found".to_string()))?
43            .index();
44
45        // Get video stream
46        let video_stream = input_ctx
47            .stream(video_stream_index)
48            .ok_or_else(|| MemvidError::Video("Failed to get video stream".to_string()))?;
49
50        // Create decoder context
51        let context_decoder =
52            ffmpeg_next::codec::context::Context::from_parameters(video_stream.parameters())
53                .map_err(|e| {
54                    MemvidError::Video(format!("Failed to create decoder context: {}", e))
55                })?;
56
57        let mut decoder = context_decoder
58            .decoder()
59            .video()
60            .map_err(|e| MemvidError::Video(format!("Failed to create video decoder: {}", e)))?;
61
62        // Get frame dimensions
63        let width = decoder.width();
64        let height = decoder.height();
65
66        log::info!("Video dimensions: {}x{}", width, height);
67
68        // Set up frame conversion
69        let mut scaler = ffmpeg_next::software::scaling::Context::get(
70            decoder.format(),
71            width,
72            height,
73            ffmpeg_next::format::Pixel::RGB24,
74            width,
75            height,
76            ffmpeg_next::software::scaling::Flags::BILINEAR,
77        )
78        .map_err(|e| MemvidError::Video(format!("Failed to create scaler: {}", e)))?;
79
80        let mut frames = Vec::new();
81        let mut frame_count = 0;
82
83        // Process packets
84        for (stream, packet) in input_ctx.packets() {
85            if stream.index() == video_stream_index {
86                decoder
87                    .send_packet(&packet)
88                    .map_err(|e| MemvidError::Video(format!("Failed to send packet: {}", e)))?;
89
90                self.receive_frames(&mut decoder, &mut scaler, &mut frames, &mut frame_count)
91                    .await?;
92            }
93        }
94
95        // Flush decoder
96        decoder
97            .send_eof()
98            .map_err(|e| MemvidError::Video(format!("Failed to send EOF: {}", e)))?;
99
100        self.receive_frames(&mut decoder, &mut scaler, &mut frames, &mut frame_count)
101            .await?;
102
103        log::info!("Extracted {} frames from video", frames.len());
104        Ok(frames)
105    }
106
107    /// Extract specific frame by number (0-indexed) - EFFICIENT VERSION
108    pub async fn extract_frame(&self, video_path: &str, frame_number: u32) -> Result<DynamicImage> {
109        let path = Path::new(video_path);
110        if !path.exists() {
111            return Err(MemvidError::Video(format!(
112                "Video file not found: {}",
113                video_path
114            )));
115        }
116
117        log::debug!(
118            "Extracting frame {} from video: {}",
119            frame_number,
120            video_path
121        );
122
123        // Open input video
124        let mut input_ctx = ffmpeg_next::format::input(&path)
125            .map_err(|e| MemvidError::Video(format!("Failed to open video file: {}", e)))?;
126
127        // Find video stream
128        let video_stream_index = input_ctx
129            .streams()
130            .best(ffmpeg_next::media::Type::Video)
131            .ok_or_else(|| MemvidError::Video("No video stream found".to_string()))?
132            .index();
133
134        // Get video stream and extract needed parameters before borrowing mutably
135        let (time_base, avg_frame_rate, stream_parameters) = {
136            let video_stream = input_ctx
137                .stream(video_stream_index)
138                .ok_or_else(|| MemvidError::Video("Failed to get video stream".to_string()))?;
139            (
140                video_stream.time_base(),
141                video_stream.avg_frame_rate(),
142                video_stream.parameters(),
143            )
144        };
145
146        // Calculate timestamp for the target frame
147        let frame_duration = if avg_frame_rate.denominator() > 0 && avg_frame_rate.numerator() > 0 {
148            time_base.denominator() as f64
149                / (time_base.numerator() as f64 * avg_frame_rate.numerator() as f64
150                    / avg_frame_rate.denominator() as f64)
151        } else {
152            // Fallback: assume 30 FPS
153            time_base.denominator() as f64 / (time_base.numerator() as f64 * 30.0)
154        };
155
156        let target_timestamp = (frame_number as f64 * frame_duration) as i64;
157
158        // Seek to the target frame
159        if frame_number > 0 {
160            input_ctx
161                .seek(
162                    target_timestamp,
163                    ..target_timestamp + (frame_duration as i64),
164                )
165                .map_err(|e| {
166                    MemvidError::Video(format!("Failed to seek to frame {}: {}", frame_number, e))
167                })?;
168        }
169
170        // Create decoder context
171        let context_decoder = ffmpeg_next::codec::context::Context::from_parameters(
172            stream_parameters,
173        )
174        .map_err(|e| MemvidError::Video(format!("Failed to create decoder context: {}", e)))?;
175
176        let mut decoder = context_decoder
177            .decoder()
178            .video()
179            .map_err(|e| MemvidError::Video(format!("Failed to create video decoder: {}", e)))?;
180
181        // Get frame dimensions
182        let width = decoder.width();
183        let height = decoder.height();
184
185        // Set up frame conversion
186        let mut scaler = ffmpeg_next::software::scaling::Context::get(
187            decoder.format(),
188            width,
189            height,
190            ffmpeg_next::format::Pixel::RGB24,
191            width,
192            height,
193            ffmpeg_next::software::scaling::Flags::BILINEAR,
194        )
195        .map_err(|e| MemvidError::Video(format!("Failed to create scaler: {}", e)))?;
196
197        let mut current_frame = 0u32;
198
199        // Process packets until we find our target frame
200        for (stream, packet) in input_ctx.packets() {
201            if stream.index() == video_stream_index {
202                decoder
203                    .send_packet(&packet)
204                    .map_err(|e| MemvidError::Video(format!("Failed to send packet: {}", e)))?;
205
206                let mut decoded_frame = ffmpeg_next::frame::Video::empty();
207                while decoder.receive_frame(&mut decoded_frame).is_ok() {
208                    if current_frame == frame_number {
209                        // Found our target frame, convert and return it
210                        let mut rgb_frame = ffmpeg_next::frame::Video::new(
211                            ffmpeg_next::format::Pixel::RGB24,
212                            width,
213                            height,
214                        );
215
216                        scaler.run(&decoded_frame, &mut rgb_frame).map_err(|e| {
217                            MemvidError::Video(format!("Failed to scale frame: {}", e))
218                        })?;
219
220                        let image = self.frame_to_image(&rgb_frame)?;
221
222                        log::debug!("Successfully extracted frame {} from video", frame_number);
223                        return Ok(image);
224                    }
225                    current_frame += 1;
226
227                    // Stop processing if we've gone past our target
228                    if current_frame > frame_number {
229                        break;
230                    }
231                }
232
233                if current_frame > frame_number {
234                    break;
235                }
236            }
237        }
238
239        Err(MemvidError::Video(format!(
240            "Frame number {} not found (video has fewer frames or seeking failed)",
241            frame_number
242        )))
243    }
244
245    /// Extract specific frame by number (0-indexed) - DEPRECATED SLOW VERSION
246    pub async fn extract_frame_slow(
247        &self,
248        video_path: &str,
249        frame_number: u32,
250    ) -> Result<DynamicImage> {
251        let frames = self.extract_frames(video_path).await?;
252
253        if frame_number as usize >= frames.len() {
254            return Err(MemvidError::Video(format!(
255                "Frame number {} out of range (video has {} frames)",
256                frame_number,
257                frames.len()
258            )));
259        }
260
261        Ok(frames[frame_number as usize].clone())
262    }
263
264    /// Get video information without extracting frames
265    pub async fn get_video_info(&self, video_path: &str) -> Result<VideoInfo> {
266        let path = Path::new(video_path);
267        if !path.exists() {
268            return Err(MemvidError::Video(format!(
269                "Video file not found: {}",
270                video_path
271            )));
272        }
273
274        // Open input video
275        let input_ctx = ffmpeg_next::format::input(&path)
276            .map_err(|e| MemvidError::Video(format!("Failed to open video file: {}", e)))?;
277
278        // Find video stream
279        let video_stream = input_ctx
280            .streams()
281            .best(ffmpeg_next::media::Type::Video)
282            .ok_or_else(|| MemvidError::Video("No video stream found".to_string()))?;
283
284        // Get decoder context for metadata
285        let context_decoder =
286            ffmpeg_next::codec::context::Context::from_parameters(video_stream.parameters())
287                .map_err(|e| {
288                    MemvidError::Video(format!("Failed to create decoder context: {}", e))
289                })?;
290
291        let decoder = context_decoder
292            .decoder()
293            .video()
294            .map_err(|e| MemvidError::Video(format!("Failed to create video decoder: {}", e)))?;
295
296        // Calculate duration and frame count
297        let duration_seconds = if video_stream.duration() > 0 {
298            let time_base: f64 = video_stream.time_base().into();
299            video_stream.duration() as f64 * time_base
300        } else {
301            input_ctx.duration() as f64 / ffmpeg_next::ffi::AV_TIME_BASE as f64
302        };
303
304        let fps: f64 = video_stream.avg_frame_rate().into();
305        let frame_count = if fps > 0.0 {
306            (duration_seconds * fps) as u32
307        } else {
308            0
309        };
310
311        Ok(VideoInfo {
312            width: decoder.width(),
313            height: decoder.height(),
314            fps,
315            duration_seconds,
316            frame_count,
317            format: format!("{:?}", decoder.format()),
318            codec: "H.264".to_string(), // Default for memvid videos
319        })
320    }
321
322    /// Helper to receive and convert frames
323    async fn receive_frames(
324        &self,
325        decoder: &mut ffmpeg_next::decoder::Video,
326        scaler: &mut ffmpeg_next::software::scaling::Context,
327        frames: &mut Vec<DynamicImage>,
328        frame_count: &mut u32,
329    ) -> Result<()> {
330        let mut decoded_frame = ffmpeg_next::frame::Video::empty();
331
332        while decoder.receive_frame(&mut decoded_frame).is_ok() {
333            let width = decoded_frame.width();
334            let height = decoded_frame.height();
335
336            // Create RGB frame
337            let mut rgb_frame =
338                ffmpeg_next::frame::Video::new(ffmpeg_next::format::Pixel::RGB24, width, height);
339
340            // Scale/convert to RGB
341            scaler
342                .run(&decoded_frame, &mut rgb_frame)
343                .map_err(|e| MemvidError::Video(format!("Failed to scale frame: {}", e)))?;
344
345            // Convert to image
346            let image = self.frame_to_image(&rgb_frame)?;
347            frames.push(image);
348
349            *frame_count += 1;
350
351            if *frame_count % 10 == 0 {
352                log::info!("Processed {} frames", *frame_count);
353            }
354        }
355
356        Ok(())
357    }
358
359    /// Convert FFmpeg frame to DynamicImage
360    fn frame_to_image(&self, frame: &ffmpeg_next::frame::Video) -> Result<DynamicImage> {
361        let width = frame.width();
362        let height = frame.height();
363
364        // Get RGB data from frame
365        let data = frame.data(0);
366        let linesize = frame.stride(0);
367
368        // Create image buffer
369        let mut rgb_data = Vec::with_capacity((width * height * 3) as usize);
370
371        for y in 0..height {
372            let row_start = (y * linesize as u32) as usize;
373            let row_end = row_start + (width * 3) as usize;
374            rgb_data.extend_from_slice(&data[row_start..row_end]);
375        }
376
377        // Create RGB image
378        let rgb_image = image::RgbImage::from_raw(width, height, rgb_data).ok_or_else(|| {
379            MemvidError::Video("Failed to create RGB image from frame data".to_string())
380        })?;
381
382        Ok(DynamicImage::ImageRgb8(rgb_image))
383    }
384
385    /// Extract frames within a specific range - EFFICIENT VERSION
386    pub async fn extract_frames_range(
387        &self,
388        video_path: &str,
389        start_frame: u32,
390        end_frame: u32,
391    ) -> Result<Vec<DynamicImage>> {
392        if start_frame > end_frame {
393            return Err(MemvidError::Video(format!(
394                "Invalid frame range: start {} > end {}",
395                start_frame, end_frame
396            )));
397        }
398
399        let path = Path::new(video_path);
400        if !path.exists() {
401            return Err(MemvidError::Video(format!(
402                "Video file not found: {}",
403                video_path
404            )));
405        }
406
407        log::debug!(
408            "Extracting frames {}-{} from video: {}",
409            start_frame,
410            end_frame,
411            video_path
412        );
413
414        // Open input video
415        let mut input_ctx = ffmpeg_next::format::input(&path)
416            .map_err(|e| MemvidError::Video(format!("Failed to open video file: {}", e)))?;
417
418        // Find video stream
419        let video_stream_index = input_ctx
420            .streams()
421            .best(ffmpeg_next::media::Type::Video)
422            .ok_or_else(|| MemvidError::Video("No video stream found".to_string()))?
423            .index();
424
425        // Get video stream and extract needed parameters before borrowing mutably
426        let (time_base, avg_frame_rate, stream_parameters) = {
427            let video_stream = input_ctx
428                .stream(video_stream_index)
429                .ok_or_else(|| MemvidError::Video("Failed to get video stream".to_string()))?;
430            (
431                video_stream.time_base(),
432                video_stream.avg_frame_rate(),
433                video_stream.parameters(),
434            )
435        };
436
437        // Calculate timestamp for the start frame
438        let frame_duration = if avg_frame_rate.denominator() > 0 && avg_frame_rate.numerator() > 0 {
439            time_base.denominator() as f64
440                / (time_base.numerator() as f64 * avg_frame_rate.numerator() as f64
441                    / avg_frame_rate.denominator() as f64)
442        } else {
443            // Fallback: assume 30 FPS
444            time_base.denominator() as f64 / (time_base.numerator() as f64 * 30.0)
445        };
446
447        let start_timestamp = (start_frame as f64 * frame_duration) as i64;
448
449        // Seek to the start frame
450        if start_frame > 0 {
451            input_ctx
452                .seek(start_timestamp, ..start_timestamp + (frame_duration as i64))
453                .map_err(|e| {
454                    MemvidError::Video(format!("Failed to seek to frame {}: {}", start_frame, e))
455                })?;
456        }
457
458        // Create decoder context
459        let context_decoder = ffmpeg_next::codec::context::Context::from_parameters(
460            stream_parameters,
461        )
462        .map_err(|e| MemvidError::Video(format!("Failed to create decoder context: {}", e)))?;
463
464        let mut decoder = context_decoder
465            .decoder()
466            .video()
467            .map_err(|e| MemvidError::Video(format!("Failed to create video decoder: {}", e)))?;
468
469        // Get frame dimensions
470        let width = decoder.width();
471        let height = decoder.height();
472
473        // Set up frame conversion
474        let mut scaler = ffmpeg_next::software::scaling::Context::get(
475            decoder.format(),
476            width,
477            height,
478            ffmpeg_next::format::Pixel::RGB24,
479            width,
480            height,
481            ffmpeg_next::software::scaling::Flags::BILINEAR,
482        )
483        .map_err(|e| MemvidError::Video(format!("Failed to create scaler: {}", e)))?;
484
485        let mut current_frame = 0u32;
486        let mut extracted_frames = Vec::new();
487
488        // Process packets and collect frames in the range
489        for (stream, packet) in input_ctx.packets() {
490            if stream.index() == video_stream_index {
491                decoder
492                    .send_packet(&packet)
493                    .map_err(|e| MemvidError::Video(format!("Failed to send packet: {}", e)))?;
494
495                let mut decoded_frame = ffmpeg_next::frame::Video::empty();
496                while decoder.receive_frame(&mut decoded_frame).is_ok() {
497                    if current_frame >= start_frame && current_frame <= end_frame {
498                        // Frame is in our target range, convert and store it
499                        let mut rgb_frame = ffmpeg_next::frame::Video::new(
500                            ffmpeg_next::format::Pixel::RGB24,
501                            width,
502                            height,
503                        );
504
505                        scaler.run(&decoded_frame, &mut rgb_frame).map_err(|e| {
506                            MemvidError::Video(format!("Failed to scale frame: {}", e))
507                        })?;
508
509                        let image = self.frame_to_image(&rgb_frame)?;
510                        extracted_frames.push(image);
511                    }
512
513                    current_frame += 1;
514
515                    // Stop processing if we've gone past our target range
516                    if current_frame > end_frame {
517                        log::debug!(
518                            "Successfully extracted {} frames ({}-{})",
519                            extracted_frames.len(),
520                            start_frame,
521                            end_frame
522                        );
523                        return Ok(extracted_frames);
524                    }
525                }
526
527                // If we've collected all frames in range, stop processing
528                if current_frame > end_frame {
529                    break;
530                }
531            }
532        }
533
534        log::debug!(
535            "Successfully extracted {} frames ({}-{})",
536            extracted_frames.len(),
537            start_frame,
538            end_frame
539        );
540        Ok(extracted_frames)
541    }
542
543    /// Extract frames within a specific range - DEPRECATED SLOW VERSION
544    pub async fn extract_frames_range_slow(
545        &self,
546        video_path: &str,
547        start_frame: u32,
548        end_frame: u32,
549    ) -> Result<Vec<DynamicImage>> {
550        let all_frames = self.extract_frames(video_path).await?;
551
552        let start_idx = start_frame as usize;
553        let end_idx = (end_frame + 1) as usize;
554
555        if start_idx >= all_frames.len() {
556            return Err(MemvidError::Video(format!(
557                "Start frame {} out of range (video has {} frames)",
558                start_frame,
559                all_frames.len()
560            )));
561        }
562
563        let end_idx = end_idx.min(all_frames.len());
564        Ok(all_frames[start_idx..end_idx].to_vec())
565    }
566}
567
568impl Default for VideoDecoder {
569    fn default() -> Self {
570        Self::new().unwrap_or(Self {})
571    }
572}
573
574/// Video metadata information
575#[derive(Debug, Clone)]
576pub struct VideoInfo {
577    /// Video width in pixels
578    pub width: u32,
579    /// Video height in pixels  
580    pub height: u32,
581    /// Frames per second
582    pub fps: f64,
583    /// Duration in seconds
584    pub duration_seconds: f64,
585    /// Total number of frames
586    pub frame_count: u32,
587    /// Pixel format
588    pub format: String,
589    /// Video codec
590    pub codec: String,
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596
597    #[tokio::test]
598    async fn test_decoder_creation() {
599        let decoder = VideoDecoder::new();
600        assert!(decoder.is_ok());
601    }
602
603    #[tokio::test]
604    async fn test_nonexistent_video() {
605        let decoder = VideoDecoder::new().unwrap();
606        let result = decoder.extract_frames("nonexistent.mp4").await;
607        assert!(result.is_err());
608    }
609}