Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## 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.

### Fixed

- `extract_audio/3` and `transcode/3` re-encode a plain mono or stereo
Expand Down
17 changes: 17 additions & 0 deletions native/exmpeg_native/src/ffi_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
47 changes: 36 additions & 11 deletions native/exmpeg_native/src/probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String>,
/// Stream-level bit rate in bps; `0` when unset.
Expand Down Expand Up @@ -192,17 +193,14 @@ fn collect_streams(input: &AVFormatContextInput) -> Vec<ProbeStream> {
}

fn resolve_codec(codec_id: ffi::AVCodecID) -> (String, Option<String>) {
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)
},
)
}
Expand All @@ -228,3 +226,30 @@ fn ticks_to_seconds(ticks: i64, denom: i64) -> Option<f64> {
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)
);
}
}