Fix Linux V4L2 primary node selection, V4L2 PU/CT ioctls, and MJPEG capture
This commit is contained in:
2 files changed
+298
-20
No files matched your search
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -130,12 +138,28 @@ fn open_linux_v4l2_node(vid: u16, pid: u16) -> Option<std::fs::File> {
|
|||||||
.write(true)
|
.write(true)
|
||||||
.open(&dev_path)
|
.open(&dev_path)
|
||||||
{
|
{
|
||||||
return Some(file);
|
// 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);
|
||||||
|
}
|
||||||
|
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,11 +416,53 @@ 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 {
|
||||||
let mut buf = payload.to_vec();
|
if is_xu {
|
||||||
if linux_uvcioc_ctrl_query(v4l, unit_id, selector, UvcRequest::SetCur as u8, &mut buf)
|
let mut buf = payload.to_vec();
|
||||||
|
if linux_uvcioc_ctrl_query(
|
||||||
|
v4l,
|
||||||
|
unit_id,
|
||||||
|
selector,
|
||||||
|
UvcRequest::SetCur as u8,
|
||||||
|
&mut buf,
|
||||||
|
)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else if unit_id == self.info.unit_ids.input_terminal_id
|
||||||
|
&& selector == ct_selectors::PANTILT_ABSOLUTE
|
||||||
|
&& payload.len() >= 8
|
||||||
{
|
{
|
||||||
return Ok(());
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,9 +524,49 @@ 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 {
|
||||||
let mut buf = vec![0u8; actual_len];
|
if is_xu {
|
||||||
if linux_uvcioc_ctrl_query(v4l, unit_id, selector, req as u8, &mut buf).is_ok() {
|
let mut buf = vec![0u8; actual_len];
|
||||||
return Ok(buf);
|
if linux_uvcioc_ctrl_query(v4l, unit_id, selector, req as u8, &mut buf).is_ok() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user