Fix Linux V4L2 primary node selection, V4L2 PU/CT ioctls, and MJPEG capture

This commit is contained in:
Lucas Oskorep committed 2026-10-19 18:34:20 -04:00
1 parent 4dccfe3284
commit 28b9ff6a57
2 files changed
+292 -14

No files matched your search

+42 -7
View File
@@ -198,10 +198,29 @@ impl VideoPreviewEngine {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
for i in 0..10 { for i in 0..16 {
let dev_path = format!("/dev/video{}", i); let dev_path = format!("/dev/video{}", i);
if Path::new(&dev_path).exists() { if !Path::new(&dev_path).exists() {
continue;
}
let sys_dir = PathBuf::from(format!("/sys/class/video4linux/video{}", i));
// Skip secondary metadata V4L2 nodes (`index != 0`)
let idx = std::fs::read_to_string(sys_dir.join("index"))
.unwrap_or_default()
.trim()
.to_string();
if !idx.is_empty() && idx != "0" {
continue;
}
let card_name = std::fs::read_to_string(sys_dir.join("name"))
.unwrap_or_default()
.trim()
.to_string();
if card_name.is_empty() {
self.available_devices.push(dev_path); self.available_devices.push(dev_path);
} else {
self.available_devices
.push(format!("[{}] {}", dev_path, card_name));
} }
} }
} }
@@ -246,11 +265,14 @@ impl VideoPreviewEngine {
let device_arg = if device_spec.starts_with('[') { let device_arg = if device_spec.starts_with('[') {
if let Some(end) = device_spec.find(']') { if let Some(end) = device_spec.find(']') {
let bracketed = device_spec[1..end].trim();
let name = device_spec[end + 1..].trim(); let name = device_spec[end + 1..].trim();
if !name.is_empty() { if bracketed.starts_with("/dev/") {
bracketed.to_string()
} else if !name.is_empty() {
name.to_string() name.to_string()
} else { } else {
device_spec[1..end].to_string() bracketed.to_string()
} }
} else { } else {
device_spec.to_string() device_spec.to_string()
@@ -322,11 +344,24 @@ impl VideoPreviewEngine {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
let fps_str = fps.to_string(); let hw_fps = fps.min(30).to_string();
let out_size_arg = format!("{out_w}x{out_h}"); let hw_size = match (out_w, out_h) {
(3840, 2160) => "3840x2160",
(1280, 720) => "1280x720",
_ => "1920x1080",
};
cmd.args(["-hide_banner", "-loglevel", "error", "-f", "v4l2"]); cmd.args(["-hide_banner", "-loglevel", "error", "-f", "v4l2"]);
if attempt == 0 { if attempt == 0 {
cmd.args(["-framerate", &fps_str, "-video_size", &out_size_arg]); cmd.args([
"-input_format",
"mjpeg",
"-framerate",
&hw_fps,
"-video_size",
hw_size,
]);
} else {
cmd.args(["-framerate", "30", "-video_size", "1280x720"]);
} }
cmd.args([ cmd.args([
"-i", "-i",
+250 -7
View File
@@ -100,16 +100,24 @@ pub struct NusbTransport {
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn open_linux_v4l2_node(vid: u16, pid: u16) -> Option<std::fs::File> { fn open_linux_v4l2_node(vid: u16, pid: u16, xu1_id: u8) -> Option<std::fs::File> {
let vid_hex = format!("{:04x}", vid); let vid_hex = format!("{:04x}", vid);
let pid_hex = format!("{:04x}", pid); let pid_hex = format!("{:04x}", pid);
let entries = std::fs::read_dir("/sys/class/video4linux").ok()?; let entries = std::fs::read_dir("/sys/class/video4linux").ok()?;
let mut candidates: Vec<(u32, std::path::PathBuf, String)> = Vec::new();
for entry in entries.flatten() { for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string(); let name = entry.file_name().to_string_lossy().to_string();
if !name.starts_with("video") { if let Some(num_str) = name.strip_prefix("video") {
continue; if let Ok(num) = num_str.parse::<u32>() {
candidates.push((num, entry.path(), name));
} }
let dev_link = entry.path().join("device"); }
}
candidates.sort_by_key(|(num, _, _)| *num);
let mut fallback: Option<std::fs::File> = None;
for (_, path, name) in candidates {
let dev_link = path.join("device");
let Ok(canon) = std::fs::canonicalize(&dev_link) else { let Ok(canon) = std::fs::canonicalize(&dev_link) else {
continue; continue;
}; };
@@ -129,13 +137,29 @@ fn open_linux_v4l2_node(vid: u16, pid: u16) -> Option<std::fs::File> {
.read(true) .read(true)
.write(true) .write(true)
.open(&dev_path) .open(&dev_path)
{
// Verify this V4L2 node is the primary video capture node (which supports
// UVCIOC_CTRL_QUERY), not the secondary metadata node (/dev/video1).
let mut len_buf = [0u8; 2];
if linux_uvcioc_ctrl_query(
&file,
xu1_id,
0x03,
UvcRequest::GetLen as u8,
&mut len_buf,
)
.is_ok()
{ {
return Some(file); return Some(file);
} }
if fallback.is_none() {
fallback = Some(file);
} }
} }
} }
None }
}
fallback
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -148,8 +172,141 @@ struct LinuxUvcXuControlQuery {
data: *mut u8, data: *mut u8,
} }
#[cfg(target_os = "linux")]
#[repr(C)]
struct LinuxV4l2Control {
id: u32,
value: i32,
}
#[cfg(target_os = "linux")]
#[repr(C)]
struct LinuxV4l2QueryCtrl {
id: u32,
ctrl_type: u32,
name: [u8; 32],
minimum: i32,
maximum: i32,
step: i32,
default_value: i32,
flags: u32,
reserved: [u32; 2],
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
const UVCIOC_CTRL_QUERY: libc::c_ulong = 0xC010_7521; const UVCIOC_CTRL_QUERY: libc::c_ulong = 0xC010_7521;
#[cfg(target_os = "linux")]
const VIDIOC_G_CTRL: libc::c_ulong = 0xC008_561B;
#[cfg(target_os = "linux")]
const VIDIOC_S_CTRL: libc::c_ulong = 0xC008_561C;
#[cfg(target_os = "linux")]
const VIDIOC_QUERYCTRL: libc::c_ulong = 0xC044_5624;
#[cfg(target_os = "linux")]
const V4L2_CID_BRIGHTNESS: u32 = 0x0098_0900;
#[cfg(target_os = "linux")]
const V4L2_CID_CONTRAST: u32 = 0x0098_0901;
#[cfg(target_os = "linux")]
const V4L2_CID_SATURATION: u32 = 0x0098_0902;
#[cfg(target_os = "linux")]
const V4L2_CID_AUTO_WHITE_BALANCE: u32 = 0x0098_090C;
#[cfg(target_os = "linux")]
const V4L2_CID_POWER_LINE_FREQUENCY: u32 = 0x0098_0918;
#[cfg(target_os = "linux")]
const V4L2_CID_WHITE_BALANCE_TEMPERATURE: u32 = 0x0098_091A;
#[cfg(target_os = "linux")]
const V4L2_CID_SHARPNESS: u32 = 0x0098_091B;
#[cfg(target_os = "linux")]
const V4L2_CID_PAN_ABSOLUTE: u32 = 0x009A_0908;
#[cfg(target_os = "linux")]
const V4L2_CID_TILT_ABSOLUTE: u32 = 0x009A_0909;
#[cfg(target_os = "linux")]
const V4L2_CID_FOCUS_ABSOLUTE: u32 = 0x009A_090A;
#[cfg(target_os = "linux")]
const V4L2_CID_FOCUS_AUTO: u32 = 0x009A_090C;
#[cfg(target_os = "linux")]
const V4L2_CID_ZOOM_ABSOLUTE: u32 = 0x009A_090D;
#[cfg(target_os = "linux")]
fn linux_uvc_to_v4l2_cid(unit_ids: &UvcUnitIds, unit_id: u8, selector: u8) -> Option<u32> {
if unit_id == unit_ids.processing_unit_id {
match selector {
pu_selectors::BRIGHTNESS => Some(V4L2_CID_BRIGHTNESS),
pu_selectors::CONTRAST => Some(V4L2_CID_CONTRAST),
pu_selectors::SATURATION => Some(V4L2_CID_SATURATION),
pu_selectors::SHARPNESS => Some(V4L2_CID_SHARPNESS),
pu_selectors::WHITE_BALANCE_TEMPERATURE => Some(V4L2_CID_WHITE_BALANCE_TEMPERATURE),
pu_selectors::WHITE_BALANCE_TEMPERATURE_AUTO => Some(V4L2_CID_AUTO_WHITE_BALANCE),
pu_selectors::POWER_LINE_FREQUENCY => Some(V4L2_CID_POWER_LINE_FREQUENCY),
_ => None,
}
} else if unit_id == unit_ids.input_terminal_id {
match selector {
ct_selectors::ZOOM_ABSOLUTE => Some(V4L2_CID_ZOOM_ABSOLUTE),
ct_selectors::FOCUS_ABSOLUTE => Some(V4L2_CID_FOCUS_ABSOLUTE),
ct_selectors::FOCUS_AUTO => Some(V4L2_CID_FOCUS_AUTO),
_ => None,
}
} else {
None
}
}
#[cfg(target_os = "linux")]
fn linux_v4l2_set_ctrl(file: &std::fs::File, id: u32, value: i32) -> Result<(), TransportError> {
use std::os::unix::io::AsRawFd;
let mut ctrl = LinuxV4l2Control { id, value };
let rc = unsafe { libc::ioctl(file.as_raw_fd(), VIDIOC_S_CTRL, &mut ctrl) };
if rc < 0 {
Err(TransportError::Usb(
std::io::Error::last_os_error().to_string(),
))
} else {
Ok(())
}
}
#[cfg(target_os = "linux")]
fn linux_v4l2_get_ctrl(file: &std::fs::File, id: u32) -> Result<i32, TransportError> {
use std::os::unix::io::AsRawFd;
let mut ctrl = LinuxV4l2Control { id, value: 0 };
let rc = unsafe { libc::ioctl(file.as_raw_fd(), VIDIOC_G_CTRL, &mut ctrl) };
if rc < 0 {
Err(TransportError::Usb(
std::io::Error::last_os_error().to_string(),
))
} else {
Ok(ctrl.value)
}
}
#[cfg(target_os = "linux")]
fn linux_v4l2_query_ctrl(
file: &std::fs::File,
id: u32,
) -> Result<LinuxV4l2QueryCtrl, TransportError> {
use std::os::unix::io::AsRawFd;
let mut q = LinuxV4l2QueryCtrl {
id,
ctrl_type: 0,
name: [0u8; 32],
minimum: 0,
maximum: 0,
step: 0,
default_value: 0,
flags: 0,
reserved: [0u32; 2],
};
let rc = unsafe { libc::ioctl(file.as_raw_fd(), VIDIOC_QUERYCTRL, &mut q) };
if rc < 0 {
Err(TransportError::Usb(
std::io::Error::last_os_error().to_string(),
))
} else {
Ok(q)
}
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn linux_uvcioc_ctrl_query( fn linux_uvcioc_ctrl_query(
@@ -209,7 +366,11 @@ impl NusbTransport {
.ok(); .ok();
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let v4l_file = open_linux_v4l2_node(info.vendor_id, info.product_id); let v4l_file = open_linux_v4l2_node(
info.vendor_id,
info.product_id,
info.unit_ids.extension_unit_id,
);
Ok(Self { Ok(Self {
info, info,
@@ -255,12 +416,54 @@ impl UvcControlTransport for NusbTransport {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if let Some(v4l) = &self.v4l_file { if let Some(v4l) = &self.v4l_file {
if is_xu {
let mut buf = payload.to_vec(); let mut buf = payload.to_vec();
if linux_uvcioc_ctrl_query(v4l, unit_id, selector, UvcRequest::SetCur as u8, &mut buf) if linux_uvcioc_ctrl_query(
v4l,
unit_id,
selector,
UvcRequest::SetCur as u8,
&mut buf,
)
.is_ok() .is_ok()
{ {
return Ok(()); return Ok(());
} }
} else if unit_id == self.info.unit_ids.input_terminal_id
&& selector == ct_selectors::PANTILT_ABSOLUTE
&& payload.len() >= 8
{
let pan = i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
let tilt = i32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]);
let r1 = linux_v4l2_set_ctrl(v4l, V4L2_CID_PAN_ABSOLUTE, pan);
let r2 = linux_v4l2_set_ctrl(v4l, V4L2_CID_TILT_ABSOLUTE, tilt);
if r1.is_ok() || r2.is_ok() {
return Ok(());
}
} else if let Some(cid) =
linux_uvc_to_v4l2_cid(&self.info.unit_ids, unit_id, selector)
{
let val: i32 = match payload.len() {
1 => payload[0] as i32,
2 => {
if cid == V4L2_CID_BRIGHTNESS {
i16::from_le_bytes([payload[0], payload[1]]) as i32
} else {
u16::from_le_bytes([payload[0], payload[1]]) as i32
}
}
4 => i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]),
_ => 0,
};
if linux_v4l2_set_ctrl(v4l, cid, val).is_ok() {
return Ok(());
}
if cid == V4L2_CID_POWER_LINE_FREQUENCY && val == 3 {
if linux_v4l2_set_ctrl(v4l, cid, 2).is_ok() {
return Ok(());
}
}
}
} }
let hdr = UvcControlPacketHeader::new( let hdr = UvcControlPacketHeader::new(
@@ -321,10 +524,50 @@ impl UvcControlTransport for NusbTransport {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if let Some(v4l) = &self.v4l_file { if let Some(v4l) = &self.v4l_file {
if is_xu {
let mut buf = vec![0u8; actual_len]; let mut buf = vec![0u8; actual_len];
if linux_uvcioc_ctrl_query(v4l, unit_id, selector, req as u8, &mut buf).is_ok() { if linux_uvcioc_ctrl_query(v4l, unit_id, selector, req as u8, &mut buf).is_ok() {
return Ok(buf); return Ok(buf);
} }
} else if unit_id == self.info.unit_ids.input_terminal_id
&& selector == ct_selectors::PANTILT_ABSOLUTE
&& req == UvcRequest::GetCur
{
if let (Ok(pan), Ok(tilt)) = (
linux_v4l2_get_ctrl(v4l, V4L2_CID_PAN_ABSOLUTE),
linux_v4l2_get_ctrl(v4l, V4L2_CID_TILT_ABSOLUTE),
) {
let mut out = vec![0u8; 8];
out[0..4].copy_from_slice(&pan.to_le_bytes());
out[4..8].copy_from_slice(&tilt.to_le_bytes());
return Ok(out);
}
} else if let Some(cid) =
linux_uvc_to_v4l2_cid(&self.info.unit_ids, unit_id, selector)
{
let v4l_val: Result<i32, TransportError> = match req {
UvcRequest::GetCur => linux_v4l2_get_ctrl(v4l, cid),
UvcRequest::GetMin => linux_v4l2_query_ctrl(v4l, cid).map(|q| q.minimum),
UvcRequest::GetMax => linux_v4l2_query_ctrl(v4l, cid).map(|q| q.maximum),
UvcRequest::GetRes => linux_v4l2_query_ctrl(v4l, cid).map(|q| q.step),
UvcRequest::GetDef => linux_v4l2_query_ctrl(v4l, cid).map(|q| q.default_value),
_ => Err(TransportError::Usb("unsupported V4L2 request".to_string())),
};
if let Ok(val) = v4l_val {
return Ok(match actual_len {
1 => vec![val as u8],
2 => (val as i16).to_le_bytes().to_vec(),
4 => val.to_le_bytes().to_vec(),
_ => {
let mut b = vec![0u8; actual_len];
let le = val.to_le_bytes();
let n = actual_len.min(4);
b[..n].copy_from_slice(&le[..n]);
b
}
});
}
}
} }
let hdr = UvcControlPacketHeader::new( let hdr = UvcControlPacketHeader::new(