use std::time::Duration;
use crate::color::ColorSpace;
use crate::decode::Hardware;
use crate::error::{Error, Result};
use crate::frame::Frame;
use crate::identify::Encoding;
use crate::media::{Packet, Rational};
pub trait Encoder: Send {
fn encoding(&self) -> Encoding;
fn backend(&self) -> Backend;
fn is_hardware(&self) -> bool;
fn config_record(&self) -> Option<Vec<u8>>;
fn encode(&mut self, frame: &Frame) -> Result<Vec<Packet>>;
fn finish(&mut self) -> Result<Vec<Packet>>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Backend {
VideoToolbox,
MediaFoundation,
MediaCodec,
GStreamer,
Rav1e,
}
impl Backend {
pub fn can_use_hardware(self) -> bool {
self != Backend::Rav1e
}
pub fn feature(self) -> &'static str {
match self {
Backend::VideoToolbox
| Backend::MediaFoundation
| Backend::MediaCodec
| Backend::GStreamer => "encode-platform",
Backend::Rav1e => "encode-av1",
}
}
pub fn handles(self, encoding: Encoding) -> bool {
match self {
Backend::VideoToolbox
| Backend::MediaFoundation
| Backend::MediaCodec
| Backend::GStreamer => encoding == Encoding::H264,
Backend::Rav1e => encoding == Encoding::Av1,
}
}
pub fn is_available(self) -> bool {
match self {
Backend::VideoToolbox => {
cfg!(all(feature = "encode-platform", target_vendor = "apple"))
}
Backend::MediaFoundation => cfg!(all(feature = "encode-platform", windows)),
Backend::MediaCodec => cfg!(all(feature = "encode-platform", target_os = "android")),
Backend::GStreamer => cfg!(all(
feature = "encode-platform",
unix,
not(target_vendor = "apple"),
not(target_os = "android")
)),
Backend::Rav1e => cfg!(feature = "encode-av1"),
}
}
fn exists_here(self) -> bool {
match self {
Backend::VideoToolbox => cfg!(target_vendor = "apple"),
Backend::MediaFoundation => cfg!(windows),
Backend::MediaCodec => cfg!(target_os = "android"),
Backend::GStreamer => cfg!(all(unix, not(target_vendor = "apple"), not(target_os = "android"))),
Backend::Rav1e => true,
}
}
}
impl std::fmt::Display for Backend {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Backend::VideoToolbox => "VideoToolbox",
Backend::MediaFoundation => "Media Foundation",
Backend::MediaCodec => "MediaCodec",
Backend::GStreamer => "GStreamer",
Backend::Rav1e => "rav1e",
})
}
}
pub const BACKENDS: [Backend; 5] = [
Backend::VideoToolbox,
Backend::MediaFoundation,
Backend::MediaCodec,
Backend::GStreamer,
Backend::Rav1e,
];
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Level {
#[default]
L4_1,
Auto,
}
const LEVEL_4_1_FRAME: u64 = 8_192;
const LEVEL_4_1_RATE: u64 = 245_760;
pub fn fit_level(width: u32, height: u32, frame_rate: Rational, level: Level) -> (u32, u32) {
let fits = |width: u32, height: u32| {
let blocks = u64::from(width.div_ceil(16)) * u64::from(height.div_ceil(16));
let per_second = blocks as f64 * frame_rate.as_f64().max(1.0);
blocks <= LEVEL_4_1_FRAME
&& per_second <= LEVEL_4_1_RATE as f64
&& width.div_ceil(16) <= 256
&& height.div_ceil(16) <= 256
};
if level == Level::Auto || fits(width, height) {
return (width, height);
}
let budget = (LEVEL_4_1_FRAME as f64)
.min(LEVEL_4_1_RATE as f64 / frame_rate.as_f64().max(1.0))
* 256.0;
let mut scale = (budget / (f64::from(width) * f64::from(height))).sqrt().min(1.0);
let shaved = loop {
let (fitted_width, fitted_height) = (
crate::scale::even(f64::from(width) * scale),
crate::scale::even(f64::from(height) * scale),
);
if fits(fitted_width, fitted_height) || fitted_width <= 16 {
break (fitted_width, fitted_height);
}
scale *= 0.99;
};
let standard = [1080_u32, 720, 540, 480, 360]
.into_iter()
.filter(|&standard| standard < height)
.map(|standard| {
(
crate::scale::even(f64::from(width) * f64::from(standard) / f64::from(height)),
standard,
)
})
.find(|&(fitted_width, fitted_height)| fits(fitted_width, fitted_height));
match standard {
Some(standard) if standard.0 * standard.1 >= shaved.0 * shaved.1 => standard,
_ => shaved,
}
}
#[derive(Clone, Debug)]
pub struct EncoderConfig {
pub encoding: Encoding,
pub width: u32,
pub height: u32,
pub frame_rate: Rational,
pub color: ColorSpace,
pub quality: f32,
pub keyframe_interval: Duration,
pub realtime: bool,
pub hardware: Hardware,
pub level: Level,
pub threads: usize,
}
impl EncoderConfig {
pub fn new(
encoding: Encoding,
width: u32,
height: u32,
frame_rate: Rational,
color: ColorSpace,
) -> Self {
EncoderConfig {
encoding,
width,
height,
frame_rate,
color,
quality: 0.68,
keyframe_interval: Duration::from_secs(2),
realtime: false,
hardware: Hardware::Prefer,
level: Level::L4_1,
threads: 0,
}
}
pub fn keyframe_frames(&self) -> u32 {
(self.keyframe_interval.as_secs_f64() * self.frame_rate.as_f64())
.round()
.max(1.0) as u32
}
pub fn bitrate(&self) -> u32 {
let bits_per_pixel = 0.05 + 0.15 * f64::from(self.quality.clamp(0.0, 1.0));
let pixels = f64::from(self.width) * f64::from(self.height);
(pixels * self.frame_rate.as_f64().max(1.0) * bits_per_pixel).min(f64::from(u32::MAX))
as u32
}
}
pub fn backends_for(encoding: Encoding, hardware: Hardware) -> Vec<Backend> {
BACKENDS
.into_iter()
.filter(|backend| backend.handles(encoding) && backend.is_available())
.filter(|backend| hardware != Hardware::Require || backend.can_use_hardware())
.collect()
}
pub fn is_available(encoding: Encoding, hardware: Hardware) -> bool {
!backends_for(encoding, hardware).is_empty()
}
pub fn default_encoding(hardware: Hardware) -> Option<Encoding> {
is_available(Encoding::H264, hardware).then_some(Encoding::H264)
}
pub fn open(config: &EncoderConfig) -> Result<Box<dyn Encoder>> {
if !config.encoding.is_encodable() {
return Err(Error::NoEncoder {
encoding: config.encoding,
remedy: "vtome writes H.264 (through the OS) and AV1, nothing else".to_string(),
});
}
if config.width == 0 || config.height == 0 {
return Err(Error::Encode {
reason: format!("cannot encode a {}×{} picture", config.width, config.height),
});
}
let candidates = backends_for(config.encoding, config.hardware);
if candidates.is_empty() {
return Err(Error::NoEncoder {
encoding: config.encoding,
remedy: remedy_for(config.encoding, config.hardware),
});
}
let mut refusal = None;
for backend in candidates {
match instantiate(backend, config) {
Some(Ok(encoder)) => return Ok(encoder),
Some(Err(error)) => refusal = Some(error),
None => {}
}
}
Err(refusal.unwrap_or_else(|| Error::NoEncoder {
encoding: config.encoding,
remedy: remedy_for(config.encoding, config.hardware),
}))
}
#[allow(unused_variables)]
fn instantiate(backend: Backend, config: &EncoderConfig) -> Option<Result<Box<dyn Encoder>>> {
match backend {
#[cfg(all(feature = "encode-platform", target_vendor = "apple"))]
Backend::VideoToolbox => Some(
crate::encode_videotoolbox::VideoToolboxEncoder::new(config)
.map(|encoder| Box::new(encoder) as Box<dyn Encoder>),
),
#[cfg(all(feature = "encode-platform", windows))]
Backend::MediaFoundation => Some(
crate::media_foundation::MediaFoundationEncoder::new(config)
.map(|encoder| Box::new(encoder) as Box<dyn Encoder>),
),
#[cfg(all(feature = "encode-platform", target_os = "android"))]
Backend::MediaCodec => Some(
crate::media_codec::MediaCodecEncoder::new(config)
.map(|encoder| Box::new(encoder) as Box<dyn Encoder>),
),
#[cfg(all(
feature = "encode-platform",
unix,
not(target_vendor = "apple"),
not(target_os = "android")
))]
Backend::GStreamer => Some(
crate::gstreamer::GStreamerEncoder::new(config)
.map(|encoder| Box::new(encoder) as Box<dyn Encoder>),
),
#[cfg(feature = "encode-av1")]
Backend::Rav1e => Some(
crate::encode_av1::Rav1eEncoder::new(config)
.map(|encoder| Box::new(encoder) as Box<dyn Encoder>),
),
_ => None,
}
}
fn remedy_for(encoding: Encoding, hardware: Hardware) -> String {
let compiled_but_ruled_out = BACKENDS
.into_iter()
.any(|backend| backend.handles(encoding) && backend.is_available());
if compiled_but_ruled_out && hardware == Hardware::Require {
return format!(
"hardware encoding was required, and {encoding} is encoded here only in \
software (rav1e) — allow software, or write H.264"
);
}
let here: Vec<Backend> = BACKENDS
.into_iter()
.filter(|backend| backend.handles(encoding) && backend.exists_here())
.collect();
match encoding {
Encoding::H264 if here.iter().any(|backend| backend.feature() == "encode-platform") => {
if cfg!(feature = "encode-platform") {
format!(
"{} would encode it here and is not implemented yet (planning/TODO.md \
§14.1); write AV1 instead",
here[0]
)
} else {
"enable the encode-platform feature (the OS's own H.264 encoder)".to_string()
}
}
Encoding::H264 => "this platform has no H.264 encoder vtome may use — H.264 is only \
ever the OS's, never bundled — so write AV1 instead"
.to_string(),
_ => "enable the encode-av1 feature (rav1e)".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn level_4_1_holds_1080p30_and_720p60_and_scales_anything_bigger() {
let thirty = Rational::new(30, 1);
let sixty = Rational::new(60, 1);
assert_eq!(fit_level(1920, 1080, thirty, Level::L4_1), (1920, 1080));
assert_eq!(fit_level(1280, 720, sixty, Level::L4_1), (1280, 720));
assert_eq!(fit_level(3840, 2160, thirty, Level::L4_1), (1920, 1080));
let (width, height) = fit_level(4000, 3000, thirty, Level::L4_1);
let blocks = u64::from(width.div_ceil(16)) * u64::from(height.div_ceil(16));
assert!(blocks <= LEVEL_4_1_FRAME, "{width}×{height}");
assert!((f64::from(width) / f64::from(height) - 4.0 / 3.0).abs() < 0.02);
assert!(width % 2 == 0 && height % 2 == 0);
let (width, height) = fit_level(1920, 1080, sixty, Level::L4_1);
let blocks = u64::from(width.div_ceil(16)) * u64::from(height.div_ceil(16));
assert!(blocks * 60 <= LEVEL_4_1_RATE, "{width}×{height} at 60 fps");
assert_eq!(fit_level(3840, 2160, thirty, Level::Auto), (3840, 2160));
}
#[test]
fn ntsc_1080p_fits_level_4_1() {
assert_eq!(
fit_level(1920, 1080, Rational::new(30_000, 1001), Level::L4_1),
(1920, 1080)
);
}
#[test]
fn the_defaults_are_the_spec() {
let config = EncoderConfig::new(
Encoding::H264,
1920,
1080,
Rational::new(30, 1),
ColorSpace::default(),
);
assert_eq!(config.keyframe_frames(), 60, "two seconds at 30 fps");
assert_eq!(config.level, Level::L4_1);
assert!((0.65..=0.70).contains(&config.quality));
assert_eq!(config.hardware, Hardware::Prefer);
let bitrate = config.bitrate();
assert!((8_000_000..11_000_000).contains(&bitrate), "{bitrate}");
}
#[test]
fn nothing_but_h264_and_av1_is_ever_opened() {
let config = EncoderConfig::new(
Encoding::Vp9,
64,
64,
Rational::new(30, 1),
ColorSpace::default(),
);
assert!(matches!(open(&config), Err(Error::NoEncoder { .. })));
}
#[test]
fn required_hardware_rules_out_the_software_av1_encoder() {
assert!(backends_for(Encoding::Av1, Hardware::Require).is_empty());
let mut config = EncoderConfig::new(
Encoding::Av1,
64,
64,
Rational::new(30, 1),
ColorSpace::default(),
);
config.hardware = Hardware::Require;
let Err(Error::NoEncoder { remedy, .. }) = open(&config) else {
panic!("AV1 in hardware is not something vtome has");
};
assert!(remedy.contains("hardware") || remedy.contains("encode-av1"), "{remedy}");
}
#[test]
fn h264_is_never_offered_by_a_bundled_encoder() {
assert!(!Backend::Rav1e.handles(Encoding::H264));
assert!(BACKENDS
.into_iter()
.filter(|backend| backend.handles(Encoding::H264))
.all(|backend| backend.feature() == "encode-platform"));
}
}