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::time::{Rational, Timestamp};
34use ff_format::{AudioFrame, AudioStreamInfo, NetworkOptions, SampleFormat};
35use ff_sys::{
36    AVCodecContext, AVCodecID, AVFormatContext, AVFrame, AVMediaType_AVMEDIA_TYPE_AUDIO, AVPacket,
37    AVSampleFormat, SwrContext,
38};
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/// RAII guard for `SwrContext` to ensure proper cleanup.
227struct SwrContextGuard(*mut SwrContext);
228
229impl SwrContextGuard {
230    /// Consumes the guard and returns the raw pointer without dropping.
231    #[allow(dead_code)]
232    fn into_raw(self) -> *mut SwrContext {
233        let ptr = self.0;
234        std::mem::forget(self);
235        ptr
236    }
237}
238
239impl Drop for SwrContextGuard {
240    fn drop(&mut self) {
241        if !self.0.is_null() {
242            // SAFETY: self.0 is valid and owned by this guard
243            unsafe {
244                ff_sys::swr_free(&mut (self.0 as *mut _));
245            }
246        }
247    }
248}
249
250/// Internal decoder state holding FFmpeg contexts.
251///
252/// This structure manages the lifecycle of FFmpeg objects and is responsible
253/// for proper cleanup when dropped.
254pub(crate) struct AudioDecoderInner {
255    /// Format context for reading the media file
256    format_ctx: *mut AVFormatContext,
257    /// Codec context for decoding audio frames
258    codec_ctx: *mut AVCodecContext,
259    /// Audio stream index in the format context
260    stream_index: i32,
261    /// SwResample context for sample format conversion (optional)
262    swr_ctx: Option<*mut SwrContext>,
263    /// Target output sample format (if conversion is needed)
264    output_format: Option<SampleFormat>,
265    /// Target output sample rate (if resampling is needed)
266    output_sample_rate: Option<u32>,
267    /// Target output channel count (if remixing is needed)
268    output_channels: Option<u32>,
269    /// Whether the source is a live/streaming input (seeking is not supported)
270    is_live: bool,
271    /// Whether end of file has been reached
272    eof: bool,
273    /// Current playback position
274    position: Duration,
275    /// Reusable packet for reading from file
276    packet: *mut AVPacket,
277    /// Reusable frame for decoding
278    frame: *mut AVFrame,
279    /// URL used to open this source — `None` for file-path sources.
280    url: Option<String>,
281    /// Network options used for the initial open (timeouts, reconnect config).
282    network_opts: NetworkOptions,
283    /// Number of successful reconnects so far (for logging).
284    reconnect_count: u32,
285}
286
287impl AudioDecoderInner {
288    /// Opens a media file and initializes the audio decoder.
289    ///
290    /// # Arguments
291    ///
292    /// * `path` - Path to the media file
293    /// * `output_format` - Optional target sample format for conversion
294    /// * `output_sample_rate` - Optional target sample rate for resampling
295    /// * `output_channels` - Optional target channel count for remixing
296    ///
297    /// # Errors
298    ///
299    /// Returns an error if:
300    /// - The file cannot be opened
301    /// - No audio stream is found
302    /// - The codec is not supported
303    /// - Decoder initialization fails
304    #[allow(clippy::too_many_arguments)]
305    pub(crate) fn new(
306        path: &Path,
307        output_format: Option<SampleFormat>,
308        output_sample_rate: Option<u32>,
309        output_channels: Option<u32>,
310        network_opts: Option<NetworkOptions>,
311    ) -> Result<(Self, AudioStreamInfo, ContainerInfo), DecodeError> {
312        // Ensure FFmpeg is initialized (thread-safe and idempotent)
313        ff_sys::ensure_initialized();
314
315        let path_str = path.to_str().unwrap_or("");
316        let is_network_url = crate::network::is_url(path_str);
317
318        let url = if is_network_url {
319            Some(path_str.to_owned())
320        } else {
321            None
322        };
323        let stored_network_opts = network_opts.clone().unwrap_or_default();
324
325        // Verify SRT availability before attempting to open (feature + runtime check).
326        if is_network_url {
327            crate::network::check_srt_url(path_str)?;
328        }
329
330        // Open the input source (with RAII guard)
331        // SAFETY: Path is valid, AvFormatContextGuard ensures cleanup
332        let format_ctx_guard = unsafe {
333            if is_network_url {
334                let network = network_opts.unwrap_or_default();
335                log::info!(
336                    "opening network audio source url={} connect_timeout_ms={} read_timeout_ms={}",
337                    crate::network::sanitize_url(path_str),
338                    network.connect_timeout.as_millis(),
339                    network.read_timeout.as_millis()
340                );
341                AvFormatContextGuard::new_url(path_str, &network)?
342            } else {
343                AvFormatContextGuard::new(path)?
344            }
345        };
346        let format_ctx = format_ctx_guard.as_ptr();
347
348        // Read stream information
349        // SAFETY: format_ctx is valid and owned by guard
350        unsafe {
351            ff_sys::avformat::find_stream_info(format_ctx).map_err(|e| DecodeError::Ffmpeg {
352                code: e,
353                message: format!("Failed to find stream info: {}", ff_sys::av_error_string(e)),
354            })?;
355        }
356
357        // Detect live/streaming source via the AVFMT_TS_DISCONT flag on AVInputFormat.
358        // SAFETY: format_ctx is valid and non-null; iformat is set by avformat_open_input
359        //         and is non-null for all successfully opened formats.
360        let is_live = unsafe {
361            let iformat = (*format_ctx).iformat;
362            !iformat.is_null() && ((*iformat).flags & ff_sys::AVFMT_TS_DISCONT) != 0
363        };
364
365        // Find the audio stream
366        // SAFETY: format_ctx is valid
367        let (stream_index, codec_id) =
368            unsafe { Self::find_audio_stream(format_ctx) }.ok_or_else(|| {
369                DecodeError::NoAudioStream {
370                    path: path.to_path_buf(),
371                }
372            })?;
373
374        // Find the decoder for this codec
375        // SAFETY: codec_id is valid from FFmpeg
376        let codec_name = unsafe { Self::extract_codec_name(codec_id) };
377        let codec = unsafe {
378            ff_sys::avcodec::find_decoder(codec_id).ok_or_else(|| {
379                DecodeError::UnsupportedCodec {
380                    codec: format!("{codec_name} (codec_id={codec_id:?})"),
381                }
382            })?
383        };
384
385        // Allocate codec context (with RAII guard)
386        // SAFETY: codec pointer is valid, AvCodecContextGuard ensures cleanup
387        let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
388        let codec_ctx = codec_ctx_guard.as_ptr();
389
390        // Copy codec parameters from stream to context
391        // SAFETY: format_ctx and codec_ctx are valid, stream_index is valid
392        unsafe {
393            let stream = (*format_ctx).streams.add(stream_index as usize);
394            let codecpar = (*(*stream)).codecpar;
395            ff_sys::avcodec::parameters_to_context(codec_ctx, codecpar).map_err(|e| {
396                DecodeError::Ffmpeg {
397                    code: e,
398                    message: format!(
399                        "Failed to copy codec parameters: {}",
400                        ff_sys::av_error_string(e)
401                    ),
402                }
403            })?;
404        }
405
406        // Open the codec
407        // SAFETY: codec_ctx and codec are valid
408        unsafe {
409            ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
410                DecodeError::Ffmpeg {
411                    code: e,
412                    message: format!("Failed to open codec: {}", ff_sys::av_error_string(e)),
413                }
414            })?;
415        }
416
417        // Extract stream information
418        // SAFETY: All pointers are valid
419        let stream_info =
420            unsafe { Self::extract_stream_info(format_ctx, stream_index as i32, codec_ctx)? };
421
422        // Extract container information
423        // SAFETY: format_ctx is valid and avformat_find_stream_info has been called
424        let container_info = unsafe { Self::extract_container_info(format_ctx) };
425
426        // Allocate packet and frame (with RAII guards)
427        // SAFETY: FFmpeg is initialized, guards ensure cleanup
428        let packet_guard = unsafe { AvPacketGuard::new()? };
429        let frame_guard = unsafe { AvFrameGuard::new()? };
430
431        // All initialization successful - transfer ownership to AudioDecoderInner
432        Ok((
433            Self {
434                format_ctx: format_ctx_guard.into_raw(),
435                codec_ctx: codec_ctx_guard.into_raw(),
436                stream_index: stream_index as i32,
437                swr_ctx: None,
438                output_format,
439                output_sample_rate,
440                output_channels,
441                is_live,
442                eof: false,
443                position: Duration::ZERO,
444                packet: packet_guard.into_raw(),
445                frame: frame_guard.into_raw(),
446                url,
447                network_opts: stored_network_opts,
448                reconnect_count: 0,
449            },
450            stream_info,
451            container_info,
452        ))
453    }
454
455    /// Finds the first audio stream in the format context.
456    ///
457    /// # Returns
458    ///
459    /// Returns `Some((index, codec_id))` if an audio stream is found, `None` otherwise.
460    ///
461    /// # Safety
462    ///
463    /// Caller must ensure `format_ctx` is valid and initialized.
464    unsafe fn find_audio_stream(format_ctx: *mut AVFormatContext) -> Option<(usize, AVCodecID)> {
465        // SAFETY: Caller ensures format_ctx is valid
466        unsafe {
467            let nb_streams = (*format_ctx).nb_streams as usize;
468
469            for i in 0..nb_streams {
470                let stream = (*format_ctx).streams.add(i);
471                let codecpar = (*(*stream)).codecpar;
472
473                if (*codecpar).codec_type == AVMediaType_AVMEDIA_TYPE_AUDIO {
474                    return Some((i, (*codecpar).codec_id));
475                }
476            }
477
478            None
479        }
480    }
481
482    /// Returns the human-readable codec name for a given `AVCodecID`.
483    unsafe fn extract_codec_name(codec_id: ff_sys::AVCodecID) -> String {
484        // SAFETY: avcodec_get_name is safe for any codec ID value
485        let name_ptr = unsafe { ff_sys::avcodec_get_name(codec_id) };
486        if name_ptr.is_null() {
487            return String::from("unknown");
488        }
489        // SAFETY: avcodec_get_name returns a valid C string with static lifetime
490        unsafe { CStr::from_ptr(name_ptr).to_string_lossy().into_owned() }
491    }
492
493    /// Extracts audio stream information from FFmpeg structures.
494    unsafe fn extract_stream_info(
495        format_ctx: *mut AVFormatContext,
496        stream_index: i32,
497        codec_ctx: *mut AVCodecContext,
498    ) -> Result<AudioStreamInfo, DecodeError> {
499        // SAFETY: Caller ensures all pointers are valid
500        let (sample_rate, channels, sample_fmt, duration_val, channel_layout, codec_id) = unsafe {
501            let stream = (*format_ctx).streams.add(stream_index as usize);
502            let codecpar = (*(*stream)).codecpar;
503
504            (
505                (*codecpar).sample_rate as u32,
506                (*codecpar).ch_layout.nb_channels as u32,
507                (*codec_ctx).sample_fmt,
508                (*format_ctx).duration,
509                (*codecpar).ch_layout,
510                (*codecpar).codec_id,
511            )
512        };
513
514        // Extract duration
515        let duration = if duration_val > 0 {
516            let duration_secs = duration_val as f64 / 1_000_000.0;
517            Some(Duration::from_secs_f64(duration_secs))
518        } else {
519            None
520        };
521
522        // Extract sample format
523        let sample_format = Self::convert_sample_format(sample_fmt);
524
525        // Extract channel layout
526        let channel_layout_enum = Self::convert_channel_layout(&channel_layout, channels);
527
528        // Extract codec
529        let codec = Self::convert_codec(codec_id);
530        let codec_name = unsafe { Self::extract_codec_name(codec_id) };
531
532        // Build stream info
533        let mut builder = AudioStreamInfo::builder()
534            .index(stream_index as u32)
535            .codec(codec)
536            .codec_name(codec_name)
537            .sample_rate(sample_rate)
538            .channels(channels)
539            .sample_format(sample_format)
540            .channel_layout(channel_layout_enum);
541
542        if let Some(d) = duration {
543            builder = builder.duration(d);
544        }
545
546        Ok(builder.build())
547    }
548
549    /// Extracts container-level information from the `AVFormatContext`.
550    ///
551    /// # Safety
552    ///
553    /// Caller must ensure `format_ctx` is valid and `avformat_find_stream_info` has been called.
554    unsafe fn extract_container_info(format_ctx: *mut AVFormatContext) -> ContainerInfo {
555        // SAFETY: Caller ensures format_ctx is valid
556        unsafe {
557            let format_name = if (*format_ctx).iformat.is_null() {
558                String::new()
559            } else {
560                let ptr = (*(*format_ctx).iformat).name;
561                if ptr.is_null() {
562                    String::new()
563                } else {
564                    CStr::from_ptr(ptr).to_string_lossy().into_owned()
565                }
566            };
567
568            let bit_rate = {
569                let br = (*format_ctx).bit_rate;
570                if br > 0 { Some(br as u64) } else { None }
571            };
572
573            let nb_streams = (*format_ctx).nb_streams as u32;
574
575            let mut builder = ContainerInfo::builder()
576                .format_name(format_name)
577                .nb_streams(nb_streams);
578            if let Some(br) = bit_rate {
579                builder = builder.bit_rate(br);
580            }
581            builder.build()
582        }
583    }
584
585    /// Converts FFmpeg sample format to our `SampleFormat` enum.
586    fn convert_sample_format(fmt: AVSampleFormat) -> SampleFormat {
587        if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8 {
588            SampleFormat::U8
589        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16 {
590            SampleFormat::I16
591        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32 {
592            SampleFormat::I32
593        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLT {
594            SampleFormat::F32
595        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBL {
596            SampleFormat::F64
597        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8P {
598            SampleFormat::U8p
599        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16P {
600            SampleFormat::I16p
601        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32P {
602            SampleFormat::I32p
603        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLTP {
604            SampleFormat::F32p
605        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBLP {
606            SampleFormat::F64p
607        } else {
608            log::warn!(
609                "sample_format unsupported, falling back to F32 requested={fmt} fallback=F32"
610            );
611            SampleFormat::F32
612        }
613    }
614
615    /// Converts FFmpeg channel layout to our `ChannelLayout` enum.
616    fn convert_channel_layout(layout: &ff_sys::AVChannelLayout, channels: u32) -> ChannelLayout {
617        if layout.order == ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_NATIVE {
618            // SAFETY: When order is AV_CHANNEL_ORDER_NATIVE, the mask field is valid
619            let mask = unsafe { layout.u.mask };
620            match mask {
621                0x4 => ChannelLayout::Mono,
622                0x3 => ChannelLayout::Stereo,
623                0x103 => ChannelLayout::Stereo2_1,
624                0x7 => ChannelLayout::Surround3_0,
625                0x33 => ChannelLayout::Quad,
626                0x37 => ChannelLayout::Surround5_0,
627                0x3F => ChannelLayout::Surround5_1,
628                0x13F => ChannelLayout::Surround6_1,
629                0x63F => ChannelLayout::Surround7_1,
630                _ => {
631                    log::warn!(
632                        "channel_layout mask has no mapping, deriving from channel count \
633                         mask={mask} channels={channels}"
634                    );
635                    ChannelLayout::from_channels(channels)
636                }
637            }
638        } else {
639            log::warn!(
640                "channel_layout order is not NATIVE, deriving from channel count \
641                 order={order} channels={channels}",
642                order = layout.order
643            );
644            ChannelLayout::from_channels(channels)
645        }
646    }
647
648    /// Creates an `AVChannelLayout` from channel count.
649    ///
650    /// # Safety
651    ///
652    /// The returned layout must be freed with `av_channel_layout_uninit`.
653    unsafe fn create_channel_layout(channels: u32) -> ff_sys::AVChannelLayout {
654        // SAFETY: Zeroing AVChannelLayout is safe
655        let mut layout = unsafe { std::mem::zeroed::<ff_sys::AVChannelLayout>() };
656        // SAFETY: Caller ensures proper cleanup
657        unsafe {
658            ff_sys::av_channel_layout_default(&raw mut layout, channels as i32);
659        }
660        layout
661    }
662
663    /// Converts FFmpeg codec ID to our `AudioCodec` enum.
664    fn convert_codec(codec_id: AVCodecID) -> AudioCodec {
665        if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_AAC {
666            AudioCodec::Aac
667        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_MP3 {
668            AudioCodec::Mp3
669        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_OPUS {
670            AudioCodec::Opus
671        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_VORBIS {
672            AudioCodec::Vorbis
673        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_FLAC {
674            AudioCodec::Flac
675        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_PCM_S16LE {
676            AudioCodec::Pcm
677        } else {
678            log::warn!(
679                "audio codec unsupported, falling back to Aac codec_id={codec_id} fallback=Aac"
680            );
681            AudioCodec::Aac
682        }
683    }
684
685    /// Decodes the next audio frame.
686    ///
687    /// Transparently reconnects on `StreamInterrupted` when
688    /// `NetworkOptions::reconnect_on_error` is enabled.
689    ///
690    /// # Returns
691    ///
692    /// - `Ok(Some(frame))` - Successfully decoded a frame
693    /// - `Ok(None)` - End of stream reached
694    /// - `Err(_)` - Decoding error occurred
695    pub(crate) fn decode_one(&mut self) -> Result<Option<AudioFrame>, DecodeError> {
696        loop {
697            match self.decode_one_inner() {
698                Ok(frame) => return Ok(frame),
699                Err(DecodeError::StreamInterrupted { .. })
700                    if self.url.is_some() && self.network_opts.reconnect_on_error =>
701                {
702                    self.attempt_reconnect()?;
703                }
704                Err(e) => return Err(e),
705            }
706        }
707    }
708
709    fn decode_one_inner(&mut self) -> Result<Option<AudioFrame>, DecodeError> {
710        if self.eof {
711            return Ok(None);
712        }
713
714        unsafe {
715            loop {
716                // Try to receive a frame from the decoder
717                let ret = ff_sys::avcodec_receive_frame(self.codec_ctx, self.frame);
718
719                if ret == 0 {
720                    // Successfully received a frame
721                    let audio_frame = self.convert_frame_to_audio_frame()?;
722
723                    // Update position based on frame timestamp
724                    let pts = (*self.frame).pts;
725                    if pts != ff_sys::AV_NOPTS_VALUE {
726                        let stream = (*self.format_ctx).streams.add(self.stream_index as usize);
727                        let time_base = (*(*stream)).time_base;
728                        let timestamp_secs =
729                            pts as f64 * time_base.num as f64 / time_base.den as f64;
730                        self.position = Duration::from_secs_f64(timestamp_secs);
731                    }
732
733                    return Ok(Some(audio_frame));
734                } else if ret == ff_sys::error_codes::EAGAIN {
735                    // Need to send more packets to the decoder
736                    // Read a packet from the file
737                    let read_ret = ff_sys::av_read_frame(self.format_ctx, self.packet);
738
739                    if read_ret == ff_sys::error_codes::EOF {
740                        // End of file - flush the decoder
741                        ff_sys::avcodec_send_packet(self.codec_ctx, ptr::null());
742                        self.eof = true;
743                        continue;
744                    } else if read_ret < 0 {
745                        return Err(if let Some(url) = &self.url {
746                            // Network source: map to typed variant so reconnect can detect it.
747                            crate::network::map_network_error(
748                                read_ret,
749                                crate::network::sanitize_url(url),
750                            )
751                        } else {
752                            DecodeError::Ffmpeg {
753                                code: read_ret,
754                                message: format!(
755                                    "Failed to read frame: {}",
756                                    ff_sys::av_error_string(read_ret)
757                                ),
758                            }
759                        });
760                    }
761
762                    // Check if this packet belongs to the audio stream
763                    if (*self.packet).stream_index == self.stream_index {
764                        // Send the packet to the decoder
765                        let send_ret = ff_sys::avcodec_send_packet(self.codec_ctx, self.packet);
766                        ff_sys::av_packet_unref(self.packet);
767
768                        if send_ret < 0 && send_ret != ff_sys::error_codes::EAGAIN {
769                            return Err(DecodeError::Ffmpeg {
770                                code: send_ret,
771                                message: format!(
772                                    "Failed to send packet: {}",
773                                    ff_sys::av_error_string(send_ret)
774                                ),
775                            });
776                        }
777                    } else {
778                        // Not our stream, unref and continue
779                        ff_sys::av_packet_unref(self.packet);
780                    }
781                } else if ret == ff_sys::error_codes::EOF {
782                    // Decoder has been fully flushed
783                    self.eof = true;
784                    return Ok(None);
785                } else {
786                    return Err(DecodeError::DecodingFailed {
787                        timestamp: Some(self.position),
788                        reason: ff_sys::av_error_string(ret),
789                    });
790                }
791            }
792        }
793    }
794
795    /// Converts an AVFrame to an AudioFrame, applying sample format conversion if needed.
796    unsafe fn convert_frame_to_audio_frame(&mut self) -> Result<AudioFrame, DecodeError> {
797        // SAFETY: Caller ensures self.frame is valid
798        unsafe {
799            let nb_samples = (*self.frame).nb_samples as usize;
800            let channels = (*self.frame).ch_layout.nb_channels as u32;
801            let sample_rate = (*self.frame).sample_rate as u32;
802            let src_format = (*self.frame).format;
803
804            // Determine if we need conversion
805            let needs_conversion = self.output_format.is_some()
806                || self.output_sample_rate.is_some()
807                || self.output_channels.is_some();
808
809            if needs_conversion {
810                self.convert_with_swr(nb_samples, channels, sample_rate, src_format)
811            } else {
812                self.av_frame_to_audio_frame(self.frame)
813            }
814        }
815    }
816
817    /// Converts sample format/rate/channels using SwResample.
818    unsafe fn convert_with_swr(
819        &mut self,
820        nb_samples: usize,
821        src_channels: u32,
822        src_sample_rate: u32,
823        src_format: i32,
824    ) -> Result<AudioFrame, DecodeError> {
825        // Determine target parameters
826        let dst_format = self
827            .output_format
828            .map_or(src_format, Self::sample_format_to_av);
829        let dst_sample_rate = self.output_sample_rate.unwrap_or(src_sample_rate);
830        let dst_channels = self.output_channels.unwrap_or(src_channels);
831
832        // If no conversion is needed, return the frame directly
833        if src_format == dst_format
834            && src_sample_rate == dst_sample_rate
835            && src_channels == dst_channels
836        {
837            return unsafe { self.av_frame_to_audio_frame(self.frame) };
838        }
839
840        // Create channel layouts for source and destination
841        // SAFETY: We'll properly clean up these layouts
842        let mut src_ch_layout = unsafe { Self::create_channel_layout(src_channels) };
843        let mut dst_ch_layout = unsafe { Self::create_channel_layout(dst_channels) };
844
845        // Create SwrContext using swr_alloc_set_opts2
846        let mut swr_ctx: *mut SwrContext = ptr::null_mut();
847
848        // SAFETY: FFmpeg API call with valid parameters
849        let ret = unsafe {
850            ff_sys::swr_alloc_set_opts2(
851                &raw mut swr_ctx,
852                &raw const dst_ch_layout,
853                dst_format,
854                dst_sample_rate as i32,
855                &raw const src_ch_layout,
856                src_format,
857                src_sample_rate as i32,
858                0,
859                ptr::null_mut(),
860            )
861        };
862
863        if ret < 0 {
864            // Clean up channel layouts
865            unsafe {
866                ff_sys::av_channel_layout_uninit(&raw mut src_ch_layout);
867                ff_sys::av_channel_layout_uninit(&raw mut dst_ch_layout);
868            }
869            return Err(DecodeError::Ffmpeg {
870                code: ret,
871                message: format!(
872                    "Failed to allocate SwrContext: {}",
873                    ff_sys::av_error_string(ret)
874                ),
875            });
876        }
877
878        // Wrap in RAII guard for automatic cleanup
879        let _swr_guard = SwrContextGuard(swr_ctx);
880
881        // Initialize the resampler
882        // SAFETY: swr_ctx is valid
883        let ret = unsafe { ff_sys::swr_init(swr_ctx) };
884        if ret < 0 {
885            // Clean up channel layouts
886            unsafe {
887                ff_sys::av_channel_layout_uninit(&raw mut src_ch_layout);
888                ff_sys::av_channel_layout_uninit(&raw mut dst_ch_layout);
889            }
890            return Err(DecodeError::Ffmpeg {
891                code: ret,
892                message: format!(
893                    "Failed to initialize SwrContext: {}",
894                    ff_sys::av_error_string(ret)
895                ),
896            });
897        }
898
899        // Calculate output sample count
900        // SAFETY: swr_ctx is valid and initialized
901        let out_samples = unsafe { ff_sys::swr_get_out_samples(swr_ctx, nb_samples as i32) };
902
903        if out_samples < 0 {
904            // Clean up channel layouts
905            unsafe {
906                ff_sys::av_channel_layout_uninit(&raw mut src_ch_layout);
907                ff_sys::av_channel_layout_uninit(&raw mut dst_ch_layout);
908            }
909            return Err(DecodeError::Ffmpeg {
910                code: 0,
911                message: "Failed to calculate output sample count".to_string(),
912            });
913        }
914
915        let out_samples = out_samples as usize;
916
917        // Calculate buffer size for output
918        let dst_sample_fmt = Self::convert_sample_format(dst_format);
919        let bytes_per_sample = dst_sample_fmt.bytes_per_sample();
920        let is_planar = dst_sample_fmt.is_planar();
921
922        // Allocate output buffer
923        let buffer_size = if is_planar {
924            // For planar formats, each plane has samples * bytes_per_sample
925            out_samples * bytes_per_sample * dst_channels as usize
926        } else {
927            // For packed formats, interleaved samples
928            out_samples * bytes_per_sample * dst_channels as usize
929        };
930
931        let mut out_buffer = vec![0u8; buffer_size];
932
933        // Prepare output pointers for swr_convert
934        let mut out_ptrs = if is_planar {
935            // For planar formats, create separate pointers for each channel
936            let plane_size = out_samples * bytes_per_sample;
937            (0..dst_channels)
938                .map(|i| {
939                    let offset = i as usize * plane_size;
940                    out_buffer[offset..].as_mut_ptr()
941                })
942                .collect::<Vec<_>>()
943        } else {
944            // For packed formats, single pointer
945            vec![out_buffer.as_mut_ptr()]
946        };
947
948        // Get input data pointers from frame
949        // SAFETY: self.frame is valid
950        let in_ptrs = unsafe { (*self.frame).data };
951
952        // Convert samples using SwResample
953        // SAFETY: All pointers are valid and buffers are properly sized
954        let converted_samples = unsafe {
955            ff_sys::swr_convert(
956                swr_ctx,
957                out_ptrs.as_mut_ptr(),
958                out_samples as i32,
959                in_ptrs.as_ptr() as *mut *const u8,
960                nb_samples as i32,
961            )
962        };
963
964        // Clean up channel layouts
965        unsafe {
966            ff_sys::av_channel_layout_uninit(&raw mut src_ch_layout);
967            ff_sys::av_channel_layout_uninit(&raw mut dst_ch_layout);
968        }
969
970        if converted_samples < 0 {
971            return Err(DecodeError::Ffmpeg {
972                code: converted_samples,
973                message: format!(
974                    "Failed to convert samples: {}",
975                    ff_sys::av_error_string(converted_samples)
976                ),
977            });
978        }
979
980        // Extract timestamp from original frame
981        // SAFETY: self.frame is valid
982        let timestamp = unsafe {
983            let pts = (*self.frame).pts;
984            if pts != ff_sys::AV_NOPTS_VALUE {
985                let stream = (*self.format_ctx).streams.add(self.stream_index as usize);
986                let time_base = (*(*stream)).time_base;
987                Timestamp::new(pts, Rational::new(time_base.num, time_base.den))
988            } else {
989                Timestamp::invalid()
990            }
991        };
992
993        // Create planes for AudioFrame
994        let planes = if is_planar {
995            let plane_size = converted_samples as usize * bytes_per_sample;
996            (0..dst_channels)
997                .map(|i| {
998                    let offset = i as usize * plane_size;
999                    out_buffer[offset..offset + plane_size].to_vec()
1000                })
1001                .collect()
1002        } else {
1003            // For packed formats, single plane with all data
1004            vec![
1005                out_buffer[..converted_samples as usize * bytes_per_sample * dst_channels as usize]
1006                    .to_vec(),
1007            ]
1008        };
1009
1010        AudioFrame::new(
1011            planes,
1012            converted_samples as usize,
1013            dst_channels,
1014            dst_sample_rate,
1015            dst_sample_fmt,
1016            timestamp,
1017        )
1018        .map_err(|e| DecodeError::Ffmpeg {
1019            code: 0,
1020            message: format!("Failed to create AudioFrame: {e}"),
1021        })
1022    }
1023
1024    /// Converts an AVFrame to an AudioFrame.
1025    unsafe fn av_frame_to_audio_frame(
1026        &self,
1027        frame: *const AVFrame,
1028    ) -> Result<AudioFrame, DecodeError> {
1029        // SAFETY: Caller ensures frame and format_ctx are valid
1030        unsafe {
1031            let nb_samples = (*frame).nb_samples as usize;
1032            let channels = (*frame).ch_layout.nb_channels as u32;
1033            let sample_rate = (*frame).sample_rate as u32;
1034            let format = Self::convert_sample_format((*frame).format);
1035
1036            // Extract timestamp
1037            let pts = (*frame).pts;
1038            let timestamp = if pts != ff_sys::AV_NOPTS_VALUE {
1039                let stream = (*self.format_ctx).streams.add(self.stream_index as usize);
1040                let time_base = (*(*stream)).time_base;
1041                Timestamp::new(
1042                    pts as i64,
1043                    Rational::new(time_base.num as i32, time_base.den as i32),
1044                )
1045            } else {
1046                Timestamp::invalid()
1047            };
1048
1049            // Convert frame to planes
1050            let planes = Self::extract_planes(frame, nb_samples, channels, format)?;
1051
1052            AudioFrame::new(planes, nb_samples, channels, sample_rate, format, timestamp).map_err(
1053                |e| DecodeError::Ffmpeg {
1054                    code: 0,
1055                    message: format!("Failed to create AudioFrame: {e}"),
1056                },
1057            )
1058        }
1059    }
1060
1061    /// Extracts planes from an AVFrame.
1062    unsafe fn extract_planes(
1063        frame: *const AVFrame,
1064        nb_samples: usize,
1065        channels: u32,
1066        format: SampleFormat,
1067    ) -> Result<Vec<Vec<u8>>, DecodeError> {
1068        // SAFETY: Caller ensures frame is valid and format matches actual frame format
1069        unsafe {
1070            let mut planes = Vec::new();
1071            let bytes_per_sample = format.bytes_per_sample();
1072
1073            if format.is_planar() {
1074                // Planar: one plane per channel
1075                for ch in 0..channels as usize {
1076                    let plane_size = nb_samples * bytes_per_sample;
1077                    let mut plane_data = vec![0u8; plane_size];
1078
1079                    let src_ptr = (*frame).data[ch];
1080                    std::ptr::copy_nonoverlapping(src_ptr, plane_data.as_mut_ptr(), plane_size);
1081
1082                    planes.push(plane_data);
1083                }
1084            } else {
1085                // Packed: single plane with interleaved samples
1086                let plane_size = nb_samples * channels as usize * bytes_per_sample;
1087                let mut plane_data = vec![0u8; plane_size];
1088
1089                let src_ptr = (*frame).data[0];
1090                std::ptr::copy_nonoverlapping(src_ptr, plane_data.as_mut_ptr(), plane_size);
1091
1092                planes.push(plane_data);
1093            }
1094
1095            Ok(planes)
1096        }
1097    }
1098
1099    /// Converts our `SampleFormat` to FFmpeg `AVSampleFormat`.
1100    fn sample_format_to_av(format: SampleFormat) -> AVSampleFormat {
1101        match format {
1102            SampleFormat::U8 => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8,
1103            SampleFormat::I16 => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16,
1104            SampleFormat::I32 => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32,
1105            SampleFormat::F32 => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLT,
1106            SampleFormat::F64 => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBL,
1107            SampleFormat::U8p => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8P,
1108            SampleFormat::I16p => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16P,
1109            SampleFormat::I32p => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32P,
1110            SampleFormat::F32p => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLTP,
1111            SampleFormat::F64p => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBLP,
1112            _ => {
1113                log::warn!(
1114                    "sample_format has no AV mapping, falling back to F32 format={format:?} fallback=AV_SAMPLE_FMT_FLT"
1115                );
1116                ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLT
1117            }
1118        }
1119    }
1120
1121    /// Returns the current playback position.
1122    pub(crate) fn position(&self) -> Duration {
1123        self.position
1124    }
1125
1126    /// Returns whether end of file has been reached.
1127    pub(crate) fn is_eof(&self) -> bool {
1128        self.eof
1129    }
1130
1131    /// Returns whether the source is a live or streaming input.
1132    ///
1133    /// Live sources have the `AVFMT_TS_DISCONT` flag set on their `AVInputFormat`.
1134    /// Seeking is not meaningful on live sources.
1135    pub(crate) fn is_live(&self) -> bool {
1136        self.is_live
1137    }
1138
1139    /// Converts a `Duration` to a presentation timestamp (PTS) in stream time_base units.
1140    fn duration_to_pts(&self, duration: Duration) -> i64 {
1141        // SAFETY: format_ctx and stream_index are valid (owned by AudioDecoderInner)
1142        let time_base = unsafe {
1143            let stream = (*self.format_ctx).streams.add(self.stream_index as usize);
1144            (*(*stream)).time_base
1145        };
1146
1147        // Convert: duration (seconds) * (time_base.den / time_base.num) = PTS
1148        let time_base_f64 = time_base.den as f64 / time_base.num as f64;
1149        (duration.as_secs_f64() * time_base_f64) as i64
1150    }
1151
1152    /// Seeks to a specified position in the audio stream.
1153    ///
1154    /// # Arguments
1155    ///
1156    /// * `position` - Target position to seek to.
1157    /// * `mode` - Seek mode (Keyframe, Exact, or Backward).
1158    ///
1159    /// # Errors
1160    ///
1161    /// Returns [`DecodeError::SeekFailed`] if the seek operation fails.
1162    pub(crate) fn seek(
1163        &mut self,
1164        position: Duration,
1165        mode: crate::SeekMode,
1166    ) -> Result<(), DecodeError> {
1167        use crate::SeekMode;
1168
1169        let timestamp = self.duration_to_pts(position);
1170        let flags = ff_sys::avformat::seek_flags::BACKWARD;
1171
1172        // 1. Clear any pending packet and frame
1173        // SAFETY: packet and frame are valid (owned by AudioDecoderInner)
1174        unsafe {
1175            ff_sys::av_packet_unref(self.packet);
1176            ff_sys::av_frame_unref(self.frame);
1177        }
1178
1179        // 2. Seek in the format context
1180        // SAFETY: format_ctx and stream_index are valid
1181        unsafe {
1182            ff_sys::avformat::seek_frame(self.format_ctx, self.stream_index, timestamp, flags)
1183                .map_err(|e| DecodeError::SeekFailed {
1184                    target: position,
1185                    reason: ff_sys::av_error_string(e),
1186                })?;
1187        }
1188
1189        // 3. Flush decoder buffers
1190        // SAFETY: codec_ctx is valid (owned by AudioDecoderInner)
1191        unsafe {
1192            ff_sys::avcodec::flush_buffers(self.codec_ctx);
1193        }
1194
1195        // 4. Drain any remaining frames from the decoder after flush
1196        // SAFETY: codec_ctx and frame are valid
1197        unsafe {
1198            loop {
1199                let ret = ff_sys::avcodec_receive_frame(self.codec_ctx, self.frame);
1200                if ret == ff_sys::error_codes::EAGAIN || ret == ff_sys::error_codes::EOF {
1201                    break;
1202                } else if ret == 0 {
1203                    ff_sys::av_frame_unref(self.frame);
1204                } else {
1205                    break;
1206                }
1207            }
1208        }
1209
1210        // 5. Reset internal state
1211        self.eof = false;
1212
1213        // 6. For exact mode, skip frames to reach exact position
1214        if mode == SeekMode::Exact {
1215            self.skip_to_exact(position)?;
1216        }
1217        // For Keyframe/Backward modes, we're already at the keyframe after av_seek_frame
1218
1219        Ok(())
1220    }
1221
1222    /// Skips frames until reaching the exact target position.
1223    ///
1224    /// This is used by [`Self::seek`] when `SeekMode::Exact` is specified.
1225    ///
1226    /// # Arguments
1227    ///
1228    /// * `target` - The exact target position.
1229    fn skip_to_exact(&mut self, target: Duration) -> Result<(), DecodeError> {
1230        // Decode frames until we reach or pass the target
1231        while let Some(frame) = self.decode_one()? {
1232            let frame_time = frame.timestamp().as_duration();
1233            if frame_time >= target {
1234                // We've reached the target position
1235                break;
1236            }
1237            // Continue decoding to get closer (frames are automatically dropped)
1238        }
1239        Ok(())
1240    }
1241
1242    /// Flushes the decoder's internal buffers.
1243    pub(crate) fn flush(&mut self) {
1244        // SAFETY: codec_ctx is valid and owned by this instance
1245        unsafe {
1246            ff_sys::avcodec::flush_buffers(self.codec_ctx);
1247        }
1248        self.eof = false;
1249    }
1250
1251    // ── Reconnect helpers ─────────────────────────────────────────────────────
1252
1253    /// Attempts to reconnect to the stream URL using exponential backoff.
1254    ///
1255    /// Called from `decode_one()` when `StreamInterrupted` is received and
1256    /// `NetworkOptions::reconnect_on_error` is `true`. After all attempts fail,
1257    /// returns a `StreamInterrupted` error.
1258    fn attempt_reconnect(&mut self) -> Result<(), DecodeError> {
1259        let url = match self.url.as_deref() {
1260            Some(u) => u.to_owned(),
1261            None => return Ok(()), // file-path source: no reconnect
1262        };
1263        let max = self.network_opts.max_reconnect_attempts;
1264
1265        for attempt in 1..=max {
1266            let backoff_ms = 100u64 * (1u64 << (attempt - 1).min(10));
1267            log::warn!(
1268                "reconnecting attempt={attempt} url={} backoff_ms={backoff_ms}",
1269                crate::network::sanitize_url(&url)
1270            );
1271            std::thread::sleep(Duration::from_millis(backoff_ms));
1272            match self.reopen(&url) {
1273                Ok(()) => {
1274                    self.reconnect_count += 1;
1275                    log::info!(
1276                        "reconnected attempt={attempt} url={} total_reconnects={}",
1277                        crate::network::sanitize_url(&url),
1278                        self.reconnect_count
1279                    );
1280                    return Ok(());
1281                }
1282                Err(e) => log::warn!("reconnect attempt={attempt} failed err={e}"),
1283            }
1284        }
1285
1286        Err(DecodeError::StreamInterrupted {
1287            code: 0,
1288            endpoint: crate::network::sanitize_url(&url),
1289            message: format!("stream did not recover after {max} attempts"),
1290        })
1291    }
1292
1293    /// Closes the current `AVFormatContext`, re-opens the URL, re-reads stream info,
1294    /// re-finds the audio stream, and flushes the codec.
1295    fn reopen(&mut self, url: &str) -> Result<(), DecodeError> {
1296        // Close the current format context. `avformat_close_input` sets the pointer
1297        // to null — this matches the null check in Drop so no double-free occurs.
1298        // SAFETY: self.format_ctx is valid and owned exclusively by self.
1299        unsafe {
1300            ff_sys::avformat::close_input(std::ptr::addr_of_mut!(self.format_ctx));
1301        }
1302
1303        // Re-open the URL with the stored network timeouts.
1304        // SAFETY: url is a valid UTF-8 network URL string.
1305        self.format_ctx = unsafe {
1306            ff_sys::avformat::open_input_url(
1307                url,
1308                self.network_opts.connect_timeout,
1309                self.network_opts.read_timeout,
1310            )
1311            .map_err(|e| crate::network::map_network_error(e, crate::network::sanitize_url(url)))?
1312        };
1313
1314        // Re-read stream information.
1315        // SAFETY: self.format_ctx is valid and freshly opened.
1316        unsafe {
1317            ff_sys::avformat::find_stream_info(self.format_ctx).map_err(|e| {
1318                DecodeError::Ffmpeg {
1319                    code: e,
1320                    message: format!(
1321                        "reconnect find_stream_info failed: {}",
1322                        ff_sys::av_error_string(e)
1323                    ),
1324                }
1325            })?;
1326        }
1327
1328        // Re-find the audio stream (index may differ in theory after reconnect).
1329        // SAFETY: self.format_ctx is valid.
1330        let (stream_index, _) = unsafe { Self::find_audio_stream(self.format_ctx) }
1331            .ok_or_else(|| DecodeError::NoAudioStream { path: url.into() })?;
1332        self.stream_index = stream_index as i32;
1333
1334        // Flush codec buffers to discard stale decoded state from before the drop.
1335        // SAFETY: self.codec_ctx is valid and has not been freed.
1336        unsafe {
1337            ff_sys::avcodec::flush_buffers(self.codec_ctx);
1338        }
1339
1340        self.eof = false;
1341        Ok(())
1342    }
1343}
1344
1345impl Drop for AudioDecoderInner {
1346    fn drop(&mut self) {
1347        // Free SwResample context if allocated
1348        if let Some(swr_ctx) = self.swr_ctx {
1349            // SAFETY: swr_ctx is valid and owned by this instance
1350            unsafe {
1351                // swr_free frees a SwrContext
1352                ff_sys::swr_free(&mut (swr_ctx as *mut _));
1353            }
1354        }
1355
1356        // Free frame and packet
1357        if !self.frame.is_null() {
1358            // SAFETY: self.frame is valid and owned by this instance
1359            unsafe {
1360                ff_sys::av_frame_free(&mut (self.frame as *mut _));
1361            }
1362        }
1363
1364        if !self.packet.is_null() {
1365            // SAFETY: self.packet is valid and owned by this instance
1366            unsafe {
1367                ff_sys::av_packet_free(&mut (self.packet as *mut _));
1368            }
1369        }
1370
1371        // Free codec context
1372        if !self.codec_ctx.is_null() {
1373            // SAFETY: self.codec_ctx is valid and owned by this instance
1374            unsafe {
1375                ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
1376            }
1377        }
1378
1379        // Close format context
1380        if !self.format_ctx.is_null() {
1381            // SAFETY: self.format_ctx is valid and owned by this instance
1382            unsafe {
1383                ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
1384            }
1385        }
1386    }
1387}
1388
1389// SAFETY: AudioDecoderInner manages FFmpeg contexts which are thread-safe when not shared.
1390// We don't expose mutable access across threads, so Send is safe.
1391unsafe impl Send for AudioDecoderInner {}
1392
1393#[cfg(test)]
1394#[allow(unsafe_code)]
1395mod tests {
1396    use ff_format::channel::ChannelLayout;
1397
1398    use super::AudioDecoderInner;
1399
1400    /// Constructs an `AVChannelLayout` with `AV_CHANNEL_ORDER_NATIVE` and the given mask.
1401    fn native_layout(mask: u64, nb_channels: i32) -> ff_sys::AVChannelLayout {
1402        ff_sys::AVChannelLayout {
1403            order: ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_NATIVE,
1404            nb_channels,
1405            u: ff_sys::AVChannelLayout__bindgen_ty_1 { mask },
1406            opaque: std::ptr::null_mut(),
1407        }
1408    }
1409
1410    /// Constructs an `AVChannelLayout` with `AV_CHANNEL_ORDER_UNSPEC`.
1411    fn unspec_layout(nb_channels: i32) -> ff_sys::AVChannelLayout {
1412        ff_sys::AVChannelLayout {
1413            order: ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_UNSPEC,
1414            nb_channels,
1415            u: ff_sys::AVChannelLayout__bindgen_ty_1 { mask: 0 },
1416            opaque: std::ptr::null_mut(),
1417        }
1418    }
1419
1420    #[test]
1421    fn native_mask_mono() {
1422        let layout = native_layout(0x4, 1);
1423        assert_eq!(
1424            AudioDecoderInner::convert_channel_layout(&layout, 1),
1425            ChannelLayout::Mono
1426        );
1427    }
1428
1429    #[test]
1430    fn native_mask_stereo() {
1431        let layout = native_layout(0x3, 2);
1432        assert_eq!(
1433            AudioDecoderInner::convert_channel_layout(&layout, 2),
1434            ChannelLayout::Stereo
1435        );
1436    }
1437
1438    #[test]
1439    fn native_mask_stereo2_1() {
1440        let layout = native_layout(0x103, 3);
1441        assert_eq!(
1442            AudioDecoderInner::convert_channel_layout(&layout, 3),
1443            ChannelLayout::Stereo2_1
1444        );
1445    }
1446
1447    #[test]
1448    fn native_mask_surround3_0() {
1449        let layout = native_layout(0x7, 3);
1450        assert_eq!(
1451            AudioDecoderInner::convert_channel_layout(&layout, 3),
1452            ChannelLayout::Surround3_0
1453        );
1454    }
1455
1456    #[test]
1457    fn native_mask_quad() {
1458        let layout = native_layout(0x33, 4);
1459        assert_eq!(
1460            AudioDecoderInner::convert_channel_layout(&layout, 4),
1461            ChannelLayout::Quad
1462        );
1463    }
1464
1465    #[test]
1466    fn native_mask_surround5_0() {
1467        let layout = native_layout(0x37, 5);
1468        assert_eq!(
1469            AudioDecoderInner::convert_channel_layout(&layout, 5),
1470            ChannelLayout::Surround5_0
1471        );
1472    }
1473
1474    #[test]
1475    fn native_mask_surround5_1() {
1476        let layout = native_layout(0x3F, 6);
1477        assert_eq!(
1478            AudioDecoderInner::convert_channel_layout(&layout, 6),
1479            ChannelLayout::Surround5_1
1480        );
1481    }
1482
1483    #[test]
1484    fn native_mask_surround6_1() {
1485        let layout = native_layout(0x13F, 7);
1486        assert_eq!(
1487            AudioDecoderInner::convert_channel_layout(&layout, 7),
1488            ChannelLayout::Surround6_1
1489        );
1490    }
1491
1492    #[test]
1493    fn native_mask_surround7_1() {
1494        let layout = native_layout(0x63F, 8);
1495        assert_eq!(
1496            AudioDecoderInner::convert_channel_layout(&layout, 8),
1497            ChannelLayout::Surround7_1
1498        );
1499    }
1500
1501    #[test]
1502    fn native_mask_unknown_falls_back_to_from_channels() {
1503        // mask=0x1 is not a standard layout; should fall back to from_channels(2)
1504        let layout = native_layout(0x1, 2);
1505        assert_eq!(
1506            AudioDecoderInner::convert_channel_layout(&layout, 2),
1507            ChannelLayout::from_channels(2)
1508        );
1509    }
1510
1511    #[test]
1512    fn non_native_order_falls_back_to_from_channels() {
1513        let layout = unspec_layout(6);
1514        assert_eq!(
1515            AudioDecoderInner::convert_channel_layout(&layout, 6),
1516            ChannelLayout::from_channels(6)
1517        );
1518    }
1519
1520    // -------------------------------------------------------------------------
1521    // extract_codec_name
1522    // -------------------------------------------------------------------------
1523
1524    #[test]
1525    fn codec_name_should_return_h264_for_h264_codec_id() {
1526        let name =
1527            unsafe { AudioDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_H264) };
1528        assert_eq!(name, "h264");
1529    }
1530
1531    #[test]
1532    fn codec_name_should_return_none_for_none_codec_id() {
1533        let name =
1534            unsafe { AudioDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_NONE) };
1535        assert_eq!(name, "none");
1536    }
1537
1538    #[test]
1539    fn unsupported_codec_error_should_include_codec_name() {
1540        let codec_id = ff_sys::AVCodecID_AV_CODEC_ID_MP3;
1541        let codec_name = unsafe { AudioDecoderInner::extract_codec_name(codec_id) };
1542        let error = crate::error::DecodeError::UnsupportedCodec {
1543            codec: format!("{codec_name} (codec_id={codec_id:?})"),
1544        };
1545        let msg = error.to_string();
1546        assert!(msg.contains("mp3"), "expected codec name in error: {msg}");
1547        assert!(
1548            msg.contains("codec_id="),
1549            "expected codec_id in error: {msg}"
1550        );
1551    }
1552}