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