1use std::time::Duration;
8
9use crate::error::StreamError;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum HlsSegmentFormat {
17 #[default]
19 Ts,
20 Fmp4,
24}
25
26pub struct HlsOutput {
46 output_dir: String,
47 input_path: Option<String>,
48 segment_duration: Duration,
49 keyframe_interval: u32,
50 target_bitrate: Option<u64>,
51 target_video_size: Option<(u32, u32)>,
52 segment_format: HlsSegmentFormat,
53}
54
55impl HlsOutput {
56 #[must_use]
64 pub fn new(output_dir: &str) -> Self {
65 Self {
66 output_dir: output_dir.to_owned(),
67 input_path: None,
68 segment_duration: Duration::from_secs(6),
69 keyframe_interval: 48,
70 target_bitrate: None,
71 target_video_size: None,
72 segment_format: HlsSegmentFormat::Ts,
73 }
74 }
75
76 #[must_use]
81 pub fn input(mut self, path: &str) -> Self {
82 self.input_path = Some(path.to_owned());
83 self
84 }
85
86 #[must_use]
92 pub fn segment_duration(mut self, d: Duration) -> Self {
93 self.segment_duration = d;
94 self
95 }
96
97 #[must_use]
101 pub fn bitrate(mut self, bps: u64) -> Self {
102 self.target_bitrate = Some(bps);
103 self
104 }
105
106 #[must_use]
110 pub fn video_size(mut self, width: u32, height: u32) -> Self {
111 self.target_video_size = Some((width, height));
112 self
113 }
114
115 #[must_use]
121 pub fn keyframe_interval(mut self, frames: u32) -> Self {
122 self.keyframe_interval = frames;
123 self
124 }
125
126 #[must_use]
132 pub fn segment_format(mut self, fmt: HlsSegmentFormat) -> Self {
133 self.segment_format = fmt;
134 self
135 }
136
137 pub fn build(self) -> Result<Self, StreamError> {
157 if self.output_dir.is_empty() {
158 return Err(StreamError::InvalidConfig {
159 reason: "output_dir must not be empty".into(),
160 });
161 }
162 if self.input_path.is_none() {
163 return Err(StreamError::InvalidConfig {
164 reason: "input path is required".into(),
165 });
166 }
167 log::info!(
168 "hls output configured output_dir={} segment_duration={:.1}s keyframe_interval={} \
169 bitrate={:?} video_size={:?}",
170 self.output_dir,
171 self.segment_duration.as_secs_f64(),
172 self.keyframe_interval,
173 self.target_bitrate,
174 self.target_video_size,
175 );
176 Ok(self)
177 }
178
179 pub fn write(self) -> Result<(), StreamError> {
191 let input_path = self.input_path.ok_or_else(|| StreamError::InvalidConfig {
192 reason: "input path missing after build (internal error)".into(),
193 })?;
194 let seg_secs = self.segment_duration.as_secs_f64();
195 log::info!(
196 "hls write starting input={input_path} output_dir={} \
197 segment_duration={seg_secs:.1}s keyframe_interval={}",
198 self.output_dir,
199 self.keyframe_interval
200 );
201 let target_bitrate = self.target_bitrate.map_or(0i64, |b| b.cast_signed());
202 let (target_width, target_height) = self
203 .target_video_size
204 .map_or((0i32, 0i32), |(w, h)| (w.cast_signed(), h.cast_signed()));
205 crate::hls_inner::write_hls(
206 &input_path,
207 &self.output_dir,
208 seg_secs,
209 self.keyframe_interval,
210 target_bitrate,
211 target_width,
212 target_height,
213 self.segment_format,
214 )
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn new_should_store_output_dir() {
224 let h = HlsOutput::new("/tmp/hls");
225 assert_eq!(h.output_dir, "/tmp/hls");
226 }
227
228 #[test]
229 fn input_should_store_input_path() {
230 let h = HlsOutput::new("/tmp/hls").input("/src/video.mp4");
231 assert_eq!(h.input_path.as_deref(), Some("/src/video.mp4"));
232 }
233
234 #[test]
235 fn segment_duration_should_store_duration() {
236 let d = Duration::from_secs(10);
237 let h = HlsOutput::new("/tmp/hls").segment_duration(d);
238 assert_eq!(h.segment_duration, d);
239 }
240
241 #[test]
242 fn keyframe_interval_should_store_interval() {
243 let h = HlsOutput::new("/tmp/hls").keyframe_interval(24);
244 assert_eq!(h.keyframe_interval, 24);
245 }
246
247 #[test]
248 fn build_without_input_should_return_invalid_config() {
249 let result = HlsOutput::new("/tmp/hls").build();
250 assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
251 }
252
253 #[test]
254 fn build_with_empty_output_dir_should_return_invalid_config() {
255 let result = HlsOutput::new("").input("/src/video.mp4").build();
256 assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
257 }
258
259 #[test]
260 fn build_with_valid_config_should_succeed() {
261 let result = HlsOutput::new("/tmp/hls").input("/src/video.mp4").build();
262 assert!(result.is_ok());
263 }
264
265 #[test]
266 fn segment_format_default_should_be_ts() {
267 let h = HlsOutput::new("/tmp/hls");
268 assert_eq!(h.segment_format, HlsSegmentFormat::Ts);
269 }
270
271 #[test]
272 fn segment_format_setter_should_store_fmp4() {
273 let h = HlsOutput::new("/tmp/hls").segment_format(HlsSegmentFormat::Fmp4);
274 assert_eq!(h.segment_format, HlsSegmentFormat::Fmp4);
275 }
276
277 #[test]
278 fn write_without_build_should_return_invalid_config() {
279 let result = HlsOutput::new("/tmp/hls").write();
281 assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
282 }
283
284 #[test]
285 fn bitrate_should_store_bitrate() {
286 let h = HlsOutput::new("/tmp/hls").bitrate(3_000_000);
287 assert_eq!(h.target_bitrate, Some(3_000_000));
288 }
289
290 #[test]
291 fn video_size_should_store_dimensions() {
292 let h = HlsOutput::new("/tmp/hls").video_size(1280, 720);
293 assert_eq!(h.target_video_size, Some((1280, 720)));
294 }
295
296 #[test]
297 fn bitrate_default_should_be_none() {
298 let h = HlsOutput::new("/tmp/hls");
299 assert_eq!(h.target_bitrate, None);
300 }
301
302 #[test]
303 fn video_size_default_should_be_none() {
304 let h = HlsOutput::new("/tmp/hls");
305 assert_eq!(h.target_video_size, None);
306 }
307
308 #[test]
309 fn build_with_bitrate_and_video_size_should_succeed() {
310 let result = HlsOutput::new("/tmp/hls")
311 .input("/src/video.mp4")
312 .bitrate(4_000_000)
313 .video_size(1920, 1080)
314 .build();
315 assert!(result.is_ok());
316 let h = result.unwrap();
317 assert_eq!(h.target_bitrate, Some(4_000_000));
318 assert_eq!(h.target_video_size, Some((1920, 1080)));
319 }
320}