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, 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    /// Returns the raw pointer.
63    const fn as_ptr(&self) -> *mut AVFormatContext {
64        self.0
65    }
66
67    /// Consumes the guard and returns the raw pointer without dropping.
68    fn into_raw(self) -> *mut AVFormatContext {
69        let ptr = self.0;
70        std::mem::forget(self);
71        ptr
72    }
73}
74
75impl Drop for AvFormatContextGuard {
76    fn drop(&mut self) {
77        if !self.0.is_null() {
78            // SAFETY: self.0 is valid and owned by this guard
79            unsafe {
80                ff_sys::avformat::close_input(&mut (self.0 as *mut _));
81            }
82        }
83    }
84}
85
86/// RAII guard for `AVCodecContext` to ensure proper cleanup.
87struct AvCodecContextGuard(*mut AVCodecContext);
88
89impl AvCodecContextGuard {
90    /// Creates a new guard by allocating a codec context.
91    ///
92    /// # Safety
93    ///
94    /// Caller must ensure codec pointer is valid.
95    unsafe fn new(codec: *const ff_sys::AVCodec) -> Result<Self, DecodeError> {
96        // SAFETY: Caller ensures codec pointer is valid
97        let codec_ctx = unsafe {
98            ff_sys::avcodec::alloc_context3(codec).map_err(|e| DecodeError::Ffmpeg {
99                code: e,
100                message: format!("Failed to allocate codec context: {e}"),
101            })?
102        };
103        Ok(Self(codec_ctx))
104    }
105
106    /// Returns the raw pointer.
107    const fn as_ptr(&self) -> *mut AVCodecContext {
108        self.0
109    }
110
111    /// Consumes the guard and returns the raw pointer without dropping.
112    fn into_raw(self) -> *mut AVCodecContext {
113        let ptr = self.0;
114        std::mem::forget(self);
115        ptr
116    }
117}
118
119impl Drop for AvCodecContextGuard {
120    fn drop(&mut self) {
121        if !self.0.is_null() {
122            // SAFETY: self.0 is valid and owned by this guard
123            unsafe {
124                ff_sys::avcodec::free_context(&mut (self.0 as *mut _));
125            }
126        }
127    }
128}
129
130/// RAII guard for `AVPacket` to ensure proper cleanup.
131struct AvPacketGuard(*mut AVPacket);
132
133impl AvPacketGuard {
134    /// Creates a new guard by allocating a packet.
135    ///
136    /// # Safety
137    ///
138    /// Must be called after FFmpeg initialization.
139    unsafe fn new() -> Result<Self, DecodeError> {
140        // SAFETY: Caller ensures FFmpeg is initialized
141        let packet = unsafe { ff_sys::av_packet_alloc() };
142        if packet.is_null() {
143            return Err(DecodeError::Ffmpeg {
144                code: 0,
145                message: "Failed to allocate packet".to_string(),
146            });
147        }
148        Ok(Self(packet))
149    }
150
151    /// Consumes the guard and returns the raw pointer without dropping.
152    fn into_raw(self) -> *mut AVPacket {
153        let ptr = self.0;
154        std::mem::forget(self);
155        ptr
156    }
157}
158
159impl Drop for AvPacketGuard {
160    fn drop(&mut self) {
161        if !self.0.is_null() {
162            // SAFETY: self.0 is valid and owned by this guard
163            unsafe {
164                ff_sys::av_packet_free(&mut (self.0 as *mut _));
165            }
166        }
167    }
168}
169
170/// RAII guard for `AVFrame` to ensure proper cleanup.
171struct AvFrameGuard(*mut AVFrame);
172
173impl AvFrameGuard {
174    /// Creates a new guard by allocating a frame.
175    ///
176    /// # Safety
177    ///
178    /// Must be called after FFmpeg initialization.
179    unsafe fn new() -> Result<Self, DecodeError> {
180        // SAFETY: Caller ensures FFmpeg is initialized
181        let frame = unsafe { ff_sys::av_frame_alloc() };
182        if frame.is_null() {
183            return Err(DecodeError::Ffmpeg {
184                code: 0,
185                message: "Failed to allocate frame".to_string(),
186            });
187        }
188        Ok(Self(frame))
189    }
190
191    /// Consumes the guard and returns the raw pointer without dropping.
192    fn into_raw(self) -> *mut AVFrame {
193        let ptr = self.0;
194        std::mem::forget(self);
195        ptr
196    }
197}
198
199impl Drop for AvFrameGuard {
200    fn drop(&mut self) {
201        if !self.0.is_null() {
202            // SAFETY: self.0 is valid and owned by this guard
203            unsafe {
204                ff_sys::av_frame_free(&mut (self.0 as *mut _));
205            }
206        }
207    }
208}
209
210/// RAII guard for `SwrContext` to ensure proper cleanup.
211struct SwrContextGuard(*mut SwrContext);
212
213impl SwrContextGuard {
214    /// Consumes the guard and returns the raw pointer without dropping.
215    #[allow(dead_code)]
216    fn into_raw(self) -> *mut SwrContext {
217        let ptr = self.0;
218        std::mem::forget(self);
219        ptr
220    }
221}
222
223impl Drop for SwrContextGuard {
224    fn drop(&mut self) {
225        if !self.0.is_null() {
226            // SAFETY: self.0 is valid and owned by this guard
227            unsafe {
228                ff_sys::swr_free(&mut (self.0 as *mut _));
229            }
230        }
231    }
232}
233
234/// Internal decoder state holding FFmpeg contexts.
235///
236/// This structure manages the lifecycle of FFmpeg objects and is responsible
237/// for proper cleanup when dropped.
238pub(crate) struct AudioDecoderInner {
239    /// Format context for reading the media file
240    format_ctx: *mut AVFormatContext,
241    /// Codec context for decoding audio frames
242    codec_ctx: *mut AVCodecContext,
243    /// Audio stream index in the format context
244    stream_index: i32,
245    /// SwResample context for sample format conversion (optional)
246    swr_ctx: Option<*mut SwrContext>,
247    /// Target output sample format (if conversion is needed)
248    output_format: Option<SampleFormat>,
249    /// Target output sample rate (if resampling is needed)
250    output_sample_rate: Option<u32>,
251    /// Target output channel count (if remixing is needed)
252    output_channels: Option<u32>,
253    /// Whether end of file has been reached
254    eof: bool,
255    /// Current playback position
256    position: Duration,
257    /// Reusable packet for reading from file
258    packet: *mut AVPacket,
259    /// Reusable frame for decoding
260    frame: *mut AVFrame,
261}
262
263impl AudioDecoderInner {
264    /// Opens a media file and initializes the audio decoder.
265    ///
266    /// # Arguments
267    ///
268    /// * `path` - Path to the media file
269    /// * `output_format` - Optional target sample format for conversion
270    /// * `output_sample_rate` - Optional target sample rate for resampling
271    /// * `output_channels` - Optional target channel count for remixing
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if:
276    /// - The file cannot be opened
277    /// - No audio stream is found
278    /// - The codec is not supported
279    /// - Decoder initialization fails
280    pub(crate) fn new(
281        path: &Path,
282        output_format: Option<SampleFormat>,
283        output_sample_rate: Option<u32>,
284        output_channels: Option<u32>,
285    ) -> Result<(Self, AudioStreamInfo, ContainerInfo), DecodeError> {
286        // Ensure FFmpeg is initialized (thread-safe and idempotent)
287        ff_sys::ensure_initialized();
288
289        // Open the input file (with RAII guard)
290        // SAFETY: Path is valid, AvFormatContextGuard ensures cleanup
291        let format_ctx_guard = unsafe { AvFormatContextGuard::new(path)? };
292        let format_ctx = format_ctx_guard.as_ptr();
293
294        // Read stream information
295        // SAFETY: format_ctx is valid and owned by guard
296        unsafe {
297            ff_sys::avformat::find_stream_info(format_ctx).map_err(|e| DecodeError::Ffmpeg {
298                code: e,
299                message: format!("Failed to find stream info: {}", ff_sys::av_error_string(e)),
300            })?;
301        }
302
303        // Find the audio stream
304        // SAFETY: format_ctx is valid
305        let (stream_index, codec_id) =
306            unsafe { Self::find_audio_stream(format_ctx) }.ok_or_else(|| {
307                DecodeError::NoAudioStream {
308                    path: path.to_path_buf(),
309                }
310            })?;
311
312        // Find the decoder for this codec
313        // SAFETY: codec_id is valid from FFmpeg
314        let codec_name = unsafe { Self::extract_codec_name(codec_id) };
315        let codec = unsafe {
316            ff_sys::avcodec::find_decoder(codec_id).ok_or_else(|| {
317                DecodeError::UnsupportedCodec {
318                    codec: format!("{codec_name} (codec_id={codec_id:?})"),
319                }
320            })?
321        };
322
323        // Allocate codec context (with RAII guard)
324        // SAFETY: codec pointer is valid, AvCodecContextGuard ensures cleanup
325        let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
326        let codec_ctx = codec_ctx_guard.as_ptr();
327
328        // Copy codec parameters from stream to context
329        // SAFETY: format_ctx and codec_ctx are valid, stream_index is valid
330        unsafe {
331            let stream = (*format_ctx).streams.add(stream_index as usize);
332            let codecpar = (*(*stream)).codecpar;
333            ff_sys::avcodec::parameters_to_context(codec_ctx, codecpar).map_err(|e| {
334                DecodeError::Ffmpeg {
335                    code: e,
336                    message: format!(
337                        "Failed to copy codec parameters: {}",
338                        ff_sys::av_error_string(e)
339                    ),
340                }
341            })?;
342        }
343
344        // Open the codec
345        // SAFETY: codec_ctx and codec are valid
346        unsafe {
347            ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
348                DecodeError::Ffmpeg {
349                    code: e,
350                    message: format!("Failed to open codec: {}", ff_sys::av_error_string(e)),
351                }
352            })?;
353        }
354
355        // Extract stream information
356        // SAFETY: All pointers are valid
357        let stream_info =
358            unsafe { Self::extract_stream_info(format_ctx, stream_index as i32, codec_ctx)? };
359
360        // Extract container information
361        // SAFETY: format_ctx is valid and avformat_find_stream_info has been called
362        let container_info = unsafe { Self::extract_container_info(format_ctx) };
363
364        // Allocate packet and frame (with RAII guards)
365        // SAFETY: FFmpeg is initialized, guards ensure cleanup
366        let packet_guard = unsafe { AvPacketGuard::new()? };
367        let frame_guard = unsafe { AvFrameGuard::new()? };
368
369        // All initialization successful - transfer ownership to AudioDecoderInner
370        Ok((
371            Self {
372                format_ctx: format_ctx_guard.into_raw(),
373                codec_ctx: codec_ctx_guard.into_raw(),
374                stream_index: stream_index as i32,
375                swr_ctx: None,
376                output_format,
377                output_sample_rate,
378                output_channels,
379                eof: false,
380                position: Duration::ZERO,
381                packet: packet_guard.into_raw(),
382                frame: frame_guard.into_raw(),
383            },
384            stream_info,
385            container_info,
386        ))
387    }
388
389    /// Finds the first audio stream in the format context.
390    ///
391    /// # Returns
392    ///
393    /// Returns `Some((index, codec_id))` if an audio stream is found, `None` otherwise.
394    ///
395    /// # Safety
396    ///
397    /// Caller must ensure `format_ctx` is valid and initialized.
398    unsafe fn find_audio_stream(format_ctx: *mut AVFormatContext) -> Option<(usize, AVCodecID)> {
399        // SAFETY: Caller ensures format_ctx is valid
400        unsafe {
401            let nb_streams = (*format_ctx).nb_streams as usize;
402
403            for i in 0..nb_streams {
404                let stream = (*format_ctx).streams.add(i);
405                let codecpar = (*(*stream)).codecpar;
406
407                if (*codecpar).codec_type == AVMediaType_AVMEDIA_TYPE_AUDIO {
408                    return Some((i, (*codecpar).codec_id));
409                }
410            }
411
412            None
413        }
414    }
415
416    /// Returns the human-readable codec name for a given `AVCodecID`.
417    unsafe fn extract_codec_name(codec_id: ff_sys::AVCodecID) -> String {
418        // SAFETY: avcodec_get_name is safe for any codec ID value
419        let name_ptr = unsafe { ff_sys::avcodec_get_name(codec_id) };
420        if name_ptr.is_null() {
421            return String::from("unknown");
422        }
423        // SAFETY: avcodec_get_name returns a valid C string with static lifetime
424        unsafe { CStr::from_ptr(name_ptr).to_string_lossy().into_owned() }
425    }
426
427    /// Extracts audio stream information from FFmpeg structures.
428    unsafe fn extract_stream_info(
429        format_ctx: *mut AVFormatContext,
430        stream_index: i32,
431        codec_ctx: *mut AVCodecContext,
432    ) -> Result<AudioStreamInfo, DecodeError> {
433        // SAFETY: Caller ensures all pointers are valid
434        let (sample_rate, channels, sample_fmt, duration_val, channel_layout, codec_id) = unsafe {
435            let stream = (*format_ctx).streams.add(stream_index as usize);
436            let codecpar = (*(*stream)).codecpar;
437
438            (
439                (*codecpar).sample_rate as u32,
440                (*codecpar).ch_layout.nb_channels as u32,
441                (*codec_ctx).sample_fmt,
442                (*format_ctx).duration,
443                (*codecpar).ch_layout,
444                (*codecpar).codec_id,
445            )
446        };
447
448        // Extract duration
449        let duration = if duration_val > 0 {
450            let duration_secs = duration_val as f64 / 1_000_000.0;
451            Some(Duration::from_secs_f64(duration_secs))
452        } else {
453            None
454        };
455
456        // Extract sample format
457        let sample_format = Self::convert_sample_format(sample_fmt);
458
459        // Extract channel layout
460        let channel_layout_enum = Self::convert_channel_layout(&channel_layout, channels);
461
462        // Extract codec
463        let codec = Self::convert_codec(codec_id);
464        let codec_name = unsafe { Self::extract_codec_name(codec_id) };
465
466        // Build stream info
467        let mut builder = AudioStreamInfo::builder()
468            .index(stream_index as u32)
469            .codec(codec)
470            .codec_name(codec_name)
471            .sample_rate(sample_rate)
472            .channels(channels)
473            .sample_format(sample_format)
474            .channel_layout(channel_layout_enum);
475
476        if let Some(d) = duration {
477            builder = builder.duration(d);
478        }
479
480        Ok(builder.build())
481    }
482
483    /// Extracts container-level information from the `AVFormatContext`.
484    ///
485    /// # Safety
486    ///
487    /// Caller must ensure `format_ctx` is valid and `avformat_find_stream_info` has been called.
488    unsafe fn extract_container_info(format_ctx: *mut AVFormatContext) -> ContainerInfo {
489        // SAFETY: Caller ensures format_ctx is valid
490        unsafe {
491            let format_name = if (*format_ctx).iformat.is_null() {
492                String::new()
493            } else {
494                let ptr = (*(*format_ctx).iformat).name;
495                if ptr.is_null() {
496                    String::new()
497                } else {
498                    CStr::from_ptr(ptr).to_string_lossy().into_owned()
499                }
500            };
501
502            let bit_rate = {
503                let br = (*format_ctx).bit_rate;
504                if br > 0 { Some(br as u64) } else { None }
505            };
506
507            let nb_streams = (*format_ctx).nb_streams as u32;
508
509            let mut builder = ContainerInfo::builder()
510                .format_name(format_name)
511                .nb_streams(nb_streams);
512            if let Some(br) = bit_rate {
513                builder = builder.bit_rate(br);
514            }
515            builder.build()
516        }
517    }
518
519    /// Converts FFmpeg sample format to our `SampleFormat` enum.
520    fn convert_sample_format(fmt: AVSampleFormat) -> SampleFormat {
521        if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8 {
522            SampleFormat::U8
523        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16 {
524            SampleFormat::I16
525        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32 {
526            SampleFormat::I32
527        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLT {
528            SampleFormat::F32
529        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBL {
530            SampleFormat::F64
531        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8P {
532            SampleFormat::U8p
533        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16P {
534            SampleFormat::I16p
535        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32P {
536            SampleFormat::I32p
537        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLTP {
538            SampleFormat::F32p
539        } else if fmt == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBLP {
540            SampleFormat::F64p
541        } else {
542            log::warn!(
543                "sample_format unsupported, falling back to F32 requested={fmt} fallback=F32"
544            );
545            SampleFormat::F32
546        }
547    }
548
549    /// Converts FFmpeg channel layout to our `ChannelLayout` enum.
550    fn convert_channel_layout(layout: &ff_sys::AVChannelLayout, channels: u32) -> ChannelLayout {
551        if layout.order == ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_NATIVE {
552            // SAFETY: When order is AV_CHANNEL_ORDER_NATIVE, the mask field is valid
553            let mask = unsafe { layout.u.mask };
554            match mask {
555                0x4 => ChannelLayout::Mono,
556                0x3 => ChannelLayout::Stereo,
557                0x103 => ChannelLayout::Stereo2_1,
558                0x7 => ChannelLayout::Surround3_0,
559                0x33 => ChannelLayout::Quad,
560                0x37 => ChannelLayout::Surround5_0,
561                0x3F => ChannelLayout::Surround5_1,
562                0x13F => ChannelLayout::Surround6_1,
563                0x63F => ChannelLayout::Surround7_1,
564                _ => {
565                    log::warn!(
566                        "channel_layout mask has no mapping, deriving from channel count \
567                         mask={mask} channels={channels}"
568                    );
569                    ChannelLayout::from_channels(channels)
570                }
571            }
572        } else {
573            log::warn!(
574                "channel_layout order is not NATIVE, deriving from channel count \
575                 order={order} channels={channels}",
576                order = layout.order
577            );
578            ChannelLayout::from_channels(channels)
579        }
580    }
581
582    /// Creates an `AVChannelLayout` from channel count.
583    ///
584    /// # Safety
585    ///
586    /// The returned layout must be freed with `av_channel_layout_uninit`.
587    unsafe fn create_channel_layout(channels: u32) -> ff_sys::AVChannelLayout {
588        // SAFETY: Zeroing AVChannelLayout is safe
589        let mut layout = unsafe { std::mem::zeroed::<ff_sys::AVChannelLayout>() };
590        // SAFETY: Caller ensures proper cleanup
591        unsafe {
592            ff_sys::av_channel_layout_default(&raw mut layout, channels as i32);
593        }
594        layout
595    }
596
597    /// Converts FFmpeg codec ID to our `AudioCodec` enum.
598    fn convert_codec(codec_id: AVCodecID) -> AudioCodec {
599        if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_AAC {
600            AudioCodec::Aac
601        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_MP3 {
602            AudioCodec::Mp3
603        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_OPUS {
604            AudioCodec::Opus
605        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_VORBIS {
606            AudioCodec::Vorbis
607        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_FLAC {
608            AudioCodec::Flac
609        } else if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_PCM_S16LE {
610            AudioCodec::Pcm
611        } else {
612            log::warn!(
613                "audio codec unsupported, falling back to Aac codec_id={codec_id} fallback=Aac"
614            );
615            AudioCodec::Aac
616        }
617    }
618
619    /// Decodes the next audio frame.
620    ///
621    /// # Returns
622    ///
623    /// - `Ok(Some(frame))` - Successfully decoded a frame
624    /// - `Ok(None)` - End of stream reached
625    /// - `Err(_)` - Decoding error occurred
626    pub(crate) fn decode_one(&mut self) -> Result<Option<AudioFrame>, DecodeError> {
627        if self.eof {
628            return Ok(None);
629        }
630
631        unsafe {
632            loop {
633                // Try to receive a frame from the decoder
634                let ret = ff_sys::avcodec_receive_frame(self.codec_ctx, self.frame);
635
636                if ret == 0 {
637                    // Successfully received a frame
638                    let audio_frame = self.convert_frame_to_audio_frame()?;
639
640                    // Update position based on frame timestamp
641                    let pts = (*self.frame).pts;
642                    if pts != ff_sys::AV_NOPTS_VALUE {
643                        let stream = (*self.format_ctx).streams.add(self.stream_index as usize);
644                        let time_base = (*(*stream)).time_base;
645                        let timestamp_secs =
646                            pts as f64 * time_base.num as f64 / time_base.den as f64;
647                        self.position = Duration::from_secs_f64(timestamp_secs);
648                    }
649
650                    return Ok(Some(audio_frame));
651                } else if ret == ff_sys::error_codes::EAGAIN {
652                    // Need to send more packets to the decoder
653                    // Read a packet from the file
654                    let read_ret = ff_sys::av_read_frame(self.format_ctx, self.packet);
655
656                    if read_ret == ff_sys::error_codes::EOF {
657                        // End of file - flush the decoder
658                        ff_sys::avcodec_send_packet(self.codec_ctx, ptr::null());
659                        self.eof = true;
660                        continue;
661                    } else if read_ret < 0 {
662                        return Err(DecodeError::Ffmpeg {
663                            code: read_ret,
664                            message: format!(
665                                "Failed to read frame: {}",
666                                ff_sys::av_error_string(read_ret)
667                            ),
668                        });
669                    }
670
671                    // Check if this packet belongs to the audio stream
672                    if (*self.packet).stream_index == self.stream_index {
673                        // Send the packet to the decoder
674                        let send_ret = ff_sys::avcodec_send_packet(self.codec_ctx, self.packet);
675                        ff_sys::av_packet_unref(self.packet);
676
677                        if send_ret < 0 && send_ret != ff_sys::error_codes::EAGAIN {
678                            return Err(DecodeError::Ffmpeg {
679                                code: send_ret,
680                                message: format!(
681                                    "Failed to send packet: {}",
682                                    ff_sys::av_error_string(send_ret)
683                                ),
684                            });
685                        }
686                    } else {
687                        // Not our stream, unref and continue
688                        ff_sys::av_packet_unref(self.packet);
689                    }
690                } else if ret == ff_sys::error_codes::EOF {
691                    // Decoder has been fully flushed
692                    self.eof = true;
693                    return Ok(None);
694                } else {
695                    return Err(DecodeError::DecodingFailed {
696                        timestamp: Some(self.position),
697                        reason: ff_sys::av_error_string(ret),
698                    });
699                }
700            }
701        }
702    }
703
704    /// Converts an AVFrame to an AudioFrame, applying sample format conversion if needed.
705    unsafe fn convert_frame_to_audio_frame(&mut self) -> Result<AudioFrame, DecodeError> {
706        // SAFETY: Caller ensures self.frame is valid
707        unsafe {
708            let nb_samples = (*self.frame).nb_samples as usize;
709            let channels = (*self.frame).ch_layout.nb_channels as u32;
710            let sample_rate = (*self.frame).sample_rate as u32;
711            let src_format = (*self.frame).format;
712
713            // Determine if we need conversion
714            let needs_conversion = self.output_format.is_some()
715                || self.output_sample_rate.is_some()
716                || self.output_channels.is_some();
717
718            if needs_conversion {
719                self.convert_with_swr(nb_samples, channels, sample_rate, src_format)
720            } else {
721                self.av_frame_to_audio_frame(self.frame)
722            }
723        }
724    }
725
726    /// Converts sample format/rate/channels using SwResample.
727    unsafe fn convert_with_swr(
728        &mut self,
729        nb_samples: usize,
730        src_channels: u32,
731        src_sample_rate: u32,
732        src_format: i32,
733    ) -> Result<AudioFrame, DecodeError> {
734        // Determine target parameters
735        let dst_format = self
736            .output_format
737            .map_or(src_format, Self::sample_format_to_av);
738        let dst_sample_rate = self.output_sample_rate.unwrap_or(src_sample_rate);
739        let dst_channels = self.output_channels.unwrap_or(src_channels);
740
741        // If no conversion is needed, return the frame directly
742        if src_format == dst_format
743            && src_sample_rate == dst_sample_rate
744            && src_channels == dst_channels
745        {
746            return unsafe { self.av_frame_to_audio_frame(self.frame) };
747        }
748
749        // Create channel layouts for source and destination
750        // SAFETY: We'll properly clean up these layouts
751        let mut src_ch_layout = unsafe { Self::create_channel_layout(src_channels) };
752        let mut dst_ch_layout = unsafe { Self::create_channel_layout(dst_channels) };
753
754        // Create SwrContext using swr_alloc_set_opts2
755        let mut swr_ctx: *mut SwrContext = ptr::null_mut();
756
757        // SAFETY: FFmpeg API call with valid parameters
758        let ret = unsafe {
759            ff_sys::swr_alloc_set_opts2(
760                &raw mut swr_ctx,
761                &raw const dst_ch_layout,
762                dst_format,
763                dst_sample_rate as i32,
764                &raw const src_ch_layout,
765                src_format,
766                src_sample_rate as i32,
767                0,
768                ptr::null_mut(),
769            )
770        };
771
772        if ret < 0 {
773            // Clean up channel layouts
774            unsafe {
775                ff_sys::av_channel_layout_uninit(&raw mut src_ch_layout);
776                ff_sys::av_channel_layout_uninit(&raw mut dst_ch_layout);
777            }
778            return Err(DecodeError::Ffmpeg {
779                code: ret,
780                message: format!(
781                    "Failed to allocate SwrContext: {}",
782                    ff_sys::av_error_string(ret)
783                ),
784            });
785        }
786
787        // Wrap in RAII guard for automatic cleanup
788        let _swr_guard = SwrContextGuard(swr_ctx);
789
790        // Initialize the resampler
791        // SAFETY: swr_ctx is valid
792        let ret = unsafe { ff_sys::swr_init(swr_ctx) };
793        if ret < 0 {
794            // Clean up channel layouts
795            unsafe {
796                ff_sys::av_channel_layout_uninit(&raw mut src_ch_layout);
797                ff_sys::av_channel_layout_uninit(&raw mut dst_ch_layout);
798            }
799            return Err(DecodeError::Ffmpeg {
800                code: ret,
801                message: format!(
802                    "Failed to initialize SwrContext: {}",
803                    ff_sys::av_error_string(ret)
804                ),
805            });
806        }
807
808        // Calculate output sample count
809        // SAFETY: swr_ctx is valid and initialized
810        let out_samples = unsafe { ff_sys::swr_get_out_samples(swr_ctx, nb_samples as i32) };
811
812        if out_samples < 0 {
813            // Clean up channel layouts
814            unsafe {
815                ff_sys::av_channel_layout_uninit(&raw mut src_ch_layout);
816                ff_sys::av_channel_layout_uninit(&raw mut dst_ch_layout);
817            }
818            return Err(DecodeError::Ffmpeg {
819                code: 0,
820                message: "Failed to calculate output sample count".to_string(),
821            });
822        }
823
824        let out_samples = out_samples as usize;
825
826        // Calculate buffer size for output
827        let dst_sample_fmt = Self::convert_sample_format(dst_format);
828        let bytes_per_sample = dst_sample_fmt.bytes_per_sample();
829        let is_planar = dst_sample_fmt.is_planar();
830
831        // Allocate output buffer
832        let buffer_size = if is_planar {
833            // For planar formats, each plane has samples * bytes_per_sample
834            out_samples * bytes_per_sample * dst_channels as usize
835        } else {
836            // For packed formats, interleaved samples
837            out_samples * bytes_per_sample * dst_channels as usize
838        };
839
840        let mut out_buffer = vec![0u8; buffer_size];
841
842        // Prepare output pointers for swr_convert
843        let mut out_ptrs = if is_planar {
844            // For planar formats, create separate pointers for each channel
845            let plane_size = out_samples * bytes_per_sample;
846            (0..dst_channels)
847                .map(|i| {
848                    let offset = i as usize * plane_size;
849                    out_buffer[offset..].as_mut_ptr()
850                })
851                .collect::<Vec<_>>()
852        } else {
853            // For packed formats, single pointer
854            vec![out_buffer.as_mut_ptr()]
855        };
856
857        // Get input data pointers from frame
858        // SAFETY: self.frame is valid
859        let in_ptrs = unsafe { (*self.frame).data };
860
861        // Convert samples using SwResample
862        // SAFETY: All pointers are valid and buffers are properly sized
863        let converted_samples = unsafe {
864            ff_sys::swr_convert(
865                swr_ctx,
866                out_ptrs.as_mut_ptr(),
867                out_samples as i32,
868                in_ptrs.as_ptr() as *mut *const u8,
869                nb_samples as i32,
870            )
871        };
872
873        // Clean up channel layouts
874        unsafe {
875            ff_sys::av_channel_layout_uninit(&raw mut src_ch_layout);
876            ff_sys::av_channel_layout_uninit(&raw mut dst_ch_layout);
877        }
878
879        if converted_samples < 0 {
880            return Err(DecodeError::Ffmpeg {
881                code: converted_samples,
882                message: format!(
883                    "Failed to convert samples: {}",
884                    ff_sys::av_error_string(converted_samples)
885                ),
886            });
887        }
888
889        // Extract timestamp from original frame
890        // SAFETY: self.frame is valid
891        let timestamp = unsafe {
892            let pts = (*self.frame).pts;
893            if pts != ff_sys::AV_NOPTS_VALUE {
894                let stream = (*self.format_ctx).streams.add(self.stream_index as usize);
895                let time_base = (*(*stream)).time_base;
896                Timestamp::new(pts, Rational::new(time_base.num, time_base.den))
897            } else {
898                Timestamp::invalid()
899            }
900        };
901
902        // Create planes for AudioFrame
903        let planes = if is_planar {
904            let plane_size = converted_samples as usize * bytes_per_sample;
905            (0..dst_channels)
906                .map(|i| {
907                    let offset = i as usize * plane_size;
908                    out_buffer[offset..offset + plane_size].to_vec()
909                })
910                .collect()
911        } else {
912            // For packed formats, single plane with all data
913            vec![
914                out_buffer[..converted_samples as usize * bytes_per_sample * dst_channels as usize]
915                    .to_vec(),
916            ]
917        };
918
919        AudioFrame::new(
920            planes,
921            converted_samples as usize,
922            dst_channels,
923            dst_sample_rate,
924            dst_sample_fmt,
925            timestamp,
926        )
927        .map_err(|e| DecodeError::Ffmpeg {
928            code: 0,
929            message: format!("Failed to create AudioFrame: {e}"),
930        })
931    }
932
933    /// Converts an AVFrame to an AudioFrame.
934    unsafe fn av_frame_to_audio_frame(
935        &self,
936        frame: *const AVFrame,
937    ) -> Result<AudioFrame, DecodeError> {
938        // SAFETY: Caller ensures frame and format_ctx are valid
939        unsafe {
940            let nb_samples = (*frame).nb_samples as usize;
941            let channels = (*frame).ch_layout.nb_channels as u32;
942            let sample_rate = (*frame).sample_rate as u32;
943            let format = Self::convert_sample_format((*frame).format);
944
945            // Extract timestamp
946            let pts = (*frame).pts;
947            let timestamp = if pts != ff_sys::AV_NOPTS_VALUE {
948                let stream = (*self.format_ctx).streams.add(self.stream_index as usize);
949                let time_base = (*(*stream)).time_base;
950                Timestamp::new(
951                    pts as i64,
952                    Rational::new(time_base.num as i32, time_base.den as i32),
953                )
954            } else {
955                Timestamp::invalid()
956            };
957
958            // Convert frame to planes
959            let planes = Self::extract_planes(frame, nb_samples, channels, format)?;
960
961            AudioFrame::new(planes, nb_samples, channels, sample_rate, format, timestamp).map_err(
962                |e| DecodeError::Ffmpeg {
963                    code: 0,
964                    message: format!("Failed to create AudioFrame: {e}"),
965                },
966            )
967        }
968    }
969
970    /// Extracts planes from an AVFrame.
971    unsafe fn extract_planes(
972        frame: *const AVFrame,
973        nb_samples: usize,
974        channels: u32,
975        format: SampleFormat,
976    ) -> Result<Vec<Vec<u8>>, DecodeError> {
977        // SAFETY: Caller ensures frame is valid and format matches actual frame format
978        unsafe {
979            let mut planes = Vec::new();
980            let bytes_per_sample = format.bytes_per_sample();
981
982            if format.is_planar() {
983                // Planar: one plane per channel
984                for ch in 0..channels as usize {
985                    let plane_size = nb_samples * bytes_per_sample;
986                    let mut plane_data = vec![0u8; plane_size];
987
988                    let src_ptr = (*frame).data[ch];
989                    std::ptr::copy_nonoverlapping(src_ptr, plane_data.as_mut_ptr(), plane_size);
990
991                    planes.push(plane_data);
992                }
993            } else {
994                // Packed: single plane with interleaved samples
995                let plane_size = nb_samples * channels as usize * bytes_per_sample;
996                let mut plane_data = vec![0u8; plane_size];
997
998                let src_ptr = (*frame).data[0];
999                std::ptr::copy_nonoverlapping(src_ptr, plane_data.as_mut_ptr(), plane_size);
1000
1001                planes.push(plane_data);
1002            }
1003
1004            Ok(planes)
1005        }
1006    }
1007
1008    /// Converts our `SampleFormat` to FFmpeg `AVSampleFormat`.
1009    fn sample_format_to_av(format: SampleFormat) -> AVSampleFormat {
1010        match format {
1011            SampleFormat::U8 => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8,
1012            SampleFormat::I16 => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16,
1013            SampleFormat::I32 => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32,
1014            SampleFormat::F32 => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLT,
1015            SampleFormat::F64 => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBL,
1016            SampleFormat::U8p => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8P,
1017            SampleFormat::I16p => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16P,
1018            SampleFormat::I32p => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32P,
1019            SampleFormat::F32p => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLTP,
1020            SampleFormat::F64p => ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBLP,
1021            _ => {
1022                log::warn!(
1023                    "sample_format has no AV mapping, falling back to F32 format={format:?} fallback=AV_SAMPLE_FMT_FLT"
1024                );
1025                ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLT
1026            }
1027        }
1028    }
1029
1030    /// Returns the current playback position.
1031    pub(crate) fn position(&self) -> Duration {
1032        self.position
1033    }
1034
1035    /// Returns whether end of file has been reached.
1036    pub(crate) fn is_eof(&self) -> bool {
1037        self.eof
1038    }
1039
1040    /// Converts a `Duration` to a presentation timestamp (PTS) in stream time_base units.
1041    fn duration_to_pts(&self, duration: Duration) -> i64 {
1042        // SAFETY: format_ctx and stream_index are valid (owned by AudioDecoderInner)
1043        let time_base = unsafe {
1044            let stream = (*self.format_ctx).streams.add(self.stream_index as usize);
1045            (*(*stream)).time_base
1046        };
1047
1048        // Convert: duration (seconds) * (time_base.den / time_base.num) = PTS
1049        let time_base_f64 = time_base.den as f64 / time_base.num as f64;
1050        (duration.as_secs_f64() * time_base_f64) as i64
1051    }
1052
1053    /// Seeks to a specified position in the audio stream.
1054    ///
1055    /// # Arguments
1056    ///
1057    /// * `position` - Target position to seek to.
1058    /// * `mode` - Seek mode (Keyframe, Exact, or Backward).
1059    ///
1060    /// # Errors
1061    ///
1062    /// Returns [`DecodeError::SeekFailed`] if the seek operation fails.
1063    pub(crate) fn seek(
1064        &mut self,
1065        position: Duration,
1066        mode: crate::SeekMode,
1067    ) -> Result<(), DecodeError> {
1068        use crate::SeekMode;
1069
1070        let timestamp = self.duration_to_pts(position);
1071        let flags = ff_sys::avformat::seek_flags::BACKWARD;
1072
1073        // 1. Clear any pending packet and frame
1074        // SAFETY: packet and frame are valid (owned by AudioDecoderInner)
1075        unsafe {
1076            ff_sys::av_packet_unref(self.packet);
1077            ff_sys::av_frame_unref(self.frame);
1078        }
1079
1080        // 2. Seek in the format context
1081        // SAFETY: format_ctx and stream_index are valid
1082        unsafe {
1083            ff_sys::avformat::seek_frame(self.format_ctx, self.stream_index, timestamp, flags)
1084                .map_err(|e| DecodeError::SeekFailed {
1085                    target: position,
1086                    reason: ff_sys::av_error_string(e),
1087                })?;
1088        }
1089
1090        // 3. Flush decoder buffers
1091        // SAFETY: codec_ctx is valid (owned by AudioDecoderInner)
1092        unsafe {
1093            ff_sys::avcodec::flush_buffers(self.codec_ctx);
1094        }
1095
1096        // 4. Drain any remaining frames from the decoder after flush
1097        // SAFETY: codec_ctx and frame are valid
1098        unsafe {
1099            loop {
1100                let ret = ff_sys::avcodec_receive_frame(self.codec_ctx, self.frame);
1101                if ret == ff_sys::error_codes::EAGAIN || ret == ff_sys::error_codes::EOF {
1102                    break;
1103                } else if ret == 0 {
1104                    ff_sys::av_frame_unref(self.frame);
1105                } else {
1106                    break;
1107                }
1108            }
1109        }
1110
1111        // 5. Reset internal state
1112        self.eof = false;
1113
1114        // 6. For exact mode, skip frames to reach exact position
1115        if mode == SeekMode::Exact {
1116            self.skip_to_exact(position)?;
1117        }
1118        // For Keyframe/Backward modes, we're already at the keyframe after av_seek_frame
1119
1120        Ok(())
1121    }
1122
1123    /// Skips frames until reaching the exact target position.
1124    ///
1125    /// This is used by [`Self::seek`] when `SeekMode::Exact` is specified.
1126    ///
1127    /// # Arguments
1128    ///
1129    /// * `target` - The exact target position.
1130    fn skip_to_exact(&mut self, target: Duration) -> Result<(), DecodeError> {
1131        // Decode frames until we reach or pass the target
1132        while let Some(frame) = self.decode_one()? {
1133            let frame_time = frame.timestamp().as_duration();
1134            if frame_time >= target {
1135                // We've reached the target position
1136                break;
1137            }
1138            // Continue decoding to get closer (frames are automatically dropped)
1139        }
1140        Ok(())
1141    }
1142
1143    /// Flushes the decoder's internal buffers.
1144    pub(crate) fn flush(&mut self) {
1145        // SAFETY: codec_ctx is valid and owned by this instance
1146        unsafe {
1147            ff_sys::avcodec::flush_buffers(self.codec_ctx);
1148        }
1149        self.eof = false;
1150    }
1151}
1152
1153impl Drop for AudioDecoderInner {
1154    fn drop(&mut self) {
1155        // Free SwResample context if allocated
1156        if let Some(swr_ctx) = self.swr_ctx {
1157            // SAFETY: swr_ctx is valid and owned by this instance
1158            unsafe {
1159                // swr_free frees a SwrContext
1160                ff_sys::swr_free(&mut (swr_ctx as *mut _));
1161            }
1162        }
1163
1164        // Free frame and packet
1165        if !self.frame.is_null() {
1166            // SAFETY: self.frame is valid and owned by this instance
1167            unsafe {
1168                ff_sys::av_frame_free(&mut (self.frame as *mut _));
1169            }
1170        }
1171
1172        if !self.packet.is_null() {
1173            // SAFETY: self.packet is valid and owned by this instance
1174            unsafe {
1175                ff_sys::av_packet_free(&mut (self.packet as *mut _));
1176            }
1177        }
1178
1179        // Free codec context
1180        if !self.codec_ctx.is_null() {
1181            // SAFETY: self.codec_ctx is valid and owned by this instance
1182            unsafe {
1183                ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
1184            }
1185        }
1186
1187        // Close format context
1188        if !self.format_ctx.is_null() {
1189            // SAFETY: self.format_ctx is valid and owned by this instance
1190            unsafe {
1191                ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
1192            }
1193        }
1194    }
1195}
1196
1197// SAFETY: AudioDecoderInner manages FFmpeg contexts which are thread-safe when not shared.
1198// We don't expose mutable access across threads, so Send is safe.
1199unsafe impl Send for AudioDecoderInner {}
1200
1201#[cfg(test)]
1202#[allow(unsafe_code)]
1203mod tests {
1204    use ff_format::channel::ChannelLayout;
1205
1206    use super::AudioDecoderInner;
1207
1208    /// Constructs an `AVChannelLayout` with `AV_CHANNEL_ORDER_NATIVE` and the given mask.
1209    fn native_layout(mask: u64, nb_channels: i32) -> ff_sys::AVChannelLayout {
1210        ff_sys::AVChannelLayout {
1211            order: ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_NATIVE,
1212            nb_channels,
1213            u: ff_sys::AVChannelLayout__bindgen_ty_1 { mask },
1214            opaque: std::ptr::null_mut(),
1215        }
1216    }
1217
1218    /// Constructs an `AVChannelLayout` with `AV_CHANNEL_ORDER_UNSPEC`.
1219    fn unspec_layout(nb_channels: i32) -> ff_sys::AVChannelLayout {
1220        ff_sys::AVChannelLayout {
1221            order: ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_UNSPEC,
1222            nb_channels,
1223            u: ff_sys::AVChannelLayout__bindgen_ty_1 { mask: 0 },
1224            opaque: std::ptr::null_mut(),
1225        }
1226    }
1227
1228    #[test]
1229    fn native_mask_mono() {
1230        let layout = native_layout(0x4, 1);
1231        assert_eq!(
1232            AudioDecoderInner::convert_channel_layout(&layout, 1),
1233            ChannelLayout::Mono
1234        );
1235    }
1236
1237    #[test]
1238    fn native_mask_stereo() {
1239        let layout = native_layout(0x3, 2);
1240        assert_eq!(
1241            AudioDecoderInner::convert_channel_layout(&layout, 2),
1242            ChannelLayout::Stereo
1243        );
1244    }
1245
1246    #[test]
1247    fn native_mask_stereo2_1() {
1248        let layout = native_layout(0x103, 3);
1249        assert_eq!(
1250            AudioDecoderInner::convert_channel_layout(&layout, 3),
1251            ChannelLayout::Stereo2_1
1252        );
1253    }
1254
1255    #[test]
1256    fn native_mask_surround3_0() {
1257        let layout = native_layout(0x7, 3);
1258        assert_eq!(
1259            AudioDecoderInner::convert_channel_layout(&layout, 3),
1260            ChannelLayout::Surround3_0
1261        );
1262    }
1263
1264    #[test]
1265    fn native_mask_quad() {
1266        let layout = native_layout(0x33, 4);
1267        assert_eq!(
1268            AudioDecoderInner::convert_channel_layout(&layout, 4),
1269            ChannelLayout::Quad
1270        );
1271    }
1272
1273    #[test]
1274    fn native_mask_surround5_0() {
1275        let layout = native_layout(0x37, 5);
1276        assert_eq!(
1277            AudioDecoderInner::convert_channel_layout(&layout, 5),
1278            ChannelLayout::Surround5_0
1279        );
1280    }
1281
1282    #[test]
1283    fn native_mask_surround5_1() {
1284        let layout = native_layout(0x3F, 6);
1285        assert_eq!(
1286            AudioDecoderInner::convert_channel_layout(&layout, 6),
1287            ChannelLayout::Surround5_1
1288        );
1289    }
1290
1291    #[test]
1292    fn native_mask_surround6_1() {
1293        let layout = native_layout(0x13F, 7);
1294        assert_eq!(
1295            AudioDecoderInner::convert_channel_layout(&layout, 7),
1296            ChannelLayout::Surround6_1
1297        );
1298    }
1299
1300    #[test]
1301    fn native_mask_surround7_1() {
1302        let layout = native_layout(0x63F, 8);
1303        assert_eq!(
1304            AudioDecoderInner::convert_channel_layout(&layout, 8),
1305            ChannelLayout::Surround7_1
1306        );
1307    }
1308
1309    #[test]
1310    fn native_mask_unknown_falls_back_to_from_channels() {
1311        // mask=0x1 is not a standard layout; should fall back to from_channels(2)
1312        let layout = native_layout(0x1, 2);
1313        assert_eq!(
1314            AudioDecoderInner::convert_channel_layout(&layout, 2),
1315            ChannelLayout::from_channels(2)
1316        );
1317    }
1318
1319    #[test]
1320    fn non_native_order_falls_back_to_from_channels() {
1321        let layout = unspec_layout(6);
1322        assert_eq!(
1323            AudioDecoderInner::convert_channel_layout(&layout, 6),
1324            ChannelLayout::from_channels(6)
1325        );
1326    }
1327
1328    // -------------------------------------------------------------------------
1329    // extract_codec_name
1330    // -------------------------------------------------------------------------
1331
1332    #[test]
1333    fn codec_name_should_return_h264_for_h264_codec_id() {
1334        let name =
1335            unsafe { AudioDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_H264) };
1336        assert_eq!(name, "h264");
1337    }
1338
1339    #[test]
1340    fn codec_name_should_return_none_for_none_codec_id() {
1341        let name =
1342            unsafe { AudioDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_NONE) };
1343        assert_eq!(name, "none");
1344    }
1345
1346    #[test]
1347    fn unsupported_codec_error_should_include_codec_name() {
1348        let codec_id = ff_sys::AVCodecID_AV_CODEC_ID_MP3;
1349        let codec_name = unsafe { AudioDecoderInner::extract_codec_name(codec_id) };
1350        let error = crate::error::DecodeError::UnsupportedCodec {
1351            codec: format!("{codec_name} (codec_id={codec_id:?})"),
1352        };
1353        let msg = error.to_string();
1354        assert!(msg.contains("mp3"), "expected codec name in error: {msg}");
1355        assert!(
1356            msg.contains("codec_id="),
1357            "expected codec_id in error: {msg}"
1358        );
1359    }
1360}