1#![allow(dead_code)]
2use std::collections::HashMap;
14use std::fmt;
15use std::time::Instant;
16
17use uuid::Uuid;
18
19use crate::{DistributedError, Result};
20
21#[derive(Debug, Clone)]
27pub struct SegmentInfo {
28 pub id: Uuid,
30 pub job_id: Uuid,
32 pub index: u32,
34 pub total_segments: u32,
36 pub start_time_us: i64,
38 pub end_time_us: i64,
40 pub byte_size: u64,
42 pub worker_id: Option<String>,
44 pub storage_path: String,
46 pub validated: bool,
48}
49
50impl SegmentInfo {
51 pub fn duration_us(&self) -> i64 {
53 self.end_time_us - self.start_time_us
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum MergeStrategy {
64 ByteConcat,
66 ContainerRemux,
68 FragmentedMp4,
70}
71
72impl Default for MergeStrategy {
73 fn default() -> Self {
74 Self::ContainerRemux
75 }
76}
77
78impl fmt::Display for MergeStrategy {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Self::ByteConcat => write!(f, "ByteConcat"),
82 Self::ContainerRemux => write!(f, "ContainerRemux"),
83 Self::FragmentedMp4 => write!(f, "FragmentedMp4"),
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum GapPolicy {
95 Strict,
97 AllowGaps { tolerance_us: i64 },
99 FillGaps,
101 Ignore,
103}
104
105impl Default for GapPolicy {
106 fn default() -> Self {
107 Self::AllowGaps {
108 tolerance_us: 1_000, }
110 }
111}
112
113#[derive(Debug, Clone)]
119pub struct MergeConfig {
120 pub strategy: MergeStrategy,
122 pub gap_policy: GapPolicy,
124 pub require_validation: bool,
126 pub output_path: String,
128}
129
130impl Default for MergeConfig {
131 fn default() -> Self {
132 Self {
133 strategy: MergeStrategy::default(),
134 gap_policy: GapPolicy::default(),
135 require_validation: true,
136 output_path: String::new(),
137 }
138 }
139}
140
141#[derive(Debug, Clone)]
147pub struct SegmentManifest {
148 pub job_id: Uuid,
150 pub expected_count: u32,
152 segments: HashMap<u32, SegmentInfo>,
154}
155
156impl SegmentManifest {
157 pub fn new(job_id: Uuid, expected_count: u32) -> Self {
159 Self {
160 job_id,
161 expected_count,
162 segments: HashMap::new(),
163 }
164 }
165
166 pub fn register_segment(&mut self, segment: SegmentInfo) -> Result<()> {
168 if segment.job_id != self.job_id {
169 return Err(DistributedError::Job(format!(
170 "Segment job_id {} does not match manifest job_id {}",
171 segment.job_id, self.job_id
172 )));
173 }
174 if segment.index >= self.expected_count {
175 return Err(DistributedError::Segmentation(format!(
176 "Segment index {} exceeds expected count {}",
177 segment.index, self.expected_count
178 )));
179 }
180 if self.segments.contains_key(&segment.index) {
181 return Err(DistributedError::Segmentation(format!(
182 "Segment index {} already registered",
183 segment.index
184 )));
185 }
186 self.segments.insert(segment.index, segment);
187 Ok(())
188 }
189
190 pub fn is_complete(&self) -> bool {
192 self.segments.len() as u32 == self.expected_count
193 }
194
195 pub fn registered_count(&self) -> u32 {
197 self.segments.len() as u32
198 }
199
200 pub fn missing_indices(&self) -> Vec<u32> {
202 (0..self.expected_count)
203 .filter(|i| !self.segments.contains_key(i))
204 .collect()
205 }
206
207 pub fn ordered_segments(&self) -> Vec<&SegmentInfo> {
209 let mut indices: Vec<u32> = self.segments.keys().copied().collect();
210 indices.sort();
211 indices
212 .iter()
213 .filter_map(|i| self.segments.get(i))
214 .collect()
215 }
216
217 pub fn total_byte_size(&self) -> u64 {
219 self.segments.values().map(|s| s.byte_size).sum()
220 }
221}
222
223#[derive(Debug, Clone)]
229pub struct GapInfo {
230 pub before_index: u32,
232 pub after_index: u32,
234 pub gap_us: i64,
236}
237
238impl fmt::Display for GapInfo {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 if self.gap_us >= 0 {
241 write!(
242 f,
243 "Gap of {}us between segments {} and {}",
244 self.gap_us, self.before_index, self.after_index
245 )
246 } else {
247 write!(
248 f,
249 "Overlap of {}us between segments {} and {}",
250 -self.gap_us, self.before_index, self.after_index
251 )
252 }
253 }
254}
255
256#[derive(Debug, Clone)]
262pub struct MergeResult {
263 pub job_id: Uuid,
265 pub output_path: String,
267 pub total_bytes: u64,
269 pub segment_count: u32,
271 pub total_duration_us: i64,
273 pub gaps: Vec<GapInfo>,
275 pub merge_duration: std::time::Duration,
277 pub strategy: MergeStrategy,
279}
280
281pub struct SegmentMerger {
287 config: MergeConfig,
288}
289
290impl SegmentMerger {
291 pub fn new(config: MergeConfig) -> Self {
293 Self { config }
294 }
295
296 pub fn validate(&self, manifest: &SegmentManifest) -> Result<Vec<GapInfo>> {
298 if !manifest.is_complete() {
300 let missing = manifest.missing_indices();
301 return Err(DistributedError::Segmentation(format!(
302 "Manifest incomplete; missing segments: {:?}",
303 missing
304 )));
305 }
306
307 if self.config.require_validation {
309 for seg in manifest.segments.values() {
310 if !seg.validated {
311 return Err(DistributedError::Segmentation(format!(
312 "Segment {} (index {}) has not been validated",
313 seg.id, seg.index
314 )));
315 }
316 }
317 }
318
319 let ordered = manifest.ordered_segments();
321 let mut gaps = Vec::new();
322
323 for pair in ordered.windows(2) {
324 let prev = pair[0];
325 let next = pair[1];
326 let gap_us = next.start_time_us - prev.end_time_us;
327 if gap_us != 0 {
328 gaps.push(GapInfo {
329 before_index: prev.index,
330 after_index: next.index,
331 gap_us,
332 });
333 }
334 }
335
336 match self.config.gap_policy {
338 GapPolicy::Strict => {
339 if let Some(g) = gaps.first() {
340 return Err(DistributedError::Segmentation(format!(
341 "Strict gap policy violated: {g}"
342 )));
343 }
344 }
345 GapPolicy::AllowGaps { tolerance_us } => {
346 for g in &gaps {
347 if g.gap_us.unsigned_abs() > tolerance_us as u64 {
348 return Err(DistributedError::Segmentation(format!(
349 "Gap exceeds tolerance ({tolerance_us}us): {g}"
350 )));
351 }
352 }
353 }
354 GapPolicy::FillGaps | GapPolicy::Ignore => { }
355 }
356
357 Ok(gaps)
358 }
359
360 pub fn merge(&self, manifest: &SegmentManifest) -> Result<MergeResult> {
366 let start = Instant::now();
367 let gaps = self.validate(manifest)?;
368
369 let ordered = manifest.ordered_segments();
370 let total_bytes: u64 = ordered.iter().map(|s| s.byte_size).sum();
371 let total_duration_us = ordered.last().map(|s| s.end_time_us).unwrap_or(0)
372 - ordered.first().map(|s| s.start_time_us).unwrap_or(0);
373
374 Ok(MergeResult {
375 job_id: manifest.job_id,
376 output_path: self.config.output_path.clone(),
377 total_bytes,
378 segment_count: ordered.len() as u32,
379 total_duration_us,
380 gaps,
381 merge_duration: start.elapsed(),
382 strategy: self.config.strategy,
383 })
384 }
385
386 pub fn config(&self) -> &MergeConfig {
388 &self.config
389 }
390}
391
392fn make_test_segment(
397 job_id: Uuid,
398 index: u32,
399 total: u32,
400 start_us: i64,
401 end_us: i64,
402) -> SegmentInfo {
403 SegmentInfo {
404 id: Uuid::new_v4(),
405 job_id,
406 index,
407 total_segments: total,
408 start_time_us: start_us,
409 end_time_us: end_us,
410 byte_size: 1024 * (index as u64 + 1),
411 worker_id: Some(format!("worker-{index}")),
412 storage_path: std::env::temp_dir()
413 .join(format!("oximedia-distributed-segmerge-seg_{index}.ts"))
414 .to_string_lossy()
415 .into_owned(),
416 validated: true,
417 }
418}
419
420#[cfg(test)]
425mod tests {
426 use super::*;
427
428 fn tmp_str(name: &str) -> String {
429 std::env::temp_dir()
430 .join(format!("oximedia-distributed-segmerge-{name}"))
431 .to_string_lossy()
432 .into_owned()
433 }
434
435 fn setup_manifest(count: u32, gap_us: i64) -> SegmentManifest {
436 let job_id = Uuid::new_v4();
437 let mut manifest = SegmentManifest::new(job_id, count);
438 let segment_len_us: i64 = 5_000_000; for i in 0..count {
441 let start = i as i64 * segment_len_us + i as i64 * gap_us;
442 let end = start + segment_len_us;
443 let seg = make_test_segment(job_id, i, count, start, end);
444 manifest.register_segment(seg).expect("register ok");
445 }
446 manifest
447 }
448
449 #[test]
450 fn test_manifest_creation_and_completeness() {
451 let job_id = Uuid::new_v4();
452 let manifest = SegmentManifest::new(job_id, 4);
453 assert!(!manifest.is_complete());
454 assert_eq!(manifest.missing_indices(), vec![0, 1, 2, 3]);
455 }
456
457 #[test]
458 fn test_manifest_register_and_complete() {
459 let manifest = setup_manifest(3, 0);
460 assert!(manifest.is_complete());
461 assert_eq!(manifest.registered_count(), 3);
462 assert!(manifest.missing_indices().is_empty());
463 }
464
465 #[test]
466 fn test_manifest_rejects_wrong_job_id() {
467 let job_id = Uuid::new_v4();
468 let mut manifest = SegmentManifest::new(job_id, 2);
469 let mut seg = make_test_segment(job_id, 0, 2, 0, 5_000_000);
470 seg.job_id = Uuid::new_v4(); let result = manifest.register_segment(seg);
472 assert!(result.is_err());
473 }
474
475 #[test]
476 fn test_manifest_rejects_duplicate_index() {
477 let job_id = Uuid::new_v4();
478 let mut manifest = SegmentManifest::new(job_id, 2);
479 let seg1 = make_test_segment(job_id, 0, 2, 0, 5_000_000);
480 let seg2 = make_test_segment(job_id, 0, 2, 0, 5_000_000);
481 manifest.register_segment(seg1).expect("register ok");
482 let result = manifest.register_segment(seg2);
483 assert!(result.is_err());
484 }
485
486 #[test]
487 fn test_manifest_rejects_out_of_range_index() {
488 let job_id = Uuid::new_v4();
489 let mut manifest = SegmentManifest::new(job_id, 2);
490 let seg = make_test_segment(job_id, 5, 2, 0, 5_000_000);
491 let result = manifest.register_segment(seg);
492 assert!(result.is_err());
493 }
494
495 #[test]
496 fn test_merge_no_gaps() {
497 let manifest = setup_manifest(4, 0);
498 let merger = SegmentMerger::new(MergeConfig {
499 output_path: tmp_str("output.mp4"),
500 ..MergeConfig::default()
501 });
502 let result = merger.merge(&manifest).expect("merge ok");
503 assert_eq!(result.segment_count, 4);
504 assert!(result.gaps.is_empty());
505 assert!(result.total_bytes > 0);
506 assert!(result.total_duration_us > 0);
507 }
508
509 #[test]
510 fn test_merge_small_gap_within_tolerance() {
511 let manifest = setup_manifest(3, 500);
513 let merger = SegmentMerger::new(MergeConfig {
514 output_path: tmp_str("output.mp4"),
515 gap_policy: GapPolicy::AllowGaps { tolerance_us: 1000 },
516 ..MergeConfig::default()
517 });
518 let result = merger.merge(&manifest).expect("merge ok");
519 assert_eq!(result.gaps.len(), 2); }
521
522 #[test]
523 fn test_merge_strict_rejects_gap() {
524 let manifest = setup_manifest(2, 100);
525 let merger = SegmentMerger::new(MergeConfig {
526 gap_policy: GapPolicy::Strict,
527 output_path: tmp_str("output.mp4"),
528 ..MergeConfig::default()
529 });
530 let result = merger.merge(&manifest);
531 assert!(result.is_err());
532 }
533
534 #[test]
535 fn test_merge_incomplete_manifest_fails() {
536 let job_id = Uuid::new_v4();
537 let mut manifest = SegmentManifest::new(job_id, 3);
538 let seg = make_test_segment(job_id, 0, 3, 0, 5_000_000);
539 manifest.register_segment(seg).expect("register ok");
540
541 let merger = SegmentMerger::new(MergeConfig::default());
542 let result = merger.merge(&manifest);
543 assert!(result.is_err());
544 }
545
546 #[test]
547 fn test_merge_rejects_unvalidated_segment() {
548 let job_id = Uuid::new_v4();
549 let mut manifest = SegmentManifest::new(job_id, 1);
550 let mut seg = make_test_segment(job_id, 0, 1, 0, 5_000_000);
551 seg.validated = false;
552 manifest.register_segment(seg).expect("register ok");
553
554 let merger = SegmentMerger::new(MergeConfig {
555 require_validation: true,
556 output_path: tmp_str("out.mp4"),
557 ..MergeConfig::default()
558 });
559 let result = merger.merge(&manifest);
560 assert!(result.is_err());
561 }
562
563 #[test]
564 fn test_merge_ignores_gaps_with_ignore_policy() {
565 let manifest = setup_manifest(2, 999_999);
566 let merger = SegmentMerger::new(MergeConfig {
567 gap_policy: GapPolicy::Ignore,
568 output_path: tmp_str("output.mp4"),
569 ..MergeConfig::default()
570 });
571 let result = merger.merge(&manifest).expect("merge ok");
572 assert_eq!(result.gaps.len(), 1);
573 }
574
575 #[test]
576 fn test_total_byte_size() {
577 let manifest = setup_manifest(3, 0);
578 assert_eq!(manifest.total_byte_size(), 6144);
580 }
581
582 #[test]
583 fn test_ordered_segments_returns_sorted() {
584 let job_id = Uuid::new_v4();
585 let mut manifest = SegmentManifest::new(job_id, 3);
586 for i in (0..3).rev() {
588 let seg = make_test_segment(job_id, i, 3, i as i64 * 1000, (i as i64 + 1) * 1000);
589 manifest.register_segment(seg).expect("register ok");
590 }
591 let ordered = manifest.ordered_segments();
592 for (i, seg) in ordered.iter().enumerate() {
593 assert_eq!(seg.index, i as u32);
594 }
595 }
596}