Skip to main content

mediadecode_ffmpeg/
channel_layout.rs

1//! Conversions from FFmpeg's [`ffmpeg_next::ChannelLayout`] /
2//! [`ffmpeg_next::ffi::AVChannelOrder`] to the channel-layout types
3//! mediadecode owns ([`mediadecode::channel::ChannelLayoutKind`],
4//! [`mediadecode::channel::AudioChannelOrderKind`],
5//! [`mediadecode::channel::AudioChannelSpec`],
6//! [`mediadecode::channel::AudioChannelLayout`]).
7//!
8//! These live as **free functions** (not `From` trait impls) because of
9//! Rust's orphan rule: this crate owns neither `From` nor
10//! `mediadecode::channel::*`, so we can't write the `impl` here. Calling
11//! `mediadecode_ffmpeg::audio_channel_layout_from_ffmpeg(layout)` is the
12//! ergonomic boundary instead.
13
14use core::{ffi::c_char, slice};
15
16use ffmpeg_next::{ChannelLayout, ffi};
17use mediadecode::channel::{
18  AudioChannelLayout, AudioChannelOrderKind, AudioChannelSpec, ChannelLayoutKind,
19};
20use smol_str::SmolStr;
21use std::vec::Vec;
22
23/// Maps an FFmpeg [`ChannelLayout`] to the high-level
24/// [`ChannelLayoutKind`] tag.
25///
26/// Returns [`ChannelLayoutKind::Unknown`] for layouts that don't match
27/// one of FFmpeg's named-layout constants.
28pub fn channel_layout_kind_from_ffmpeg(value: &ChannelLayout) -> ChannelLayoutKind {
29  match () {
30    () if value.eq(&ChannelLayout::MONO) => ChannelLayoutKind::Mono,
31    () if value.eq(&ChannelLayout::STEREO) => ChannelLayoutKind::Stereo,
32    () if value.eq(&ChannelLayout::STEREO_DOWNMIX) => ChannelLayoutKind::StereoDownmix,
33    () if value.eq(&ChannelLayout::SURROUND) => ChannelLayoutKind::Surround,
34    () if value.eq(&ChannelLayout::QUAD) => ChannelLayoutKind::Quad,
35    () if value.eq(&ChannelLayout::HEXAGONAL) => ChannelLayoutKind::Hexagonal,
36    () if value.eq(&ChannelLayout::OCTAGONAL) => ChannelLayoutKind::Octagonal,
37    () if value.eq(&ChannelLayout::HEXADECAGONAL) => ChannelLayoutKind::Hexadecagonal,
38    () if value.eq(&ChannelLayout::CUBE) => ChannelLayoutKind::Cube,
39    () if value.eq(&ChannelLayout::_2POINT1) => ChannelLayoutKind::Ch2_1,
40    () if value.eq(&ChannelLayout::_2_1) => ChannelLayoutKind::Ch2_1Alt,
41    () if value.eq(&ChannelLayout::_2_2) => ChannelLayoutKind::Ch2_2,
42    () if value.eq(&ChannelLayout::_3POINT1) => ChannelLayoutKind::Ch3_1,
43    () if value.eq(&ChannelLayout::_3POINT1POINT2) => ChannelLayoutKind::Ch3_1_2,
44    () if value.eq(&ChannelLayout::_4POINT0) => ChannelLayoutKind::Ch4_0,
45    () if value.eq(&ChannelLayout::_4POINT1) => ChannelLayoutKind::Ch4_1,
46    () if value.eq(&ChannelLayout::_5POINT0) => ChannelLayoutKind::Ch5_0,
47    () if value.eq(&ChannelLayout::_5POINT0_BACK) => ChannelLayoutKind::Ch5_0Back,
48    () if value.eq(&ChannelLayout::_5POINT1) => ChannelLayoutKind::Ch5_1,
49    () if value.eq(&ChannelLayout::_5POINT1_BACK) => ChannelLayoutKind::Ch5_1Back,
50    () if value.eq(&ChannelLayout::_5POINT1POINT2_BACK) => ChannelLayoutKind::Ch5_1_2Back,
51    () if value.eq(&ChannelLayout::_5POINT1POINT4_BACK) => ChannelLayoutKind::Ch5_1_4Back,
52    () if value.eq(&ChannelLayout::_6POINT0) => ChannelLayoutKind::Ch6_0,
53    () if value.eq(&ChannelLayout::_6POINT0_FRONT) => ChannelLayoutKind::Ch6_0Front,
54    () if value.eq(&ChannelLayout::_6POINT1) => ChannelLayoutKind::Ch6_1,
55    () if value.eq(&ChannelLayout::_6POINT1_BACK) => ChannelLayoutKind::Ch6_1Back,
56    () if value.eq(&ChannelLayout::_6POINT1_FRONT) => ChannelLayoutKind::Ch6_1Front,
57    () if value.eq(&ChannelLayout::_7POINT0) => ChannelLayoutKind::Ch7_0,
58    () if value.eq(&ChannelLayout::_7POINT0_FRONT) => ChannelLayoutKind::Ch7_0Front,
59    () if value.eq(&ChannelLayout::_7POINT1) => ChannelLayoutKind::Ch7_1,
60    () if value.eq(&ChannelLayout::_7POINT1_WIDE) => ChannelLayoutKind::Ch7_1Wide,
61    () if value.eq(&ChannelLayout::_7POINT1_WIDE_BACK) => ChannelLayoutKind::Ch7_1WideBack,
62    () if value.eq(&ChannelLayout::_7POINT1_TOP_BACK) => ChannelLayoutKind::Ch7_1TopBack,
63    () if value.eq(&ChannelLayout::_7POINT1POINT2) => ChannelLayoutKind::Ch7_1_2,
64    () if value.eq(&ChannelLayout::_7POINT1POINT4_BACK) => ChannelLayoutKind::Ch7_1_4Back,
65    () if value.eq(&ChannelLayout::_7POINT2POINT3) => ChannelLayoutKind::Ch7_2_3,
66    () if value.eq(&ChannelLayout::_9POINT1POINT4_BACK) => ChannelLayoutKind::Ch9_1_4Back,
67    () if value.eq(&ChannelLayout::_22POINT2) => ChannelLayoutKind::Ch22_2,
68    () => ChannelLayoutKind::Unknown,
69  }
70}
71
72/// Maps FFmpeg's [`AVChannelOrder`](ffi::AVChannelOrder) to the
73/// [`AudioChannelOrderKind`] tag.
74pub fn audio_channel_order_kind_from_ffmpeg(value: ffi::AVChannelOrder) -> AudioChannelOrderKind {
75  // Compare via integer rather than enum-matching: the caller often
76  // sources `value` from raw FFmpeg memory (`AVChannelLayout.order`),
77  // and an unknown variant would already be UB before reaching this
78  // function. Going through `as i32` here is sound because the caller
79  // is responsible for the up-conversion path; for the raw-pointer
80  // path use [`audio_channel_order_kind_from_raw`].
81  audio_channel_order_kind_from_raw(value as i32)
82}
83
84/// Variant of [`audio_channel_order_kind_from_ffmpeg`] that takes the
85/// raw integer directly. Use this when the caller has just read
86/// `AVChannelLayout.order` from FFmpeg memory and doesn't want to
87/// risk constructing an invalid bindgen enum value first.
88pub fn audio_channel_order_kind_from_raw(raw: i32) -> AudioChannelOrderKind {
89  match raw {
90    x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE as i32 => AudioChannelOrderKind::Native,
91    x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 => AudioChannelOrderKind::Custom,
92    x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC as i32 => {
93      AudioChannelOrderKind::Ambisonic
94    }
95    _ => AudioChannelOrderKind::Unspecified,
96  }
97}
98
99/// Builds a fully-populated [`AudioChannelLayout`] from an FFmpeg
100/// [`ChannelLayout`].
101///
102/// - Native / Ambisonic layouts populate `native_mask` from
103///   [`ChannelLayout::bits`] (clearing it to `None` if zero).
104/// - Custom layouts populate `custom_channels` from FFmpeg's per-channel
105///   list (`AVChannelLayout.u.map`), with each label drawn from
106///   `AVChannelCustom.name`.
107/// - `description` carries the result of `av_channel_layout_describe`
108///   (FFmpeg's human-readable rendering — e.g. `"5.1(side)"`).
109pub fn audio_channel_layout_from_ffmpeg(value: &ChannelLayout) -> AudioChannelLayout {
110  // SAFETY: `value` is a live reference; the inner `AVChannelLayout`
111  // stays valid for the duration of this call. We hand the raw
112  // address into the pointer-based variant which is the canonical
113  // implementation (avoids forming `&AVChannelLayout` over a
114  // potentially-invalid `order` discriminant).
115  unsafe { audio_channel_layout_from_raw_ptr(&value.0 as *const ffi::AVChannelLayout) }
116}
117
118/// Pointer variant of [`audio_channel_layout_from_ffmpeg`]. Safe-API
119/// callers that already hold a `&ChannelLayout` should prefer that
120/// function; the pointer form exists so the convert path
121/// (which never forms `&AVFrame`) can pass `addr_of!((*av_frame).ch_layout)`
122/// straight through without materializing a typed reference.
123///
124/// # Safety
125/// `ptr` must be a live `*const AVChannelLayout` for the duration of
126/// this call. The function reads `order` raw, then `nb_channels`,
127/// then either `u.mask` (NATIVE / AMBISONIC) or `u.map`
128/// (CUSTOM) — only after the order discriminant has been validated.
129/// It never forms a `&AVChannelLayout` reference.
130pub unsafe fn audio_channel_layout_from_raw_ptr(
131  ptr: *const ffi::AVChannelLayout,
132) -> AudioChannelLayout {
133  use core::ptr::{addr_of, read_unaligned};
134  // Read `order` as a raw integer first — never let Rust assume
135  // the field is a valid `AVChannelOrder`.
136  // SAFETY: `ptr` is a valid `*const AVChannelLayout`; `addr_of!`
137  // computes the field address without forming a reference; reading
138  // as `i32` matches the bindgen enum's `c_int` storage.
139  let order_raw = unsafe { read_unaligned(addr_of!((*ptr).order) as *const i32) };
140  let order = audio_channel_order_kind_from_raw(order_raw);
141  let nb_channels = unsafe { (*ptr).nb_channels };
142
143  // Native / Ambisonic carry the bitmask in the union. Only read
144  // `u.mask` after the order is validated so we don't trip on an
145  // unknown order writing into a future variant of the union.
146  let native_mask = match order {
147    AudioChannelOrderKind::Native | AudioChannelOrderKind::Ambisonic => {
148      // SAFETY: `u.mask` is the union variant for NATIVE/AMBISONIC.
149      let mask = unsafe { (*ptr).u.mask };
150      if mask != 0 { Some(mask) } else { None }
151    }
152    _ => None,
153  };
154
155  // Build kind / description through ffmpeg-next helpers. They take
156  // `&ChannelLayout` (which is `repr(transparent)` over
157  // `AVChannelLayout`), but at this point we've already validated
158  // `order`, so forming the reference is sound: the only enum-typed
159  // field in `AVChannelLayout` is `order`, and it now holds a value
160  // that came back from `audio_channel_order_kind_from_raw` with the
161  // unknown bucket folded into a known variant — but the *underlying
162  // struct* still has the original raw bytes. We can't form `&AVChannelLayout`
163  // over an unknown order without UB, so for those helpers we
164  // explicitly only call them when order is one of the known variants.
165  let known_kind = if matches!(order, AudioChannelOrderKind::Unspecified) {
166    ChannelLayoutKind::Unknown
167  } else {
168    // SAFETY: `order` is one of {Native, Custom, Ambisonic} — all of
169    // which are valid `AVChannelOrder` discriminants present in our
170    // bindgen output, so `&*ptr` is sound to form here.
171    let layout_ref = unsafe { &*(ptr as *const ChannelLayout) };
172    channel_layout_kind_from_ffmpeg(layout_ref)
173  };
174  let custom_channels_vec = unsafe { custom_channels_raw(ptr, order) };
175  let description = if matches!(order, AudioChannelOrderKind::Unspecified) {
176    SmolStr::default()
177  } else {
178    // SAFETY: same as above — `order` is a known, valid discriminant.
179    let layout_ref = unsafe { &*(ptr as *const ChannelLayout) };
180    describe_layout(layout_ref)
181  };
182
183  AudioChannelLayout::new(nb_channels.max(0) as u32)
184    .with_order(order)
185    .with_known_kind(known_kind)
186    .with_native_mask(native_mask)
187    .with_custom_channels(custom_channels_vec)
188    .with_description(description)
189}
190
191/// Pointer-form of `custom_channels`. `order` must be the result of
192/// reading `(*ptr).order` as `i32` and folding through
193/// [`audio_channel_order_kind_from_raw`]; this skips re-reading it.
194///
195/// # Safety
196/// `ptr` must be a live `*const AVChannelLayout`. Reads only fields
197/// (`u.map`, `nb_channels`, and the per-channel array) — no `&AVChannelLayout`
198/// reference is ever formed.
199unsafe fn custom_channels_raw(
200  ptr: *const ffi::AVChannelLayout,
201  order: AudioChannelOrderKind,
202) -> Vec<AudioChannelSpec> {
203  use core::ptr::{addr_of, read_unaligned};
204  if !matches!(order, AudioChannelOrderKind::Custom) {
205    return Vec::new();
206  }
207  let count = unsafe { (*ptr).nb_channels }.max(0) as usize;
208  if count == 0 {
209    return Vec::new();
210  }
211  // SAFETY: The `u` field is a union; reading `.map` is sound when
212  // `order == CUSTOM` per FFmpeg's documented contract. Guard
213  // explicitly for null.
214  let map_ptr = unsafe { (*ptr).u.map };
215  if map_ptr.is_null() {
216    return Vec::new();
217  }
218  // Iterate the AVChannelCustom array via raw pointers — never form
219  // `&[AVChannelCustom]` or `&AVChannelCustom`, because each entry
220  // contains `id: AVChannel`, a bindgen enum. If FFmpeg writes an
221  // unknown channel id (version skew / hostile decoder), the
222  // reference itself would be UB before the raw `id` read could
223  // sanitize it.
224  let mut out = Vec::with_capacity(count);
225  for index in 0..count {
226    // SAFETY: `map_ptr` points to `count == nb_channels` valid
227    // `AVChannelCustom` entries per FFmpeg's contract; `index < count`,
228    // so `entry_ptr` lies inside the allocation.
229    let entry_ptr: *const ffi::AVChannelCustom = unsafe { map_ptr.add(index) };
230    // SAFETY: `entry_ptr` is a valid pointer; `addr_of!((*p).field)`
231    // computes the field address without forming a reference.
232    let raw_id = unsafe { read_unaligned(addr_of!((*entry_ptr).id) as *const i32) };
233    let label = unsafe { custom_channel_label_raw(entry_ptr) };
234    out.push(AudioChannelSpec::new(index as u32, raw_id as u32).with_label(label));
235  }
236  out
237}
238
239/// Pointer-form of `custom_channel_label` — never forms
240/// `&AVChannelCustom`, since the struct contains an enum-typed `id`.
241///
242/// # Safety
243/// `entry_ptr` must be a live `*const AVChannelCustom`.
244unsafe fn custom_channel_label_raw(entry_ptr: *const ffi::AVChannelCustom) -> SmolStr {
245  use core::ptr::addr_of;
246  // SAFETY: `name: [c_char; 16]` is an inline byte array — no
247  // validity invariant beyond initialization (FFmpeg guarantees that).
248  // `addr_of!` computes the address; we then re-interpret as `*const u8`
249  // for UTF-8 lossy decoding.
250  let name_ptr = unsafe { addr_of!((*entry_ptr).name) } as *const u8;
251  // SAFETY: `name` is exactly 16 bytes wide.
252  let bytes = unsafe { slice::from_raw_parts(name_ptr, 16) };
253  let end = bytes
254    .iter()
255    .position(|byte| *byte == 0)
256    .unwrap_or(bytes.len());
257  if end == 0 {
258    return SmolStr::default();
259  }
260  SmolStr::new(std::string::String::from_utf8_lossy(&bytes[..end]))
261}
262
263#[allow(dead_code)]
264fn custom_channels(layout: &ChannelLayout) -> Vec<AudioChannelSpec> {
265  // Same raw-integer check as in `audio_channel_layout_from_ffmpeg`:
266  // never let Rust form an `AVChannelOrder` value from runtime data
267  // before we've validated its discriminant.
268  use core::ptr::{addr_of, read_unaligned};
269  // SAFETY: `layout.0` is the inner `AVChannelLayout`; reading the
270  // `order` field as `i32` matches the bindgen enum's storage.
271  let order_raw = unsafe { read_unaligned(addr_of!(layout.0.order) as *const i32) };
272  if order_raw != ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 {
273    return Vec::new();
274  }
275  let count = layout.0.nb_channels.max(0) as usize;
276  if count == 0 {
277    return Vec::new();
278  }
279  // SAFETY: The `u` field is a union; reading `.map` is sound when
280  // `order == CUSTOM` per FFmpeg's documented contract. The pointer
281  // may still be null on a malformed layout — guard explicitly.
282  let ptr = unsafe { layout.0.u.map };
283  if ptr.is_null() {
284    return Vec::new();
285  }
286  // SAFETY: AVChannelLayout's contract says `.u.map` points to
287  // `nb_channels` valid `AVChannelCustom` entries when order == CUSTOM.
288  let slice_ref = unsafe { slice::from_raw_parts(ptr, count) };
289  slice_ref
290    .iter()
291    .enumerate()
292    .map(|(index, channel)| {
293      // Read `channel.id` as raw `i32` to avoid constructing an
294      // invalid `AVChannel` enum from a value we don't recognize.
295      // SAFETY: `channel` is a valid `&AVChannelCustom`; `id` has the
296      // bindgen enum layout (c_int).
297      let raw_id = unsafe { read_unaligned(addr_of!(channel.id) as *const i32) };
298      AudioChannelSpec::new(index as u32, raw_id as u32).with_label(custom_channel_label(channel))
299    })
300    .collect()
301}
302
303fn custom_channel_label(channel: &ffi::AVChannelCustom) -> SmolStr {
304  // SAFETY: AVChannelCustom.name is a fixed-size [c_char; 16] inline
305  // buffer. Re-interpreting as bytes for UTF-8 lossy decoding is sound.
306  let bytes =
307    unsafe { slice::from_raw_parts(channel.name.as_ptr() as *const u8, channel.name.len()) };
308  let end = bytes
309    .iter()
310    .position(|byte| *byte == 0)
311    .unwrap_or(bytes.len());
312  if end == 0 {
313    return SmolStr::default();
314  }
315  SmolStr::new(std::string::String::from_utf8_lossy(&bytes[..end]))
316}
317
318fn describe_layout(layout: &ChannelLayout) -> SmolStr {
319  // `av_channel_layout_describe` returns the number of bytes needed
320  // (excluding the NUL terminator). Start with a 128-byte buffer —
321  // comfortably bigger than every named layout — and grow once if it
322  // wasn't enough. Use `c_char` for portability (signed on
323  // x86/aarch64-Apple, unsigned on aarch64-Linux).
324  let mut buf = std::vec![0 as c_char; 128];
325  let mut needed =
326    unsafe { ffi::av_channel_layout_describe(&layout.0 as *const _, buf.as_mut_ptr(), buf.len()) };
327  if needed < 0 {
328    return SmolStr::default();
329  }
330  if needed as usize >= buf.len() {
331    buf.resize(needed as usize + 1, 0 as c_char);
332    needed = unsafe {
333      ffi::av_channel_layout_describe(&layout.0 as *const _, buf.as_mut_ptr(), buf.len())
334    };
335    if needed < 0 {
336      return SmolStr::default();
337    }
338  }
339  // SAFETY: buf is heap-allocated, NUL-terminated by FFmpeg's contract.
340  let bytes = unsafe { slice::from_raw_parts(buf.as_ptr() as *const u8, buf.len()) };
341  let end = bytes
342    .iter()
343    .position(|byte| *byte == 0)
344    .unwrap_or(needed as usize);
345  if end == 0 {
346    return SmolStr::default();
347  }
348  SmolStr::new(std::string::String::from_utf8_lossy(&bytes[..end]))
349}