#![allow(dead_code)]
use crate::{Result, SplitStrategy};
use std::path::{Path, PathBuf};
use std::time::Duration;
use tracing::{debug, info, warn};
#[derive(Debug, Clone)]
pub struct VideoSegment {
pub id: String,
pub index: usize,
pub start_time: f64,
pub end_time: f64,
pub duration: f64,
pub overlap: f64,
pub source_path: PathBuf,
pub output_path: Option<PathBuf>,
pub segment_type: SegmentType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SegmentType {
Time,
Tile,
GOP,
}
pub struct VideoSegmenter {
strategy: SplitStrategy,
segment_duration: Duration,
overlap_duration: Duration,
gop_size: usize,
tile_grid: (u32, u32),
}
impl VideoSegmenter {
#[must_use]
pub fn new(strategy: SplitStrategy) -> Self {
Self {
strategy,
segment_duration: Duration::from_secs(60),
overlap_duration: Duration::from_millis(500),
gop_size: 30,
tile_grid: (2, 2),
}
}
#[must_use]
pub fn with_duration(mut self, duration: Duration) -> Self {
self.segment_duration = duration;
self
}
#[must_use]
pub fn with_overlap(mut self, overlap: Duration) -> Self {
self.overlap_duration = overlap;
self
}
#[must_use]
pub fn with_gop_size(mut self, gop_size: usize) -> Self {
self.gop_size = gop_size;
self
}
#[must_use]
pub fn with_tile_grid(mut self, width: u32, height: u32) -> Self {
self.tile_grid = (width, height);
self
}
pub async fn split_video(
&self,
source: &Path,
video_info: &VideoInfo,
) -> Result<Vec<VideoSegment>> {
match self.strategy {
SplitStrategy::SegmentBased => self.split_by_time(source, video_info).await,
SplitStrategy::TileBased => self.split_by_tiles(source, video_info).await,
SplitStrategy::GopBased => self.split_by_gop(source, video_info).await,
}
}
async fn split_by_time(
&self,
source: &Path,
video_info: &VideoInfo,
) -> Result<Vec<VideoSegment>> {
info!("Splitting video by time segments");
let mut segments = Vec::new();
let total_duration = video_info.duration;
let segment_duration = self.segment_duration.as_secs_f64();
let overlap = self.overlap_duration.as_secs_f64();
let num_segments = (total_duration / segment_duration).ceil() as usize;
for i in 0..num_segments {
let start_time = i as f64 * segment_duration;
let mut end_time = (i + 1) as f64 * segment_duration;
if end_time > total_duration {
end_time = total_duration;
}
let actual_end_time = if i < num_segments - 1 {
(end_time + overlap).min(total_duration)
} else {
end_time
};
let segment = VideoSegment {
id: format!("seg_{i:04}"),
index: i,
start_time,
end_time: actual_end_time,
duration: actual_end_time - start_time,
overlap,
source_path: source.to_path_buf(),
output_path: None,
segment_type: SegmentType::Time,
};
segments.push(segment);
}
info!("Created {} time-based segments", segments.len());
Ok(segments)
}
async fn split_by_tiles(
&self,
source: &Path,
video_info: &VideoInfo,
) -> Result<Vec<VideoSegment>> {
info!("Splitting video by spatial tiles");
let mut segments = Vec::new();
let (tile_cols, tile_rows) = self.tile_grid;
let _tile_width = video_info.width / tile_cols;
let _tile_height = video_info.height / tile_rows;
for row in 0..tile_rows {
for col in 0..tile_cols {
let index = (row * tile_cols + col) as usize;
let segment = VideoSegment {
id: format!("tile_{row}_{col}"),
index,
start_time: 0.0,
end_time: video_info.duration,
duration: video_info.duration,
overlap: 0.0,
source_path: source.to_path_buf(),
output_path: None,
segment_type: SegmentType::Tile,
};
segments.push(segment);
}
}
info!(
"Created {} tile-based segments ({}x{})",
segments.len(),
tile_cols,
tile_rows
);
Ok(segments)
}
async fn split_by_gop(
&self,
source: &Path,
video_info: &VideoInfo,
) -> Result<Vec<VideoSegment>> {
info!("Splitting video by GOP boundaries");
let gop_boundaries = self.detect_gop_boundaries(source, video_info).await?;
let mut segments = Vec::new();
let frame_rate = video_info.frame_rate;
for (i, window) in gop_boundaries.windows(2).enumerate() {
let start_frame = window[0];
let end_frame = window[1];
let start_time = start_frame as f64 / frame_rate;
let end_time = end_frame as f64 / frame_rate;
let segment = VideoSegment {
id: format!("gop_{i:04}"),
index: i,
start_time,
end_time,
duration: end_time - start_time,
overlap: 0.0,
source_path: source.to_path_buf(),
output_path: None,
segment_type: SegmentType::GOP,
};
segments.push(segment);
}
info!("Created {} GOP-based segments", segments.len());
Ok(segments)
}
async fn detect_gop_boundaries(
&self,
_source: &Path,
video_info: &VideoInfo,
) -> Result<Vec<u64>> {
debug!("Detecting GOP boundaries");
let total_frames = (video_info.duration * video_info.frame_rate) as u64;
let gop_size = self.gop_size as u64;
let mut boundaries = vec![0];
let mut current_frame = gop_size;
while current_frame < total_frames {
boundaries.push(current_frame);
current_frame += gop_size;
}
boundaries.push(total_frames);
debug!("Detected {} GOP boundaries", boundaries.len());
Ok(boundaries)
}
}
#[derive(Debug, Clone)]
pub struct VideoInfo {
pub width: u32,
pub height: u32,
pub duration: f64,
pub frame_rate: f64,
pub bitrate: u64,
pub codec: String,
pub total_frames: u64,
}
impl VideoInfo {
#[must_use]
pub fn new(width: u32, height: u32, duration: f64, frame_rate: f64) -> Self {
Self {
width,
height,
duration,
frame_rate,
bitrate: 5_000_000, codec: "h264".to_string(),
total_frames: (duration * frame_rate) as u64,
}
}
pub async fn probe(_path: &Path) -> Result<Self> {
Ok(Self {
width: 1920,
height: 1080,
duration: 600.0, frame_rate: 30.0,
bitrate: 5_000_000,
codec: "h264".to_string(),
total_frames: 18000,
})
}
}
pub struct SegmentReassembler {
strategy: ConcatenationStrategy,
normalize_bitrate: bool,
target_bitrate: Option<u64>,
}
impl SegmentReassembler {
#[must_use]
pub fn new() -> Self {
Self {
strategy: ConcatenationStrategy::Concat,
normalize_bitrate: true,
target_bitrate: None,
}
}
#[must_use]
pub fn with_strategy(mut self, strategy: ConcatenationStrategy) -> Self {
self.strategy = strategy;
self
}
#[must_use]
pub fn with_normalization(mut self, enable: bool) -> Self {
self.normalize_bitrate = enable;
self
}
#[must_use]
pub fn with_target_bitrate(mut self, bitrate: u64) -> Self {
self.target_bitrate = Some(bitrate);
self
}
pub async fn reassemble(
&self,
segments: &[VideoSegment],
output: &Path,
) -> Result<ReassemblyResult> {
info!("Reassembling {} segments", segments.len());
match self.strategy {
ConcatenationStrategy::Concat => self.concat_segments(segments, output).await,
ConcatenationStrategy::Blend => self.blend_segments(segments, output).await,
ConcatenationStrategy::Stitch => self.stitch_tiles(segments, output).await,
}
}
async fn concat_segments(
&self,
segments: &[VideoSegment],
output: &Path,
) -> Result<ReassemblyResult> {
info!("Concatenating segments");
let mut sorted_segments = segments.to_vec();
sorted_segments.sort_by_key(|s| s.index);
let processed_segments = if sorted_segments.iter().any(|s| s.overlap > 0.0) {
self.trim_overlaps(&sorted_segments)?
} else {
sorted_segments
};
debug!(
"Concatenating {} segments to {:?}",
processed_segments.len(),
output
);
Ok(ReassemblyResult {
output_path: output.to_path_buf(),
total_duration: processed_segments.iter().map(|s| s.duration).sum(),
num_segments: processed_segments.len(),
final_bitrate: self.target_bitrate.unwrap_or(5_000_000),
})
}
async fn blend_segments(
&self,
segments: &[VideoSegment],
output: &Path,
) -> Result<ReassemblyResult> {
info!("Blending segments with overlaps");
let mut sorted_segments = segments.to_vec();
sorted_segments.sort_by_key(|s| s.index);
for window in sorted_segments.windows(2) {
let seg1 = &window[0];
let seg2 = &window[1];
if seg1.overlap > 0.0 {
debug!(
"Blending overlap between {} and {} ({:.2}s)",
seg1.id, seg2.id, seg1.overlap
);
}
}
Ok(ReassemblyResult {
output_path: output.to_path_buf(),
total_duration: segments.iter().map(|s| s.duration - s.overlap).sum::<f64>()
+ segments.last().map_or(0.0, |s| s.overlap),
num_segments: segments.len(),
final_bitrate: self.target_bitrate.unwrap_or(5_000_000),
})
}
async fn stitch_tiles(
&self,
segments: &[VideoSegment],
output: &Path,
) -> Result<ReassemblyResult> {
info!("Stitching {} tiles", segments.len());
debug!("Stitching tiles to {:?}", output);
Ok(ReassemblyResult {
output_path: output.to_path_buf(),
total_duration: segments.first().map_or(0.0, |s| s.duration),
num_segments: segments.len(),
final_bitrate: self.target_bitrate.unwrap_or(5_000_000),
})
}
fn trim_overlaps(&self, segments: &[VideoSegment]) -> Result<Vec<VideoSegment>> {
let mut trimmed = Vec::new();
for (i, segment) in segments.iter().enumerate() {
let mut seg = segment.clone();
if i < segments.len() - 1 {
seg.duration -= seg.overlap;
seg.end_time -= seg.overlap;
}
trimmed.push(seg);
}
Ok(trimmed)
}
pub fn normalize_bitrates(&self, segments: &[VideoSegment]) -> Result<Vec<BitrateAdjustment>> {
if !self.normalize_bitrate {
return Ok(Vec::new());
}
let target = self.target_bitrate.unwrap_or(5_000_000);
info!("Normalizing bitrates to {} bps", target);
let adjustments: Vec<BitrateAdjustment> = segments
.iter()
.map(|s| BitrateAdjustment {
segment_id: s.id.clone(),
original_bitrate: 5_000_000, target_bitrate: target,
adjustment_factor: target as f64 / 5_000_000.0,
})
.collect();
Ok(adjustments)
}
}
impl Default for SegmentReassembler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConcatenationStrategy {
Concat,
Blend,
Stitch,
}
#[derive(Debug, Clone)]
pub struct ReassemblyResult {
pub output_path: PathBuf,
pub total_duration: f64,
pub num_segments: usize,
pub final_bitrate: u64,
}
#[derive(Debug, Clone)]
pub struct BitrateAdjustment {
pub segment_id: String,
pub original_bitrate: u64,
pub target_bitrate: u64,
pub adjustment_factor: f64,
}
pub struct GOPDetector {
min_gop_size: usize,
max_gop_size: usize,
scene_threshold: f64,
}
impl GOPDetector {
#[must_use]
pub fn new() -> Self {
Self {
min_gop_size: 10,
max_gop_size: 300,
scene_threshold: 0.3,
}
}
#[must_use]
pub fn with_gop_range(mut self, min: usize, max: usize) -> Self {
self.min_gop_size = min;
self.max_gop_size = max;
self
}
#[must_use]
pub fn with_scene_threshold(mut self, threshold: f64) -> Self {
self.scene_threshold = threshold;
self
}
pub async fn detect_gops(&self, _video_path: &Path) -> Result<Vec<GOPInfo>> {
let gops = vec![
GOPInfo {
start_frame: 0,
end_frame: 30,
keyframe_positions: vec![0],
num_p_frames: 29,
num_b_frames: 0,
},
GOPInfo {
start_frame: 30,
end_frame: 60,
keyframe_positions: vec![30],
num_p_frames: 29,
num_b_frames: 0,
},
];
Ok(gops)
}
pub fn validate_alignment(&self, segments: &[VideoSegment], gops: &[GOPInfo]) -> Result<bool> {
for segment in segments {
if segment.segment_type != SegmentType::GOP {
continue;
}
let start_aligned = gops
.iter()
.any(|g| (g.start_frame as f64 - segment.start_time * 30.0).abs() < 0.1);
let end_aligned = gops
.iter()
.any(|g| (g.end_frame as f64 - segment.end_time * 30.0).abs() < 0.1);
if !start_aligned || !end_aligned {
warn!("Segment {} not aligned with GOP boundaries", segment.id);
return Ok(false);
}
}
Ok(true)
}
}
impl Default for GOPDetector {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct GOPInfo {
pub start_frame: u64,
pub end_frame: u64,
pub keyframe_positions: Vec<u64>,
pub num_p_frames: usize,
pub num_b_frames: usize,
}
impl GOPInfo {
#[must_use]
pub fn size(&self) -> u64 {
self.end_frame - self.start_frame
}
#[must_use]
pub fn is_keyframe(&self, frame: u64) -> bool {
self.keyframe_positions.contains(&frame)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_time_based_segmentation() {
let segmenter = VideoSegmenter::new(SplitStrategy::SegmentBased)
.with_duration(Duration::from_secs(60))
.with_overlap(Duration::from_millis(500));
assert_eq!(segmenter.segment_duration.as_secs(), 60);
assert_eq!(segmenter.overlap_duration.as_millis(), 500);
}
#[test]
fn test_video_info_creation() {
let info = VideoInfo::new(1920, 1080, 600.0, 30.0);
assert_eq!(info.width, 1920);
assert_eq!(info.height, 1080);
assert_eq!(info.duration, 600.0);
assert_eq!(info.total_frames, 18000);
}
#[test]
fn test_segment_reassembler() {
let reassembler = SegmentReassembler::new()
.with_normalization(true)
.with_target_bitrate(5_000_000);
assert!(reassembler.normalize_bitrate);
assert_eq!(reassembler.target_bitrate, Some(5_000_000));
}
#[test]
fn test_gop_detector() {
let detector = GOPDetector::new().with_gop_range(10, 300);
assert_eq!(detector.min_gop_size, 10);
assert_eq!(detector.max_gop_size, 300);
}
#[test]
fn test_gop_info() {
let gop = GOPInfo {
start_frame: 0,
end_frame: 30,
keyframe_positions: vec![0, 15, 30],
num_p_frames: 27,
num_b_frames: 0,
};
assert_eq!(gop.size(), 30);
assert!(gop.is_keyframe(0));
assert!(gop.is_keyframe(15));
assert!(!gop.is_keyframe(10));
}
#[test]
fn test_concatenation_strategies() {
assert_eq!(ConcatenationStrategy::Concat, ConcatenationStrategy::Concat);
assert_ne!(ConcatenationStrategy::Concat, ConcatenationStrategy::Blend);
}
}