#![warn(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use std::{ffi::CStr, ffi::c_int, mem::MaybeUninit, ptr};
mod sys;
pub fn version() -> &'static str {
unsafe {
let ptr = sys::vmaf_version();
assert!(!ptr.is_null(), "vmaf_version() returned null pointer");
CStr::from_ptr(ptr)
.to_str()
.expect("vmaf_version() returned invalid UTF-8")
}
}
pub const BUILD_REPOSITORY: &str = sys::BUILD_METADATA_REPOSITORY;
pub const BUILD_VERSION: &str = sys::BUILD_METADATA_VERSION;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuiltinModel {
V061,
BV063,
V061Neg,
V4k061,
V4k061Neg,
}
impl BuiltinModel {
pub fn version_str(self) -> &'static str {
match self {
Self::V061 => "vmaf_v0.6.1",
Self::BV063 => "vmaf_b_v0.6.3",
Self::V061Neg => "vmaf_v0.6.1neg",
Self::V4k061 => "vmaf_4k_v0.6.1",
Self::V4k061Neg => "vmaf_4k_v0.6.1neg",
}
}
}
#[derive(Debug)]
pub enum Error {
InvalidInput(&'static str),
Ffi {
code: c_int,
function: &'static str,
},
}
impl Error {
fn check(code: c_int, function: &'static str) -> Result<(), Self> {
if code == 0 {
Ok(())
} else {
assert!(code < 0, "libvmaf returned non-negative error code: {code}");
Err(Self::Ffi { code, function })
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::InvalidInput(msg) => write!(f, "{msg}"),
Error::Ffi { code, function } => write!(
f,
"{function}() failed: {}",
std::io::Error::from_raw_os_error(-code)
),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone)]
pub struct ContextConfig {
pub log_level: LogLevel,
pub n_threads: u32,
pub n_subsample: u32,
pub cpumask: u64,
pub gpumask: u64,
}
impl Default for ContextConfig {
fn default() -> Self {
Self {
log_level: LogLevel::Error,
n_threads: 0,
n_subsample: 1,
cpumask: 0,
gpumask: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
None,
Error,
Warning,
Info,
Debug,
}
impl LogLevel {
fn to_sys(self) -> sys::VmafLogLevel {
match self {
Self::None => sys::VmafLogLevel_VMAF_LOG_LEVEL_NONE,
Self::Error => sys::VmafLogLevel_VMAF_LOG_LEVEL_ERROR,
Self::Warning => sys::VmafLogLevel_VMAF_LOG_LEVEL_WARNING,
Self::Info => sys::VmafLogLevel_VMAF_LOG_LEVEL_INFO,
Self::Debug => sys::VmafLogLevel_VMAF_LOG_LEVEL_DEBUG,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoolingMethod {
Min,
Max,
Mean,
HarmonicMean,
}
impl PoolingMethod {
fn to_sys(self) -> sys::VmafPoolingMethod {
match self {
Self::Min => sys::VmafPoolingMethod_VMAF_POOL_METHOD_MIN,
Self::Max => sys::VmafPoolingMethod_VMAF_POOL_METHOD_MAX,
Self::Mean => sys::VmafPoolingMethod_VMAF_POOL_METHOD_MEAN,
Self::HarmonicMean => sys::VmafPoolingMethod_VMAF_POOL_METHOD_HARMONIC_MEAN,
}
}
}
pub struct Context {
inner: *mut sys::VmafContext,
flushed: bool,
}
impl Context {
pub fn new(config: ContextConfig) -> Result<Self, Error> {
let cfg = sys::VmafConfiguration {
log_level: config.log_level.to_sys(),
n_threads: config.n_threads,
n_subsample: config.n_subsample,
cpumask: config.cpumask,
gpumask: config.gpumask,
};
let mut inner = ptr::null_mut();
Error::check(unsafe { sys::vmaf_init(&mut inner, cfg) }, "vmaf_init")?;
Ok(Self {
inner,
flushed: false,
})
}
pub fn use_model(&mut self, model: &Model) -> Result<(), Error> {
Error::check(
unsafe { sys::vmaf_use_features_from_model(self.inner, model.inner) },
"vmaf_use_features_from_model",
)
}
pub fn read_pictures(
&mut self,
mut reference: Option<Picture>,
mut distorted: Option<Picture>,
index: u32,
) -> Result<(), Error> {
if self.flushed {
return Err(Error::InvalidInput("read_pictures called after flush"));
}
let is_flush = reference.is_none() && distorted.is_none();
let ref_ptr = reference
.as_mut()
.map(|pic| &mut pic.inner as *mut _)
.unwrap_or(ptr::null_mut());
let dist_ptr = distorted
.as_mut()
.map(|pic| &mut pic.inner as *mut _)
.unwrap_or(ptr::null_mut());
Error::check(
unsafe { sys::vmaf_read_pictures(self.inner, ref_ptr, dist_ptr, index) },
"vmaf_read_pictures",
)?;
if is_flush {
self.flushed = true;
}
if let Some(pic) = reference.as_mut() {
pic.owned = false;
}
if let Some(pic) = distorted.as_mut() {
pic.owned = false;
}
Ok(())
}
pub fn score_at_index(&self, model: &Model, index: u32) -> Result<f64, Error> {
let mut score = 0.0;
Error::check(
unsafe { sys::vmaf_score_at_index(self.inner, model.inner, &mut score, index) },
"vmaf_score_at_index",
)?;
Ok(score)
}
pub fn score_pooled(
&self,
model: &Model,
method: PoolingMethod,
index_low: u32,
index_high: u32,
) -> Result<f64, Error> {
let mut score = 0.0;
Error::check(
unsafe {
sys::vmaf_score_pooled(
self.inner,
model.inner,
method.to_sys(),
&mut score,
index_low,
index_high,
)
},
"vmaf_score_pooled",
)?;
Ok(score)
}
}
impl Drop for Context {
fn drop(&mut self) {
if !self.inner.is_null() {
let ret = unsafe { sys::vmaf_close(self.inner) };
if ret != 0 {
tracing::warn!("vmaf_close() failed with error code: {ret}");
}
}
}
}
pub struct Model {
inner: *mut sys::VmafModel,
}
impl Model {
pub fn load_builtin(model: BuiltinModel) -> Result<Self, Error> {
let version = model.version_str();
let version_cstr =
std::ffi::CString::new(version).expect("builtin model version must not contain NUL");
let mut cfg = sys::VmafModelConfig {
name: ptr::null(),
flags: sys::VmafModelFlags_VMAF_MODEL_FLAGS_DEFAULT as u64,
};
let mut inner = ptr::null_mut();
Error::check(
unsafe { sys::vmaf_model_load(&mut inner, &mut cfg, version_cstr.as_ptr()) },
"vmaf_model_load",
)?;
Ok(Self { inner })
}
}
impl Drop for Model {
fn drop(&mut self) {
if !self.inner.is_null() {
unsafe { sys::vmaf_model_destroy(self.inner) };
}
}
}
pub struct Picture {
inner: sys::VmafPicture,
owned: bool,
}
impl Picture {
pub fn from_i420(y: &[u8], u: &[u8], v: &[u8], width: u32, height: u32) -> Result<Self, Error> {
if width == 0 || height == 0 {
return Err(Error::InvalidInput("width and height must be non-zero"));
}
if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
return Err(Error::InvalidInput(
"width and height must be even for I420 chroma subsampling",
));
}
let y_size = (width as usize) * (height as usize);
let uv_width = width.div_ceil(2) as usize;
let uv_height = height.div_ceil(2) as usize;
let uv_size = uv_width * uv_height;
if y.len() != y_size || u.len() != uv_size || v.len() != uv_size {
return Err(Error::InvalidInput(
"plane size does not match width and height",
));
}
let mut inner = MaybeUninit::<sys::VmafPicture>::zeroed();
Error::check(
unsafe {
sys::vmaf_picture_alloc(
inner.as_mut_ptr(),
sys::VmafPixelFormat_VMAF_PIX_FMT_YUV420P,
8,
width,
height,
)
},
"vmaf_picture_alloc",
)?;
let mut inner = unsafe { inner.assume_init() };
copy_plane(&mut inner, 0, y);
copy_plane(&mut inner, 1, u);
copy_plane(&mut inner, 2, v);
Ok(Self { inner, owned: true })
}
}
impl Drop for Picture {
fn drop(&mut self) {
if self.owned {
let ret = unsafe { sys::vmaf_picture_unref(&mut self.inner) };
if ret != 0 {
tracing::warn!("vmaf_picture_unref() failed with error code: {ret}");
}
}
}
}
fn copy_plane(pic: &mut sys::VmafPicture, plane: usize, src: &[u8]) {
let width = pic.w[plane] as usize;
let height = pic.h[plane] as usize;
if width == 0 || height == 0 {
return;
}
let expected_len = width * height;
assert!(
src.len() >= expected_len,
"plane {plane} buffer too small: expected at least {expected_len}, got {}",
src.len()
);
let stride = pic.stride[plane] as usize;
let dst_ptr = pic.data[plane] as *mut u8;
assert!(!dst_ptr.is_null());
for row in 0..height {
let src_row = &src[row * width..(row + 1) * width];
let dst_row = unsafe { dst_ptr.add(row * stride) };
unsafe {
ptr::copy_nonoverlapping(src_row.as_ptr(), dst_row, width);
if stride > width {
ptr::write_bytes(dst_row.add(width), 0, stride - width);
}
}
}
}