use std::collections::HashMap;
use std::ffi::CString;
use std::sync::{LazyLock, Mutex};
#[cfg(target_os = "linux")]
use std::{fs, path::Path};
use crate::convert::{encode_adapter_error, reject_unused_encode_options, video_encoder_options};
use crate::video_encode::VideoEncodeConfig;
use alpha2_media::{ColorMetadata, EncodeError};
use rsmpeg::avcodec::{AVCodec, AVCodecContext, AVCodecRef};
use rsmpeg::avutil::AVRational;
use rsmpeg::error::RsmpegError;
use rsmpeg::ffi;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct AvEncodeConfig {
pub video: VideoEncodeConfig,
pub audio_sample_rate: u32,
pub audio_channels: u16,
pub audio_codec: NativeAudioCodec,
pub audio_bit_rate: u32,
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum NativeVideoCodec {
H264,
Hevc,
Vp9,
Av1,
Mpeg4Part2,
}
impl NativeVideoCodec {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::H264 => "h264",
Self::Hevc => "hevc",
Self::Vp9 => "vp9",
Self::Av1 => "av1",
Self::Mpeg4Part2 => "mpeg4-part2",
}
}
#[must_use]
pub const fn model(self) -> alpha2_media::VideoCodec {
match self {
Self::H264 => alpha2_media::VideoCodec::H264,
Self::Hevc => alpha2_media::VideoCodec::Hevc,
Self::Vp9 => alpha2_media::VideoCodec::Vp9,
Self::Av1 => alpha2_media::VideoCodec::Av1,
Self::Mpeg4Part2 => alpha2_media::VideoCodec::Mpeg4Part2,
}
}
#[must_use]
pub const fn from_model(codec: alpha2_media::VideoCodec) -> Self {
match codec {
alpha2_media::VideoCodec::H264 => Self::H264,
alpha2_media::VideoCodec::Hevc => Self::Hevc,
alpha2_media::VideoCodec::Vp9 => Self::Vp9,
alpha2_media::VideoCodec::Av1 => Self::Av1,
alpha2_media::VideoCodec::Mpeg4Part2 => Self::Mpeg4Part2,
}
}
pub(crate) const fn codec_id(self) -> ffi::AVCodecID {
match self {
Self::H264 => ffi::AV_CODEC_ID_H264,
Self::Hevc => ffi::AV_CODEC_ID_HEVC,
Self::Vp9 => ffi::AV_CODEC_ID_VP9,
Self::Av1 => ffi::AV_CODEC_ID_AV1,
Self::Mpeg4Part2 => ffi::AV_CODEC_ID_MPEG4,
}
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum NativeAudioCodec {
Aac,
Opus,
}
impl NativeAudioCodec {
pub const ALL: [Self; 2] = [Self::Aac, Self::Opus];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Aac => "aac",
Self::Opus => "opus",
}
}
#[must_use]
pub const fn model(self) -> alpha2_media::AudioCodec {
match self {
Self::Aac => alpha2_media::AudioCodec::Aac,
Self::Opus => alpha2_media::AudioCodec::Opus,
}
}
#[must_use]
pub const fn from_model(codec: alpha2_media::AudioCodec) -> Self {
match codec {
alpha2_media::AudioCodec::Aac => Self::Aac,
alpha2_media::AudioCodec::Opus => Self::Opus,
}
}
pub(crate) const fn encoder_name(self) -> &'static std::ffi::CStr {
match self {
Self::Aac => c"aac",
Self::Opus => c"libopus",
}
}
}
pub(crate) fn audio_encoder(codec: NativeAudioCodec) -> Option<AVCodecRef<'static>> {
AVCodec::find_encoder_by_name(codec.encoder_name()).filter(|encoder| {
u32::try_from(encoder.capabilities).unwrap_or(0) & ffi::AV_CODEC_CAP_EXPERIMENTAL == 0
})
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum NativeEncoderClass {
Software,
Hardware,
Hybrid,
}
impl NativeEncoderClass {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Software => "software",
Self::Hardware => "hardware",
Self::Hybrid => "hybrid",
}
}
#[must_use]
pub const fn modes(self, hybrid_hardware: bool) -> &'static [bool] {
match self {
Self::Hardware => &[true],
Self::Hybrid if hybrid_hardware => &[true, false],
Self::Software | Self::Hybrid => &[false],
}
}
}
pub(crate) struct VideoEncoderCandidate {
pub(crate) encoder: AVCodecRef<'static>,
pub(crate) on_hardware: bool,
}
impl VideoEncoderCandidate {
pub(crate) fn name(&self) -> String {
self.encoder.name().to_string_lossy().into_owned()
}
}
#[must_use]
pub fn hybrid_encoders_use_hardware() -> bool {
std::env::var("ALPHA2_VIDEO_ENCODER_HYBRID")
.is_ok_and(|value| value.eq_ignore_ascii_case("hardware"))
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum NativeEncoderRejection {
Dimensions,
OpenFailed,
OptionsIgnored,
}
impl NativeEncoderRejection {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Dimensions => "dimensions",
Self::OpenFailed => "open-failed",
Self::OptionsIgnored => "options-ignored",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativeVideoEncoderFallback {
pub encoder_name: String,
pub hardware_accelerated: bool,
pub reason: NativeEncoderRejection,
pub detail: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativeVideoEncoderSelection {
pub encoder_name: String,
pub class: NativeEncoderClass,
pub hardware_accelerated: bool,
pub rejected: Vec<NativeVideoEncoderFallback>,
pub rate_control: crate::AppliedRateControl,
pub quantizer_refused: Option<String>,
pub software_threads: Option<i64>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativeVideoEncoderCapability {
pub codec: NativeVideoCodec,
pub encoder_name: String,
pub class: NativeEncoderClass,
pub hardware_accelerated: bool,
pub preferred: bool,
pub minimum_width: u32,
pub minimum_height: u32,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativeAudioEncoderCapability {
pub codec: NativeAudioCodec,
pub encoder_name: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativeCodecCapabilities {
pub video: Vec<NativeVideoEncoderCapability>,
pub audio: Vec<NativeAudioEncoderCapability>,
}
#[must_use]
pub fn codec_capabilities() -> NativeCodecCapabilities {
let video = [
NativeVideoCodec::H264,
NativeVideoCodec::Hevc,
NativeVideoCodec::Vp9,
NativeVideoCodec::Av1,
NativeVideoCodec::Mpeg4Part2,
]
.into_iter()
.flat_map(|codec| {
registered_video_encoders(codec)
.into_iter()
.filter(encoder_is_compatible)
.enumerate()
.map(move |(index, candidate)| {
let encoder_name = candidate.name();
let (minimum_width, minimum_height) = encoder_minimum_dimensions(&encoder_name);
NativeVideoEncoderCapability {
codec,
class: encoder_class(&candidate.encoder),
hardware_accelerated: candidate.on_hardware,
preferred: index == 0,
encoder_name,
minimum_width,
minimum_height,
}
})
})
.collect();
let audio = NativeAudioCodec::ALL
.into_iter()
.filter_map(|codec| {
let encoder = audio_encoder(codec)?;
Some(NativeAudioEncoderCapability {
codec,
encoder_name: encoder.name().to_string_lossy().into_owned(),
})
})
.collect();
NativeCodecCapabilities { video, audio }
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativeHardwareDecodeCapability {
pub codec: NativeVideoCodec,
pub decoder_name: Option<String>,
pub hardware_config: bool,
pub software_decoder: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativeHardwareDecodeCapabilities {
pub backend: &'static str,
pub codecs: Vec<NativeHardwareDecodeCapability>,
}
#[must_use]
pub const fn hardware_decode_backend() -> &'static str {
#[cfg(feature = "hwdecode")]
{
#[cfg(target_os = "linux")]
{
"vulkan"
}
#[cfg(target_os = "macos")]
{
"videotoolbox"
}
#[cfg(target_os = "windows")]
{
"d3d12va"
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
"none"
}
}
#[cfg(not(feature = "hwdecode"))]
{
"none"
}
}
#[must_use]
pub fn hardware_decode_capabilities() -> NativeHardwareDecodeCapabilities {
let codecs = [
NativeVideoCodec::H264,
NativeVideoCodec::Hevc,
NativeVideoCodec::Av1,
NativeVideoCodec::Vp9,
]
.into_iter()
.map(|codec| {
let decoder = AVCodec::find_decoder(codec.codec_id());
NativeHardwareDecodeCapability {
codec,
decoder_name: decoder
.as_ref()
.map(|decoder| decoder.name().to_string_lossy().into_owned()),
hardware_config: crate::decode::registered_video_decoders(codec.codec_id())
.any(|decoder| decoder_has_hardware_config(&decoder)),
software_decoder: crate::decode::software_video_decoder(codec.codec_id())
.ok()
.map(|decoder| decoder.name().to_string_lossy().into_owned()),
}
})
.collect();
NativeHardwareDecodeCapabilities {
backend: hardware_decode_backend(),
codecs,
}
}
#[allow(unused_variables)]
fn decoder_has_hardware_config(decoder: &AVCodecRef<'_>) -> bool {
#[cfg(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
))]
{
#[cfg(target_os = "linux")]
let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_VULKAN, ffi::AV_PIX_FMT_VULKAN);
#[cfg(target_os = "macos")]
let (device_type, pixel_format) = (
ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
ffi::AV_PIX_FMT_VIDEOTOOLBOX,
);
#[cfg(target_os = "windows")]
let (device_type, pixel_format) = (ffi::AV_HWDEVICE_TYPE_D3D12VA, ffi::AV_PIX_FMT_D3D12);
(0..)
.map_while(|index| decoder.hw_config(index))
.any(|config| config.device_type == device_type && config.pix_fmt == pixel_format)
}
#[cfg(not(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
)))]
{
false
}
}
#[cfg(test)]
pub(crate) fn test_skips_encoder(name: &std::ffi::CStr) -> bool {
static SKIPPED: LazyLock<Vec<String>> = LazyLock::new(|| {
std::env::var("ALPHA2_TEST_SKIP_ENCODERS")
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_owned)
.collect()
});
let name = name.to_string_lossy();
SKIPPED.iter().any(|skipped| *skipped == name)
}
#[cfg(not(test))]
const fn test_skips_encoder(_name: &std::ffi::CStr) -> bool {
false
}
pub(crate) fn registered_video_encoders(codec: NativeVideoCodec) -> Vec<VideoEncoderCandidate> {
let hybrid_hardware = hybrid_encoders_use_hardware();
let mut candidates = AVCodec::iterate()
.filter(|encoder| {
encoder.id == codec.codec_id()
&& !test_skips_encoder(encoder.name())
&& AVCodec::find_encoder_by_name(encoder.name())
.is_some_and(|found| found.as_ptr() == encoder.as_ptr())
})
.flat_map(|encoder| {
encoder_class(&encoder)
.modes(hybrid_hardware)
.iter()
.filter_map(move |&on_hardware| {
AVCodec::find_encoder_by_name(encoder.name()).map(|encoder| {
VideoEncoderCandidate {
encoder,
on_hardware,
}
})
})
})
.collect::<Vec<_>>();
candidates.sort_by_key(|candidate| !candidate.on_hardware);
candidates
}
pub(crate) fn encoder_class(encoder: &AVCodec) -> NativeEncoderClass {
let name = encoder.name().to_string_lossy();
declared_encoder_class(&name, u32::try_from(encoder.capabilities).unwrap_or(0))
}
const MISDECLARED_HARDWARE_ENCODERS: &[&str] = &["h264_videotoolbox"];
pub(crate) fn declared_encoder_class(name: &str, capabilities: u32) -> NativeEncoderClass {
if capabilities & ffi::AV_CODEC_CAP_HYBRID != 0 {
NativeEncoderClass::Hybrid
} else if capabilities & ffi::AV_CODEC_CAP_HARDWARE != 0
|| MISDECLARED_HARDWARE_ENCODERS.contains(&name)
{
NativeEncoderClass::Hardware
} else {
NativeEncoderClass::Software
}
}
pub(crate) fn encoder_is_compatible(candidate: &VideoEncoderCandidate) -> bool {
let name = candidate.name();
registration_is_compatible(candidate, &name) && encoder_runs_here(candidate, &name)
}
fn registration_is_compatible(candidate: &VideoEncoderCandidate, name: &str) -> bool {
if candidate
.encoder
.pix_fmts()
.is_some_and(|formats| !formats.contains(&ffi::AV_PIX_FMT_YUV420P))
{
return false;
}
!candidate.on_hardware || hardware_device_node(name) != Some(false)
}
pub(crate) fn needs_open_probe(on_hardware: bool, name: &str) -> bool {
(on_hardware && hardware_device_node(name).is_none())
|| name.ends_with("_mf")
|| name.ends_with("_videotoolbox")
}
fn encoder_runs_here(candidate: &VideoEncoderCandidate, name: &str) -> bool {
static RUNS: LazyLock<Mutex<HashMap<(String, bool), bool>>> = LazyLock::new(Mutex::default);
if !needs_open_probe(candidate.on_hardware, name) {
return true;
}
let key = (name.to_owned(), candidate.on_hardware);
let mut runs = RUNS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*runs.entry(key).or_insert_with(|| {
match probe_software_encode(candidate, name, 256, 144, None).0 {
None => true,
Some(error) => {
alpha2_os::diagnostic!(
"alpha2 native: video encoder {name} {} is registered but did not encode a \
frame on this machine, so it is not offered that way: {error}",
mode_label(candidate.on_hardware)
);
false
}
}
})
}
pub(crate) const fn mode_label(on_hardware: bool) -> &'static str {
if on_hardware { "on GPU" } else { "on CPU" }
}
#[cfg(target_os = "linux")]
fn hardware_device_node(name: &str) -> Option<bool> {
if name.ends_with("_nvenc") {
return Some(Path::new("/dev/nvidia0").exists() || Path::new("/dev/nvidiactl").exists());
}
if name.ends_with("_vaapi") || name.ends_with("_qsv") || name.ends_with("_amf") {
return Some(fs::read_dir("/dev/dri").is_ok_and(|entries| {
entries.flatten().any(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| name.starts_with("renderD") || name.starts_with("card"))
})
}));
}
if name.ends_with("_v4l2m2m") {
return Some((0..64).any(|index| Path::new(&format!("/dev/video{index}")).exists()));
}
None
}
#[cfg(not(target_os = "linux"))]
const fn hardware_device_node(_name: &str) -> Option<bool> {
None
}
fn unavailable_codec_error(codec: NativeVideoCodec) -> EncodeError {
let identity = crate::ffmpeg_runtime_identity();
let inventory = codec_capabilities()
.video
.iter()
.map(|capability| format!("{}:{}", capability.codec.as_str(), capability.encoder_name))
.collect::<Vec<_>>()
.join(", ");
let inventory = if inventory.is_empty() {
"none".to_owned()
} else {
inventory
};
EncodeError::Adapter(format!(
"the linked FFmpeg runtime ({}, pinned SDK {}) registers no usable {} encoder; it offers [{inventory}]",
identity.version,
identity.pinned_sdk_release,
codec.as_str()
))
}
pub(crate) fn video_encoder_candidates(
codec: NativeVideoCodec,
requested_name: Option<&str>,
) -> Result<Vec<VideoEncoderCandidate>, EncodeError> {
if let Some(name) = requested_name {
let encoder = select_video_encoder(codec, Some(name))?;
return Ok(registered_video_encoders(codec)
.into_iter()
.filter(|candidate| candidate.encoder.as_ptr() == encoder.as_ptr())
.filter(encoder_is_compatible)
.collect());
}
let candidates = registered_video_encoders(codec)
.into_iter()
.filter(encoder_is_compatible)
.collect::<Vec<_>>();
if candidates.is_empty() {
return Err(unavailable_codec_error(codec));
}
Ok(candidates)
}
pub(crate) fn select_video_encoder(
codec: NativeVideoCodec,
requested_name: Option<&str>,
) -> Result<AVCodecRef<'static>, EncodeError> {
if let Some(name) = requested_name {
let name_c = CString::new(name)
.map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
let encoder = AVCodec::find_encoder_by_name(&name_c)
.ok_or_else(|| EncodeError::Adapter(format!("video encoder {name} is unavailable")))?;
if encoder.id != codec.codec_id() {
return Err(EncodeError::Adapter(format!(
"video encoder {name} does not implement {}",
codec.as_str()
)));
}
if !codec_capabilities()
.video
.iter()
.any(|capability| capability.codec == codec && capability.encoder_name == name)
{
return Err(EncodeError::Adapter(format!(
"video encoder {name} is not compatible with the native encode path"
)));
}
return Ok(encoder);
}
let capability = codec_capabilities()
.video
.into_iter()
.find(|capability| capability.codec == codec)
.ok_or_else(|| unavailable_codec_error(codec))?;
let name = CString::new(capability.encoder_name)
.map_err(|_| EncodeError::Adapter("video encoder name contains a NUL byte".into()))?;
AVCodec::find_encoder_by_name(&name).ok_or_else(|| unavailable_codec_error(codec))
}
pub(crate) struct VideoEncoderOpenRequest<'a> {
pub config: &'a VideoEncodeConfig,
pub requested_name: Option<&'a str>,
pub threads: SoftwareEncoderThreads<'a>,
pub global_header: bool,
pub width: i32,
pub height: i32,
}
pub(crate) struct SoftwareEncoderThreads<'a> {
decide: &'a dyn Fn() -> i64,
decided: std::cell::OnceCell<i64>,
}
impl<'a> SoftwareEncoderThreads<'a> {
pub(crate) fn new(decide: &'a dyn Fn() -> i64) -> Self {
Self {
decide,
decided: std::cell::OnceCell::new(),
}
}
pub(crate) fn get(&self) -> i64 {
*self.decided.get_or_init(|| (self.decide)().max(1))
}
}
pub(crate) fn open_video_encoder(
request: &VideoEncoderOpenRequest<'_>,
) -> Result<(AVCodecContext, NativeVideoEncoderSelection), EncodeError> {
let candidates = video_encoder_candidates(request.config.codec, request.requested_name)?;
open_first_video_encoder(request, candidates)
}
pub(crate) fn open_first_video_encoder(
request: &VideoEncoderOpenRequest<'_>,
candidates: Vec<VideoEncoderCandidate>,
) -> Result<(AVCodecContext, NativeVideoEncoderSelection), EncodeError> {
let mut rejected = Vec::new();
let last = candidates.len().saturating_sub(1);
for (index, candidate) in candidates.into_iter().enumerate() {
let name = candidate.name();
let class = encoder_class(&candidate.encoder);
match open_one_video_encoder(request, &candidate, &name) {
Ok((encoder, rate_control, quantizer_refused)) => {
return Ok((
encoder,
NativeVideoEncoderSelection {
encoder_name: name,
class,
hardware_accelerated: candidate.on_hardware,
rejected,
rate_control,
quantizer_refused,
software_threads: (!candidate.on_hardware).then(|| request.threads.get()),
},
));
}
Err((reason, error)) => {
let detail = error.to_string();
if index == last {
let earlier = rejected
.iter()
.map(|fallback: &NativeVideoEncoderFallback| {
format!(
"{} {} ({}): {}",
fallback.encoder_name,
mode_label(fallback.hardware_accelerated),
fallback.reason.as_str(),
fallback.detail
)
})
.collect::<Vec<_>>();
let prefix = if earlier.is_empty() {
String::new()
} else {
format!("after {}; ", earlier.join("; "))
};
return Err(EncodeError::Adapter(format!(
"{prefix}{name} {} ({}): {detail}",
mode_label(candidate.on_hardware),
reason.as_str()
)));
}
alpha2_os::diagnostic!(
"alpha2 native export: video encoder {name} {} ({}) rejected: {detail}",
mode_label(candidate.on_hardware),
reason.as_str()
);
rejected.push(NativeVideoEncoderFallback {
encoder_name: name,
hardware_accelerated: candidate.on_hardware,
reason,
detail,
});
}
}
}
Err(unavailable_codec_error(request.config.codec))
}
fn open_one_video_encoder(
request: &VideoEncoderOpenRequest<'_>,
candidate: &VideoEncoderCandidate,
name: &str,
) -> Result<
(AVCodecContext, crate::AppliedRateControl, Option<String>),
(NativeEncoderRejection, EncodeError),
> {
validate_encoder_dimensions(name, request.config.width, request.config.height)
.map_err(|error| (NativeEncoderRejection::Dimensions, error))?;
let refused = match crate::rate_control::quantizer_request(name, request.config.rate_control) {
None => None,
Some(Err(reason)) => Some(reason),
Some(Ok(quantized)) => {
match open_video_encoder_context(request, candidate, name, Some(&quantized)) {
Ok(encoder) => return Ok((encoder, crate::AppliedRateControl::Quantizer, None)),
Err((_, error)) => Some(format!("{} refused: {error}", quantized.describe())),
}
}
};
if let Some(reason) = &refused {
alpha2_os::diagnostic!("alpha2 native export: {name} runs the bitrate fallback: {reason}");
}
open_video_encoder_context(request, candidate, name, None)
.map(|encoder| (encoder, crate::AppliedRateControl::Bitrate, refused))
}
fn open_video_encoder_context(
request: &VideoEncoderOpenRequest<'_>,
candidate: &VideoEncoderCandidate,
name: &str,
quantized: Option<&crate::rate_control::MechanismRequest>,
) -> Result<AVCodecContext, (NativeEncoderRejection, EncodeError)> {
let mut encoder = AVCodecContext::new(&candidate.encoder);
encoder.set_width(request.width);
encoder.set_height(request.height);
encoder.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
let rate = request.config.frame_rate;
let time_base = encoder_time_base(rate, max_time_base_denominator(candidate.encoder.id))
.map_err(|error| (NativeEncoderRejection::OpenFailed, error))?;
encoder.set_time_base(time_base);
let sar = request.config.display.sample_aspect_ratio;
if sar != alpha2_media::SampleAspectRatio::default() {
encoder.set_sample_aspect_ratio(rsmpeg::avutil::ra(
i32::try_from(sar.num).unwrap_or(1),
i32::try_from(sar.den).unwrap_or(1),
));
}
encoder.set_framerate(rsmpeg::avutil::ra(
i32::try_from(rate.numerator()).unwrap_or(i32::MAX),
i32::try_from(rate.denominator()).unwrap_or(1),
));
encoder.set_bit_rate(quantized.map_or_else(
|| i64::try_from(request.config.rate_control.bitrate).unwrap_or(i64::MAX),
|quantized| quantized.bit_rate,
));
encoder.set_gop_size(keyframe_interval(request.config.frame_rate));
encoder.set_max_b_frames(encoder_max_b_frames(name));
if request.global_header {
encoder.set_flags(
encoder.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0),
);
}
let threads = if candidate.on_hardware {
0
} else {
request.threads.get()
};
let base = video_encoder_options(
request.config.color,
&candidate.encoder,
candidate.on_hardware,
threads,
request.config.width,
);
let options = match quantized {
Some(quantized) => quantized.dictionary(base),
None => crate::rate_control::rate_control_options(name, request.config.rate_control, base),
};
let unused =
alpha2_ffmpeg_bridge::open_codec_context(&mut encoder, options).map_err(|error| {
let hint = match quantized {
Some(_) => "",
None => crate::rate_control::constant_rate_hint(name, request.config.rate_control),
};
let error = match encode_adapter_error(error) {
EncodeError::Adapter(message) if !hint.is_empty() => {
EncodeError::Adapter(format!("{message}{hint}"))
}
error => error,
};
(NativeEncoderRejection::OpenFailed, error)
})?;
reject_unused_encode_options(unused)
.map_err(|error| (NativeEncoderRejection::OptionsIgnored, error))?;
Ok(encoder)
}
pub(crate) fn keyframe_interval(frame_rate: alpha2_time::FrameRate) -> i32 {
let frames = (2 * u64::from(frame_rate.numerator()))
.div_ceil(u64::from(frame_rate.denominator()))
.max(1);
i32::try_from(frames).unwrap_or(i32::MAX)
}
pub(crate) const fn max_time_base_denominator(codec: ffi::AVCodecID) -> Option<u32> {
match codec {
ffi::AV_CODEC_ID_MPEG4 | ffi::AV_CODEC_ID_H263 | ffi::AV_CODEC_ID_H263P => Some(65_535),
_ => None,
}
}
pub(crate) fn encoder_time_base(
rate: alpha2_time::FrameRate,
limit: Option<u32>,
) -> Result<AVRational, EncodeError> {
let out_of_range = || EncodeError::Adapter("frame rate is out of range".into());
let (num, den) = (u64::from(rate.numerator()), u64::from(rate.denominator()));
let exact = || {
Ok(rsmpeg::avutil::ra(
i32::try_from(den).map_err(|_| out_of_range())?,
i32::try_from(num).map_err(|_| out_of_range())?,
))
};
let Some(limit) = limit.map(u64::from).filter(|limit| num > *limit) else {
return exact();
};
let mut best: Option<(u64, u64, u64)> = None;
for units in num.div_ceil(den).max(1)..=limit {
let q = num / gcd_u64(num, den * units);
let (error, scale) = (q / 2, q * units);
if best.is_none_or(|(_, best_error, best_scale)| {
u128::from(error) * u128::from(best_scale) < u128::from(best_error) * u128::from(scale)
}) {
best = Some((units, error, scale));
}
}
let (units, _, _) = best.ok_or_else(out_of_range)?;
Ok(rsmpeg::avutil::ra(
1,
i32::try_from(units).map_err(|_| out_of_range())?,
))
}
const fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
(a, b) = (b, a % b);
}
a
}
pub(crate) fn encoder_max_b_frames(name: &str) -> i32 {
if name.ends_with("_videotoolbox") {
0
} else {
2
}
}
pub(crate) fn encoder_minimum_dimensions(name: &str) -> (u32, u32) {
if name == "av1_nvenc" {
(146, 128)
} else if name.ends_with("_nvenc") {
(146, 50)
} else {
(2, 2)
}
}
pub(crate) fn validate_encoder_dimensions(
encoder_name: &str,
width: u32,
height: u32,
) -> Result<(), EncodeError> {
let (minimum_width, minimum_height) = encoder_minimum_dimensions(encoder_name);
if width < minimum_width || height < minimum_height {
Err(EncodeError::Adapter(format!(
"encoder {encoder_name} requires at least {minimum_width}x{minimum_height}, received {width}x{height}"
)))
} else {
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativeEncoderHardwareConfig {
pub pixel_format: String,
pub device_type: String,
pub methods: i32,
pub needs_hardware_frames: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[allow(clippy::struct_excessive_bools)]
pub struct NativeVideoEncoderProbe {
pub codec: NativeVideoCodec,
pub encoder_name: String,
pub class: NativeEncoderClass,
pub hardware_accelerated: bool,
pub capability_bits: u32,
pub pixel_formats: Vec<String>,
pub hardware_configs: Vec<NativeEncoderHardwareConfig>,
pub accepts_yuv420p: bool,
pub requires_hardware_frames: bool,
pub advertised: bool,
pub minimum_size: (u32, u32),
pub software_open_error: Option<String>,
pub software_packets: u32,
pub rate_control: Vec<NativeRateControlProbe>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativeRateControlProbe {
pub mechanism: crate::RateControlMechanism,
pub options: Option<String>,
pub outcome: Result<u32, String>,
pub device: Option<String>,
}
pub(crate) const fn probe_quantizer(codec: NativeVideoCodec, software: bool) -> u32 {
match codec {
NativeVideoCodec::H264 | NativeVideoCodec::Hevc => 26,
NativeVideoCodec::Av1 | NativeVideoCodec::Vp9 => {
if software {
32
} else {
100
}
}
NativeVideoCodec::Mpeg4Part2 => 4,
}
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn probe_video_encoders(width: u32, height: u32) -> Vec<NativeVideoEncoderProbe> {
let advertised = codec_capabilities();
[
NativeVideoCodec::H264,
NativeVideoCodec::Hevc,
NativeVideoCodec::Vp9,
NativeVideoCodec::Av1,
NativeVideoCodec::Mpeg4Part2,
]
.into_iter()
.flat_map(|codec| {
registered_video_encoders(codec)
.into_iter()
.map(move |candidate| (codec, candidate))
})
.map(|(codec, candidate)| {
let encoder = &candidate.encoder;
let encoder_name = candidate.name();
let pixel_formats = encoder.pix_fmts().map_or_else(Vec::new, |formats| {
formats
.iter()
.map(|format| {
alpha2_ffmpeg_bridge::pixel_format_name(*format)
.unwrap_or_else(|| "unknown".to_owned())
})
.collect()
});
let hardware_configs = (0..)
.map_while(|index| encoder.hw_config(index))
.map(|config| NativeEncoderHardwareConfig {
pixel_format: alpha2_ffmpeg_bridge::pixel_format_name(config.pix_fmt)
.unwrap_or_else(|| "unknown".to_owned()),
device_type: alpha2_ffmpeg_bridge::hardware_device_type_name(config.device_type)
.unwrap_or_else(|| "none".to_owned()),
methods: config.methods,
needs_hardware_frames: crate::decode::has_hw_config_method(
config.methods,
ffi::AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX,
),
})
.collect::<Vec<_>>();
let accepts_yuv420p = encoder
.pix_fmts()
.is_none_or(|formats| formats.contains(&ffi::AV_PIX_FMT_YUV420P));
let (software_open_error, software_packets) =
probe_software_encode(&candidate, &encoder_name, width, height, None);
let requires_hardware_frames = !accepts_yuv420p
&& hardware_configs
.iter()
.any(|config| config.needs_hardware_frames);
let rate_control = crate::RateControlMechanism::ALL
.into_iter()
.map(|mechanism| {
let request = crate::rate_control::mechanism_request(
&encoder_name,
mechanism,
2_000_000,
if encoder_name.ends_with("_d3d12va") {
26
} else {
probe_quantizer(
codec,
encoder_class(encoder) == NativeEncoderClass::Software,
)
},
);
let mut device = None;
let outcome = match &request {
None => Err("no known option for this encoder family".to_owned()),
Some(request) if requires_hardware_frames => probe_hardware_frames_encode(
encoder,
&encoder_name,
(width, height),
mechanism,
request,
)
.map(|(packets, on)| {
device = Some(on);
packets
}),
Some(request) => {
match probe_software_encode(
&candidate,
&encoder_name,
width,
height,
Some(request),
) {
(None, packets) => Ok(packets),
(Some(error), _) => Err(error),
}
}
};
NativeRateControlProbe {
mechanism,
options: request
.as_ref()
.map(crate::rate_control::MechanismRequest::describe),
outcome,
device,
}
})
.collect();
NativeVideoEncoderProbe {
codec,
class: encoder_class(encoder),
hardware_accelerated: candidate.on_hardware,
capability_bits: u32::try_from(encoder.capabilities).unwrap_or(0),
advertised: advertised.video.iter().any(|capability| {
capability.codec == codec
&& capability.encoder_name == encoder_name
&& capability.hardware_accelerated == candidate.on_hardware
}),
minimum_size: encoder_minimum_dimensions(&encoder_name),
requires_hardware_frames,
accepts_yuv420p,
pixel_formats,
hardware_configs,
encoder_name,
software_open_error,
software_packets,
rate_control,
}
})
.collect()
}
#[allow(unused_variables)]
fn probe_hardware_frames_encode(
encoder: &AVCodecRef<'static>,
name: &str,
(width, height): (u32, u32),
mechanism: crate::RateControlMechanism,
request: &crate::rate_control::MechanismRequest,
) -> Result<(u32, String), String> {
#[cfg(all(target_os = "linux", feature = "hwencode"))]
if name.ends_with("_vulkan") {
return crate::hwencode::probe_vulkan_rate_control(encoder, width, height, request);
}
#[cfg(all(target_os = "windows", feature = "hwencode"))]
if name.ends_with("_d3d12va") {
return crate::hwencode::probe_d3d12va_rate_control(
name, width, height, mechanism, request,
);
}
Err("takes hardware frames this probe cannot supply".to_owned())
}
fn probe_software_encode(
candidate: &VideoEncoderCandidate,
name: &str,
width: u32,
height: u32,
request: Option<&crate::rate_control::MechanismRequest>,
) -> (Option<String>, u32) {
let Ok(coded_width) = i32::try_from(width) else {
return (Some("width is out of range".to_owned()), 0);
};
let Ok(coded_height) = i32::try_from(height) else {
return (Some("height is out of range".to_owned()), 0);
};
if let Err(error) = validate_encoder_dimensions(name, width, height) {
return (Some(error.to_string()), 0);
}
let encoder = &candidate.encoder;
let mut context = AVCodecContext::new(encoder);
context.set_width(coded_width);
context.set_height(coded_height);
context.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P);
let time_base = rsmpeg::avutil::ra(1, 30);
context.set_time_base(time_base);
context.set_framerate(rsmpeg::avutil::ra(30, 1));
context.set_bit_rate(request.map_or(2_000_000, |request| request.bit_rate));
context.set_gop_size(12);
context.set_max_b_frames(encoder_max_b_frames(name));
let options = video_encoder_options(
ColorMetadata::EXPORT,
encoder,
candidate.on_hardware,
1,
width,
);
let options = match request {
Some(request) => request.dictionary(options),
None => options,
};
match alpha2_ffmpeg_bridge::open_codec_context(&mut context, options) {
Err(error) => return (Some(error.to_string()), 0),
Ok(unused) => {
if let Err(error) = reject_unused_encode_options(unused) {
return (Some(error.to_string()), 0);
}
}
}
let Some(image) =
rsmpeg::avutil::AVImage::new(ffi::AV_PIX_FMT_YUV420P, coded_width, coded_height, 1)
else {
return (Some("cannot allocate a probe frame".to_owned()), 0);
};
let mut frame = rsmpeg::avutil::AVFrameWithImage::new(image);
frame.set_pts(0);
frame.set_time_base(time_base);
if let Err(error) = context.send_frame(Some(&frame)) {
return (Some(error.to_string()), 0);
}
if let Err(error) = context.send_frame(None) {
return (Some(error.to_string()), 0);
}
let mut packets = 0;
loop {
match context.receive_packet() {
Ok(_) => packets += 1,
Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
Err(error) => return (Some(error.to_string()), packets),
}
}
if packets == 0 {
return (
Some("opened but produced no packet from a software frame".to_owned()),
0,
);
}
(None, packets)
}