use std::time::Duration;
use crate::error::StreamError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HlsSegmentFormat {
#[default]
Ts,
Fmp4,
}
pub struct HlsOutput {
output_dir: String,
input_path: Option<String>,
segment_duration: Duration,
keyframe_interval: u32,
target_bitrate: Option<u64>,
target_video_size: Option<(u32, u32)>,
segment_format: HlsSegmentFormat,
}
impl HlsOutput {
#[must_use]
pub fn new(output_dir: &str) -> Self {
Self {
output_dir: output_dir.to_owned(),
input_path: None,
segment_duration: Duration::from_secs(6),
keyframe_interval: 48,
target_bitrate: None,
target_video_size: None,
segment_format: HlsSegmentFormat::Ts,
}
}
#[must_use]
pub fn input(mut self, path: &str) -> Self {
self.input_path = Some(path.to_owned());
self
}
#[must_use]
pub fn segment_duration(mut self, d: Duration) -> Self {
self.segment_duration = d;
self
}
#[must_use]
pub fn bitrate(mut self, bps: u64) -> Self {
self.target_bitrate = Some(bps);
self
}
#[must_use]
pub fn video_size(mut self, width: u32, height: u32) -> Self {
self.target_video_size = Some((width, height));
self
}
#[must_use]
pub fn keyframe_interval(mut self, frames: u32) -> Self {
self.keyframe_interval = frames;
self
}
#[must_use]
pub fn segment_format(mut self, fmt: HlsSegmentFormat) -> Self {
self.segment_format = fmt;
self
}
pub fn build(self) -> Result<Self, StreamError> {
if self.output_dir.is_empty() {
return Err(StreamError::InvalidConfig {
reason: "output_dir must not be empty".into(),
});
}
if self.input_path.is_none() {
return Err(StreamError::InvalidConfig {
reason: "input path is required".into(),
});
}
log::info!(
"hls output configured output_dir={} segment_duration={:.1}s keyframe_interval={} \
bitrate={:?} video_size={:?}",
self.output_dir,
self.segment_duration.as_secs_f64(),
self.keyframe_interval,
self.target_bitrate,
self.target_video_size,
);
Ok(self)
}
pub fn write(self) -> Result<(), StreamError> {
let input_path = self.input_path.ok_or_else(|| StreamError::InvalidConfig {
reason: "input path missing after build (internal error)".into(),
})?;
let seg_secs = self.segment_duration.as_secs_f64();
log::info!(
"hls write starting input={input_path} output_dir={} \
segment_duration={seg_secs:.1}s keyframe_interval={}",
self.output_dir,
self.keyframe_interval
);
let target_bitrate = self.target_bitrate.map_or(0i64, |b| b.cast_signed());
let (target_width, target_height) = self
.target_video_size
.map_or((0i32, 0i32), |(w, h)| (w.cast_signed(), h.cast_signed()));
crate::hls_inner::write_hls(
&input_path,
&self.output_dir,
seg_secs,
self.keyframe_interval,
target_bitrate,
target_width,
target_height,
self.segment_format,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_should_store_output_dir() {
let h = HlsOutput::new("/tmp/hls");
assert_eq!(h.output_dir, "/tmp/hls");
}
#[test]
fn input_should_store_input_path() {
let h = HlsOutput::new("/tmp/hls").input("/src/video.mp4");
assert_eq!(h.input_path.as_deref(), Some("/src/video.mp4"));
}
#[test]
fn segment_duration_should_store_duration() {
let d = Duration::from_secs(10);
let h = HlsOutput::new("/tmp/hls").segment_duration(d);
assert_eq!(h.segment_duration, d);
}
#[test]
fn keyframe_interval_should_store_interval() {
let h = HlsOutput::new("/tmp/hls").keyframe_interval(24);
assert_eq!(h.keyframe_interval, 24);
}
#[test]
fn build_without_input_should_return_invalid_config() {
let result = HlsOutput::new("/tmp/hls").build();
assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
}
#[test]
fn build_with_empty_output_dir_should_return_invalid_config() {
let result = HlsOutput::new("").input("/src/video.mp4").build();
assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
}
#[test]
fn build_with_valid_config_should_succeed() {
let result = HlsOutput::new("/tmp/hls").input("/src/video.mp4").build();
assert!(result.is_ok());
}
#[test]
fn segment_format_default_should_be_ts() {
let h = HlsOutput::new("/tmp/hls");
assert_eq!(h.segment_format, HlsSegmentFormat::Ts);
}
#[test]
fn segment_format_setter_should_store_fmp4() {
let h = HlsOutput::new("/tmp/hls").segment_format(HlsSegmentFormat::Fmp4);
assert_eq!(h.segment_format, HlsSegmentFormat::Fmp4);
}
#[test]
fn write_without_build_should_return_invalid_config() {
let result = HlsOutput::new("/tmp/hls").write();
assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
}
#[test]
fn bitrate_should_store_bitrate() {
let h = HlsOutput::new("/tmp/hls").bitrate(3_000_000);
assert_eq!(h.target_bitrate, Some(3_000_000));
}
#[test]
fn video_size_should_store_dimensions() {
let h = HlsOutput::new("/tmp/hls").video_size(1280, 720);
assert_eq!(h.target_video_size, Some((1280, 720)));
}
#[test]
fn bitrate_default_should_be_none() {
let h = HlsOutput::new("/tmp/hls");
assert_eq!(h.target_bitrate, None);
}
#[test]
fn video_size_default_should_be_none() {
let h = HlsOutput::new("/tmp/hls");
assert_eq!(h.target_video_size, None);
}
#[test]
fn build_with_bitrate_and_video_size_should_succeed() {
let result = HlsOutput::new("/tmp/hls")
.input("/src/video.mp4")
.bitrate(4_000_000)
.video_size(1920, 1080)
.build();
assert!(result.is_ok());
let h = result.unwrap();
assert_eq!(h.target_bitrate, Some(4_000_000));
assert_eq!(h.target_video_size, Some((1920, 1080)));
}
}