1#[cfg(any(feature = "live-recording", feature = "live-streaming"))]
12mod core;
13#[cfg(any(feature = "live-recording", feature = "live-streaming"))]
14pub mod hls;
15#[cfg(feature = "live-recording")]
16pub mod recording;
17#[cfg(feature = "live-streaming")]
18pub mod streaming;
19
20#[cfg(feature = "live-streaming")]
21pub use core::LiveFragment;
22use std::fmt;
23#[cfg(feature = "live-recording")]
24use std::path::PathBuf;
25use std::sync::Arc;
26use std::time::Duration;
27
28#[cfg(any(feature = "live-recording", feature = "live-streaming"))]
29pub use hls::{HlsPlaylist, HlsSegment, HlsVariant};
30#[cfg(feature = "live-recording")]
31pub use recording::FfmpegLiveRecorder;
32#[cfg(feature = "live-recording")]
33pub use recording::LiveRecorder;
34#[cfg(feature = "live-streaming")]
35pub use streaming::{LiveFragmentStream, LiveFragmentStreamer};
36use tokio_util::sync::CancellationToken;
37
38use crate::Downloader;
39use crate::error::{Error, Result};
40#[cfg(feature = "live-recording")]
41use crate::events::types::RecordingMethod;
42use crate::model::Video;
43use crate::model::format::{Format, Protocol};
44
45#[cfg(feature = "live-recording")]
47pub struct RecordingConfig {
48 pub stream_url: String,
50 pub output_path: PathBuf,
52 pub video_id: String,
54 pub quality: String,
56 pub max_duration: Option<Duration>,
58 pub cancellation_token: CancellationToken,
60 pub event_bus: crate::events::EventBus,
62}
63
64#[cfg(feature = "live-streaming")]
66pub struct LiveStreamConfig {
67 pub stream_url: String,
69 pub video_id: String,
71 pub quality: String,
73 pub max_duration: Option<Duration>,
75 pub cancellation_token: CancellationToken,
77 pub event_bus: crate::events::EventBus,
79}
80
81#[cfg(feature = "live-recording")]
83#[derive(Debug, Clone)]
84pub struct RecordingResult {
85 pub output_path: PathBuf,
87 pub total_bytes: u64,
89 pub total_duration: Duration,
91 pub segments_downloaded: u64,
93}
94
95#[cfg(feature = "live-recording")]
96impl fmt::Display for RecordingResult {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(
99 f,
100 "RecordingResult(output={}, bytes={}, duration={:.1}s, segments={})",
101 self.output_path.display(),
102 self.total_bytes,
103 self.total_duration.as_secs_f64(),
104 self.segments_downloaded
105 )
106 }
107}
108
109#[cfg(feature = "live-recording")]
137pub struct LiveRecordingBuilder<'a> {
138 downloader: &'a Downloader,
139 video: &'a Video,
140 output_path: PathBuf,
141 method: RecordingMethod,
142 max_duration: Option<Duration>,
143 format: Option<&'a Format>,
144 cancellation_token: Option<CancellationToken>,
145}
146
147#[cfg(feature = "live-recording")]
148impl<'a> LiveRecordingBuilder<'a> {
149 pub(crate) fn new(downloader: &'a Downloader, video: &'a Video, output_path: impl Into<PathBuf>) -> Self {
157 Self {
158 downloader,
159 video,
160 output_path: output_path.into(),
161 method: RecordingMethod::Native,
162 max_duration: None,
163 format: None,
164 cancellation_token: None,
165 }
166 }
167
168 pub fn with_method(mut self, method: RecordingMethod) -> Self {
174 self.method = method;
175 self
176 }
177
178 pub fn with_max_duration(mut self, duration: Duration) -> Self {
184 self.max_duration = Some(duration);
185 self
186 }
187
188 pub fn with_format(mut self, format: &'a Format) -> Self {
196 self.format = Some(format);
197 self
198 }
199
200 pub fn with_cancellation_token(mut self, token: CancellationToken) -> Self {
208 self.cancellation_token = Some(token);
209 self
210 }
211
212 pub async fn execute(self) -> Result<RecordingResult> {
223 let resolved = resolve_live_format(self.video, self.format, LiveMode::Recording)?;
224 let cancellation_token = self
225 .cancellation_token
226 .unwrap_or_else(|| self.downloader.cancellation_token.child_token());
227
228 tracing::info!(
229 video_id = self.video.id,
230 method = ?self.method,
231 quality = resolved.quality,
232 output = ?self.output_path,
233 "📥 Starting live recording"
234 );
235
236 match self.method {
237 RecordingMethod::Native => {
238 let client = Arc::new(
239 reqwest::Client::builder()
240 .tcp_nodelay(true)
241 .build()
242 .map_err(|e| Error::http(&resolved.stream_url, "building HTTP client", e))?,
243 );
244
245 let config = RecordingConfig {
246 stream_url: resolved.stream_url,
247 output_path: self.output_path,
248 video_id: self.video.id.clone(),
249 quality: resolved.quality,
250 max_duration: self.max_duration,
251 cancellation_token,
252 event_bus: self.downloader.event_bus.clone(),
253 };
254
255 let recorder = LiveRecorder::new(config, client);
256 recorder.record().await
257 }
258 RecordingMethod::Fallback => {
259 let config = RecordingConfig {
260 stream_url: resolved.stream_url,
261 output_path: self.output_path,
262 video_id: self.video.id.clone(),
263 quality: resolved.quality,
264 max_duration: self.max_duration,
265 cancellation_token,
266 event_bus: self.downloader.event_bus.clone(),
267 };
268
269 let recorder = FfmpegLiveRecorder::new(config, &self.downloader.libraries.ffmpeg);
270 recorder.record().await
271 }
272 }
273 }
274}
275
276#[cfg(feature = "live-streaming")]
281pub struct LiveStreamBuilder<'a> {
282 downloader: &'a Downloader,
283 video: &'a Video,
284 max_duration: Option<Duration>,
285 format: Option<&'a Format>,
286 cancellation_token: Option<CancellationToken>,
287}
288
289#[cfg(feature = "live-streaming")]
290impl fmt::Debug for LiveStreamBuilder<'_> {
291 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292 f.debug_struct("LiveStreamBuilder")
293 .field("video_id", &self.video.id)
294 .field("max_duration", &self.max_duration)
295 .field("has_format", &self.format.is_some())
296 .field("has_token", &self.cancellation_token.is_some())
297 .finish()
298 }
299}
300
301#[cfg(feature = "live-streaming")]
302impl<'a> LiveStreamBuilder<'a> {
303 pub(crate) fn new(downloader: &'a Downloader, video: &'a Video) -> Self {
310 Self {
311 downloader,
312 video,
313 max_duration: None,
314 format: None,
315 cancellation_token: None,
316 }
317 }
318
319 pub fn with_max_duration(mut self, duration: Duration) -> Self {
325 self.max_duration = Some(duration);
326 self
327 }
328
329 pub fn with_format(mut self, format: &'a Format) -> Self {
337 self.format = Some(format);
338 self
339 }
340
341 pub fn with_cancellation_token(mut self, token: CancellationToken) -> Self {
349 self.cancellation_token = Some(token);
350 self
351 }
352
353 pub async fn execute(self) -> Result<LiveFragmentStream> {
364 let resolved = resolve_live_format(self.video, self.format, LiveMode::Streaming)?;
365 let cancellation_token = self
366 .cancellation_token
367 .unwrap_or_else(|| self.downloader.cancellation_token.child_token());
368
369 tracing::info!(
370 video_id = self.video.id,
371 quality = resolved.quality,
372 "📥 Starting live fragment stream"
373 );
374
375 let client = Arc::new(
376 reqwest::Client::builder()
377 .tcp_nodelay(true)
378 .build()
379 .map_err(|e| Error::http(&resolved.stream_url, "building HTTP client", e))?,
380 );
381
382 let config = LiveStreamConfig {
383 stream_url: resolved.stream_url,
384 video_id: self.video.id.clone(),
385 quality: resolved.quality,
386 max_duration: self.max_duration,
387 cancellation_token,
388 event_bus: self.downloader.event_bus.clone(),
389 };
390
391 let streamer = LiveFragmentStreamer::new(config, client);
392 streamer.stream().await
393 }
394}
395
396#[cfg(any(feature = "live-recording", feature = "live-streaming"))]
397#[derive(Debug, Clone)]
398struct ResolvedLiveFormat {
399 stream_url: String,
400 quality: String,
401}
402
403#[cfg(any(feature = "live-recording", feature = "live-streaming"))]
404#[derive(Debug, Clone, Copy)]
405enum LiveMode {
406 #[cfg(feature = "live-recording")]
407 Recording,
408 #[cfg(feature = "live-streaming")]
409 Streaming,
410}
411
412#[cfg(any(feature = "live-recording", feature = "live-streaming"))]
413fn resolve_live_format(video: &Video, format: Option<&Format>, mode: LiveMode) -> Result<ResolvedLiveFormat> {
414 if !video.is_currently_live() {
415 return Err(Error::live_unavailable(
416 video.webpage_url.as_deref().unwrap_or("unknown"),
417 &video.live_status,
418 "video is not currently live",
419 ));
420 }
421
422 let live_formats = video.live_formats();
423 let format = match format {
424 Some(f) => {
425 if f.protocol != Protocol::M3U8Native {
426 return Err(live_format_error(video, mode, "format is not an HLS manifest"));
427 }
428 f
429 }
430 None => live_formats
431 .last()
432 .ok_or_else(|| live_format_error(video, mode, "no HLS formats available"))?,
433 };
434
435 let stream_url = format.url()?.clone();
436 let quality = format
437 .video_resolution
438 .height
439 .map(|h| format!("{h}p"))
440 .unwrap_or_else(|| "unknown".to_string());
441
442 Ok(ResolvedLiveFormat { stream_url, quality })
443}
444
445#[cfg(any(feature = "live-recording", feature = "live-streaming"))]
446fn live_format_error(video: &Video, mode: LiveMode, reason: &str) -> Error {
447 let url = video.webpage_url.as_deref().unwrap_or("unknown");
448
449 match mode {
450 #[cfg(feature = "live-recording")]
451 LiveMode::Recording => Error::live_recording(url, reason),
452 #[cfg(feature = "live-streaming")]
453 LiveMode::Streaming => Error::live_streaming(url, reason),
454 }
455}
456
457#[cfg(feature = "live-recording")]
458impl fmt::Debug for LiveRecordingBuilder<'_> {
459 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460 f.debug_struct("LiveRecordingBuilder")
461 .field("video_id", &self.video.id)
462 .field("output_path", &self.output_path)
463 .field("method", &self.method)
464 .field("max_duration", &self.max_duration)
465 .field("has_format", &self.format.is_some())
466 .field("has_token", &self.cancellation_token.is_some())
467 .finish()
468 }
469}