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