Skip to main content

ff_decode/audio/
decoder_inner.rs

1//! Internal audio decoder implementation using FFmpeg.
2//!
3//! This module contains the low-level decoder logic that directly interacts
4//! with FFmpeg's C API through the ff-sys crate. It is not exposed publicly.
5
6// Allow unsafe code in this module as it's necessary for FFmpeg FFI
7#![allow(unsafe_code)]
8// Allow specific clippy lints for FFmpeg FFI code
9#![allow(clippy::similar_names)]
10#![allow(clippy::too_many_lines)]
11#![allow(clippy::cast_sign_loss)]
12#![allow(clippy::cast_possible_truncation)]
13#![allow(clippy::cast_possible_wrap)]
14#![allow(clippy::module_name_repetitions)]
15#![allow(clippy::match_same_arms)]
16#![allow(clippy::ptr_as_ptr)]
17#![allow(clippy::doc_markdown)]
18#![allow(clippy::unnecessary_cast)]
19#![allow(clippy::if_not_else)]
20#![allow(clippy::unnecessary_wraps)]
21#![allow(clippy::cast_precision_loss)]
22#![allow(clippy::if_same_then_else)]
23#![allow(clippy::cast_lossless)]
24
25use std::ffi::CStr;
26use std::path::Path;
27use std::ptr;
28use std::time::Duration;
29
30use ff_format::channel::ChannelLayout;
31use ff_format::codec::AudioCodec;
32use ff_format::container::ContainerInfo;
33use ff_format::{AudioFrame, AudioStreamInfo, NetworkOptions, SampleFormat};
34use ff_sys::{
35    AVCodecContext, AVCodecID, AVFormatContext, AVFrame, AVMediaType_AVMEDIA_TYPE_AUDIO, AVPacket,
36};
37
38use super::resample_inner;
39
40use crate::error::DecodeError;
41
42/// RAII guard for `AVFormatContext` to ensure proper cleanup.
43struct AvFormatContextGuard(*mut AVFormatContext);
44
45impl AvFormatContextGuard {
46    /// Creates a new guard by opening an input file.
47    ///
48    /// # Safety
49    ///
50    /// Caller must ensure FFmpeg is initialized and path is valid.
51    unsafe fn new(path: &Path) -> Result<Self, DecodeError> {
52        // SAFETY: Caller ensures FFmpeg is initialized and path is valid
53        let format_ctx = unsafe {
54            ff_sys::avformat::open_input(path).map_err(|e| DecodeError::Ffmpeg {
55                code: e,
56                message: format!("Failed to open file: {}", ff_sys::av_error_string(e)),
57            })?
58        };
59        Ok(Self(format_ctx))
60    }
61
62    /// Opens a network URL using the supplied network options.
63    ///
64    /// # Safety
65    ///
66    /// Caller must ensure FFmpeg is initialized and URL is valid.
67    unsafe fn new_url(url: &str, network: &NetworkOptions) -> Result<Self, DecodeError> {
68        // SAFETY: Caller ensures FFmpeg is initialized and URL is valid
69        let format_ctx = unsafe {
70            ff_sys::avformat::open_input_url(url, network.connect_timeout, network.read_timeout)
71                .map_err(|e| {
72                    crate::network::map_network_error(e, crate::network::sanitize_url(url))
73                })?
74        };
75        Ok(Self(format_ctx))
76    }
77
78    /// Returns the raw pointer.
79    const fn as_ptr(&self) -> *mut AVFormatContext {
80        self.0
81    }
82
83    /// Consumes the guard and returns the raw pointer without dropping.
84    fn into_raw(self) -> *mut AVFormatContext {
85        let ptr = self.0;
86        std::mem::forget(self);
87        ptr
88    }
89}
90
91impl Drop for AvFormatContextGuard {
92    fn drop(&mut self) {
93        if !self.0.is_null() {
94            // SAFETY: self.0 is valid and owned by this guard
95            unsafe {
96                ff_sys::avformat::close_input(&mut (self.0 as *mut _));
97            }
98        }
99    }
100}
101
102/// RAII guard for `AVCodecContext` to ensure proper cleanup.
103struct AvCodecContextGuard(*mut AVCodecContext);
104
105impl AvCodecContextGuard {
106    /// Creates a new guard by allocating a codec context.
107    ///
108    /// # Safety
109    ///
110    /// Caller must ensure codec pointer is valid.
111    unsafe fn new(codec: *const ff_sys::AVCodec) -> Result<Self, DecodeError> {
112        // SAFETY: Caller ensures codec pointer is valid
113        let codec_ctx = unsafe {
114            ff_sys::avcodec::alloc_context3(codec).map_err(|e| DecodeError::Ffmpeg {
115                code: e,
116                message: format!("Failed to allocate codec context: {e}"),
117            })?
118        };
119        Ok(Self(codec_ctx))
120    }
121
122    /// Returns the raw pointer.
123    const fn as_ptr(&self) -> *mut AVCodecContext {
124        self.0
125    }
126
127    /// Consumes the guard and returns the raw pointer without dropping.
128    fn into_raw(self) -> *mut AVCodecContext {
129        let ptr = self.0;
130        std::mem::forget(self);
131        ptr
132    }
133}
134
135impl Drop for AvCodecContextGuard {
136    fn drop(&mut self) {
137        if !self.0.is_null() {
138            // SAFETY: self.0 is valid and owned by this guard
139            unsafe {
140                ff_sys::avcodec::free_context(&mut (self.0 as *mut _));
141            }
142        }
143    }
144}
145
146/// RAII guard for `AVPacket` to ensure proper cleanup.
147struct AvPacketGuard(*mut AVPacket);
148
149impl AvPacketGuard {
150    /// Creates a new guard by allocating a packet.
151    ///
152    /// # Safety
153    ///
154    /// Must be called after FFmpeg initialization.
155    unsafe fn new() -> Result<Self, DecodeError> {
156        // SAFETY: Caller ensures FFmpeg is initialized
157        let packet = unsafe { ff_sys::av_packet_alloc() };
158        if packet.is_null() {
159            return Err(DecodeError::Ffmpeg {
160                code: 0,
161                message: "Failed to allocate packet".to_string(),
162            });
163        }
164        Ok(Self(packet))
165    }
166
167    /// Consumes the guard and returns the raw pointer without dropping.
168    fn into_raw(self) -> *mut AVPacket {
169        let ptr = self.0;
170        std::mem::forget(self);
171        ptr
172    }
173}
174
175impl Drop for AvPacketGuard {
176    fn drop(&mut self) {
177        if !self.0.is_null() {
178            // SAFETY: self.0 is valid and owned by this guard
179            unsafe {
180                ff_sys::av_packet_free(&mut (self.0 as *mut _));
181            }
182        }
183    }
184}
185
186/// RAII guard for `AVFrame` to ensure proper cleanup.
187struct AvFrameGuard(*mut AVFrame);
188
189impl AvFrameGuard {
190    /// Creates a new guard by allocating a frame.
191    ///
192    /// # Safety
193    ///
194    /// Must be called after FFmpeg initialization.
195    unsafe fn new() -> Result<Self, DecodeError> {
196        // SAFETY: Caller ensures FFmpeg is initialized
197        let frame = unsafe { ff_sys::av_frame_alloc() };
198        if frame.is_null() {
199            return Err(DecodeError::Ffmpeg {
200                code: 0,
201                message: "Failed to allocate frame".to_string(),
202            });
203        }
204        Ok(Self(frame))
205    }
206
207    /// Consumes the guard and returns the raw pointer without dropping.
208    fn into_raw(self) -> *mut AVFrame {
209        let ptr = self.0;
210        std::mem::forget(self);
211        ptr
212    }
213}
214
215impl Drop for AvFrameGuard {
216    fn drop(&mut self) {
217        if !self.0.is_null() {
218            // SAFETY: self.0 is valid and owned by this guard
219            unsafe {
220                ff_sys::av_frame_free(&mut (self.0 as *mut _));
221            }
222        }
223    }
224}
225
226/// Internal decoder state holding FFmpeg contexts.
227///
228/// This structure manages the lifecycle of FFmpeg objects and is responsible
229/// for proper cleanup when dropped.
230pub(crate) struct AudioDecoderInner {
231    /// Format context for reading the media file
232    format_ctx: *mut AVFormatContext,
233    /// Codec context for decoding audio frames
234    codec_ctx: *mut AVCodecContext,
235    /// Audio stream index in the format context
236    stream_index: i32,
237    /// Target output sample format (if conversion is needed)
238    output_format: Option<SampleFormat>,
239    /// Target output sample rate (if resampling is needed)
240    output_sample_rate: Option<u32>,
241    /// Target output channel count (if remixing is needed)
242    output_channels: Option<u32>,
243    /// Cached `SwrContext` — reused across frames to preserve FIR filter state.
244    swr_ctx: Option<resample_inner::SwrContextGuard>,
245    /// Key for the cached context; rebuilt when source or target parameters change.
246    swr_key: Option<resample_inner::SwrKey>,
247    /// Whether the source is a live/streaming input (seeking is not supported)
248    is_live: bool,
249    /// Whether end of file has been reached
250    eof: bool,
251    /// Current playback position
252    position: Duration,
253    /// Reusable packet for reading from file
254    packet: *mut AVPacket,
255    /// Reusable frame for decoding
256    frame: *mut AVFrame,
257    /// URL used to open this source — `None` for file-path sources.
258    url: Option<String>,
259    /// Network options used for the initial open (timeouts, reconnect config).
260    network_opts: NetworkOptions,
261    /// Number of successful reconnects so far (for logging).
262    reconnect_count: u32,
263}
264
265impl AudioDecoderInner {
266    /// Opens a media file and initializes the audio decoder.
267    ///
268    /// # Arguments
269    ///
270    /// * `path` - Path to the media file
271    /// * `output_format` - Optional target sample format for conversion
272    /// * `output_sample_rate` - Optional target sample rate for resampling
273    /// * `output_channels` - Optional target channel count for remixing
274    ///
275    /// # Errors
276    ///
277    /// Returns an error if:
278    /// - The file cannot be opened
279    /// - No audio stream is found
280    /// - The codec is not supported
281    /// - Decoder initialization fails
282    #[allow(clippy::too_many_arguments)]
283    pub(crate) fn new(
284        path: &Path,
285        output_format: Option<SampleFormat>,
286        output_sample_rate: Option<u32>,
287        output_channels: Option<u32>,
288        network_opts: Option<NetworkOptions>,
289    ) -> Result<(Self, AudioStreamInfo, ContainerInfo), DecodeError> {
290        // Ensure FFmpeg is initialized (thread-safe and idempotent)
291        ff_sys::ensure_initialized();
292
293        let path_str = path.to_str().unwrap_or("");
294        let is_network_url = crate::network::is_url(path_str);
295
296        let url = if is_network_url {
297            Some(path_str.to_owned())
298        } else {
299            None
300        };
301        let stored_network_opts = network_opts.clone().unwrap_or_default();
302
303        // Verify SRT availability before attempting to open (feature + runtime check).
304        if is_network_url {
305            crate::network::check_srt_url(path_str)?;
306        }
307
308        // Open the input source (with RAII guard)
309        // SAFETY: Path is valid, AvFormatContextGuard ensures cleanup
310        let format_ctx_guard = unsafe {
311            if is_network_url {
312                let network = network_opts.unwrap_or_default();
313                log::info!(
314                    "opening network audio source url={} connect_timeout_ms={} read_timeout_ms={}",
315                    crate::network::sanitize_url(path_str),
316                    network.connect_timeout.as_millis(),
317                    network.read_timeout.as_millis()
318                );
319                AvFormatContextGuard::new_url(path_str, &network)?
320            } else {
321                AvFormatContextGuard::new(path)?
322            }
323        };
324        let format_ctx = format_ctx_guard.as_ptr();
325
326        // Read stream information
327        // SAFETY: format_ctx is valid and owned by guard
328        unsafe {
329            ff_sys::avformat::find_stream_info(format_ctx).map_err(|e| DecodeError::Ffmpeg {
330                code: e,
331                message: format!("Failed to find stream info: {}", ff_sys::av_error_string(e)),
332            })?;
333        }
334
335        // Detect live/streaming source via the AVFMT_TS_DISCONT flag on AVInputFormat.
336        // SAFETY: format_ctx is valid and non-null; iformat is set by avformat_open_input
337        //         and is non-null for all successfully opened formats.
338        let is_live = unsafe {
339            let iformat = (*format_ctx).iformat;
340            !iformat.is_null() && ((*iformat).flags & ff_sys::AVFMT_TS_DISCONT) != 0
341        };
342
343        // Find the audio stream
344        // SAFETY: format_ctx is valid
345        let (stream_index, codec_id) =
346            unsafe { Self::find_audio_stream(format_ctx) }.ok_or_else(|| {
347                DecodeError::NoAudioStream {
348                    path: path.to_path_buf(),
349                }
350            })?;
351
352        // Find the decoder for this codec
353        // SAFETY: codec_id is valid from FFmpeg
354        let codec_name = unsafe { Self::extract_codec_name(codec_id) };
355        let codec = unsafe {
356            ff_sys::avcodec::find_decoder(codec_id).ok_or_else(|| {
357                DecodeError::UnsupportedCodec {
358                    codec: format!("{codec_name} (codec_id={codec_id:?})"),
359                }
360            })?
361        };
362
363        // Allocate codec context (with RAII guard)
364        // SAFETY: codec pointer is valid, AvCodecContextGuard ensures cleanup
365        let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
366        let codec_ctx = codec_ctx_guard.as_ptr();
367
368        // Copy codec parameters from stream to context
369        // SAFETY: format_ctx and codec_ctx are valid, stream_index is valid
370        unsafe {
371            let stream = (*format_ctx).streams.add(stream_index as usize);
372            let codecpar = (*(*stream)).codecpar;
373            ff_sys::avcodec::parameters_to_context(codec_ctx, codecpar).map_err(|e| {
374                DecodeError::Ffmpeg {
375                    code: e,
376                    message: format!(
377                        "Failed to copy codec parameters: {}",
378                        ff_sys::av_error_string(e)
379                    ),
380                }
381            })?;
382        }
383
384        // Open the codec
385        // SAFETY: codec_ctx and codec are valid
386        unsafe {
387            ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
388                DecodeError::Ffmpeg {
389                    code: e,
390                    message: format!("Failed to open codec: {}", ff_sys::av_error_string(e)),
391                }
392            })?;
393        }
394
395        // Extract stream information
396        // SAFETY: All pointers are valid
397        let stream_info =
398            unsafe { Self::extract_stream_info(format_ctx, stream_index as i32, codec_ctx)? };
399
400        // Extract container information
401        // SAFETY: format_ctx is valid and avformat_find_stream_info has been called
402        let container_info = unsafe { Self::extract_container_info(format_ctx) };
403
404        // Allocate packet and frame (with RAII guards)
405        // SAFETY: FFmpeg is initialized, guards ensure cleanup
406        let packet_guard = unsafe { AvPacketGuard::new()? };
407        let frame_guard = unsafe { AvFrameGuard::new()? };
408
409        // All initialization successful - transfer ownership to AudioDecoderInner
410        Ok((
411            Self {
412                format_ctx: format_ctx_guard.into_raw(),
413                codec_ctx: codec_ctx_guard.into_raw(),
414                stream_index: stream_index as i32,
415                output_format,
416                output_sample_rate,
417                output_channels,
418                swr_ctx: None,
419                swr_key: None,
420                is_live,
421                eof: false,
422                position: Duration::ZERO,
423                packet: packet_guard.into_raw(),
424                frame: frame_guard.into_raw(),
425                url,
426                network_opts: stored_network_opts,
427                reconnect_count: 0,
428            },
429            stream_info,
430            container_info,
431        ))
432    }
433
434    /// Finds the first audio stream in the format context.
435    ///
436    /// # Returns
437    ///
438    /// Returns `Some((index, codec_id))` if an audio stream is found, `None` otherwise.
439    ///
440    /// # Safety
441    ///
442    /// Caller must ensure `format_ctx` is valid and initialized.
443    unsafe fn find_audio_stream(format_ctx: *mut AVFormatContext) -> Option<(usize, AVCodecID)> {
444        // SAFETY: Caller ensures format_ctx is valid
445        unsafe {
446            let nb_streams = (*format_ctx).nb_streams as usize;
447
448            for i in 0..nb_streams {
449                let stream = (*format_ctx).streams.add(i);
450                let codecpar = (*(*stream)).codecpar;
451
452                if (*codecpar).codec_type == AVMediaType_AVMEDIA_TYPE_AUDIO {
453                    return Some((i, (*codecpar).codec_id));
454                }
455            }
456
457            None
458        }
459    }
460
461    /// Returns the human-readable codec name for a given `AVCodecID`.
462    unsafe fn extract_codec_name(codec_id: ff_sys::AVCodecID) -> String {
463        // SAFETY: avcodec_get_name is safe for any codec ID value
464        let name_ptr = unsafe { ff_sys::avcodec_get_name(codec_id) };
465        if name_ptr.is_null() {
466            return String::from("unknown");
467        }
468        // SAFETY: avcodec_get_name returns a valid C string with static lifetime
469        unsafe { CStr::from_ptr(name_ptr).to_string_lossy().into_owned() }
470    }
471
472    /// Extracts audio stream information from FFmpeg structures.
473    unsafe fn extract_stream_info(
474        format_ctx: *mut AVFormatContext,
475        stream_index: i32,
476        codec_ctx: *mut AVCodecContext,
477    ) -> Result<AudioStreamInfo, DecodeError> {
478        // SAFETY: Caller ensures all pointers are valid
479        let (sample_rate, channels, sample_fmt, duration_val, channel_layout, codec_id) = unsafe {
480            let stream = (*format_ctx).streams.add(stream_index as usize);
481            let codecpar = (*(*stream)).codecpar;
482
483            (
484                (*codecpar).sample_rate as u32,
485                (*codecpar).ch_layout.nb_channels as u32,
486                (*codec_ctx).sample_fmt,
487                (*format_ctx).duration,
488                (*codecpar).ch_layout,
489                (*codecpar).codec_id,
490            )
491        };
492
493        // Extract duration
494        let duration = if duration_val > 0 {
495            let duration_secs = duration_val as f64 / 1_000_000.0;
496            Some(Duration::from_secs_f64(duration_secs))
497        } else {
498            None
499        };
500
501        // Extract sample format
502        let sample_format = resample_inner::convert_sample_format(sample_fmt);
503
504        // Extract channel layout
505        let channel_layout_enum = Self::convert_channel_layout(&channel_layout, channels);
506
507        // Extract codec
508        let codec = Self::convert_codec(codec_id);
509        let codec_name = unsafe { Self::extract_codec_name(codec_id) };
510
511        // Build stream info
512        let mut builder = AudioStreamInfo::builder()
513            .index(stream_index as u32)
514            .codec(codec)
515            .codec_name(codec_name)
516            .sample_rate(sample_rate)
517            .channels(channels)
518            .sample_format(sample_format)
519            .channel_layout(channel_layout_enum);
520
521        if let Some(d) = duration {
522            builder = builder.duration(d);
523        }
524
525        Ok(builder.build())
526    }
527
528    /// Extracts container-level information from the `AVFormatContext`.
529    ///
530    /// # Safety
531    ///
532    /// Caller must ensure `format_ctx` is valid and `avformat_find_stream_info` has been called.
533    unsafe fn extract_container_info(format_ctx: *mut AVFormatContext) -> ContainerInfo {
534        // SAFETY: Caller ensures format_ctx is valid
535        unsafe {
536            let format_name = if (*format_ctx).iformat.is_null() {
537                String::new()
538            } else {
539                let ptr = (*(*format_ctx).iformat).name;
540                if ptr.is_null() {
541                    String::new()
542                } else {
543                    CStr::from_ptr(ptr).to_string_lossy().into_owned()
544                }
545            };
546
547            let bit_rate = {
548                let br = (*format_ctx).bit_rate;
549                if br > 0 { Some(br as u64) } else { None }
550            };
551
552            let nb_streams = (*format_ctx).nb_streams as u32;
553
554            let mut builder = ContainerInfo::builder()
555                .format_name(format_name)
556                .nb_streams(nb_streams);
557            if let Some(br) = bit_rate {
558                builder = builder.bit_rate(br);
559            }
560            builder.build()
561        }
562    }
563
564    /// Converts FFmpeg channel layout to our `ChannelLayout` enum.
565    fn convert_channel_layout(layout: &ff_sys::AVChannelLayout, channels: u32) -> ChannelLayout {
566        if layout.order == ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_NATIVE {
567            // SAFETY: When order is AV_CHANNEL_ORDER_NATIVE, the mask field is valid
568            let mask = unsafe { layout.u.mask };
569            match mask {
570                0x4 => ChannelLayout::Mono,
571                0x3 => ChannelLayout::Stereo,
572                0x103 => ChannelLayout::Stereo2_1,
573                0x7 => ChannelLayout::Surround3_0,
574                0x33 => ChannelLayout::Quad,
575                0x37 => ChannelLayout::Surround5_0,
576                0x3F => ChannelLayout::Surround5_1,
577                0x13F => ChannelLayout::Surround6_1,
578                0x63F => ChannelLayout::Surround7_1,
579                _ => {
580                    log::warn!(
581                        "channel_layout mask has no mapping, deriving from channel count \
582                         mask={mask} channels={channels}"
583                    );
584                    ChannelLayout::from_channels(channels)
585                }
586            }
587        } else {
588            log::warn!(
589                "channel_layout order is not NATIVE, deriving from channel count \
590                 order={order} channels={channels}",
591                order = layout.order
592            );
593            ChannelLayout::from_channels(channels)
594        }
595    }
596
597    /// Converts FFmpeg codec ID to our `AudioCodec` enum.
598    fn convert_codec(codec_id: AVCodecID) -> AudioCodec {
599        if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_AAC {
600            AudioCodec::Aac
601        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_MP3 {
602            AudioCodec::Mp3
603        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_OPUS {
604            AudioCodec::Opus
605        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_VORBIS {
606            AudioCodec::Vorbis
607        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_FLAC {
608            AudioCodec::Flac
609        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_PCM_S16LE {
610            AudioCodec::Pcm
611        } else {
612            log::warn!(
613                "audio codec unsupported, falling back to Aac codec_id={codec_id} fallback=Aac"
614            );
615            AudioCodec::Aac
616        }
617    }
618
619    /// Decodes the next audio frame.
620    ///
621    /// Transparently reconnects on `StreamInterrupted` when
622    /// `NetworkOptions::reconnect_on_error` is enabled.
623    ///
624    /// # Returns
625    ///
626    /// - `Ok(Some(frame))` - Successfully decoded a frame
627    /// - `Ok(None)` - End of stream reached
628    /// - `Err(_)` - Decoding error occurred
629    pub(crate) fn decode_one(&mut self) -> Result<Option<AudioFrame>, DecodeError> {
630        loop {
631            match self.decode_one_inner() {
632                Ok(frame) => return Ok(frame),
633                Err(DecodeError::StreamInterrupted { .. })
634                    if self.url.is_some() && self.network_opts.reconnect_on_error =>
635                {
636                    self.attempt_reconnect()?;
637                }
638                Err(e) => return Err(e),
639            }
640        }
641    }
642
643    fn decode_one_inner(&mut self) -> Result<Option<AudioFrame>, DecodeError> {
644        if self.eof {
645            return Ok(None);
646        }
647
648        unsafe {
649            loop {
650                // Try to receive a frame from the decoder
651                let ret = ff_sys::avcodec_receive_frame(self.codec_ctx, self.frame);
652
653                if ret == 0 {
654                    // Successfully received a frame
655                    let audio_frame = resample_inner::convert_frame_to_audio_frame(
656                        self.frame,
657                        self.format_ctx,
658                        self.stream_index,
659                        self.output_format,
660                        self.output_sample_rate,
661                        self.output_channels,
662                        &mut self.swr_ctx,
663                        &mut self.swr_key,
664                    )?;
665
666                    // Update position based on frame timestamp
667                    let pts = (*self.frame).pts;
668                    if pts != ff_sys::AV_NOPTS_VALUE {
669                        let stream = (*self.format_ctx).streams.add(self.stream_index as usize);
670                        let time_base = (*(*stream)).time_base;
671                        let timestamp_secs =
672                            pts as f64 * time_base.num as f64 / time_base.den as f64;
673                        self.position = Duration::from_secs_f64(timestamp_secs);
674                    }
675
676                    return Ok(Some(audio_frame));
677                } else if ret == ff_sys::error_codes::EAGAIN {
678                    // Need to send more packets to the decoder
679                    // Read a packet from the file
680                    let read_ret = ff_sys::av_read_frame(self.format_ctx, self.packet);
681
682                    if read_ret == ff_sys::error_codes::EOF {
683                        // End of file - flush the decoder
684                        ff_sys::avcodec_send_packet(self.codec_ctx, ptr::null());
685                        self.eof = true;
686                        continue;
687                    } else if read_ret < 0 {
688                        return Err(if let Some(url) = &self.url {
689                            // Network source: map to typed variant so reconnect can detect it.
690                            crate::network::map_network_error(
691                                read_ret,
692                                crate::network::sanitize_url(url),
693                            )
694                        } else {
695                            DecodeError::Ffmpeg {
696                                code: read_ret,
697                                message: format!(
698                                    "Failed to read frame: {}",
699                                    ff_sys::av_error_string(read_ret)
700                                ),
701                            }
702                        });
703                    }
704
705                    // Check if this packet belongs to the audio stream
706                    if (*self.packet).stream_index == self.stream_index {
707                        // Send the packet to the decoder
708                        let send_ret = ff_sys::avcodec_send_packet(self.codec_ctx, self.packet);
709                        ff_sys::av_packet_unref(self.packet);
710
711                        if send_ret < 0 && send_ret != ff_sys::error_codes::EAGAIN {
712                            return Err(DecodeError::Ffmpeg {
713                                code: send_ret,
714                                message: format!(
715                                    "Failed to send packet: {}",
716                                    ff_sys::av_error_string(send_ret)
717                                ),
718                            });
719                        }
720                    } else {
721                        // Not our stream, unref and continue
722                        ff_sys::av_packet_unref(self.packet);
723                    }
724                } else if ret == ff_sys::error_codes::EOF {
725                    // Decoder has been fully flushed
726                    self.eof = true;
727                    return Ok(None);
728                } else {
729                    return Err(DecodeError::DecodingFailed {
730                        timestamp: Some(self.position),
731                        reason: ff_sys::av_error_string(ret),
732                    });
733                }
734            }
735        }
736    }
737
738    /// Returns the current playback position.
739    pub(crate) fn position(&self) -> Duration {
740        self.position
741    }
742
743    /// Returns whether end of file has been reached.
744    pub(crate) fn is_eof(&self) -> bool {
745        self.eof
746    }
747
748    /// Returns whether the source is a live or streaming input.
749    ///
750    /// Live sources have the `AVFMT_TS_DISCONT` flag set on their `AVInputFormat`.
751    /// Seeking is not meaningful on live sources.
752    pub(crate) fn is_live(&self) -> bool {
753        self.is_live
754    }
755
756    /// Converts a `Duration` to a presentation timestamp (PTS) in stream time_base units.
757    fn duration_to_pts(&self, duration: Duration) -> i64 {
758        // SAFETY: format_ctx and stream_index are valid (owned by AudioDecoderInner)
759        let time_base = unsafe {
760            let stream = (*self.format_ctx).streams.add(self.stream_index as usize);
761            (*(*stream)).time_base
762        };
763
764        // Convert: duration (seconds) * (time_base.den / time_base.num) = PTS
765        let time_base_f64 = time_base.den as f64 / time_base.num as f64;
766        (duration.as_secs_f64() * time_base_f64) as i64
767    }
768
769    /// Seeks to a specified position in the audio stream.
770    ///
771    /// # Arguments
772    ///
773    /// * `position` - Target position to seek to.
774    /// * `mode` - Seek mode (Keyframe, Exact, or Backward).
775    ///
776    /// # Errors
777    ///
778    /// Returns [`DecodeError::SeekFailed`] if the seek operation fails.
779    pub(crate) fn seek(
780        &mut self,
781        position: Duration,
782        mode: crate::SeekMode,
783    ) -> Result<(), DecodeError> {
784        use crate::SeekMode;
785
786        let timestamp = self.duration_to_pts(position);
787        let flags = ff_sys::avformat::seek_flags::BACKWARD;
788
789        // 1. Clear any pending packet and frame
790        // SAFETY: packet and frame are valid (owned by AudioDecoderInner)
791        unsafe {
792            ff_sys::av_packet_unref(self.packet);
793            ff_sys::av_frame_unref(self.frame);
794        }
795
796        // 2. Seek in the format context
797        // SAFETY: format_ctx and stream_index are valid
798        unsafe {
799            ff_sys::avformat::seek_frame(self.format_ctx, self.stream_index, timestamp, flags)
800                .map_err(|e| DecodeError::SeekFailed {
801                    target: position,
802                    reason: ff_sys::av_error_string(e),
803                })?;
804        }
805
806        // 3. Flush decoder buffers and reset the cached SwrContext so the
807        //    resampler does not carry stale delay samples across the seek point.
808        // SAFETY: codec_ctx is valid (owned by AudioDecoderInner)
809        unsafe {
810            ff_sys::avcodec::flush_buffers(self.codec_ctx);
811        }
812        self.swr_ctx = None;
813        self.swr_key = None;
814
815        // 4. Drain any remaining frames from the decoder after flush
816        // SAFETY: codec_ctx and frame are valid
817        unsafe {
818            loop {
819                let ret = ff_sys::avcodec_receive_frame(self.codec_ctx, self.frame);
820                if ret == ff_sys::error_codes::EAGAIN || ret == ff_sys::error_codes::EOF {
821                    break;
822                } else if ret == 0 {
823                    ff_sys::av_frame_unref(self.frame);
824                } else {
825                    break;
826                }
827            }
828        }
829
830        // 5. Reset internal state
831        self.eof = false;
832
833        // 6. For exact mode, skip frames to reach exact position
834        if mode == SeekMode::Exact {
835            self.skip_to_exact(position)?;
836        }
837        // For Keyframe/Backward modes, we're already at the keyframe after av_seek_frame
838
839        Ok(())
840    }
841
842    /// Skips frames until reaching the exact target position.
843    ///
844    /// This is used by [`Self::seek`] when `SeekMode::Exact` is specified.
845    ///
846    /// # Arguments
847    ///
848    /// * `target` - The exact target position.
849    fn skip_to_exact(&mut self, target: Duration) -> Result<(), DecodeError> {
850        // Decode frames until we reach or pass the target
851        while let Some(frame) = self.decode_one()? {
852            let frame_time = frame.timestamp().as_duration();
853            if frame_time >= target {
854                // We've reached the target position
855                break;
856            }
857            // Continue decoding to get closer (frames are automatically dropped)
858        }
859        Ok(())
860    }
861
862    /// Flushes the decoder's internal buffers.
863    pub(crate) fn flush(&mut self) {
864        // SAFETY: codec_ctx is valid and owned by this instance
865        unsafe {
866            ff_sys::avcodec::flush_buffers(self.codec_ctx);
867        }
868        self.eof = false;
869    }
870
871    // ── Reconnect helpers ─────────────────────────────────────────────────────
872
873    /// Attempts to reconnect to the stream URL using exponential backoff.
874    ///
875    /// Called from `decode_one()` when `StreamInterrupted` is received and
876    /// `NetworkOptions::reconnect_on_error` is `true`. After all attempts fail,
877    /// returns a `StreamInterrupted` error.
878    fn attempt_reconnect(&mut self) -> Result<(), DecodeError> {
879        let url = match self.url.as_deref() {
880            Some(u) => u.to_owned(),
881            None => return Ok(()), // file-path source: no reconnect
882        };
883        let max = self.network_opts.max_reconnect_attempts;
884
885        for attempt in 1..=max {
886            let backoff_ms = 100u64 * (1u64 << (attempt - 1).min(10));
887            log::warn!(
888                "reconnecting attempt={attempt} url={} backoff_ms={backoff_ms}",
889                crate::network::sanitize_url(&url)
890            );
891            std::thread::sleep(Duration::from_millis(backoff_ms));
892            match self.reopen(&url) {
893                Ok(()) => {
894                    self.reconnect_count += 1;
895                    log::info!(
896                        "reconnected attempt={attempt} url={} total_reconnects={}",
897                        crate::network::sanitize_url(&url),
898                        self.reconnect_count
899                    );
900                    return Ok(());
901                }
902                Err(e) => log::warn!("reconnect attempt={attempt} failed err={e}"),
903            }
904        }
905
906        Err(DecodeError::StreamInterrupted {
907            code: 0,
908            endpoint: crate::network::sanitize_url(&url),
909            message: format!("stream did not recover after {max} attempts"),
910        })
911    }
912
913    /// Closes the current `AVFormatContext`, re-opens the URL, re-reads stream info,
914    /// re-finds the audio stream, and flushes the codec.
915    fn reopen(&mut self, url: &str) -> Result<(), DecodeError> {
916        // Close the current format context. `avformat_close_input` sets the pointer
917        // to null — this matches the null check in Drop so no double-free occurs.
918        // SAFETY: self.format_ctx is valid and owned exclusively by self.
919        unsafe {
920            ff_sys::avformat::close_input(std::ptr::addr_of_mut!(self.format_ctx));
921        }
922
923        // Re-open the URL with the stored network timeouts.
924        // SAFETY: url is a valid UTF-8 network URL string.
925        self.format_ctx = unsafe {
926            ff_sys::avformat::open_input_url(
927                url,
928                self.network_opts.connect_timeout,
929                self.network_opts.read_timeout,
930            )
931            .map_err(|e| crate::network::map_network_error(e, crate::network::sanitize_url(url)))?
932        };
933
934        // Re-read stream information.
935        // SAFETY: self.format_ctx is valid and freshly opened.
936        unsafe {
937            ff_sys::avformat::find_stream_info(self.format_ctx).map_err(|e| {
938                DecodeError::Ffmpeg {
939                    code: e,
940                    message: format!(
941                        "reconnect find_stream_info failed: {}",
942                        ff_sys::av_error_string(e)
943                    ),
944                }
945            })?;
946        }
947
948        // Re-find the audio stream (index may differ in theory after reconnect).
949        // SAFETY: self.format_ctx is valid.
950        let (stream_index, _) = unsafe { Self::find_audio_stream(self.format_ctx) }
951            .ok_or_else(|| DecodeError::NoAudioStream { path: url.into() })?;
952        self.stream_index = stream_index as i32;
953
954        // Flush codec buffers to discard stale decoded state from before the drop.
955        // SAFETY: self.codec_ctx is valid and has not been freed.
956        unsafe {
957            ff_sys::avcodec::flush_buffers(self.codec_ctx);
958        }
959
960        self.eof = false;
961        Ok(())
962    }
963}
964
965impl Drop for AudioDecoderInner {
966    fn drop(&mut self) {
967        // Free frame and packet
968        if !self.frame.is_null() {
969            // SAFETY: self.frame is valid and owned by this instance
970            unsafe {
971                ff_sys::av_frame_free(&mut (self.frame as *mut _));
972            }
973        }
974
975        if !self.packet.is_null() {
976            // SAFETY: self.packet is valid and owned by this instance
977            unsafe {
978                ff_sys::av_packet_free(&mut (self.packet as *mut _));
979            }
980        }
981
982        // Free codec context
983        if !self.codec_ctx.is_null() {
984            // SAFETY: self.codec_ctx is valid and owned by this instance
985            unsafe {
986                ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
987            }
988        }
989
990        // Close format context
991        if !self.format_ctx.is_null() {
992            // SAFETY: self.format_ctx is valid and owned by this instance
993            unsafe {
994                ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
995            }
996        }
997    }
998}
999
1000// SAFETY: AudioDecoderInner manages FFmpeg contexts which are thread-safe when not shared.
1001// We don't expose mutable access across threads, so Send is safe.
1002unsafe impl Send for AudioDecoderInner {}
1003
1004#[cfg(test)]
1005#[allow(unsafe_code)]
1006mod tests {
1007    use ff_format::channel::ChannelLayout;
1008
1009    use super::AudioDecoderInner;
1010
1011    /// Constructs an `AVChannelLayout` with `AV_CHANNEL_ORDER_NATIVE` and the given mask.
1012    fn native_layout(mask: u64, nb_channels: i32) -> ff_sys::AVChannelLayout {
1013        ff_sys::AVChannelLayout {
1014            order: ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_NATIVE,
1015            nb_channels,
1016            u: ff_sys::AVChannelLayout__bindgen_ty_1 { mask },
1017            opaque: std::ptr::null_mut(),
1018        }
1019    }
1020
1021    /// Constructs an `AVChannelLayout` with `AV_CHANNEL_ORDER_UNSPEC`.
1022    fn unspec_layout(nb_channels: i32) -> ff_sys::AVChannelLayout {
1023        ff_sys::AVChannelLayout {
1024            order: ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_UNSPEC,
1025            nb_channels,
1026            u: ff_sys::AVChannelLayout__bindgen_ty_1 { mask: 0 },
1027            opaque: std::ptr::null_mut(),
1028        }
1029    }
1030
1031    #[test]
1032    fn native_mask_mono() {
1033        let layout = native_layout(0x4, 1);
1034        assert_eq!(
1035            AudioDecoderInner::convert_channel_layout(&layout, 1),
1036            ChannelLayout::Mono
1037        );
1038    }
1039
1040    #[test]
1041    fn native_mask_stereo() {
1042        let layout = native_layout(0x3, 2);
1043        assert_eq!(
1044            AudioDecoderInner::convert_channel_layout(&layout, 2),
1045            ChannelLayout::Stereo
1046        );
1047    }
1048
1049    #[test]
1050    fn native_mask_stereo2_1() {
1051        let layout = native_layout(0x103, 3);
1052        assert_eq!(
1053            AudioDecoderInner::convert_channel_layout(&layout, 3),
1054            ChannelLayout::Stereo2_1
1055        );
1056    }
1057
1058    #[test]
1059    fn native_mask_surround3_0() {
1060        let layout = native_layout(0x7, 3);
1061        assert_eq!(
1062            AudioDecoderInner::convert_channel_layout(&layout, 3),
1063            ChannelLayout::Surround3_0
1064        );
1065    }
1066
1067    #[test]
1068    fn native_mask_quad() {
1069        let layout = native_layout(0x33, 4);
1070        assert_eq!(
1071            AudioDecoderInner::convert_channel_layout(&layout, 4),
1072            ChannelLayout::Quad
1073        );
1074    }
1075
1076    #[test]
1077    fn native_mask_surround5_0() {
1078        let layout = native_layout(0x37, 5);
1079        assert_eq!(
1080            AudioDecoderInner::convert_channel_layout(&layout, 5),
1081            ChannelLayout::Surround5_0
1082        );
1083    }
1084
1085    #[test]
1086    fn native_mask_surround5_1() {
1087        let layout = native_layout(0x3F, 6);
1088        assert_eq!(
1089            AudioDecoderInner::convert_channel_layout(&layout, 6),
1090            ChannelLayout::Surround5_1
1091        );
1092    }
1093
1094    #[test]
1095    fn native_mask_surround6_1() {
1096        let layout = native_layout(0x13F, 7);
1097        assert_eq!(
1098            AudioDecoderInner::convert_channel_layout(&layout, 7),
1099            ChannelLayout::Surround6_1
1100        );
1101    }
1102
1103    #[test]
1104    fn native_mask_surround7_1() {
1105        let layout = native_layout(0x63F, 8);
1106        assert_eq!(
1107            AudioDecoderInner::convert_channel_layout(&layout, 8),
1108            ChannelLayout::Surround7_1
1109        );
1110    }
1111
1112    #[test]
1113    fn native_mask_unknown_falls_back_to_from_channels() {
1114        // mask=0x1 is not a standard layout; should fall back to from_channels(2)
1115        let layout = native_layout(0x1, 2);
1116        assert_eq!(
1117            AudioDecoderInner::convert_channel_layout(&layout, 2),
1118            ChannelLayout::from_channels(2)
1119        );
1120    }
1121
1122    #[test]
1123    fn non_native_order_falls_back_to_from_channels() {
1124        let layout = unspec_layout(6);
1125        assert_eq!(
1126            AudioDecoderInner::convert_channel_layout(&layout, 6),
1127            ChannelLayout::from_channels(6)
1128        );
1129    }
1130
1131    // -------------------------------------------------------------------------
1132    // extract_codec_name
1133    // -------------------------------------------------------------------------
1134
1135    #[test]
1136    fn codec_name_should_return_h264_for_h264_codec_id() {
1137        let name =
1138            unsafe { AudioDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_H264) };
1139        assert_eq!(name, "h264");
1140    }
1141
1142    #[test]
1143    fn codec_name_should_return_none_for_none_codec_id() {
1144        let name =
1145            unsafe { AudioDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_NONE) };
1146        assert_eq!(name, "none");
1147    }
1148
1149    #[test]
1150    fn unsupported_codec_error_should_include_codec_name() {
1151        let codec_id = ff_sys::AVCodecID_AV_CODEC_ID_MP3;
1152        let codec_name = unsafe { AudioDecoderInner::extract_codec_name(codec_id) };
1153        let error = crate::error::DecodeError::UnsupportedCodec {
1154            codec: format!("{codec_name} (codec_id={codec_id:?})"),
1155        };
1156        let msg = error.to_string();
1157        assert!(msg.contains("mp3"), "expected codec name in error: {msg}");
1158        assert!(
1159            msg.contains("codec_id="),
1160            "expected codec_id in error: {msg}"
1161        );
1162    }
1163}