mod info;
mod ioctl;
mod media;
mod request;
mod topology;
mod util;
mod video;
pub mod vp8;
pub use info::DeviceInfo;
use log::{debug, trace};
pub use media::Media;
pub use request::Request;
pub use topology::{EntityFunction, InterfaceDevnode, LinkFlags, PadFlags, Topology};
pub use video::{
BufType, Buffer, Capability, CapsFlags, ExportBuffer, ExtControl, ExtControls, FmtDesc, Format,
MappedBuffer, Memory, Rect, Selection, Video,
};
use core::fmt;
use rustix::fd::OwnedFd;
use std::collections::HashMap;
use std::io;
use std::os::linux::fs::MetadataExt;
use std::path::Path;
use std::rc::Rc;
pub struct DrmFormats;
impl DrmFormats {
pub const VP8F: u32 = 0x46385056;
pub const NV12: u32 = 0x3231564e;
pub const NM12: u32 = 0x32314d4e;
pub const ST12: u32 = 0x32315453;
pub const JPEG: u32 = 0x4745504a;
pub const UYVY: u32 = 0x59565955;
pub const YUYV: u32 = 0x56595559;
pub const YM12: u32 = 0x32314d59;
}
struct DrmModifiers;
impl DrmModifiers {
pub const LINEAR: u64 = 0;
pub const ALLWINNER_TILED: u64 = 0x09000000_00000001;
}
fn v4l2_format_to_drm_format_and_modifier(format: u32) -> (u32, u64) {
match format {
DrmFormats::ST12 => (DrmFormats::NV12, DrmModifiers::ALLWINNER_TILED),
fmt => (fmt, DrmModifiers::LINEAR),
}
}
#[derive(Debug)]
pub enum Error {
IoError(io::Error),
Errno(rustix::io::Errno),
NoM2M,
FormatNotFound,
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "media::Error")
}
}
impl std::error::Error for Error {}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::IoError(err)
}
}
impl From<rustix::io::Errno> for Error {
fn from(err: rustix::io::Errno) -> Error {
Error::Errno(err)
}
}
#[derive(Debug)]
pub struct Plane {
pub offset: u32,
pub pitch: u32,
}
#[derive(Debug)]
pub struct Image {
width: u32,
height: u32,
format: u32,
modifier: u64,
planes: [Option<Plane>; 4],
out: Buffer,
cap: Buffer,
}
impl Image {
fn new(
width: u32,
height: u32,
format: u32,
modifier: u64,
planes: [Option<Plane>; 4],
is_mplane: bool,
num_out_planes: usize,
num_cap_planes: usize,
) -> Image {
let (out, cap);
if is_mplane {
let out_planes = vec![video::Plane::new(); num_out_planes];
let cap_planes = vec![video::Plane::new(); num_cap_planes];
out = Buffer::with_planes(BufType::VideoOutputMplane, out_planes);
cap = Buffer::with_planes(BufType::VideoCaptureMplane, cap_planes);
} else {
out = Buffer::new(BufType::VideoOutput);
cap = Buffer::new(BufType::VideoCapture);
}
Image {
width,
height,
format,
modifier,
planes,
out,
cap,
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn format(&self) -> u32 {
self.format
}
pub fn modifier(&self) -> u64 {
self.modifier
}
pub fn iter_planes(&self) -> impl Iterator<Item = &Plane> {
self.planes.iter().filter_map(|plane| plane.as_ref())
}
pub fn out_mut(&mut self) -> &mut Buffer {
&mut self.out
}
pub fn out(&self) -> &Buffer {
&self.out
}
pub fn cap_mut(&mut self) -> &mut Buffer {
&mut self.cap
}
pub fn cap(&self) -> &Buffer {
&self.cap
}
}
pub struct Decoder {
media: Rc<Media>,
video: Rc<Video>,
is_mplane: bool,
ctrl: Option<vp8::CtrlVp8Frame>,
}
pub struct JpegEncoder {
video: Rc<Video>,
is_mplane: bool,
}
pub struct Onix {
decoders: HashMap<u32, (Rc<Media>, Rc<Video>, bool)>,
encoders: HashMap<u32, (Rc<Media>, Rc<Video>, bool)>,
}
impl Onix {
#[inline(never)]
pub fn find_devices() -> Result<Onix, Error> {
let mut videos = HashMap::new();
let mut medias = HashMap::new();
for dir_entry in std::fs::read_dir("/dev")? {
let dir_entry = dir_entry?;
let file_name = dir_entry.file_name();
let file_name = file_name.to_str().unwrap();
if file_name.starts_with("video") {
let path = dir_entry.path();
if let Ok(metadata) = path.metadata() {
let dev_t = metadata.st_rdev();
trace!("Found video {} ({dev_t})", path.display());
videos.insert(dev_t, path);
}
} else if file_name.starts_with("media") {
let path = dir_entry.path();
if let Some((media, media_interfaces)) = Self::discover_media(&path) {
let media = Rc::new(media);
for interface in media_interfaces {
medias.insert(interface, Rc::clone(&media));
}
} else {
debug!("{} doesn’t have a valid decoder, skipping.", path.display());
}
}
}
let mut decoders = HashMap::new();
let mut encoders = HashMap::new();
for ((is_encoder, interface), media) in medias {
let path = &videos[&interface];
if let Ok((video, is_mplane, formats)) = Self::discover_video(path, is_encoder) {
let video = Rc::new(video);
let devices = if is_encoder {
&mut encoders
} else {
&mut decoders
};
for format in formats {
devices.insert(format, (Rc::clone(&media), Rc::clone(&video), is_mplane));
}
} else {
debug!("{} isn’t a valid decoder, skipping.", path.display());
}
}
Ok(Onix { decoders, encoders })
}
pub fn load_from_memory(&self, buffer: &[u8]) -> Result<(Decoder, Image), Error> {
if buffer.starts_with(b"\xff\xd8\xff") {
let mut p = buffer;
let (width, height);
loop {
if p[0] != 0xff {
return Err(Error::IoError(io::Error::new(
io::ErrorKind::Other,
"invalid marker in JPEG file",
)));
}
let marker = u16::from_be_bytes([p[0], p[1]]);
p = &p[2..];
if marker == 0xffd8 {
continue;
}
let size = u16::from_be_bytes([p[0], p[1]]);
if marker == 0xffc0 {
p = &p[2..];
height = u16::from_be_bytes([p[1], p[2]]) as u32;
width = u16::from_be_bytes([p[3], p[4]]) as u32;
break;
}
p = &p[size as usize..];
}
let input_format = DrmFormats::JPEG;
let mut decoder = self.get_decoder(input_format, None).unwrap();
let mut image = decoder.prepare(width, height, input_format, buffer.len() as u32)?;
{
let buf = image.out_mut();
let map = decoder.mmap(buf, 0)?;
map.as_mut_slice().copy_from_slice(buffer);
buf.set_bytesused(0, buffer.len());
}
decoder.queue(&mut image)?;
Ok((decoder, image))
} else if buffer.starts_with(b"RIFF") {
assert_eq!(&buffer[..4], b"RIFF");
let riff_len = u32::from_le_bytes([buffer[4], buffer[5], buffer[6], buffer[7]]);
assert_eq!(riff_len as usize, buffer.len() - 8);
assert_eq!(&buffer[8..12], b"WEBP");
assert_eq!(&buffer[12..16], b"VP8 ");
let vp8_len = u32::from_le_bytes([buffer[16], buffer[17], buffer[18], buffer[19]]);
assert_eq!(vp8_len as usize, buffer.len() - 20);
let vp8 = &buffer[20..];
let ctrl = {
let mut parser = crate::vp8::Parser::new(vp8);
parser.parse_vp8().unwrap()
};
let width = ctrl.width as u32;
let height = ctrl.height as u32;
let input_format = DrmFormats::VP8F;
let mut decoder = self.get_decoder(input_format, Some(ctrl)).unwrap();
let mut image = decoder.prepare(width, height, input_format, vp8_len)?;
{
let buf = image.out_mut();
let map = decoder.mmap(buf, 0)?;
map.as_mut_slice().copy_from_slice(vp8);
buf.set_bytesused(0, vp8_len as usize);
}
decoder.queue(&mut image)?;
Ok((decoder, image))
} else {
Err(Error::IoError(io::Error::new(
io::ErrorKind::Other,
"unknown file format",
)))
}
}
pub fn open<P: AsRef<Path>>(&self, path: P) -> Result<(Decoder, Image), Error> {
let buffer = std::fs::read(path)?;
self.load_from_memory(&buffer)
}
pub fn get_decoder(&self, format: u32, ctrl: Option<vp8::CtrlVp8Frame>) -> Option<Decoder> {
if let Some((media, video, is_mplane)) = self.decoders.get(&format) {
Some(Decoder {
media: media.clone(),
video: video.clone(),
is_mplane: *is_mplane,
ctrl,
})
} else {
None
}
}
pub fn jpeg_encoder(&self) -> Option<JpegEncoder> {
if let Some((_, video, is_mplane)) = self.encoders.get(&DrmFormats::JPEG) {
Some(JpegEncoder {
video: video.clone(),
is_mplane: *is_mplane,
})
} else {
None
}
}
fn discover_media(path: &Path) -> Option<(Media, Vec<(bool, u64)>)> {
let media = Media::open(path).ok()?;
let topology = media.get_topology().ok()?;
trace!("Found media {}", path.display());
for interface in topology.interfaces() {
trace!(" {interface:?}");
}
for pad in topology.pads() {
trace!(" {pad:?}");
}
for link in topology.links() {
trace!(" {link:?}");
}
let mut interfaces = Vec::new();
for entity in topology.entities() {
trace!(" {entity:?}");
if entity.function() == EntityFunction::ProcVideoDecoder {
trace!(" … is a decoder!");
let mut interface_id = None;
for pad in topology.get_pads_for_entity(entity.id()) {
trace!(" {pad:?}");
let pad_id = if pad.flags().contains(PadFlags::SOURCE) {
let link = topology.get_link_by_source_id(pad.id())?;
trace!(" {link:?}");
link.sink_id()
} else
{
let link = topology.get_link_by_sink_id(pad.id())?;
trace!(" {link:?}");
link.source_id()
};
let pad = topology.get_pad(pad_id)?;
trace!(" {pad:?}");
let entity = topology.get_entity(pad.entity_id())?;
trace!(" {entity:?}");
let link = topology.get_link_by_sink_id(entity.id())?;
trace!(" {link:?}");
assert!(link.flags().contains(LinkFlags::INTERFACE_LINK));
if let Some(interface_id) = interface_id {
assert_eq!(interface_id, link.source_id());
} else {
interface_id = Some(link.source_id());
}
}
let interface = topology.get_interface(interface_id.unwrap())?;
trace!(" {interface:?}");
let InterfaceDevnode { major, minor } = interface.devnode();
let dev_t = rustix::fs::makedev(major, minor);
trace!(" {major},{minor} -> {dev_t}");
interfaces.push((false, dev_t));
} else if entity.function() == EntityFunction::ProcVideoEncoder {
trace!(" … is an encoder!");
let mut interface_id = None;
for pad in topology.get_pads_for_entity(entity.id()) {
trace!(" {pad:?}");
let pad_id = if pad.flags().contains(PadFlags::SOURCE) {
let link = topology.get_link_by_source_id(pad.id())?;
trace!(" {link:?}");
link.sink_id()
} else
{
let link = topology.get_link_by_sink_id(pad.id())?;
trace!(" {link:?}");
link.source_id()
};
let pad = topology.get_pad(pad_id)?;
trace!(" {pad:?}");
let entity = topology.get_entity(pad.entity_id())?;
trace!(" {entity:?}");
let link = topology.get_link_by_sink_id(entity.id())?;
trace!(" {link:?}");
assert!(link.flags().contains(LinkFlags::INTERFACE_LINK));
if let Some(interface_id) = interface_id {
assert_eq!(interface_id, link.source_id());
} else {
interface_id = Some(link.source_id());
}
}
let interface = topology.get_interface(interface_id.unwrap())?;
trace!(" {interface:?}");
let InterfaceDevnode { major, minor } = interface.devnode();
let dev_t = rustix::fs::makedev(major, minor);
trace!(" {major},{minor} -> {dev_t}");
interfaces.push((true, dev_t));
}
}
Some((media, interfaces))
}
fn discover_video(path: &Path, is_encoder: bool) -> Result<(Video, bool, Vec<u32>), Error> {
let video = Video::open(path)?;
let caps = video.querycap()?;
let is_mplane = if caps.capabilities().contains(CapsFlags::VIDEO_M2M) {
false
} else if caps.capabilities().contains(CapsFlags::VIDEO_M2M_MPLANE) {
true
} else {
return Err(Error::NoM2M);
};
let buf_type = if is_encoder {
BufType::capture(is_mplane)
} else {
BufType::output(is_mplane)
};
let formats = video
.enum_fmts(buf_type)?
.iter()
.map(|fmt| fmt.pixelformat())
.collect();
Ok((video, is_mplane, formats))
}
}
impl JpegEncoder {
pub fn video_path(&self) -> &Path {
self.video.path()
}
pub fn video_querycap(&self) -> Result<Capability, Error> {
Ok(self.video.querycap()?)
}
pub fn prepare(
&mut self,
width: u32,
height: u32,
input_format: u32,
compression_quality: i32,
) -> Result<Image, Error> {
let num_planes = match input_format {
DrmFormats::YUYV => 1,
DrmFormats::UYVY => 1,
DrmFormats::NM12 => 2,
DrmFormats::YM12 => 3,
format => panic!("TODO: Add support for format {format}"),
};
let out_type = BufType::output(self.is_mplane);
let cap_type = BufType::capture(self.is_mplane);
let format = DrmFormats::JPEG;
let mut format = Format::new(cap_type, width, height, format);
self.video.s_fmt(&mut format)?;
let mut format = Format::new(out_type, width, height, input_format);
self.video.s_fmt(&mut format)?;
const V4L2_CID_JPEG_COMPRESSION_QUALITY: u32 = 0x009d0903;
let mut query = video::QueryCtrl::new(V4L2_CID_JPEG_COMPRESSION_QUALITY);
self.video.queryctrl(&mut query)?;
let compression_quality = compression_quality.clamp(query.minimum(), query.maximum());
let mut ctrl = video::Control::new(V4L2_CID_JPEG_COMPRESSION_QUALITY, compression_quality);
self.video.s_ctrl(&mut ctrl)?;
const V4L2_SEL_TGT_CROP: u32 = 0;
let rect = Rect::new(0, 0, width, height);
let mut selection =
Selection::new(BufType::output(self.is_mplane), V4L2_SEL_TGT_CROP, rect);
self.video.s_selection(&mut selection)?;
let pitch = format.bytesperline(0);
let planes = [Some(Plane { offset: 0, pitch }), None, None, None];
let mut image = Image::new(
width,
height,
DrmFormats::JPEG,
DrmModifiers::LINEAR,
planes,
self.is_mplane,
num_planes,
1,
);
self.video.reqbufs(Memory::Mmap, out_type, 1)?;
self.video.querybuf(&mut image.out)?;
self.video.reqbufs(Memory::Mmap, cap_type, 1)?;
self.video.querybuf(&mut image.cap)?;
self.video.streamon(out_type)?;
self.video.streamon(cap_type)?;
Ok(image)
}
pub fn mmap(&mut self, buf: &mut Buffer, num_plane: usize) -> Result<MappedBuffer, Error> {
Ok(self.video.mmap(buf, num_plane)?)
}
pub fn queue(&mut self, image: &mut Image) -> Result<(), Error> {
self.video.qbuf(&mut image.out)?;
self.video.qbuf(&mut image.cap)?;
Ok(())
}
pub fn dequeue(&mut self, image: &mut Image) -> Result<(), Error> {
self.video.dqbuf(&mut image.out)?;
self.video.dqbuf(&mut image.cap)?;
if image.cap.is_error() {
Err(io::Error::new(io::ErrorKind::Other, "error while encoding"))?;
}
Ok(())
}
pub fn stop(&mut self) -> Result<(), Error> {
self.video.streamoff(BufType::output(self.is_mplane))?;
self.video.streamoff(BufType::capture(self.is_mplane))?;
Ok(())
}
}
impl Decoder {
pub fn media_device_info(&self) -> Result<DeviceInfo, Error> {
Ok(self.media.device_info()?)
}
pub fn video_querycap(&self) -> Result<Capability, Error> {
Ok(self.video.querycap()?)
}
pub fn prepare(
&mut self,
width: u32,
height: u32,
input_format: u32,
length: u32,
) -> Result<Image, Error> {
let out_type = BufType::output(self.is_mplane);
let cap_type = BufType::capture(self.is_mplane);
if let Ok(sizes) = self.video.enum_framesizes(input_format) {
for size in sizes {
trace!("{size:#?}");
}
}
let mut format = Format::new(out_type, width, height, input_format);
format.set_sizeimage(0, length);
self.video.s_fmt(&mut format)?;
let mut format = self.video.g_fmt(cap_type)?;
let pitch = format.bytesperline(0);
let offset = format.height() * pitch;
let (drm_format, drm_modifier) =
v4l2_format_to_drm_format_and_modifier(format.pixelformat());
let planes = [
Some(Plane { offset: 0, pitch }),
Some(Plane { offset, pitch }),
None,
None,
];
let mut image = Image::new(
width,
height,
drm_format,
drm_modifier,
planes,
self.is_mplane,
1,
1,
);
self.video.reqbufs(Memory::Mmap, out_type, 1)?;
self.video.querybuf(&mut image.out)?;
self.video.reqbufs(Memory::Mmap, cap_type, 1)?;
self.video.querybuf(&mut image.cap)?;
self.video.streamon(cap_type)?;
self.video.streamon(out_type)?;
Ok(image)
}
pub fn queue(&mut self, image: &mut Image) -> Result<(), Error> {
let request = if let Some(ctrl) = &mut self.ctrl {
let request = self.media.request_alloc()?;
image.out.set_request(&request);
let mut ext_ctrl = ExtControl::new(vp8::uapi::V4L2_CID_STATELESS_VP8_FRAME, ctrl);
let mut ext_ctrls = ExtControls::new(1, &mut ext_ctrl);
ext_ctrls.set_request(&request);
self.video.s_ext_ctrls(&mut ext_ctrls)?;
Some(request)
} else {
None
};
self.video.qbuf(&mut image.cap)?;
self.video.qbuf(&mut image.out)?;
if let Some(request) = request {
request.queue()?;
}
Ok(())
}
pub fn expbuf(&mut self, buf: &Buffer, num_plane: usize) -> Result<OwnedFd, Error> {
const O_CLOEXEC: u32 = 0x00080000;
let mut expbuf = ExportBuffer::new(buf.type_(), buf.index(), num_plane as u32, O_CLOEXEC);
self.video.expbuf(&mut expbuf)?;
Ok(expbuf.into_fd())
}
pub fn mmap(&mut self, buf: &mut Buffer, num_plane: usize) -> io::Result<MappedBuffer> {
Ok(self.video.mmap(buf, num_plane)?)
}
pub fn dequeue(&mut self, image: &mut Image) -> Result<(), Error> {
self.video.dqbuf(&mut image.out)?;
self.video.dqbuf(&mut image.cap)?;
if image.cap.is_error() {
Err(io::Error::new(io::ErrorKind::Other, "error while decoding"))?;
}
Ok(())
}
pub fn stop(&mut self) -> Result<(), Error> {
self.video.streamoff(BufType::output(self.is_mplane))?;
self.video.streamoff(BufType::capture(self.is_mplane))?;
Ok(())
}
}