Skip to main content

ff_decode/audio/
builder.rs

1//! Audio decoder builder for constructing audio decoders with custom configuration.
2//!
3//! This module provides the [`AudioDecoderBuilder`] type which enables fluent
4//! configuration of audio decoders. Use [`AudioDecoder::open()`] to start building.
5
6use std::path::{Path, PathBuf};
7use std::time::Duration;
8
9use ff_format::{AudioFrame, AudioStreamInfo, ContainerInfo, NetworkOptions, SampleFormat};
10
11use crate::audio::decoder_inner::AudioDecoderInner;
12use crate::error::DecodeError;
13
14/// Internal configuration for the audio decoder.
15#[derive(Debug, Clone, Default)]
16#[allow(clippy::struct_field_names)]
17#[allow(dead_code)] // Fields will be used when SwResample is fully implemented
18pub(crate) struct AudioDecoderConfig {
19    /// Output sample format (None = use source format)
20    pub output_format: Option<SampleFormat>,
21    /// Output sample rate (None = use source sample rate)
22    pub output_sample_rate: Option<u32>,
23    /// Output channel count (None = use source channel count)
24    pub output_channels: Option<u32>,
25}
26
27/// Builder for configuring and constructing an [`AudioDecoder`].
28///
29/// This struct provides a fluent interface for setting up decoder options
30/// before opening an audio file. It is created by calling [`AudioDecoder::open()`].
31///
32/// # Examples
33///
34/// ## Basic Usage
35///
36/// ```ignore
37/// use ff_decode::AudioDecoder;
38///
39/// let decoder = AudioDecoder::open("audio.mp3")?
40///     .build()?;
41/// ```
42///
43/// ## With Custom Format and Sample Rate
44///
45/// ```ignore
46/// use ff_decode::AudioDecoder;
47/// use ff_format::SampleFormat;
48///
49/// let decoder = AudioDecoder::open("audio.mp3")?
50///     .output_format(SampleFormat::F32)
51///     .output_sample_rate(48000)
52///     .build()?;
53/// ```
54#[derive(Debug)]
55pub struct AudioDecoderBuilder {
56    /// Path to the media file
57    path: PathBuf,
58    /// Output sample format (None = use source format)
59    output_format: Option<SampleFormat>,
60    /// Output sample rate (None = use source sample rate)
61    output_sample_rate: Option<u32>,
62    /// Output channel count (None = use source channel count)
63    output_channels: Option<u32>,
64    /// Network options for URL-based sources (None = use defaults)
65    network_opts: Option<NetworkOptions>,
66}
67
68impl AudioDecoderBuilder {
69    /// Creates a new builder for the specified file path.
70    ///
71    /// This is an internal constructor; use [`AudioDecoder::open()`] instead.
72    pub(crate) fn new(path: PathBuf) -> Self {
73        Self {
74            path,
75            output_format: None,
76            output_sample_rate: None,
77            output_channels: None,
78            network_opts: None,
79        }
80    }
81
82    /// Sets the output sample format for decoded frames.
83    ///
84    /// If not set, frames are returned in the source format. Setting an
85    /// output format enables automatic conversion during decoding.
86    ///
87    /// # Common Formats
88    ///
89    /// - [`SampleFormat::F32`] - 32-bit float, most common for editing
90    /// - [`SampleFormat::I16`] - 16-bit integer, CD quality
91    /// - [`SampleFormat::F32p`] - Planar 32-bit float, efficient for processing
92    ///
93    /// # Examples
94    ///
95    /// ```ignore
96    /// use ff_decode::AudioDecoder;
97    /// use ff_format::SampleFormat;
98    ///
99    /// let decoder = AudioDecoder::open("audio.mp3")?
100    ///     .output_format(SampleFormat::F32)
101    ///     .build()?;
102    /// ```
103    #[must_use]
104    pub fn output_format(mut self, format: SampleFormat) -> Self {
105        self.output_format = Some(format);
106        self
107    }
108
109    /// Sets the output sample rate in Hz.
110    ///
111    /// If not set, frames are returned at the source sample rate. Setting an
112    /// output sample rate enables automatic resampling during decoding.
113    ///
114    /// # Common Sample Rates
115    ///
116    /// - 44100 Hz - CD quality audio
117    /// - 48000 Hz - Professional audio, most common in video
118    /// - 96000 Hz - High-resolution audio
119    ///
120    /// # Examples
121    ///
122    /// ```ignore
123    /// use ff_decode::AudioDecoder;
124    ///
125    /// // Resample to 48kHz
126    /// let decoder = AudioDecoder::open("audio.mp3")?
127    ///     .output_sample_rate(48000)
128    ///     .build()?;
129    /// ```
130    #[must_use]
131    pub fn output_sample_rate(mut self, sample_rate: u32) -> Self {
132        self.output_sample_rate = Some(sample_rate);
133        self
134    }
135
136    /// Sets the output channel count.
137    ///
138    /// If not set, frames are returned with the source channel count. Setting an
139    /// output channel count enables automatic channel remixing during decoding.
140    ///
141    /// # Common Channel Counts
142    ///
143    /// - 1 - Mono
144    /// - 2 - Stereo
145    /// - 6 - 5.1 surround sound
146    ///
147    /// # Examples
148    ///
149    /// ```ignore
150    /// use ff_decode::AudioDecoder;
151    ///
152    /// // Convert to stereo
153    /// let decoder = AudioDecoder::open("audio.mp3")?
154    ///     .output_channels(2)
155    ///     .build()?;
156    /// ```
157    #[must_use]
158    pub fn output_channels(mut self, channels: u32) -> Self {
159        self.output_channels = Some(channels);
160        self
161    }
162
163    /// Sets network options for URL-based audio sources (HTTP, RTSP, RTMP, etc.).
164    ///
165    /// When set, the builder skips the file-existence check and passes connect
166    /// and read timeouts to `avformat_open_input` via an `AVDictionary`.
167    /// Call this before `.build()` when opening `rtmp://`, `rtsp://`, `http://`,
168    /// `https://`, `udp://`, `srt://`, or `rtp://` URLs.
169    ///
170    /// # HLS / M3U8 Playlists
171    ///
172    /// Audio-only HLS playlists (`.m3u8` pointing to AAC or MP3 segments) are
173    /// detected automatically by `FFmpeg`. Pass the full HTTP(S) URL:
174    ///
175    /// ```ignore
176    /// use ff_decode::AudioDecoder;
177    /// use ff_format::NetworkOptions;
178    ///
179    /// let decoder = AudioDecoder::open("https://example.com/audio/index.m3u8")
180    ///     .network(NetworkOptions::default())
181    ///     .build()?;
182    /// ```
183    ///
184    /// # UDP / MPEG-TS
185    ///
186    /// `udp://` URLs are always live — `is_live()` returns `true` and seeking
187    /// is not supported. Two extra `AVDictionary` options are set automatically
188    /// to reduce packet loss on high-bitrate streams:
189    ///
190    /// | Option | Value | Reason |
191    /// |---|---|---|
192    /// | `buffer_size` | `65536` | Enlarges the UDP receive buffer |
193    /// | `overrun_nonfatal` | `1` | Discards excess data instead of erroring |
194    ///
195    /// # SRT (Secure Reliable Transport)
196    ///
197    /// SRT URLs (`srt://host:port`) require the `srt` feature flag **and** a
198    /// libsrt-enabled `FFmpeg` build.  Enable the feature in `Cargo.toml`:
199    ///
200    /// ```toml
201    /// [dependencies]
202    /// ff-decode = { version = "*", features = ["srt"] }
203    /// ```
204    ///
205    /// Without the `srt` feature, opening an `srt://` URL returns
206    /// [`DecodeError::ConnectionFailed`]. If the feature is enabled but the
207    /// linked `FFmpeg` was not built with `--enable-libsrt`, the same error is
208    /// returned with a message directing you to rebuild `FFmpeg`.
209    ///
210    /// ```ignore
211    /// use ff_decode::AudioDecoder;
212    /// use ff_format::NetworkOptions;
213    ///
214    /// let decoder = AudioDecoder::open("srt://ingest.example.com:4200")
215    ///     .network(NetworkOptions::default())
216    ///     .build()?;
217    /// ```
218    ///
219    /// # Credentials
220    ///
221    /// HTTP basic-auth credentials must be embedded directly in the URL:
222    /// `https://user:password@cdn.example.com/audio/index.m3u8`.
223    /// The password is redacted in log output.
224    ///
225    /// # DRM Limitation
226    ///
227    /// DRM-protected streams are **not** supported:
228    /// - HLS: `FairPlay`, Widevine, AES-128 with external key servers
229    /// - DASH: CENC, `PlayReady`, Widevine
230    ///
231    /// `FFmpeg` can parse the manifest and fetch segments, but key delivery
232    /// to a DRM license server is outside the scope of this API.
233    ///
234    /// # Examples
235    ///
236    /// ```ignore
237    /// use ff_decode::AudioDecoder;
238    /// use ff_format::NetworkOptions;
239    /// use std::time::Duration;
240    ///
241    /// let decoder = AudioDecoder::open("http://stream.example.com/audio.aac")
242    ///     .network(NetworkOptions {
243    ///         connect_timeout: Duration::from_secs(5),
244    ///         ..Default::default()
245    ///     })
246    ///     .build()?;
247    /// ```
248    #[must_use]
249    pub fn network(mut self, opts: NetworkOptions) -> Self {
250        self.network_opts = Some(opts);
251        self
252    }
253
254    /// Returns the configured file path.
255    #[must_use]
256    pub fn path(&self) -> &Path {
257        &self.path
258    }
259
260    /// Returns the configured output format, if any.
261    #[must_use]
262    pub fn get_output_format(&self) -> Option<SampleFormat> {
263        self.output_format
264    }
265
266    /// Returns the configured output sample rate, if any.
267    #[must_use]
268    pub fn get_output_sample_rate(&self) -> Option<u32> {
269        self.output_sample_rate
270    }
271
272    /// Returns the configured output channel count, if any.
273    #[must_use]
274    pub fn get_output_channels(&self) -> Option<u32> {
275        self.output_channels
276    }
277
278    /// Builds the audio decoder with the configured options.
279    ///
280    /// This method opens the media file, initializes the decoder context,
281    /// and prepares for frame decoding.
282    ///
283    /// # Errors
284    ///
285    /// Returns an error if:
286    /// - The file cannot be found ([`DecodeError::FileNotFound`])
287    /// - The file contains no audio stream ([`DecodeError::NoAudioStream`])
288    /// - The codec is not supported ([`DecodeError::UnsupportedCodec`])
289    /// - Other `FFmpeg` errors occur ([`DecodeError::Ffmpeg`])
290    ///
291    /// # Examples
292    ///
293    /// ```ignore
294    /// use ff_decode::AudioDecoder;
295    ///
296    /// let decoder = AudioDecoder::open("audio.mp3")?
297    ///     .build()?;
298    ///
299    /// // Start decoding
300    /// for result in &mut decoder {
301    ///     let frame = result?;
302    ///     // Process frame...
303    /// }
304    /// ```
305    pub fn build(self) -> Result<AudioDecoder, DecodeError> {
306        // Network URLs skip the file-existence check (literal path does not exist).
307        let is_network_url = self.path.to_str().is_some_and(crate::network::is_url);
308        if !is_network_url && !self.path.exists() {
309            return Err(DecodeError::FileNotFound {
310                path: self.path.clone(),
311            });
312        }
313
314        // Build the internal configuration
315        let config = AudioDecoderConfig {
316            output_format: self.output_format,
317            output_sample_rate: self.output_sample_rate,
318            output_channels: self.output_channels,
319        };
320
321        // Create the decoder inner
322        let (inner, stream_info, container_info) = AudioDecoderInner::new(
323            &self.path,
324            self.output_format,
325            self.output_sample_rate,
326            self.output_channels,
327            self.network_opts,
328        )?;
329
330        Ok(AudioDecoder {
331            path: self.path,
332            config,
333            inner,
334            stream_info,
335            container_info,
336            fused: false,
337        })
338    }
339}
340
341/// An audio decoder for extracting audio frames from media files.
342///
343/// The decoder provides frame-by-frame access to audio content with support
344/// for resampling and format conversion.
345///
346/// # Construction
347///
348/// Use [`AudioDecoder::open()`] to create a builder, then call [`AudioDecoderBuilder::build()`]:
349///
350/// ```ignore
351/// use ff_decode::AudioDecoder;
352/// use ff_format::SampleFormat;
353///
354/// let decoder = AudioDecoder::open("audio.mp3")?
355///     .output_format(SampleFormat::F32)
356///     .output_sample_rate(48000)
357///     .build()?;
358/// ```
359///
360/// # Frame Decoding
361///
362/// Frames can be decoded one at a time or using an iterator:
363///
364/// ```ignore
365/// // Decode one frame
366/// if let Some(frame) = decoder.decode_one()? {
367///     println!("Frame with {} samples", frame.samples());
368/// }
369///
370/// // Iterator form — AudioDecoder implements Iterator directly
371/// for result in &mut decoder {
372///     let frame = result?;
373///     // Process frame...
374/// }
375/// ```
376///
377/// # Seeking
378///
379/// The decoder supports seeking to specific positions:
380///
381/// ```ignore
382/// use std::time::Duration;
383///
384/// // Seek to 30 seconds
385/// decoder.seek(Duration::from_secs(30))?;
386/// ```
387pub struct AudioDecoder {
388    /// Path to the media file
389    path: PathBuf,
390    /// Decoder configuration
391    #[allow(dead_code)]
392    config: AudioDecoderConfig,
393    /// Internal decoder state
394    inner: AudioDecoderInner,
395    /// Audio stream information
396    stream_info: AudioStreamInfo,
397    /// Container-level metadata
398    container_info: ContainerInfo,
399    /// Set to `true` after a decoding error; causes [`Iterator::next`] to return `None`.
400    fused: bool,
401}
402
403impl AudioDecoder {
404    /// Opens a media file and returns a builder for configuring the decoder.
405    ///
406    /// This is the entry point for creating a decoder. The returned builder
407    /// allows setting options before the decoder is fully initialized.
408    ///
409    /// # Arguments
410    ///
411    /// * `path` - Path to the media file to decode.
412    ///
413    /// # Examples
414    ///
415    /// ```ignore
416    /// use ff_decode::AudioDecoder;
417    ///
418    /// // Simple usage
419    /// let decoder = AudioDecoder::open("audio.mp3")?
420    ///     .build()?;
421    ///
422    /// // With options
423    /// let decoder = AudioDecoder::open("audio.mp3")?
424    ///     .output_format(SampleFormat::F32)
425    ///     .output_sample_rate(48000)
426    ///     .build()?;
427    /// ```
428    ///
429    /// # Note
430    ///
431    /// This method does not validate that the file exists or is a valid
432    /// media file. Validation occurs when [`AudioDecoderBuilder::build()`] is called.
433    pub fn open(path: impl AsRef<Path>) -> AudioDecoderBuilder {
434        AudioDecoderBuilder::new(path.as_ref().to_path_buf())
435    }
436
437    // =========================================================================
438    // Information Methods
439    // =========================================================================
440
441    /// Returns the audio stream information.
442    ///
443    /// This contains metadata about the audio stream including sample rate,
444    /// channel count, codec, and format characteristics.
445    #[must_use]
446    pub fn stream_info(&self) -> &AudioStreamInfo {
447        &self.stream_info
448    }
449
450    /// Returns the sample rate in Hz.
451    #[must_use]
452    pub fn sample_rate(&self) -> u32 {
453        self.stream_info.sample_rate()
454    }
455
456    /// Returns the number of audio channels.
457    ///
458    /// The type is `u32` to match `FFmpeg` and professional audio APIs. When
459    /// integrating with `rodio` or `cpal` (which require `u16`), cast with
460    /// `decoder.channels() as u16` — channel counts never exceed `u16::MAX`
461    /// in practice.
462    #[must_use]
463    pub fn channels(&self) -> u32 {
464        self.stream_info.channels()
465    }
466
467    /// Returns the total duration of the audio.
468    ///
469    /// Returns [`Duration::ZERO`] if duration is unknown.
470    #[must_use]
471    pub fn duration(&self) -> Duration {
472        self.stream_info.duration().unwrap_or(Duration::ZERO)
473    }
474
475    /// Returns the total duration of the audio, or `None` for live streams
476    /// or formats that do not carry duration information.
477    #[must_use]
478    pub fn duration_opt(&self) -> Option<Duration> {
479        self.stream_info.duration()
480    }
481
482    /// Returns container-level metadata (format name, bitrate, stream count).
483    #[must_use]
484    pub fn container_info(&self) -> &ContainerInfo {
485        &self.container_info
486    }
487
488    /// Returns the current playback position.
489    #[must_use]
490    pub fn position(&self) -> Duration {
491        self.inner.position()
492    }
493
494    /// Returns `true` if the end of stream has been reached.
495    #[must_use]
496    pub fn is_eof(&self) -> bool {
497        self.inner.is_eof()
498    }
499
500    /// Returns the file path being decoded.
501    #[must_use]
502    pub fn path(&self) -> &Path {
503        &self.path
504    }
505
506    // =========================================================================
507    // Decoding Methods
508    // =========================================================================
509
510    /// Decodes the next audio frame.
511    ///
512    /// This method reads and decodes a single frame from the audio stream.
513    ///
514    /// # Returns
515    ///
516    /// - `Ok(Some(frame))` - A frame was successfully decoded
517    /// - `Ok(None)` - End of stream reached, no more frames
518    /// - `Err(_)` - An error occurred during decoding
519    ///
520    /// # Errors
521    ///
522    /// Returns [`DecodeError`] if:
523    /// - Reading from the file fails
524    /// - Decoding the frame fails
525    /// - Sample format conversion fails
526    ///
527    /// # Examples
528    ///
529    /// ```ignore
530    /// use ff_decode::AudioDecoder;
531    ///
532    /// let mut decoder = AudioDecoder::open("audio.mp3")?.build()?;
533    ///
534    /// while let Some(frame) = decoder.decode_one()? {
535    ///     println!("Frame with {} samples", frame.samples());
536    ///     // Process frame...
537    /// }
538    /// ```
539    pub fn decode_one(&mut self) -> Result<Option<AudioFrame>, DecodeError> {
540        self.inner.decode_one()
541    }
542
543    /// Decodes all frames and returns their raw PCM data.
544    ///
545    /// This method decodes the entire audio file and returns all samples
546    /// as a contiguous byte buffer.
547    ///
548    /// # Performance
549    ///
550    /// - Memory scales with audio duration and quality
551    /// - For 10 minutes of stereo 48kHz F32 audio: ~110 MB
552    ///
553    /// # Returns
554    ///
555    /// A byte vector containing all audio samples in the configured output format.
556    ///
557    /// # Errors
558    ///
559    /// Returns [`DecodeError`] if:
560    /// - Decoding any frame fails
561    /// - The file cannot be read
562    ///
563    /// # Examples
564    ///
565    /// ```ignore
566    /// use ff_decode::AudioDecoder;
567    /// use ff_format::SampleFormat;
568    ///
569    /// let mut decoder = AudioDecoder::open("audio.mp3")?
570    ///     .output_format(SampleFormat::F32)
571    ///     .build()?;
572    ///
573    /// let samples = decoder.decode_all()?;
574    /// println!("Decoded {} bytes", samples.len());
575    /// ```
576    ///
577    /// # Memory Usage
578    ///
579    /// Stereo 48kHz F32 audio:
580    /// - 1 minute: ~11 MB
581    /// - 5 minutes: ~55 MB
582    /// - 10 minutes: ~110 MB
583    pub fn decode_all(&mut self) -> Result<Vec<u8>, DecodeError> {
584        let mut buffer = Vec::new();
585
586        while let Some(frame) = self.decode_one()? {
587            // Collect samples from all planes
588            for plane in frame.planes() {
589                buffer.extend_from_slice(plane);
590            }
591        }
592
593        Ok(buffer)
594    }
595
596    /// Decodes all frames within a specified time range.
597    ///
598    /// This method seeks to the start position and decodes all frames until
599    /// the end position is reached. Frames outside the range are skipped.
600    ///
601    /// # Arguments
602    ///
603    /// * `start` - Start of the time range (inclusive).
604    /// * `end` - End of the time range (exclusive).
605    ///
606    /// # Returns
607    ///
608    /// A byte vector containing audio samples within `[start, end)`.
609    ///
610    /// # Errors
611    ///
612    /// Returns [`DecodeError`] if:
613    /// - Seeking to the start position fails
614    /// - Decoding frames fails
615    /// - The time range is invalid (start >= end)
616    ///
617    /// # Examples
618    ///
619    /// ```ignore
620    /// use ff_decode::AudioDecoder;
621    /// use std::time::Duration;
622    ///
623    /// let mut decoder = AudioDecoder::open("audio.mp3")?.build()?;
624    ///
625    /// // Decode audio from 5s to 10s
626    /// let samples = decoder.decode_range(
627    ///     Duration::from_secs(5),
628    ///     Duration::from_secs(10),
629    /// )?;
630    ///
631    /// println!("Decoded {} bytes", samples.len());
632    /// ```
633    pub fn decode_range(&mut self, start: Duration, end: Duration) -> Result<Vec<u8>, DecodeError> {
634        // Validate range
635        if start >= end {
636            return Err(DecodeError::DecodingFailed {
637                timestamp: Some(start),
638                reason: format!(
639                    "Invalid time range: start ({start:?}) must be before end ({end:?})"
640                ),
641            });
642        }
643
644        // Seek to start position (keyframe mode for efficiency)
645        self.seek(start, crate::SeekMode::Keyframe)?;
646
647        // Collect frames in the range
648        let mut buffer = Vec::new();
649
650        while let Some(frame) = self.decode_one()? {
651            let frame_time = frame.timestamp().as_duration();
652
653            // Stop if we've passed the end of the range
654            if frame_time >= end {
655                break;
656            }
657
658            // Only collect frames within the range
659            if frame_time >= start {
660                for plane in frame.planes() {
661                    buffer.extend_from_slice(plane);
662                }
663            }
664        }
665
666        Ok(buffer)
667    }
668
669    // =========================================================================
670    // Seeking Methods
671    // =========================================================================
672
673    /// Seeks to a specified position in the audio stream.
674    ///
675    /// This method performs efficient seeking without reopening the file.
676    ///
677    /// # Arguments
678    ///
679    /// * `position` - Target position to seek to.
680    /// * `mode` - Seek mode (Keyframe, Exact, or Backward).
681    ///
682    /// # Errors
683    ///
684    /// Returns [`DecodeError::SeekFailed`] if:
685    /// - The target position is beyond the audio duration
686    /// - The file format doesn't support seeking
687    /// - The seek operation fails internally
688    ///
689    /// # Examples
690    ///
691    /// ```ignore
692    /// use ff_decode::{AudioDecoder, SeekMode};
693    /// use std::time::Duration;
694    ///
695    /// let mut decoder = AudioDecoder::open("audio.mp3")?.build()?;
696    ///
697    /// // Seek to 30 seconds with keyframe mode (fast)
698    /// decoder.seek(Duration::from_secs(30), SeekMode::Keyframe)?;
699    ///
700    /// // Seek to exact position (slower but precise)
701    /// decoder.seek(Duration::from_secs(45), SeekMode::Exact)?;
702    ///
703    /// // Decode next frame
704    /// if let Some(frame) = decoder.decode_one()? {
705    ///     println!("Frame at {:?}", frame.timestamp().as_duration());
706    /// }
707    /// ```
708    pub fn seek(&mut self, position: Duration, mode: crate::SeekMode) -> Result<(), DecodeError> {
709        if self.inner.is_live() {
710            return Err(DecodeError::SeekNotSupported);
711        }
712        self.inner.seek(position, mode)
713    }
714
715    /// Returns `true` if the source is a live or streaming input.
716    ///
717    /// Live sources (HLS live playlists, RTMP, RTSP, MPEG-TS) have the
718    /// `AVFMT_TS_DISCONT` flag set on their `AVInputFormat`. Seeking is not
719    /// supported on live sources — [`AudioDecoder::seek`] will return
720    /// [`DecodeError::SeekNotSupported`].
721    #[must_use]
722    pub fn is_live(&self) -> bool {
723        self.inner.is_live()
724    }
725
726    /// Flushes the decoder's internal buffers.
727    ///
728    /// This method clears any cached frames and resets the decoder state.
729    /// The decoder is ready to receive new packets after flushing.
730    pub fn flush(&mut self) {
731        self.inner.flush();
732    }
733}
734
735impl Iterator for AudioDecoder {
736    type Item = Result<AudioFrame, DecodeError>;
737
738    fn next(&mut self) -> Option<Self::Item> {
739        if self.fused {
740            return None;
741        }
742        match self.decode_one() {
743            Ok(Some(frame)) => Some(Ok(frame)),
744            Ok(None) => None,
745            Err(e) => {
746                self.fused = true;
747                Some(Err(e))
748            }
749        }
750    }
751}
752
753impl std::iter::FusedIterator for AudioDecoder {}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758    use std::path::PathBuf;
759
760    #[test]
761    fn test_builder_default_values() {
762        let builder = AudioDecoderBuilder::new(PathBuf::from("test.mp3"));
763
764        assert_eq!(builder.path(), Path::new("test.mp3"));
765        assert!(builder.get_output_format().is_none());
766        assert!(builder.get_output_sample_rate().is_none());
767        assert!(builder.get_output_channels().is_none());
768    }
769
770    #[test]
771    fn test_builder_output_format() {
772        let builder =
773            AudioDecoderBuilder::new(PathBuf::from("test.mp3")).output_format(SampleFormat::F32);
774
775        assert_eq!(builder.get_output_format(), Some(SampleFormat::F32));
776    }
777
778    #[test]
779    fn test_builder_output_sample_rate() {
780        let builder = AudioDecoderBuilder::new(PathBuf::from("test.mp3")).output_sample_rate(48000);
781
782        assert_eq!(builder.get_output_sample_rate(), Some(48000));
783    }
784
785    #[test]
786    fn test_builder_output_channels() {
787        let builder = AudioDecoderBuilder::new(PathBuf::from("test.mp3")).output_channels(2);
788
789        assert_eq!(builder.get_output_channels(), Some(2));
790    }
791
792    #[test]
793    fn test_builder_chaining() {
794        let builder = AudioDecoderBuilder::new(PathBuf::from("test.mp3"))
795            .output_format(SampleFormat::F32)
796            .output_sample_rate(48000)
797            .output_channels(2);
798
799        assert_eq!(builder.get_output_format(), Some(SampleFormat::F32));
800        assert_eq!(builder.get_output_sample_rate(), Some(48000));
801        assert_eq!(builder.get_output_channels(), Some(2));
802    }
803
804    #[test]
805    fn test_decoder_open() {
806        let builder = AudioDecoder::open("audio.mp3");
807        assert_eq!(builder.path(), Path::new("audio.mp3"));
808    }
809
810    #[test]
811    fn test_build_file_not_found() {
812        let result = AudioDecoder::open("nonexistent_file_12345.mp3").build();
813
814        assert!(result.is_err());
815        match result {
816            Err(DecodeError::FileNotFound { path }) => {
817                assert!(
818                    path.to_string_lossy()
819                        .contains("nonexistent_file_12345.mp3")
820                );
821            }
822            Err(e) => panic!("Expected FileNotFound error, got: {e:?}"),
823            Ok(_) => panic!("Expected error, got Ok"),
824        }
825    }
826
827    #[test]
828    fn test_decoder_config_default() {
829        let config = AudioDecoderConfig::default();
830
831        assert!(config.output_format.is_none());
832        assert!(config.output_sample_rate.is_none());
833        assert!(config.output_channels.is_none());
834    }
835
836    #[test]
837    fn network_setter_should_store_options() {
838        let opts = NetworkOptions::default();
839        let builder = AudioDecoderBuilder::new(PathBuf::from("test.mp3")).network(opts.clone());
840        assert_eq!(builder.network_opts, Some(opts));
841    }
842
843    #[test]
844    fn build_should_bypass_file_existence_check_for_network_url() {
845        // A network URL that clearly does not exist locally should not return
846        // FileNotFound — it will return a different error (or succeed) from
847        // FFmpeg's network layer. The important thing is that FileNotFound is
848        // NOT returned.
849        let result = AudioDecoder::open("http://192.0.2.1/nonexistent.aac").build();
850        assert!(
851            !matches!(result, Err(DecodeError::FileNotFound { .. })),
852            "FileNotFound must not be returned for network URLs"
853        );
854    }
855}