1use std::time::Duration;
8
9use crate::error::StreamError;
10
11pub struct HlsOutput {
31 output_dir: String,
32 input_path: Option<String>,
33 segment_duration: Duration,
34 keyframe_interval: u32,
35 target_bitrate: Option<u64>,
36 target_video_size: Option<(u32, u32)>,
37}
38
39impl HlsOutput {
40 #[must_use]
48 pub fn new(output_dir: &str) -> Self {
49 Self {
50 output_dir: output_dir.to_owned(),
51 input_path: None,
52 segment_duration: Duration::from_secs(6),
53 keyframe_interval: 48,
54 target_bitrate: None,
55 target_video_size: None,
56 }
57 }
58
59 #[must_use]
64 pub fn input(mut self, path: &str) -> Self {
65 self.input_path = Some(path.to_owned());
66 self
67 }
68
69 #[must_use]
75 pub fn segment_duration(mut self, d: Duration) -> Self {
76 self.segment_duration = d;
77 self
78 }
79
80 #[must_use]
84 pub fn bitrate(mut self, bps: u64) -> Self {
85 self.target_bitrate = Some(bps);
86 self
87 }
88
89 #[must_use]
93 pub fn video_size(mut self, width: u32, height: u32) -> Self {
94 self.target_video_size = Some((width, height));
95 self
96 }
97
98 #[must_use]
104 pub fn keyframe_interval(mut self, frames: u32) -> Self {
105 self.keyframe_interval = frames;
106 self
107 }
108
109 pub fn build(self) -> Result<Self, StreamError> {
129 if self.output_dir.is_empty() {
130 return Err(StreamError::InvalidConfig {
131 reason: "output_dir must not be empty".into(),
132 });
133 }
134 if self.input_path.is_none() {
135 return Err(StreamError::InvalidConfig {
136 reason: "input path is required".into(),
137 });
138 }
139 log::info!(
140 "hls output configured output_dir={} segment_duration={:.1}s keyframe_interval={} \
141 bitrate={:?} video_size={:?}",
142 self.output_dir,
143 self.segment_duration.as_secs_f64(),
144 self.keyframe_interval,
145 self.target_bitrate,
146 self.target_video_size,
147 );
148 Ok(self)
149 }
150
151 pub fn write(self) -> Result<(), StreamError> {
163 let input_path = self.input_path.ok_or_else(|| StreamError::InvalidConfig {
164 reason: "input path missing after build (internal error)".into(),
165 })?;
166 let seg_secs = self.segment_duration.as_secs_f64();
167 log::info!(
168 "hls write starting input={input_path} output_dir={} \
169 segment_duration={seg_secs:.1}s keyframe_interval={}",
170 self.output_dir,
171 self.keyframe_interval
172 );
173 let target_bitrate = self.target_bitrate.map_or(0i64, |b| b.cast_signed());
174 let (target_width, target_height) = self
175 .target_video_size
176 .map_or((0i32, 0i32), |(w, h)| (w.cast_signed(), h.cast_signed()));
177 crate::hls_inner::write_hls(
178 &input_path,
179 &self.output_dir,
180 seg_secs,
181 self.keyframe_interval,
182 target_bitrate,
183 target_width,
184 target_height,
185 )
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn new_should_store_output_dir() {
195 let h = HlsOutput::new("/tmp/hls");
196 assert_eq!(h.output_dir, "/tmp/hls");
197 }
198
199 #[test]
200 fn input_should_store_input_path() {
201 let h = HlsOutput::new("/tmp/hls").input("/src/video.mp4");
202 assert_eq!(h.input_path.as_deref(), Some("/src/video.mp4"));
203 }
204
205 #[test]
206 fn segment_duration_should_store_duration() {
207 let d = Duration::from_secs(10);
208 let h = HlsOutput::new("/tmp/hls").segment_duration(d);
209 assert_eq!(h.segment_duration, d);
210 }
211
212 #[test]
213 fn keyframe_interval_should_store_interval() {
214 let h = HlsOutput::new("/tmp/hls").keyframe_interval(24);
215 assert_eq!(h.keyframe_interval, 24);
216 }
217
218 #[test]
219 fn build_without_input_should_return_invalid_config() {
220 let result = HlsOutput::new("/tmp/hls").build();
221 assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
222 }
223
224 #[test]
225 fn build_with_empty_output_dir_should_return_invalid_config() {
226 let result = HlsOutput::new("").input("/src/video.mp4").build();
227 assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
228 }
229
230 #[test]
231 fn build_with_valid_config_should_succeed() {
232 let result = HlsOutput::new("/tmp/hls").input("/src/video.mp4").build();
233 assert!(result.is_ok());
234 }
235
236 #[test]
237 fn write_without_build_should_return_invalid_config() {
238 let result = HlsOutput::new("/tmp/hls").write();
240 assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
241 }
242
243 #[test]
244 fn bitrate_should_store_bitrate() {
245 let h = HlsOutput::new("/tmp/hls").bitrate(3_000_000);
246 assert_eq!(h.target_bitrate, Some(3_000_000));
247 }
248
249 #[test]
250 fn video_size_should_store_dimensions() {
251 let h = HlsOutput::new("/tmp/hls").video_size(1280, 720);
252 assert_eq!(h.target_video_size, Some((1280, 720)));
253 }
254
255 #[test]
256 fn bitrate_default_should_be_none() {
257 let h = HlsOutput::new("/tmp/hls");
258 assert_eq!(h.target_bitrate, None);
259 }
260
261 #[test]
262 fn video_size_default_should_be_none() {
263 let h = HlsOutput::new("/tmp/hls");
264 assert_eq!(h.target_video_size, None);
265 }
266
267 #[test]
268 fn build_with_bitrate_and_video_size_should_succeed() {
269 let result = HlsOutput::new("/tmp/hls")
270 .input("/src/video.mp4")
271 .bitrate(4_000_000)
272 .video_size(1920, 1080)
273 .build();
274 assert!(result.is_ok());
275 let h = result.unwrap();
276 assert_eq!(h.target_bitrate, Some(4_000_000));
277 assert_eq!(h.target_video_size, Some((1920, 1080)));
278 }
279}