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}