Skip to main content

yt_dlp/live/
mod.rs

1//! Live stream recording and streaming module.
2//!
3//! Provides two recording engines for HLS live streams:
4//! - **Reqwest** (primary): Pure-Rust segment fetcher with zero-copy writes.
5//! - **FFmpeg** (fallback): Delegates to an FFmpeg process with `-c copy`.
6//!
7//! Recording is controlled via a [`CancellationToken`](tokio_util::sync::CancellationToken)
8//! and optionally bounded by a maximum duration. Events are emitted through
9//! the crate's event bus for progress tracking.
10
11#[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/// Common configuration shared across live recording engines.
46#[cfg(feature = "live-recording")]
47pub struct RecordingConfig {
48    /// The HLS stream URL to record.
49    pub stream_url: String,
50    /// The output file path.
51    pub output_path: PathBuf,
52    /// The video ID (for event emission).
53    pub video_id: String,
54    /// Quality label (e.g. "1080p").
55    pub quality: String,
56    /// Optional maximum recording duration.
57    pub max_duration: Option<Duration>,
58    /// Cancellation token for graceful stop.
59    pub cancellation_token: CancellationToken,
60    /// The event bus for emitting recording events.
61    pub event_bus: crate::events::EventBus,
62}
63
64/// Common configuration shared across live fragment streaming.
65#[cfg(feature = "live-streaming")]
66pub struct LiveStreamConfig {
67    /// The HLS stream URL to stream live fragments from.
68    pub stream_url: String,
69    /// The video ID (for event emission).
70    pub video_id: String,
71    /// Quality label (e.g. "1080p").
72    pub quality: String,
73    /// Optional maximum streaming duration for this live fragment session.
74    pub max_duration: Option<Duration>,
75    /// Cancellation token for graceful stop.
76    pub cancellation_token: CancellationToken,
77    /// The event bus for emitting streaming events.
78    pub event_bus: crate::events::EventBus,
79}
80
81/// The result of a live recording session.
82#[cfg(feature = "live-recording")]
83#[derive(Debug, Clone)]
84pub struct RecordingResult {
85    /// The path to the recorded file.
86    pub output_path: PathBuf,
87    /// Total bytes written.
88    pub total_bytes: u64,
89    /// Total recording duration.
90    pub total_duration: Duration,
91    /// Number of HLS segments downloaded (0 for FFmpeg engine).
92    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/// Fluent builder for configuring and starting a live recording.
110///
111/// Created via [`Downloader::record_live`]. Allows configuring the recording method,
112/// format selection, maximum duration, and cancellation token before starting.
113///
114/// # Examples
115///
116/// ```rust,no_run
117/// # use yt_dlp::Downloader;
118/// # use yt_dlp::client::deps::Libraries;
119/// # use std::path::PathBuf;
120/// # use std::time::Duration;
121/// # #[tokio::main]
122/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
123/// # let libraries = Libraries::new(PathBuf::from("libs/yt-dlp"), PathBuf::from("libs/ffmpeg"));
124/// # let downloader = Downloader::builder(libraries, "output").build().await?;
125/// let video = downloader.fetch_video_infos("https://youtube.com/watch?v=LIVE_ID").await?;
126///
127/// let result = downloader.record_live(&video, "live-recording.ts")
128///     .with_max_duration(Duration::from_secs(3600))
129///     .execute()
130///     .await?;
131///
132/// println!("Recorded {} bytes", result.total_bytes);
133/// # Ok(())
134/// # }
135/// ```
136#[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    /// Creates a new live recording builder.
150    ///
151    /// # Arguments
152    ///
153    /// * `downloader` - Reference to the downloader.
154    /// * `video` - The video metadata (must be a live stream).
155    /// * `output_path` - Where to write the recorded stream.
156    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    /// Sets the recording method.
169    ///
170    /// # Arguments
171    ///
172    /// * `method` - [`RecordingMethod::Native`] (default) or [`RecordingMethod::Fallback`].
173    pub fn with_method(mut self, method: RecordingMethod) -> Self {
174        self.method = method;
175        self
176    }
177
178    /// Sets the maximum recording duration.
179    ///
180    /// # Arguments
181    ///
182    /// * `duration` - Maximum time to record before automatically stopping.
183    pub fn with_max_duration(mut self, duration: Duration) -> Self {
184        self.max_duration = Some(duration);
185        self
186    }
187
188    /// Selects a specific HLS format for recording.
189    ///
190    /// If not set, the best quality live format is automatically selected.
191    ///
192    /// # Arguments
193    ///
194    /// * `format` - The HLS format to record.
195    pub fn with_format(mut self, format: &'a Format) -> Self {
196        self.format = Some(format);
197        self
198    }
199
200    /// Sets a custom cancellation token.
201    ///
202    /// If not set, the downloader's cancellation token is used.
203    ///
204    /// # Arguments
205    ///
206    /// * `token` - The cancellation token to control recording lifecycle.
207    pub fn with_cancellation_token(mut self, token: CancellationToken) -> Self {
208        self.cancellation_token = Some(token);
209        self
210    }
211
212    /// Starts a live recording and writes it to a file.
213    ///
214    /// # Errors
215    ///
216    /// Returns an error if the video is not a live stream, no HLS format is available,
217    /// or the recording engine encounters an error.
218    ///
219    /// # Returns
220    ///
221    /// A [`RecordingResult`] containing recording statistics.
222    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/// Fluent builder for configuring and starting a live fragment stream.
277///
278/// Created via [`Downloader::stream_live`]. Allows configuring format selection,
279/// maximum duration, and cancellation token before starting.
280#[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    /// Creates a new live stream builder.
304    ///
305    /// # Arguments
306    ///
307    /// * `downloader` - Reference to the downloader.
308    /// * `video` - The video metadata (must be a live stream).
309    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    /// Sets the maximum streaming duration.
320    ///
321    /// # Arguments
322    ///
323    /// * `duration` - Maximum time to stream before automatically stopping.
324    pub fn with_max_duration(mut self, duration: Duration) -> Self {
325        self.max_duration = Some(duration);
326        self
327    }
328
329    /// Selects a specific HLS format for streaming.
330    ///
331    /// If not set, the best quality live format is automatically selected.
332    ///
333    /// # Arguments
334    ///
335    /// * `format` - The HLS format to stream.
336    pub fn with_format(mut self, format: &'a Format) -> Self {
337        self.format = Some(format);
338        self
339    }
340
341    /// Sets a custom cancellation token.
342    ///
343    /// If not set, the downloader's cancellation token is used.
344    ///
345    /// # Arguments
346    ///
347    /// * `token` - The cancellation token to control streaming lifecycle.
348    pub fn with_cancellation_token(mut self, token: CancellationToken) -> Self {
349        self.cancellation_token = Some(token);
350        self
351    }
352
353    /// Starts streaming live fragments.
354    ///
355    /// # Errors
356    ///
357    /// Returns an error if the video is not a live stream, no HLS format is available,
358    /// or the streaming engine encounters an error.
359    ///
360    /// # Returns
361    ///
362    /// A [`LiveFragmentStream`] that yields HLS fragments as they are downloaded.
363    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}