use std::os::fd::{AsFd, OwnedFd};
use std::sync::{Arc, Mutex};
use bytes::Bytes;
use moq_net::Timestamp;
use moq_vaapi::decode::{Config as VaapiConfig, Decoder, ExportedFrame};
use super::{Backend, Codec, Config};
use crate::frame::{DmaBuf, DmaBufFrame, DmaBufPlane, DrmFormat, I420, Surface};
use crate::{Error, Frame};
pub(crate) const NAME: &str = "vaapi";
pub(crate) struct Vaapi {
decoder: Decoder,
gpu_frames: bool,
has_exported: bool,
}
unsafe impl Send for Vaapi {}
impl Vaapi {
pub(crate) fn open(codec: Codec, config: &Config) -> Result<Box<dyn Backend>, Error> {
if codec != Codec::H264 {
return Err(Error::Codec(anyhow::anyhow!("VAAPI cannot decode {}", codec.label())));
}
let decoder =
Decoder::new(VaapiConfig::new()).map_err(|e| Error::Codec(anyhow::anyhow!("VAAPI decoder init: {e:?}")))?;
tracing::info!(decoder = NAME, gpu_frames = config.gpu_frames, "opened H.264 decoder");
Ok(Box::new(Self {
decoder,
gpu_frames: config.gpu_frames,
has_exported: false,
}))
}
fn decode_shared(&mut self, access_unit: &Bytes, timestamp: u64) -> Option<Result<Vec<Frame>, Error>> {
if !self.gpu_frames {
return None;
}
let exported = self.decoder.decode_exported(access_unit, timestamp).and_then(share);
Some(self.exported(exported))
}
fn flush_shared(&mut self) -> Option<Result<Vec<Frame>, Error>> {
if !self.gpu_frames {
return None;
}
let exported = self.decoder.flush_exported().and_then(share);
Some(self.exported(exported))
}
fn exported(&mut self, exported: anyhow::Result<Vec<Frame>>) -> Result<Vec<Frame>, Error> {
match exported {
Ok(frames) => {
self.has_exported |= !frames.is_empty();
Ok(frames)
}
Err(err) if self.has_exported => Err(Error::Codec(err.context("VAAPI decode to a shared surface"))),
Err(err) => {
tracing::warn!(%err, "VAAPI cannot hand out decoded surfaces; downloading them instead");
self.gpu_frames = false;
Ok(Vec::new())
}
}
}
}
impl Backend for Vaapi {
fn decode(&mut self, access_unit: Bytes, timestamp: Timestamp, _keyframe: bool) -> Result<Vec<Frame>, Error> {
let timestamp = timestamp.as_micros() as u64;
if let Some(frames) = self.decode_shared(&access_unit, timestamp) {
return frames;
}
let decoded = self
.decoder
.decode(&access_unit, timestamp)
.map_err(|e| Error::Codec(anyhow::anyhow!("VAAPI decode: {e:?}")))?;
convert(decoded)
}
fn flush(&mut self) -> Result<Vec<Frame>, Error> {
if let Some(frames) = self.flush_shared() {
return frames;
}
let decoded = self
.decoder
.flush()
.map_err(|e| Error::Codec(anyhow::anyhow!("VAAPI flush: {e:?}")))?;
convert(decoded)
}
fn name(&self) -> &str {
NAME
}
}
fn convert(decoded: Vec<moq_vaapi::decode::Frame>) -> Result<Vec<Frame>, Error> {
decoded
.into_iter()
.map(|frame| {
let i420 = I420::from_nv12(&frame.data, frame.width, frame.height)?;
let timestamp = Timestamp::from_micros(frame.timestamp).unwrap_or(Timestamp::ZERO);
Ok(Frame::new(Surface::I420(i420), timestamp))
})
.collect()
}
fn share(exported: Vec<ExportedFrame>) -> anyhow::Result<Vec<Frame>> {
exported
.into_iter()
.map(|frame| {
let timestamp = Timestamp::from_micros(frame.timestamp).unwrap_or(Timestamp::ZERO);
Ok(Frame::new(Surface::DmaBuf(adopt(frame)?), timestamp))
})
.collect()
}
fn adopt(frame: ExportedFrame) -> anyhow::Result<DmaBuf> {
let (width, height) = (frame.width, frame.height);
let [object] = frame.descriptor.objects.as_slice() else {
anyhow::bail!(
"VA-API exported {} objects, expected one holding every plane",
frame.descriptor.objects.len()
);
};
let [layer] = frame.descriptor.layers.as_slice() else {
anyhow::bail!(
"VA-API exported {} layers, expected one composed layer",
frame.descriptor.layers.len()
);
};
if layer.drm_format != DrmFormat::NV12.as_raw() {
anyhow::bail!("VA-API exported DRM format {:#x}, expected NV12", layer.drm_format);
}
let count = layer.num_planes as usize;
anyhow::ensure!(
count <= layer.offset.len(),
"VA-API exported {count} planes, more than a PRIME descriptor holds"
);
let planes = (0..count)
.map(|plane| DmaBufPlane::new(layer.offset[plane], layer.pitch[plane]))
.collect();
let modifier = object.drm_format_modifier;
DmaBuf::new(
DrmFormat::NV12,
modifier,
width,
height,
planes,
None,
Arc::new(Exported::new(frame)),
)
.map_err(|e| anyhow::anyhow!("{e}"))
}
struct Exported {
frame: Mutex<ExportedFrame>,
}
impl Exported {
fn new(frame: ExportedFrame) -> Self {
Self {
frame: Mutex::new(frame),
}
}
}
impl DmaBufFrame for Exported {
fn export(&self) -> std::io::Result<OwnedFd> {
let frame = self.frame.lock().expect("poisoned");
let object = frame.descriptor.objects.first().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "the VA-API export carries no object")
})?;
object.fd.as_fd().try_clone_to_owned()
}
fn download_i420(&self) -> Result<I420, Error> {
let frame = self.frame.lock().expect("poisoned");
let nv12 = frame
.download()
.map_err(|e| Error::Codec(anyhow::anyhow!("read a VA-API decode surface back: {e:?}")))?;
I420::from_nv12(&nv12.data, nv12.width, nv12.height)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decode::{Config as DecodeConfig, Kind as DecodeKind};
use crate::encode::{Config as EncodeConfig, Encoder, Kind as EncodeKind};
fn hw_available() -> bool {
Decoder::new(VaapiConfig::new()).is_ok()
}
fn decode_config() -> DecodeConfig {
DecodeConfig {
kind: DecodeKind::Named(NAME.into()),
..DecodeConfig::new()
}
}
fn gpu_decode_config() -> DecodeConfig {
DecodeConfig {
gpu_frames: true,
..decode_config()
}
}
#[test]
fn missing_driver_errors_instead_of_panicking() {
if hw_available() {
return;
}
assert!(Vaapi::open(Codec::H264, &decode_config()).is_err());
}
fn gradient_rgba(width: u32, height: u32) -> Vec<u8> {
let (w, h) = (width as usize, height as usize);
let mut buf = vec![0u8; w * h * 4];
for y in 0..h {
for x in 0..w {
let i = (y * w + x) * 4;
buf[i] = (x * 255 / w) as u8;
buf[i + 1] = (y * 255 / h) as u8;
buf[i + 2] = ((x + y) * 255 / (w + h)) as u8;
buf[i + 3] = 255;
}
}
buf
}
fn mae(a: &[u8], b: &[u8]) -> u64 {
assert_eq!(a.len(), b.len());
a.iter().zip(b).map(|(x, y)| x.abs_diff(*y) as u64).sum::<u64>() / a.len() as u64
}
#[test]
fn vaapi_h264_round_trip() {
if !hw_available() {
return;
}
let (w, h) = (320u32, 240u32);
let rgba = gradient_rgba(w, h);
let expected = I420::from_rgba(&rgba, w * 4, w, h).unwrap();
let mut encoder = Encoder::new(&EncodeConfig {
kind: EncodeKind::Software,
..EncodeConfig::new(w, h, 30)
})
.unwrap();
let mut decoder = Vaapi::open(Codec::H264, &decode_config()).expect("VAAPI H.264 decoder");
let mut decoded = Vec::new();
for i in 0..10u64 {
if i == 0 {
encoder.keyframe();
}
let surface = Surface::rgba(&rgba, crate::Size::new(w, h)).unwrap();
let frame = Frame::new(surface, Timestamp::from_micros(i * 33_333).unwrap());
for encoded in encoder.encode(&frame).unwrap() {
decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, i == 0).unwrap());
}
}
assert!(!decoded.is_empty(), "VAAPI produced no frames");
for (i, frame) in decoded.iter().enumerate() {
assert_eq!(
frame.timestamp.as_micros(),
i as u128 * 33_333,
"timestamp did not ride the picture"
);
let i420 = frame.surface.to_i420().unwrap();
assert_eq!((i420.width(), i420.height()), (w, h));
assert!(mae(i420.y(), expected.y()) < 8, "Y plane corrupt");
assert!(mae(i420.u(), expected.u()) < 8, "U plane corrupt");
assert!(mae(i420.v(), expected.v()) < 8, "V plane corrupt");
}
}
#[test]
fn gpu_frames_still_answer_into_i420() {
if !hw_available() {
return;
}
let (w, h) = (320u32, 240u32);
let rgba = gradient_rgba(w, h);
let mut encoder = Encoder::new(&EncodeConfig {
kind: EncodeKind::Software,
..EncodeConfig::new(w, h, 30)
})
.unwrap();
let mut exporting = Vaapi::open(Codec::H264, &gpu_decode_config()).expect("VAAPI H.264 decoder");
let mut downloading = Vaapi::open(Codec::H264, &decode_config()).expect("a second decoder");
let mut exported = Vec::new();
let mut downloaded = Vec::new();
for i in 0..10u64 {
if i == 0 {
encoder.keyframe();
}
let surface = Surface::rgba(&rgba, crate::Size::new(w, h)).unwrap();
let frame = Frame::new(surface, Timestamp::from_micros(i * 33_333).unwrap());
for encoded in encoder.encode(&frame).unwrap() {
let (payload, timestamp) = (encoded.payload, encoded.timestamp);
exported.extend(exporting.decode(payload.clone(), timestamp, i == 0).unwrap());
downloaded.extend(downloading.decode(payload, timestamp, i == 0).unwrap());
}
}
assert!(!exported.is_empty(), "VAAPI produced no frames");
assert_eq!(exported.len(), downloaded.len(), "the two decoders disagreed");
for (i, (gpu, cpu)) in exported.iter().zip(&downloaded).enumerate() {
let Surface::DmaBuf(buffer) = &gpu.surface else {
panic!("frame {i} did not come back GPU-resident");
};
assert_eq!(buffer.format(), crate::DrmFormat::NV12);
assert_eq!((buffer.width(), buffer.height()), (w, h));
assert_eq!(gpu.timestamp, cpu.timestamp, "frame {i} lost its timestamp");
let Surface::I420(reference) = &cpu.surface else {
panic!("frame {i} came back GPU-resident without gpu_frames");
};
let read_back = gpu.surface.to_i420().expect("read the decoded surface back");
assert_eq!(
(read_back.width(), read_back.height()),
(reference.width(), reference.height())
);
assert!(
read_back.y() == reference.y(),
"frame {i} read back a different Y plane"
);
assert!(
read_back.u() == reference.u(),
"frame {i} read back a different U plane"
);
assert!(
read_back.v() == reference.v(),
"frame {i} read back a different V plane"
);
}
}
#[test]
fn flushing_returns_the_tail_the_dpb_holds() {
if !hw_available() {
return;
}
flushing_returns_the_tail(&decode_config());
}
#[test]
fn flushing_returns_the_tail_of_a_gpu_stream() {
if !hw_available() {
return;
}
flushing_returns_the_tail(&gpu_decode_config());
}
fn flushing_returns_the_tail(config: &DecodeConfig) {
const FRAMES: u64 = 5;
let (w, h) = (320u32, 240u32);
let rgba = gradient_rgba(w, h);
let mut encoder = Encoder::new(&EncodeConfig {
kind: EncodeKind::Software,
..EncodeConfig::new(w, h, 30)
})
.unwrap();
let mut decoder = Vaapi::open(Codec::H264, config).expect("VAAPI H.264 decoder");
let mut streamed = Vec::new();
for i in 0..FRAMES {
if i == 0 {
encoder.keyframe();
}
let surface = Surface::rgba(&rgba, crate::Size::new(w, h)).unwrap();
let frame = Frame::new(surface, Timestamp::from_micros(i * 33_333).unwrap());
for encoded in encoder.encode(&frame).unwrap() {
streamed.extend(decoder.decode(encoded.payload, encoded.timestamp, i == 0).unwrap());
}
}
assert!(
(streamed.len() as u64) < FRAMES,
"the DPB held nothing back, so this test proves nothing"
);
let flushed = decoder.flush().unwrap();
let timestamps: Vec<u128> = streamed
.iter()
.chain(&flushed)
.map(|frame| frame.timestamp.as_micros())
.collect();
let expected: Vec<u128> = (0..FRAMES as u128).map(|i| i * 33_333).collect();
assert_eq!(timestamps, expected, "the stream lost pictures at its end");
assert!(decoder.flush().unwrap().is_empty());
}
}