#[cfg(target_os = "linux")]
use ados_protocol::hwcaps::Evidence;
use ados_protocol::hwcaps::{AbsenceReason, EncoderDevice, ProbePhase, Probed};
#[cfg(target_os = "linux")]
const FOURCC_H264: [u8; 4] = *b"H264";
#[cfg(target_os = "linux")]
const STAGE1_NODE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500);
#[cfg(target_os = "linux")]
const STAGE2_TRIAL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
pub fn probe_h264_encoder(phase: ProbePhase) -> Probed<EncoderDevice> {
if !phase.allows_destructive() {
return Probed::NotProbed;
}
probe_h264_encoder_impl()
}
#[cfg(target_os = "linux")]
fn probe_h264_encoder_impl() -> Probed<EncoderDevice> {
let node = match find_h264_m2m_node() {
Some(node) => node,
None => {
return Probed::absent(AbsenceReason::NodeMissing);
}
};
let argv = [
"ffmpeg",
"-hide_banner",
"-f",
"lavfi",
"-i",
"testsrc=size=320x240:rate=5",
"-t",
"0.3",
"-c:v",
"h264_v4l2m2m",
"-f",
"null",
"-",
];
match crate::runner::run_probe_sandboxed(&argv, STAGE2_TRIAL_TIMEOUT) {
crate::runner::ProbeOutcome::ExitedOk { ms } => Probed::present(
EncoderDevice {
node: node.clone(),
fourcc: FOURCC_H264,
},
Evidence::TrialInit { node, ms },
),
crate::runner::ProbeOutcome::ExitedAfterMs { ms } => {
Probed::absent(AbsenceReason::TrialInitFailed { exit_after_ms: ms })
}
crate::runner::ProbeOutcome::TimedOutHung => {
Probed::absent(AbsenceReason::TrialInitHung)
}
}
}
#[cfg(not(target_os = "linux"))]
fn probe_h264_encoder_impl() -> Probed<EncoderDevice> {
Probed::absent(AbsenceReason::NodeMissing)
}
#[cfg(target_os = "linux")]
fn find_h264_m2m_node() -> Option<String> {
for path in video_nodes() {
let probe_path = path.clone();
let handle = std::thread::spawn(move || node_is_h264_m2m(&probe_path));
let deadline = std::time::Instant::now() + STAGE1_NODE_TIMEOUT;
loop {
if handle.is_finished() {
if let Ok(true) = handle.join() {
return Some(path);
}
break;
}
if std::time::Instant::now() >= deadline {
tracing::warn!(node = %path, "v4l2 query timed out; skipping node");
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
None
}
#[cfg(target_os = "linux")]
fn video_nodes() -> Vec<String> {
let mut nodes: Vec<String> = match std::fs::read_dir("/dev") {
Ok(rd) => rd
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| {
n.starts_with("video")
&& n["video".len()..].chars().all(|c| c.is_ascii_digit())
})
.unwrap_or(false)
})
.filter_map(|p| p.to_str().map(|s| s.to_string()))
.collect(),
Err(_) => Vec::new(),
};
nodes.sort();
nodes
}
#[cfg(target_os = "linux")]
fn node_is_h264_m2m(path: &str) -> bool {
use std::ffi::CString;
let cpath = match CString::new(path) {
Ok(c) => c,
Err(_) => return false,
};
let fd = unsafe { nix::libc::open(cpath.as_ptr(), nix::libc::O_RDWR | nix::libc::O_NONBLOCK) };
if fd < 0 {
return false;
}
let result = (|| {
let caps = match query_cap(fd) {
Some(c) => c,
None => return false,
};
let effective = if caps.capabilities & V4L2_CAP_DEVICE_CAPS != 0 {
caps.device_caps
} else {
caps.capabilities
};
let is_m2m = effective & (V4L2_CAP_VIDEO_M2M | V4L2_CAP_VIDEO_M2M_MPLANE) != 0;
if !is_m2m {
return false;
}
for buf_type in [
V4L2_BUF_TYPE_VIDEO_OUTPUT,
V4L2_BUF_TYPE_VIDEO_CAPTURE,
V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
] {
if enum_fmt_has_h264(fd, buf_type) {
return true;
}
}
false
})();
unsafe {
nix::libc::close(fd);
}
result
}
#[cfg(target_os = "linux")]
#[repr(C)]
#[derive(Clone, Copy)]
struct V4l2Capability {
driver: [u8; 16],
card: [u8; 32],
bus_info: [u8; 32],
version: u32,
capabilities: u32,
device_caps: u32,
reserved: [u32; 3],
}
#[cfg(target_os = "linux")]
#[repr(C)]
#[derive(Clone, Copy)]
struct V4l2Fmtdesc {
index: u32,
buf_type: u32,
flags: u32,
description: [u8; 32],
pixelformat: u32,
reserved: [u32; 4],
}
#[cfg(target_os = "linux")]
const V4L2_CAP_VIDEO_M2M: u32 = 0x0000_8000;
#[cfg(target_os = "linux")]
const V4L2_CAP_VIDEO_M2M_MPLANE: u32 = 0x0000_4000;
#[cfg(target_os = "linux")]
const V4L2_CAP_DEVICE_CAPS: u32 = 0x8000_0000;
#[cfg(target_os = "linux")]
const V4L2_BUF_TYPE_VIDEO_CAPTURE: u32 = 1;
#[cfg(target_os = "linux")]
const V4L2_BUF_TYPE_VIDEO_OUTPUT: u32 = 2;
#[cfg(target_os = "linux")]
const V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE: u32 = 9;
#[cfg(target_os = "linux")]
const V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE: u32 = 10;
#[cfg(target_os = "linux")]
const IOC_NRBITS: u32 = 8;
#[cfg(target_os = "linux")]
const IOC_TYPEBITS: u32 = 8;
#[cfg(target_os = "linux")]
const IOC_SIZEBITS: u32 = 14;
#[cfg(target_os = "linux")]
const IOC_NRSHIFT: u32 = 0;
#[cfg(target_os = "linux")]
const IOC_TYPESHIFT: u32 = IOC_NRSHIFT + IOC_NRBITS;
#[cfg(target_os = "linux")]
const IOC_SIZESHIFT: u32 = IOC_TYPESHIFT + IOC_TYPEBITS;
#[cfg(target_os = "linux")]
const IOC_DIRSHIFT: u32 = IOC_SIZESHIFT + IOC_SIZEBITS;
#[cfg(target_os = "linux")]
const IOC_WRITE: u32 = 1;
#[cfg(target_os = "linux")]
const IOC_READ: u32 = 2;
#[cfg(target_os = "linux")]
const V4L2_IOC_TYPE: u32 = 0x56;
#[cfg(target_os = "linux")]
const fn ioc(dir: u32, ty: u32, nr: u32, size: u32) -> u32 {
(dir << IOC_DIRSHIFT) | (ty << IOC_TYPESHIFT) | (nr << IOC_NRSHIFT) | (size << IOC_SIZESHIFT)
}
#[cfg(target_os = "linux")]
fn vidioc_querycap_code() -> u32 {
ioc(
IOC_READ,
V4L2_IOC_TYPE,
0,
std::mem::size_of::<V4l2Capability>() as u32,
)
}
#[cfg(target_os = "linux")]
fn vidioc_enum_fmt_code() -> u32 {
ioc(
IOC_READ | IOC_WRITE,
V4L2_IOC_TYPE,
2,
std::mem::size_of::<V4l2Fmtdesc>() as u32,
)
}
#[cfg(target_os = "linux")]
fn ioctl_request(code: u32) -> nix::libc::Ioctl {
code as nix::libc::Ioctl
}
#[cfg(target_os = "linux")]
fn query_cap(fd: i32) -> Option<V4l2Capability> {
let mut cap: V4l2Capability = unsafe { std::mem::zeroed() };
let rc = unsafe {
nix::libc::ioctl(
fd,
ioctl_request(vidioc_querycap_code()),
&mut cap as *mut V4l2Capability,
)
};
if rc == 0 {
Some(cap)
} else {
None
}
}
#[cfg(target_os = "linux")]
fn enum_fmt_has_h264(fd: i32, buf_type: u32) -> bool {
let want = u32::from_le_bytes(FOURCC_H264);
let req = ioctl_request(vidioc_enum_fmt_code());
for index in 0u32..64 {
let mut desc: V4l2Fmtdesc = unsafe { std::mem::zeroed() };
desc.index = index;
desc.buf_type = buf_type;
let rc = unsafe { nix::libc::ioctl(fd, req, &mut desc as *mut V4l2Fmtdesc) };
if rc != 0 {
break;
}
if desc.pixelformat == want {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_phase_defers_probe() {
assert!(matches!(
probe_h264_encoder(ProbePhase::Runtime),
Probed::NotProbed
));
}
#[cfg(not(target_os = "linux"))]
#[test]
fn non_linux_reports_node_missing() {
assert!(matches!(
probe_h264_encoder(ProbePhase::Setup),
Probed::Absent {
reason: AbsenceReason::NodeMissing
}
));
}
#[cfg(target_os = "linux")]
#[test]
fn ioctl_codes_match_uapi() {
assert_eq!(vidioc_querycap_code(), 0x8068_5600);
assert_eq!(vidioc_enum_fmt_code(), 0xC040_5602);
}
#[cfg(target_os = "linux")]
#[test]
fn struct_sizes_match_kernel_abi() {
assert_eq!(std::mem::size_of::<V4l2Capability>(), 104);
assert_eq!(std::mem::size_of::<V4l2Fmtdesc>(), 64);
}
}