rskit_media/chunking/strategy/
fixed_duration.rs1use std::time::Duration;
4
5use rskit_errors::AppResult;
6
7use crate::chunking::types::{ChunkBoundary, ChunkId, ChunkPlan, ChunkedOperation, ReassemblyPlan};
8use crate::probe::MediaMetadata;
9use crate::time::{TimeRange, Timestamp};
10
11use super::boundary::snap_to_boundary;
12use super::chunk_strategy::ChunkStrategy;
13
14pub struct FixedDurationStrategy {
19 pub chunk_duration: Duration,
21 pub snap_tolerance: Duration,
23 pub reassembly: ReassemblyPlan,
25 pub timeout_multiplier: f64,
27}
28
29impl Default for FixedDurationStrategy {
30 fn default() -> Self {
31 Self {
32 chunk_duration: Duration::from_secs(600), snap_tolerance: Duration::from_secs(5),
34 reassembly: ReassemblyPlan::Concat,
35 timeout_multiplier: 3.0,
36 }
37 }
38}
39
40impl FixedDurationStrategy {
41 #[must_use]
43 pub fn with_chunk_duration(mut self, duration: Duration) -> Self {
44 self.chunk_duration = duration;
45 self
46 }
47
48 #[must_use]
50 pub fn with_snap_tolerance(mut self, tolerance: Duration) -> Self {
51 self.snap_tolerance = tolerance;
52 self
53 }
54
55 #[must_use]
57 pub fn with_reassembly(mut self, reassembly: ReassemblyPlan) -> Self {
58 self.reassembly = reassembly;
59 self
60 }
61
62 #[must_use]
64 pub fn with_timeout_multiplier(mut self, multiplier: f64) -> Self {
65 self.timeout_multiplier = multiplier;
66 self
67 }
68}
69
70impl ChunkStrategy for FixedDurationStrategy {
71 fn name(&self) -> &str {
72 "fixed_duration"
73 }
74
75 fn plan(
76 &self,
77 metadata: &MediaMetadata,
78 boundaries: &[ChunkBoundary],
79 ) -> AppResult<ChunkedOperation> {
80 let total_duration = metadata.duration.unwrap_or_default();
81 if total_duration.is_zero() {
82 return Err(rskit_errors::AppError::new(
83 rskit_errors::ErrorCode::InvalidInput,
84 "cannot chunk media with zero duration",
85 ));
86 }
87 if self.chunk_duration.is_zero() {
88 return Err(rskit_errors::AppError::new(
89 rskit_errors::ErrorCode::InvalidInput,
90 "chunk_duration must be greater than zero",
91 ));
92 }
93
94 let threshold = self.chunk_duration.mul_f64(1.5);
96 if total_duration <= threshold {
97 let plan = ChunkPlan {
98 id: ChunkId::from_index(0),
99 index: 0,
100 range: TimeRange::new(Timestamp(0), Timestamp(total_duration.as_micros() as u64)),
101 start_is_keyframe: true,
102 suggested_timeout: Duration::from_secs_f64(
103 total_duration.as_secs_f64() * self.timeout_multiplier,
104 ),
105 };
106 return Ok(ChunkedOperation {
107 chunks: vec![plan],
108 reassembly: self.reassembly.clone(),
109 total_duration,
110 strategy_name: self.name().to_string(),
111 });
112 }
113
114 let chunk_us = self.chunk_duration.as_micros() as u64;
115 let total_us = total_duration.as_micros() as u64;
116 let snap_us = self.snap_tolerance.as_micros() as u64;
117
118 let mut chunks = Vec::new();
119 let mut current_start = Timestamp(0);
120 let mut index = 0;
121
122 while current_start.as_micros() < total_us {
123 let ideal_end_us = current_start
124 .as_micros()
125 .saturating_add(chunk_us)
126 .min(total_us);
127 let ideal_end = Timestamp(ideal_end_us);
128
129 let remaining = total_us.saturating_sub(ideal_end_us);
131 let end = if remaining < chunk_us / 2 {
132 Timestamp(total_us)
133 } else {
134 snap_to_boundary(ideal_end, boundaries, snap_us)
135 };
136
137 let is_keyframe = if index == 0 {
138 true
139 } else {
140 boundaries
141 .iter()
142 .any(|b| b.is_keyframe && b.timestamp == current_start)
143 };
144
145 let range = TimeRange::new(current_start, end);
146 let chunk_dur = range.duration();
147
148 chunks.push(ChunkPlan {
149 id: ChunkId::from_index(index),
150 index,
151 range,
152 start_is_keyframe: is_keyframe,
153 suggested_timeout: Duration::from_secs_f64(
154 chunk_dur.as_secs_f64() * self.timeout_multiplier,
155 ),
156 });
157
158 current_start = end;
159 index += 1;
160 }
161
162 Ok(ChunkedOperation {
163 chunks,
164 reassembly: self.reassembly.clone(),
165 total_duration,
166 strategy_name: self.name().to_string(),
167 })
168 }
169
170 fn min_duration(&self) -> Option<Duration> {
171 Some(self.chunk_duration.mul_f64(1.5))
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::super::test_support::{make_keyframe_boundaries, make_metadata};
178 use super::*;
179
180 #[test]
181 fn fixed_duration_single_chunk_for_short_media() {
182 let strategy = FixedDurationStrategy::default(); let metadata = make_metadata(300.0); let result = strategy.plan(&metadata, &[]).unwrap();
185 assert_eq!(result.chunk_count(), 1);
186 assert!(result.is_single_chunk());
187 }
188
189 #[test]
190 fn fixed_duration_multiple_chunks_for_long_media() {
191 let strategy = FixedDurationStrategy::default(); let metadata = make_metadata(3600.0); let boundaries = make_keyframe_boundaries(3600.0, 2.0);
194 let result = strategy.plan(&metadata, &boundaries).unwrap();
195 assert!(result.chunk_count() >= 5);
196 assert!(result.chunk_count() <= 7);
197
198 let first = &result.chunks[0];
200 let last = result.chunks.last().unwrap();
201 assert_eq!(first.range.start.as_micros(), 0);
202 assert_eq!(last.range.end.as_millis(), 3_600_000);
203 }
204
205 #[test]
206 fn fixed_duration_rejects_zero_duration() {
207 let strategy = FixedDurationStrategy::default();
208 let metadata = make_metadata(0.0);
209 assert!(strategy.plan(&metadata, &[]).is_err());
210 }
211
212 #[test]
213 fn fixed_duration_rejects_zero_chunk_duration() {
214 let strategy = FixedDurationStrategy::default().with_chunk_duration(Duration::ZERO);
215 let err = strategy.plan(&make_metadata(300.0), &[]).unwrap_err();
216 assert_eq!(err.code(), rskit_errors::ErrorCode::InvalidInput);
217 }
218
219 #[test]
220 fn fixed_duration_builders_and_min_duration_are_reported() {
221 let strategy = FixedDurationStrategy::default()
222 .with_chunk_duration(Duration::from_secs(120))
223 .with_snap_tolerance(Duration::from_secs(3))
224 .with_reassembly(ReassemblyPlan::MergeText {
225 separator: "\n".to_string(),
226 })
227 .with_timeout_multiplier(2.0);
228
229 assert_eq!(strategy.min_duration(), Some(Duration::from_secs(180)));
230 assert_eq!(strategy.snap_tolerance, Duration::from_secs(3));
231 assert_eq!(strategy.timeout_multiplier, 2.0);
232 }
233}