From daba907f824dddab6642069a496592556f6e9151 Mon Sep 17 00:00:00 2001 From: Dominic Rubas <1042243+rubas@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:59:17 +0200 Subject: [PATCH 1/2] Report the codec name from the codec descriptor in probe/1 resolve_codec used the name of the first registered decoder, so probe/1 reported "mp3float" for MP3 and "libdav1d" for AV1, and the value changed with the decoders of the FFmpeg build. Read name and long name from avcodec_descriptor_get, as ffprobe does. --- native/exmpeg_native/src/ffi_helpers.rs | 17 +++++++++ native/exmpeg_native/src/probe.rs | 47 +++++++++++++++++++------ 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/native/exmpeg_native/src/ffi_helpers.rs b/native/exmpeg_native/src/ffi_helpers.rs index b96f8e8..32cd9cf 100644 --- a/native/exmpeg_native/src/ffi_helpers.rs +++ b/native/exmpeg_native/src/ffi_helpers.rs @@ -42,6 +42,23 @@ pub(crate) fn codec_name(id: ffi::AVCodecID) -> &'static CStr { unsafe { CStr::from_ptr(ffi::avcodec_get_name(id)) } } +/// The name and long name of `id` from the codec descriptor table, such +/// as `("mp3", Some("MP3 (MPEG audio layer 3)"))`. Unlike a decoder name, +/// they do not depend on the decoders that the FFmpeg build has. `None` +/// when `id` has no descriptor. +pub(crate) fn codec_descriptor( + id: ffi::AVCodecID, +) -> Option<(&'static CStr, Option<&'static CStr>)> { + // SAFETY: `avcodec_descriptor_get` returns NULL or a pointer into a + // static table that lives for the whole program. A descriptor's `name` + // is never NULL, but its `long_name` may be. + unsafe { + let desc = ffi::avcodec_descriptor_get(id).as_ref()?; + let long_name = (!desc.long_name.is_null()).then(|| CStr::from_ptr(desc.long_name)); + Some((CStr::from_ptr(desc.name), long_name)) + } +} + /// The out-of-band codec extradata of `params`, empty when it has none. pub(crate) fn extradata(params: &AVCodecParameters) -> &[u8] { if params.extradata.is_null() || params.extradata_size <= 0 { diff --git a/native/exmpeg_native/src/probe.rs b/native/exmpeg_native/src/probe.rs index 3920e96..52a48d7 100644 --- a/native/exmpeg_native/src/probe.rs +++ b/native/exmpeg_native/src/probe.rs @@ -4,13 +4,13 @@ //! //! Built entirely on rsmpeg's safe wrappers — no `unsafe` in this module. -use rsmpeg::avcodec::AVCodec; use rsmpeg::avformat::AVFormatContextInput; use rsmpeg::avutil::{get_pix_fmt_name, get_sample_fmt_name}; use rsmpeg::ffi; use rustler::NifMap; use crate::errors::NativeError; +use crate::ffi_helpers; use crate::input::InputSource; /// Whole-file probe report: container-level format info plus a stream @@ -47,8 +47,9 @@ pub(crate) struct ProbeFormat { pub(crate) struct ProbeStream { pub(crate) index: u32, pub(crate) kind: String, - /// Codec short name (`"h264"`, `"aac"`); `"unknown"` when ffmpeg has - /// no decoder registered for the codec id. + /// Codec name as ffprobe reports it (`"h264"`, `"mp3"`), not the name + /// of a decoder; `"unknown"` when FFmpeg has no descriptor for the + /// codec id. pub(crate) codec: String, pub(crate) codec_long_name: Option, /// Stream-level bit rate in bps; `0` when unset. @@ -192,17 +193,14 @@ fn collect_streams(input: &AVFormatContextInput) -> Vec { } fn resolve_codec(codec_id: ffi::AVCodecID) -> (String, Option) { - AVCodec::find_decoder(codec_id).map_or_else( + ffi_helpers::codec_descriptor(codec_id).map_or_else( || ("unknown".to_owned(), None), - |codec| { - let name = codec.name().to_string_lossy().into_owned(); - let long = codec - .long_name() - .to_str() - .ok() + |(name, long_name)| { + let long = long_name + .and_then(|s| s.to_str().ok()) .filter(|s| !s.is_empty()) .map(str::to_owned); - (name, long) + (name.to_string_lossy().into_owned(), long) }, ) } @@ -228,3 +226,30 @@ fn ticks_to_seconds(ticks: i64, denom: i64) -> Option { Some(ticks as f64 / denom as f64) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_codec_reports_the_codec_name_not_a_decoder_name() { + assert_eq!( + resolve_codec(ffi::AV_CODEC_ID_MP3), + ( + "mp3".to_owned(), + Some("MP3 (MPEG audio layer 3)".to_owned()) + ) + ); + assert_eq!(resolve_codec(ffi::AV_CODEC_ID_AV1).0, "av1"); + assert_eq!(resolve_codec(ffi::AV_CODEC_ID_H264).0, "h264"); + assert_eq!(resolve_codec(ffi::AV_CODEC_ID_AAC).0, "aac"); + } + + #[test] + fn resolve_codec_reports_unknown_for_an_id_without_descriptor() { + assert_eq!( + resolve_codec(ffi::AV_CODEC_ID_NONE), + ("unknown".to_owned(), None) + ); + } +} From cc5a85827581cd767e8ebdce1eb2034445196473 Mon Sep 17 00:00:00 2001 From: Dominic Rubas <1042243+rubas@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:59:17 +0200 Subject: [PATCH 2/2] CHANGELOG: probe/1 codec is the codec name, not the decoder name --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75760d1..15686a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## Unreleased + +### Changed + +- Breaking: `Exmpeg.probe/1` now reports `codec` and `codec_long_name` + the way ffprobe reports `codec_name` and `codec_long_name`: the codec + name, such as `"mp3"` and `"av1"`, not the name of the first decoder, + such as `"mp3float"` and `"libdav1d"`. The value no longer depends on + the decoders in the FFmpeg build, so a source build and the precompiled + NIF report the same name. Names that were already codec names, such as + `"h264"`, `"aac"`, `"vp9"`, `"opus"`, and `"pcm_s16le"`, stay the + same. + ## 0.6.0 - 2026-09-23 ### Changed