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