1#![allow(unsafe_code)]
8#![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
45pub(crate) struct AudioDecoderInner {
50 format_ctx: *mut AVFormatContext,
52 codec_ctx: *mut AVCodecContext,
54 stream_index: i32,
56 output_format: Option<SampleFormat>,
58 output_sample_rate: Option<u32>,
60 output_channels: Option<u32>,
62 swr_ctx: Option<resample_inner::SwrContextGuard>,
64 swr_key: Option<resample_inner::SwrKey>,
66 is_live: bool,
68 eof: bool,
70 position: Duration,
72 packet: *mut AVPacket,
74 frame: *mut AVFrame,
76 url: Option<String>,
78 network_opts: NetworkOptions,
80 reconnect_count: u32,
82}
83
84impl AudioDecoderInner {
85 #[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 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 if is_network_url {
124 crate::network::check_srt_url(path_str)?;
125 }
126
127 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 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 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 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 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 let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
185 let codec_ctx = codec_ctx_guard.as_ptr();
186
187 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 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 let stream_info =
217 unsafe { Self::extract_stream_info(format_ctx, stream_index as i32, codec_ctx)? };
218
219 let container_info = unsafe { Self::extract_container_info(format_ctx) };
222
223 let packet_guard = unsafe { AvPacketGuard::new()? };
226 let frame_guard = unsafe { AvFrameGuard::new()? };
227
228 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 unsafe fn find_audio_stream(format_ctx: *mut AVFormatContext) -> Option<(usize, AVCodecID)> {
263 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 unsafe fn extract_codec_name(codec_id: ff_sys::AVCodecID) -> String {
282 let name_ptr = unsafe { ff_sys::avcodec_get_name(codec_id) };
284 if name_ptr.is_null() {
285 return String::from("unknown");
286 }
287 unsafe { CStr::from_ptr(name_ptr).to_string_lossy().into_owned() }
289 }
290
291 unsafe fn extract_stream_info(
293 format_ctx: *mut AVFormatContext,
294 stream_index: i32,
295 codec_ctx: *mut AVCodecContext,
296 ) -> Result<AudioStreamInfo, DecodeError> {
297 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 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 let sample_format = resample_inner::convert_sample_format(sample_fmt);
322
323 let channel_layout_enum = Self::convert_channel_layout(&channel_layout, channels);
325
326 let codec = Self::convert_codec(codec_id);
328 let codec_name = unsafe { Self::extract_codec_name(codec_id) };
329
330 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 unsafe fn extract_container_info(format_ctx: *mut AVFormatContext) -> ContainerInfo {
353 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 fn convert_channel_layout(layout: &ff_sys::AVChannelLayout, channels: u32) -> ChannelLayout {
385 if layout.order == ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_NATIVE {
386 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 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 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 let ret = ff_sys::avcodec_receive_frame(self.codec_ctx, self.frame);
471
472 if ret == 0 {
473 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 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 let read_ret = ff_sys::av_read_frame(self.format_ctx, self.packet);
500
501 if read_ret == ff_sys::error_codes::EOF {
502 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 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 if (*self.packet).stream_index == self.stream_index {
526 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 ff_sys::av_packet_unref(self.packet);
542 }
543 } else if ret == ff_sys::error_codes::EOF {
544 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 pub(crate) fn position(&self) -> Duration {
559 self.position
560 }
561
562 pub(crate) fn is_eof(&self) -> bool {
564 self.eof
565 }
566
567 pub(crate) fn is_live(&self) -> bool {
572 self.is_live
573 }
574
575 fn duration_to_pts(&self, duration: Duration) -> i64 {
577 let time_base = unsafe {
579 let stream = (*self.format_ctx).streams.add(self.stream_index as usize);
580 (*(*stream)).time_base
581 };
582
583 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 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 unsafe {
611 ff_sys::av_packet_unref(self.packet);
612 ff_sys::av_frame_unref(self.frame);
613 }
614
615 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 unsafe {
629 ff_sys::avcodec::flush_buffers(self.codec_ctx);
630 }
631 self.swr_ctx = None;
632 self.swr_key = None;
633
634 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 self.eof = false;
651
652 if mode == SeekMode::Exact {
654 self.skip_to_exact(position)?;
655 }
656 Ok(())
659 }
660
661 fn skip_to_exact(&mut self, target: Duration) -> Result<(), DecodeError> {
669 while let Some(frame) = self.decode_one()? {
671 let frame_time = frame.timestamp().as_duration();
672 if frame_time >= target {
673 break;
675 }
676 }
678 Ok(())
679 }
680
681 pub(crate) fn flush(&mut self) {
683 unsafe {
685 ff_sys::avcodec::flush_buffers(self.codec_ctx);
686 }
687 self.eof = false;
688 }
689
690 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(()), };
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 fn reopen(&mut self, url: &str) -> Result<(), DecodeError> {
735 unsafe {
739 ff_sys::avformat::close_input(std::ptr::addr_of_mut!(self.format_ctx));
740 }
741
742 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 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 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 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 if !self.frame.is_null() {
788 unsafe {
790 ff_sys::av_frame_free(&mut (self.frame as *mut _));
791 }
792 }
793
794 if !self.packet.is_null() {
795 unsafe {
797 ff_sys::av_packet_free(&mut (self.packet as *mut _));
798 }
799 }
800
801 if !self.codec_ctx.is_null() {
803 unsafe {
805 ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
806 }
807 }
808
809 if !self.format_ctx.is_null() {
811 unsafe {
813 ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
814 }
815 }
816 }
817}
818
819unsafe 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 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 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 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 #[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}