Skip to main content

oximedia_distributed/
segment.rs

1//! Video segmentation and reassembly for distributed encoding.
2//!
3//! This module handles:
4//! - Video segmentation strategies (time-based, tile-based, GOP-based)
5//! - GOP (Group of Pictures) detection
6//! - Segment overlap handling
7//! - Reassembly and concatenation
8//! - Bitrate normalization
9
10#![allow(dead_code)]
11
12use crate::{Result, SplitStrategy};
13use std::path::{Path, PathBuf};
14use std::time::Duration;
15use tracing::{debug, info, warn};
16
17/// Video segment representation
18#[derive(Debug, Clone)]
19pub struct VideoSegment {
20    /// Segment identifier
21    pub id: String,
22
23    /// Segment index
24    pub index: usize,
25
26    /// Start time in seconds
27    pub start_time: f64,
28
29    /// End time in seconds
30    pub end_time: f64,
31
32    /// Duration
33    pub duration: f64,
34
35    /// Overlap with next segment
36    pub overlap: f64,
37
38    /// Source file path
39    pub source_path: PathBuf,
40
41    /// Output file path
42    pub output_path: Option<PathBuf>,
43
44    /// Segment type
45    pub segment_type: SegmentType,
46}
47
48/// Segment type
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum SegmentType {
51    /// Time-based segment
52    Time,
53    /// Spatial tile
54    Tile,
55    /// GOP-aligned segment
56    GOP,
57}
58
59/// Video segmenter
60pub struct VideoSegmenter {
61    /// Strategy for splitting
62    strategy: SplitStrategy,
63
64    /// Segment duration (for time-based)
65    segment_duration: Duration,
66
67    /// Overlap duration
68    overlap_duration: Duration,
69
70    /// GOP size hint
71    gop_size: usize,
72
73    /// Tile grid (width, height)
74    tile_grid: (u32, u32),
75}
76
77impl VideoSegmenter {
78    /// Create a new video segmenter
79    #[must_use]
80    pub fn new(strategy: SplitStrategy) -> Self {
81        Self {
82            strategy,
83            segment_duration: Duration::from_secs(60),
84            overlap_duration: Duration::from_millis(500),
85            gop_size: 30,
86            tile_grid: (2, 2),
87        }
88    }
89
90    /// Set segment duration for time-based splitting
91    #[must_use]
92    pub fn with_duration(mut self, duration: Duration) -> Self {
93        self.segment_duration = duration;
94        self
95    }
96
97    /// Set overlap duration
98    #[must_use]
99    pub fn with_overlap(mut self, overlap: Duration) -> Self {
100        self.overlap_duration = overlap;
101        self
102    }
103
104    /// Set GOP size hint
105    #[must_use]
106    pub fn with_gop_size(mut self, gop_size: usize) -> Self {
107        self.gop_size = gop_size;
108        self
109    }
110
111    /// Set tile grid for spatial splitting
112    #[must_use]
113    pub fn with_tile_grid(mut self, width: u32, height: u32) -> Self {
114        self.tile_grid = (width, height);
115        self
116    }
117
118    /// Split video into segments
119    pub async fn split_video(
120        &self,
121        source: &Path,
122        video_info: &VideoInfo,
123    ) -> Result<Vec<VideoSegment>> {
124        match self.strategy {
125            SplitStrategy::SegmentBased => self.split_by_time(source, video_info).await,
126            SplitStrategy::TileBased => self.split_by_tiles(source, video_info).await,
127            SplitStrategy::GopBased => self.split_by_gop(source, video_info).await,
128        }
129    }
130
131    /// Split video by time segments
132    async fn split_by_time(
133        &self,
134        source: &Path,
135        video_info: &VideoInfo,
136    ) -> Result<Vec<VideoSegment>> {
137        info!("Splitting video by time segments");
138
139        let mut segments = Vec::new();
140        let total_duration = video_info.duration;
141        let segment_duration = self.segment_duration.as_secs_f64();
142        let overlap = self.overlap_duration.as_secs_f64();
143
144        let num_segments = (total_duration / segment_duration).ceil() as usize;
145
146        for i in 0..num_segments {
147            let start_time = i as f64 * segment_duration;
148            let mut end_time = (i + 1) as f64 * segment_duration;
149
150            // Adjust last segment
151            if end_time > total_duration {
152                end_time = total_duration;
153            }
154
155            // Add overlap
156            let actual_end_time = if i < num_segments - 1 {
157                (end_time + overlap).min(total_duration)
158            } else {
159                end_time
160            };
161
162            let segment = VideoSegment {
163                id: format!("seg_{i:04}"),
164                index: i,
165                start_time,
166                end_time: actual_end_time,
167                duration: actual_end_time - start_time,
168                overlap,
169                source_path: source.to_path_buf(),
170                output_path: None,
171                segment_type: SegmentType::Time,
172            };
173
174            segments.push(segment);
175        }
176
177        info!("Created {} time-based segments", segments.len());
178        Ok(segments)
179    }
180
181    /// Split video by spatial tiles
182    async fn split_by_tiles(
183        &self,
184        source: &Path,
185        video_info: &VideoInfo,
186    ) -> Result<Vec<VideoSegment>> {
187        info!("Splitting video by spatial tiles");
188
189        let mut segments = Vec::new();
190        let (tile_cols, tile_rows) = self.tile_grid;
191
192        let _tile_width = video_info.width / tile_cols;
193        let _tile_height = video_info.height / tile_rows;
194
195        for row in 0..tile_rows {
196            for col in 0..tile_cols {
197                let index = (row * tile_cols + col) as usize;
198
199                // Tiles span the entire video duration
200                let segment = VideoSegment {
201                    id: format!("tile_{row}_{col}"),
202                    index,
203                    start_time: 0.0,
204                    end_time: video_info.duration,
205                    duration: video_info.duration,
206                    overlap: 0.0,
207                    source_path: source.to_path_buf(),
208                    output_path: None,
209                    segment_type: SegmentType::Tile,
210                };
211
212                segments.push(segment);
213            }
214        }
215
216        info!(
217            "Created {} tile-based segments ({}x{})",
218            segments.len(),
219            tile_cols,
220            tile_rows
221        );
222        Ok(segments)
223    }
224
225    /// Split video by GOP boundaries
226    async fn split_by_gop(
227        &self,
228        source: &Path,
229        video_info: &VideoInfo,
230    ) -> Result<Vec<VideoSegment>> {
231        info!("Splitting video by GOP boundaries");
232
233        // Detect GOP boundaries
234        let gop_boundaries = self.detect_gop_boundaries(source, video_info).await?;
235
236        let mut segments = Vec::new();
237        let frame_rate = video_info.frame_rate;
238
239        for (i, window) in gop_boundaries.windows(2).enumerate() {
240            let start_frame = window[0];
241            let end_frame = window[1];
242
243            let start_time = start_frame as f64 / frame_rate;
244            let end_time = end_frame as f64 / frame_rate;
245
246            let segment = VideoSegment {
247                id: format!("gop_{i:04}"),
248                index: i,
249                start_time,
250                end_time,
251                duration: end_time - start_time,
252                overlap: 0.0,
253                source_path: source.to_path_buf(),
254                output_path: None,
255                segment_type: SegmentType::GOP,
256            };
257
258            segments.push(segment);
259        }
260
261        info!("Created {} GOP-based segments", segments.len());
262        Ok(segments)
263    }
264
265    /// Detect GOP boundaries in video
266    async fn detect_gop_boundaries(
267        &self,
268        _source: &Path,
269        video_info: &VideoInfo,
270    ) -> Result<Vec<u64>> {
271        debug!("Detecting GOP boundaries");
272
273        // Simplified GOP detection - assume regular GOP structure
274        let total_frames = (video_info.duration * video_info.frame_rate) as u64;
275        let gop_size = self.gop_size as u64;
276
277        let mut boundaries = vec![0];
278        let mut current_frame = gop_size;
279
280        while current_frame < total_frames {
281            boundaries.push(current_frame);
282            current_frame += gop_size;
283        }
284
285        boundaries.push(total_frames);
286
287        debug!("Detected {} GOP boundaries", boundaries.len());
288        Ok(boundaries)
289    }
290}
291
292/// Video information
293#[derive(Debug, Clone)]
294pub struct VideoInfo {
295    /// Video width
296    pub width: u32,
297
298    /// Video height
299    pub height: u32,
300
301    /// Duration in seconds
302    pub duration: f64,
303
304    /// Frame rate
305    pub frame_rate: f64,
306
307    /// Bitrate
308    pub bitrate: u64,
309
310    /// Codec
311    pub codec: String,
312
313    /// Total frames
314    pub total_frames: u64,
315}
316
317impl VideoInfo {
318    /// Create video info from basic parameters
319    #[must_use]
320    pub fn new(width: u32, height: u32, duration: f64, frame_rate: f64) -> Self {
321        Self {
322            width,
323            height,
324            duration,
325            frame_rate,
326            bitrate: 5_000_000, // Default 5 Mbps
327            codec: "h264".to_string(),
328            total_frames: (duration * frame_rate) as u64,
329        }
330    }
331
332    /// Probe video file to get information
333    pub async fn probe(_path: &Path) -> Result<Self> {
334        // In production, would use FFprobe or similar
335        // For now, return mock data
336        Ok(Self {
337            width: 1920,
338            height: 1080,
339            duration: 600.0, // 10 minutes
340            frame_rate: 30.0,
341            bitrate: 5_000_000,
342            codec: "h264".to_string(),
343            total_frames: 18000,
344        })
345    }
346}
347
348/// Segment reassembler for combining encoded segments
349pub struct SegmentReassembler {
350    /// Concatenation strategy
351    strategy: ConcatenationStrategy,
352
353    /// Bitrate normalization
354    normalize_bitrate: bool,
355
356    /// Target bitrate for normalization
357    target_bitrate: Option<u64>,
358}
359
360impl SegmentReassembler {
361    /// Create a new segment reassembler
362    #[must_use]
363    pub fn new() -> Self {
364        Self {
365            strategy: ConcatenationStrategy::Concat,
366            normalize_bitrate: true,
367            target_bitrate: None,
368        }
369    }
370
371    /// Set concatenation strategy
372    #[must_use]
373    pub fn with_strategy(mut self, strategy: ConcatenationStrategy) -> Self {
374        self.strategy = strategy;
375        self
376    }
377
378    /// Enable bitrate normalization
379    #[must_use]
380    pub fn with_normalization(mut self, enable: bool) -> Self {
381        self.normalize_bitrate = enable;
382        self
383    }
384
385    /// Set target bitrate
386    #[must_use]
387    pub fn with_target_bitrate(mut self, bitrate: u64) -> Self {
388        self.target_bitrate = Some(bitrate);
389        self
390    }
391
392    /// Reassemble segments into final output
393    pub async fn reassemble(
394        &self,
395        segments: &[VideoSegment],
396        output: &Path,
397    ) -> Result<ReassemblyResult> {
398        info!("Reassembling {} segments", segments.len());
399
400        match self.strategy {
401            ConcatenationStrategy::Concat => self.concat_segments(segments, output).await,
402            ConcatenationStrategy::Blend => self.blend_segments(segments, output).await,
403            ConcatenationStrategy::Stitch => self.stitch_tiles(segments, output).await,
404        }
405    }
406
407    /// Concatenate time-based segments
408    async fn concat_segments(
409        &self,
410        segments: &[VideoSegment],
411        output: &Path,
412    ) -> Result<ReassemblyResult> {
413        info!("Concatenating segments");
414
415        // Sort segments by index
416        let mut sorted_segments = segments.to_vec();
417        sorted_segments.sort_by_key(|s| s.index);
418
419        // Handle overlaps
420        let processed_segments = if sorted_segments.iter().any(|s| s.overlap > 0.0) {
421            self.trim_overlaps(&sorted_segments)?
422        } else {
423            sorted_segments
424        };
425
426        // In production, would use FFmpeg concat demuxer
427        debug!(
428            "Concatenating {} segments to {:?}",
429            processed_segments.len(),
430            output
431        );
432
433        Ok(ReassemblyResult {
434            output_path: output.to_path_buf(),
435            total_duration: processed_segments.iter().map(|s| s.duration).sum(),
436            num_segments: processed_segments.len(),
437            final_bitrate: self.target_bitrate.unwrap_or(5_000_000),
438        })
439    }
440
441    /// Blend segments with overlaps
442    async fn blend_segments(
443        &self,
444        segments: &[VideoSegment],
445        output: &Path,
446    ) -> Result<ReassemblyResult> {
447        info!("Blending segments with overlaps");
448
449        // Sort segments
450        let mut sorted_segments = segments.to_vec();
451        sorted_segments.sort_by_key(|s| s.index);
452
453        // Process overlapping regions with cross-fade
454        for window in sorted_segments.windows(2) {
455            let seg1 = &window[0];
456            let seg2 = &window[1];
457
458            if seg1.overlap > 0.0 {
459                debug!(
460                    "Blending overlap between {} and {} ({:.2}s)",
461                    seg1.id, seg2.id, seg1.overlap
462                );
463                // In production, apply cross-fade filter
464            }
465        }
466
467        Ok(ReassemblyResult {
468            output_path: output.to_path_buf(),
469            total_duration: segments.iter().map(|s| s.duration - s.overlap).sum::<f64>()
470                + segments.last().map_or(0.0, |s| s.overlap),
471            num_segments: segments.len(),
472            final_bitrate: self.target_bitrate.unwrap_or(5_000_000),
473        })
474    }
475
476    /// Stitch spatial tiles back together
477    async fn stitch_tiles(
478        &self,
479        segments: &[VideoSegment],
480        output: &Path,
481    ) -> Result<ReassemblyResult> {
482        info!("Stitching {} tiles", segments.len());
483
484        // In production, would use FFmpeg xstack filter
485        debug!("Stitching tiles to {:?}", output);
486
487        Ok(ReassemblyResult {
488            output_path: output.to_path_buf(),
489            total_duration: segments.first().map_or(0.0, |s| s.duration),
490            num_segments: segments.len(),
491            final_bitrate: self.target_bitrate.unwrap_or(5_000_000),
492        })
493    }
494
495    /// Trim overlapping regions from segments
496    fn trim_overlaps(&self, segments: &[VideoSegment]) -> Result<Vec<VideoSegment>> {
497        let mut trimmed = Vec::new();
498
499        for (i, segment) in segments.iter().enumerate() {
500            let mut seg = segment.clone();
501
502            // Trim the overlap from the end (except last segment)
503            if i < segments.len() - 1 {
504                seg.duration -= seg.overlap;
505                seg.end_time -= seg.overlap;
506            }
507
508            trimmed.push(seg);
509        }
510
511        Ok(trimmed)
512    }
513
514    /// Normalize bitrates across segments
515    pub fn normalize_bitrates(&self, segments: &[VideoSegment]) -> Result<Vec<BitrateAdjustment>> {
516        if !self.normalize_bitrate {
517            return Ok(Vec::new());
518        }
519
520        let target = self.target_bitrate.unwrap_or(5_000_000);
521        info!("Normalizing bitrates to {} bps", target);
522
523        // Create adjustments for each segment
524        let adjustments: Vec<BitrateAdjustment> = segments
525            .iter()
526            .map(|s| BitrateAdjustment {
527                segment_id: s.id.clone(),
528                original_bitrate: 5_000_000, // Would be detected from encoded file
529                target_bitrate: target,
530                adjustment_factor: target as f64 / 5_000_000.0,
531            })
532            .collect();
533
534        Ok(adjustments)
535    }
536}
537
538impl Default for SegmentReassembler {
539    fn default() -> Self {
540        Self::new()
541    }
542}
543
544/// Concatenation strategy
545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
546pub enum ConcatenationStrategy {
547    /// Simple concatenation
548    Concat,
549    /// Blend overlapping regions
550    Blend,
551    /// Stitch spatial tiles
552    Stitch,
553}
554
555/// Reassembly result
556#[derive(Debug, Clone)]
557pub struct ReassemblyResult {
558    /// Output file path
559    pub output_path: PathBuf,
560
561    /// Total duration
562    pub total_duration: f64,
563
564    /// Number of segments
565    pub num_segments: usize,
566
567    /// Final bitrate
568    pub final_bitrate: u64,
569}
570
571/// Bitrate adjustment information
572#[derive(Debug, Clone)]
573pub struct BitrateAdjustment {
574    pub segment_id: String,
575    pub original_bitrate: u64,
576    pub target_bitrate: u64,
577    pub adjustment_factor: f64,
578}
579
580/// GOP (Group of Pictures) detector
581pub struct GOPDetector {
582    /// Minimum GOP size
583    min_gop_size: usize,
584
585    /// Maximum GOP size
586    max_gop_size: usize,
587
588    /// Scene change threshold
589    scene_threshold: f64,
590}
591
592impl GOPDetector {
593    /// Create a new GOP detector
594    #[must_use]
595    pub fn new() -> Self {
596        Self {
597            min_gop_size: 10,
598            max_gop_size: 300,
599            scene_threshold: 0.3,
600        }
601    }
602
603    /// Set GOP size constraints
604    #[must_use]
605    pub fn with_gop_range(mut self, min: usize, max: usize) -> Self {
606        self.min_gop_size = min;
607        self.max_gop_size = max;
608        self
609    }
610
611    /// Set scene change threshold
612    #[must_use]
613    pub fn with_scene_threshold(mut self, threshold: f64) -> Self {
614        self.scene_threshold = threshold;
615        self
616    }
617
618    /// Detect GOP structure in video
619    pub async fn detect_gops(&self, _video_path: &Path) -> Result<Vec<GOPInfo>> {
620        // In production, would analyze video frames
621        // For now, return mock data
622        let gops = vec![
623            GOPInfo {
624                start_frame: 0,
625                end_frame: 30,
626                keyframe_positions: vec![0],
627                num_p_frames: 29,
628                num_b_frames: 0,
629            },
630            GOPInfo {
631                start_frame: 30,
632                end_frame: 60,
633                keyframe_positions: vec![30],
634                num_p_frames: 29,
635                num_b_frames: 0,
636            },
637        ];
638
639        Ok(gops)
640    }
641
642    /// Validate GOP alignment for segments
643    pub fn validate_alignment(&self, segments: &[VideoSegment], gops: &[GOPInfo]) -> Result<bool> {
644        for segment in segments {
645            if segment.segment_type != SegmentType::GOP {
646                continue;
647            }
648
649            // Check if segment boundaries align with GOPs
650            let start_aligned = gops
651                .iter()
652                .any(|g| (g.start_frame as f64 - segment.start_time * 30.0).abs() < 0.1);
653
654            let end_aligned = gops
655                .iter()
656                .any(|g| (g.end_frame as f64 - segment.end_time * 30.0).abs() < 0.1);
657
658            if !start_aligned || !end_aligned {
659                warn!("Segment {} not aligned with GOP boundaries", segment.id);
660                return Ok(false);
661            }
662        }
663
664        Ok(true)
665    }
666}
667
668impl Default for GOPDetector {
669    fn default() -> Self {
670        Self::new()
671    }
672}
673
674/// GOP information
675#[derive(Debug, Clone)]
676pub struct GOPInfo {
677    /// Start frame
678    pub start_frame: u64,
679
680    /// End frame
681    pub end_frame: u64,
682
683    /// Keyframe positions
684    pub keyframe_positions: Vec<u64>,
685
686    /// Number of P-frames
687    pub num_p_frames: usize,
688
689    /// Number of B-frames
690    pub num_b_frames: usize,
691}
692
693impl GOPInfo {
694    /// Get GOP size
695    #[must_use]
696    pub fn size(&self) -> u64 {
697        self.end_frame - self.start_frame
698    }
699
700    /// Check if frame is a keyframe
701    #[must_use]
702    pub fn is_keyframe(&self, frame: u64) -> bool {
703        self.keyframe_positions.contains(&frame)
704    }
705}
706
707#[cfg(test)]
708mod tests {
709    use super::*;
710    use std::time::Duration;
711
712    #[test]
713    fn test_time_based_segmentation() {
714        let segmenter = VideoSegmenter::new(SplitStrategy::SegmentBased)
715            .with_duration(Duration::from_secs(60))
716            .with_overlap(Duration::from_millis(500));
717
718        assert_eq!(segmenter.segment_duration.as_secs(), 60);
719        assert_eq!(segmenter.overlap_duration.as_millis(), 500);
720    }
721
722    #[test]
723    fn test_video_info_creation() {
724        let info = VideoInfo::new(1920, 1080, 600.0, 30.0);
725        assert_eq!(info.width, 1920);
726        assert_eq!(info.height, 1080);
727        assert_eq!(info.duration, 600.0);
728        assert_eq!(info.total_frames, 18000);
729    }
730
731    #[test]
732    fn test_segment_reassembler() {
733        let reassembler = SegmentReassembler::new()
734            .with_normalization(true)
735            .with_target_bitrate(5_000_000);
736
737        assert!(reassembler.normalize_bitrate);
738        assert_eq!(reassembler.target_bitrate, Some(5_000_000));
739    }
740
741    #[test]
742    fn test_gop_detector() {
743        let detector = GOPDetector::new().with_gop_range(10, 300);
744
745        assert_eq!(detector.min_gop_size, 10);
746        assert_eq!(detector.max_gop_size, 300);
747    }
748
749    #[test]
750    fn test_gop_info() {
751        let gop = GOPInfo {
752            start_frame: 0,
753            end_frame: 30,
754            keyframe_positions: vec![0, 15, 30],
755            num_p_frames: 27,
756            num_b_frames: 0,
757        };
758
759        assert_eq!(gop.size(), 30);
760        assert!(gop.is_keyframe(0));
761        assert!(gop.is_keyframe(15));
762        assert!(!gop.is_keyframe(10));
763    }
764
765    #[test]
766    fn test_concatenation_strategies() {
767        assert_eq!(ConcatenationStrategy::Concat, ConcatenationStrategy::Concat);
768        assert_ne!(ConcatenationStrategy::Concat, ConcatenationStrategy::Blend);
769    }
770}