use std::time::Duration;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum OperationKind {
StreamCopy,
AudioProcess,
Transcode,
Filter,
SubtitleBurn,
ThumbnailExtract,
SceneDetect,
MlInference,
Probe,
}
impl OperationKind {
pub fn default_multiplier(&self) -> f64 {
match self {
Self::Probe => 0.0, Self::StreamCopy => 0.2, Self::ThumbnailExtract => 0.1, Self::AudioProcess => 0.5, Self::SceneDetect => 1.5, Self::Filter => 2.0, Self::Transcode => 2.5, Self::SubtitleBurn => 3.0, Self::MlInference => 5.0, }
}
pub fn default_base_timeout(&self) -> Duration {
match self {
Self::Probe => Duration::from_secs(30),
Self::ThumbnailExtract => Duration::from_secs(30),
Self::StreamCopy => Duration::from_secs(60),
Self::AudioProcess => Duration::from_secs(60),
Self::SceneDetect => Duration::from_secs(120),
Self::Filter => Duration::from_secs(120),
Self::Transcode => Duration::from_secs(120),
Self::SubtitleBurn => Duration::from_secs(120),
Self::MlInference => Duration::from_secs(300),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeoutCalculator {
pub base_timeout: Duration,
pub max_timeout: Duration,
#[serde(default)]
pub multiplier_overrides: Vec<(OperationKind, f64)>,
}
impl Default for TimeoutCalculator {
fn default() -> Self {
Self {
base_timeout: Duration::from_secs(60),
max_timeout: Duration::from_secs(4 * 3600), multiplier_overrides: Vec::new(),
}
}
}
impl TimeoutCalculator {
#[must_use]
pub fn with_base_timeout(mut self, base: Duration) -> Self {
self.base_timeout = base;
self
}
#[must_use]
pub fn with_max_timeout(mut self, max: Duration) -> Self {
self.max_timeout = max;
self
}
#[must_use]
pub fn with_multiplier(mut self, kind: OperationKind, multiplier: f64) -> Self {
self.multiplier_overrides.retain(|(k, _)| *k != kind);
self.multiplier_overrides.push((kind, multiplier));
self
}
pub fn calculate(&self, media_duration: Duration, operation: OperationKind) -> Duration {
let multiplier = self
.multiplier_overrides
.iter()
.find(|(k, _)| *k == operation)
.map(|(_, m)| *m)
.unwrap_or_else(|| operation.default_multiplier());
let base = self.base_timeout.max(operation.default_base_timeout());
let variable = Duration::from_secs_f64(media_duration.as_secs_f64() * multiplier);
let total = base + variable;
total.min(self.max_timeout)
}
pub fn calculate_for_chunk(
&self,
chunk_duration: Duration,
operation: OperationKind,
) -> Duration {
self.calculate(chunk_duration, operation)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_probe_timeout_is_small() {
let calc = TimeoutCalculator::default();
let t = calc.calculate(Duration::from_secs(6000), OperationKind::Probe);
assert_eq!(t.as_secs(), 60);
}
#[test]
fn stream_copy_is_fast() {
let calc = TimeoutCalculator::default();
let t = calc.calculate(Duration::from_secs(3600), OperationKind::StreamCopy);
assert_eq!(t.as_secs(), 780);
}
#[test]
fn transcode_scales_with_duration() {
let calc = TimeoutCalculator::default();
let t = calc.calculate(Duration::from_secs(6000), OperationKind::Transcode);
assert_eq!(t, Duration::from_secs(4 * 3600));
}
#[test]
fn ml_inference_is_generous() {
let calc = TimeoutCalculator::default();
let t = calc.calculate(Duration::from_secs(600), OperationKind::MlInference);
assert_eq!(t.as_secs(), 3300);
}
#[test]
fn custom_multiplier_override() {
let calc = TimeoutCalculator::default().with_multiplier(OperationKind::Transcode, 1.0);
let t = calc.calculate(Duration::from_secs(3600), OperationKind::Transcode);
assert_eq!(t.as_secs(), 3720);
}
#[test]
fn operation_kind_defaults_cover_all_variants() {
let cases = [
(OperationKind::ThumbnailExtract, 0.1, 30),
(OperationKind::AudioProcess, 0.5, 60),
(OperationKind::SceneDetect, 1.5, 120),
(OperationKind::Filter, 2.0, 120),
(OperationKind::SubtitleBurn, 3.0, 120),
];
for (kind, multiplier, base_secs) in cases {
assert_eq!(kind.default_multiplier(), multiplier);
assert_eq!(kind.default_base_timeout(), Duration::from_secs(base_secs));
}
}
#[test]
fn max_timeout_is_respected() {
let calc = TimeoutCalculator::default().with_max_timeout(Duration::from_secs(600));
let t = calc.calculate(Duration::from_secs(3600), OperationKind::Transcode);
assert_eq!(t.as_secs(), 600);
}
#[test]
fn chunk_timeout_uses_chunk_duration() {
let calc = TimeoutCalculator::default();
let chunk_t = calc.calculate_for_chunk(Duration::from_secs(600), OperationKind::Transcode);
let full_t = calc.calculate(Duration::from_secs(6000), OperationKind::Transcode);
assert!(chunk_t < full_t);
}
}