1#![allow(dead_code)]
11
12use crate::{Result, SplitStrategy};
13use std::path::{Path, PathBuf};
14use std::time::Duration;
15use tracing::{debug, info, warn};
16
17#[derive(Debug, Clone)]
19pub struct VideoSegment {
20 pub id: String,
22
23 pub index: usize,
25
26 pub start_time: f64,
28
29 pub end_time: f64,
31
32 pub duration: f64,
34
35 pub overlap: f64,
37
38 pub source_path: PathBuf,
40
41 pub output_path: Option<PathBuf>,
43
44 pub segment_type: SegmentType,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum SegmentType {
51 Time,
53 Tile,
55 GOP,
57}
58
59pub struct VideoSegmenter {
61 strategy: SplitStrategy,
63
64 segment_duration: Duration,
66
67 overlap_duration: Duration,
69
70 gop_size: usize,
72
73 tile_grid: (u32, u32),
75}
76
77impl VideoSegmenter {
78 #[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 #[must_use]
92 pub fn with_duration(mut self, duration: Duration) -> Self {
93 self.segment_duration = duration;
94 self
95 }
96
97 #[must_use]
99 pub fn with_overlap(mut self, overlap: Duration) -> Self {
100 self.overlap_duration = overlap;
101 self
102 }
103
104 #[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 #[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 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 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 if end_time > total_duration {
152 end_time = total_duration;
153 }
154
155 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 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 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 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 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 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 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#[derive(Debug, Clone)]
294pub struct VideoInfo {
295 pub width: u32,
297
298 pub height: u32,
300
301 pub duration: f64,
303
304 pub frame_rate: f64,
306
307 pub bitrate: u64,
309
310 pub codec: String,
312
313 pub total_frames: u64,
315}
316
317impl VideoInfo {
318 #[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, codec: "h264".to_string(),
328 total_frames: (duration * frame_rate) as u64,
329 }
330 }
331
332 pub async fn probe(_path: &Path) -> Result<Self> {
334 Ok(Self {
337 width: 1920,
338 height: 1080,
339 duration: 600.0, frame_rate: 30.0,
341 bitrate: 5_000_000,
342 codec: "h264".to_string(),
343 total_frames: 18000,
344 })
345 }
346}
347
348pub struct SegmentReassembler {
350 strategy: ConcatenationStrategy,
352
353 normalize_bitrate: bool,
355
356 target_bitrate: Option<u64>,
358}
359
360impl SegmentReassembler {
361 #[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 #[must_use]
373 pub fn with_strategy(mut self, strategy: ConcatenationStrategy) -> Self {
374 self.strategy = strategy;
375 self
376 }
377
378 #[must_use]
380 pub fn with_normalization(mut self, enable: bool) -> Self {
381 self.normalize_bitrate = enable;
382 self
383 }
384
385 #[must_use]
387 pub fn with_target_bitrate(mut self, bitrate: u64) -> Self {
388 self.target_bitrate = Some(bitrate);
389 self
390 }
391
392 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 async fn concat_segments(
409 &self,
410 segments: &[VideoSegment],
411 output: &Path,
412 ) -> Result<ReassemblyResult> {
413 info!("Concatenating segments");
414
415 let mut sorted_segments = segments.to_vec();
417 sorted_segments.sort_by_key(|s| s.index);
418
419 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 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 async fn blend_segments(
443 &self,
444 segments: &[VideoSegment],
445 output: &Path,
446 ) -> Result<ReassemblyResult> {
447 info!("Blending segments with overlaps");
448
449 let mut sorted_segments = segments.to_vec();
451 sorted_segments.sort_by_key(|s| s.index);
452
453 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 }
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 async fn stitch_tiles(
478 &self,
479 segments: &[VideoSegment],
480 output: &Path,
481 ) -> Result<ReassemblyResult> {
482 info!("Stitching {} tiles", segments.len());
483
484 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 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 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 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 let adjustments: Vec<BitrateAdjustment> = segments
525 .iter()
526 .map(|s| BitrateAdjustment {
527 segment_id: s.id.clone(),
528 original_bitrate: 5_000_000, 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
546pub enum ConcatenationStrategy {
547 Concat,
549 Blend,
551 Stitch,
553}
554
555#[derive(Debug, Clone)]
557pub struct ReassemblyResult {
558 pub output_path: PathBuf,
560
561 pub total_duration: f64,
563
564 pub num_segments: usize,
566
567 pub final_bitrate: u64,
569}
570
571#[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
580pub struct GOPDetector {
582 min_gop_size: usize,
584
585 max_gop_size: usize,
587
588 scene_threshold: f64,
590}
591
592impl GOPDetector {
593 #[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 #[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 #[must_use]
613 pub fn with_scene_threshold(mut self, threshold: f64) -> Self {
614 self.scene_threshold = threshold;
615 self
616 }
617
618 pub async fn detect_gops(&self, _video_path: &Path) -> Result<Vec<GOPInfo>> {
620 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 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 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#[derive(Debug, Clone)]
676pub struct GOPInfo {
677 pub start_frame: u64,
679
680 pub end_frame: u64,
682
683 pub keyframe_positions: Vec<u64>,
685
686 pub num_p_frames: usize,
688
689 pub num_b_frames: usize,
691}
692
693impl GOPInfo {
694 #[must_use]
696 pub fn size(&self) -> u64 {
697 self.end_frame - self.start_frame
698 }
699
700 #[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}