mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 03:32:00 +08:00
752 lines
28 KiB
Rust
752 lines
28 KiB
Rust
//! V4L2 capture implementation using v4l2r (ioctl layer).
|
||
|
||
use std::fs::File;
|
||
use std::io;
|
||
use std::os::fd::AsFd;
|
||
use std::path::{Path, PathBuf};
|
||
use std::time::Duration;
|
||
|
||
use nix::poll::{poll, PollFd, PollFlags, PollTimeout};
|
||
use tracing::{debug, info, warn};
|
||
use v4l2r::bindings::{
|
||
v4l2_dv_timings, v4l2_requestbuffers, v4l2_streamparm, v4l2_streamparm__bindgen_ty_1,
|
||
V4L2_DV_BT_656_1120,
|
||
};
|
||
use v4l2r::ioctl::{
|
||
self, Capabilities, Capability as V4l2rCapability, Event as V4l2Event, EventType,
|
||
MemoryConsistency, PlaneMapping, QBufPlane, QBuffer, QueryBuffer, QueryDvTimingsError,
|
||
SubscribeEventFlags, V4l2Buffer,
|
||
};
|
||
use v4l2r::memory::{MemoryType, MmapHandle};
|
||
use v4l2r::nix::errno::Errno;
|
||
use v4l2r::{Format as V4l2rFormat, PixelFormat as V4l2rPixelFormat, QueueType};
|
||
|
||
use crate::error::{AppError, Result};
|
||
use crate::video::device::bridge::{self as csi_bridge, CsiBridgeKind, ProbeResult};
|
||
use crate::video::format::{PixelFormat, Resolution};
|
||
use crate::video::signal::SignalStatus;
|
||
|
||
/// `io::Error` payload when the driver posts `V4L2_EVENT_SOURCE_CHANGE`.
|
||
pub const SOURCE_CHANGED_MARKER: &str = "v4l2_source_changed";
|
||
|
||
pub fn is_source_changed_error(err: &io::Error) -> bool {
|
||
err.get_ref()
|
||
.map(|inner| inner.to_string() == SOURCE_CHANGED_MARKER)
|
||
.unwrap_or(false)
|
||
}
|
||
|
||
/// Metadata for a captured frame.
|
||
#[derive(Debug, Clone, Copy)]
|
||
pub struct CaptureMeta {
|
||
pub bytes_used: usize,
|
||
pub sequence: u64,
|
||
}
|
||
|
||
/// When set, DV ioctls use the subdev (rkcif: video node has no DV ioctls).
|
||
#[derive(Debug, Clone, Default)]
|
||
pub struct BridgeContext {
|
||
pub subdev_path: Option<PathBuf>,
|
||
pub kind: Option<CsiBridgeKind>,
|
||
}
|
||
|
||
impl BridgeContext {
|
||
pub fn from_parts(subdev_path: Option<PathBuf>, kind: Option<CsiBridgeKind>) -> Self {
|
||
Self { subdev_path, kind }
|
||
}
|
||
|
||
pub fn has_subdev(&self) -> bool {
|
||
self.subdev_path.is_some()
|
||
}
|
||
}
|
||
|
||
/// V4L2 capture stream backed by v4l2r ioctl.
|
||
pub struct CaptureStream {
|
||
fd: File,
|
||
queue: QueueType,
|
||
resolution: Resolution,
|
||
format: PixelFormat,
|
||
stride: u32,
|
||
timeout: Duration,
|
||
mappings: Vec<Vec<PlaneMapping>>,
|
||
subdev_fd: Option<File>,
|
||
bridge_kind: Option<CsiBridgeKind>,
|
||
}
|
||
|
||
impl CaptureStream {
|
||
/// UVC: uses `resolution`. CSI bridges: DV-probe first; may return `CaptureNoSignal`.
|
||
pub fn open(
|
||
device_path: impl AsRef<Path>,
|
||
resolution: Resolution,
|
||
format: PixelFormat,
|
||
fps: u32,
|
||
buffer_count: u32,
|
||
timeout: Duration,
|
||
) -> Result<Self> {
|
||
Self::open_with_bridge(
|
||
device_path,
|
||
resolution,
|
||
format,
|
||
fps,
|
||
buffer_count,
|
||
timeout,
|
||
BridgeContext::default(),
|
||
)
|
||
}
|
||
|
||
/// With subdev: probe DV on subdev before opening video (RK628 safety); may ignore requested size.
|
||
pub fn open_with_bridge(
|
||
device_path: impl AsRef<Path>,
|
||
resolution: Resolution,
|
||
format: PixelFormat,
|
||
fps: u32,
|
||
buffer_count: u32,
|
||
timeout: Duration,
|
||
bridge: BridgeContext,
|
||
) -> Result<Self> {
|
||
// Probe subdev before video open (RK628: no-signal must not reach capture STREAMON).
|
||
let mut subdev_fd_opt: Option<File> = None;
|
||
let mut subdev_dv_mode: Option<csi_bridge::DvTimingsMode> = None;
|
||
|
||
if let Some(subdev_path) = bridge.subdev_path.as_ref() {
|
||
let subdev_fd = csi_bridge::open_subdev(subdev_path).map_err(|e| {
|
||
AppError::VideoError(format!(
|
||
"Failed to open CSI bridge subdev {:?}: {}",
|
||
subdev_path, e
|
||
))
|
||
})?;
|
||
|
||
let kind = bridge.kind.unwrap_or(CsiBridgeKind::Unknown);
|
||
match csi_bridge::probe_signal(&subdev_fd, kind) {
|
||
ProbeResult::Locked(mode) => {
|
||
info!(
|
||
"Subdev {:?} locked: {}x{} @ {}Hz",
|
||
subdev_path, mode.width, mode.height, mode.pixelclock
|
||
);
|
||
csi_bridge::apply_dv_timings(&subdev_fd, mode.raw);
|
||
if let Err(e) = csi_bridge::subscribe_source_change(&subdev_fd) {
|
||
debug!("subdev SOURCE_CHANGE subscribe failed: {}", e);
|
||
}
|
||
subdev_dv_mode = Some(mode);
|
||
}
|
||
other => {
|
||
let status = other.as_status().unwrap_or(SignalStatus::NoSignal);
|
||
debug!(
|
||
"Subdev {:?} reports no signal ({:?}) — refusing STREAMON",
|
||
subdev_path, status
|
||
);
|
||
return Err(AppError::CaptureNoSignal {
|
||
kind: status.as_str().to_string(),
|
||
});
|
||
}
|
||
}
|
||
subdev_fd_opt = Some(subdev_fd);
|
||
}
|
||
|
||
// ── Phase 1: open the capture (video) node ─────────────────────
|
||
let mut fd = File::options()
|
||
.read(true)
|
||
.write(true)
|
||
.open(device_path.as_ref())
|
||
.map_err(|e| AppError::VideoError(format!("Failed to open device: {}", e)))?;
|
||
|
||
let caps: V4l2rCapability = ioctl::querycap(&fd)
|
||
.map_err(|e| AppError::VideoError(format!("Failed to query capabilities: {}", e)))?;
|
||
let caps_flags = caps.device_caps();
|
||
let driver_name = caps.driver.to_string();
|
||
let is_csi_bridge = is_csi_bridge_driver(&driver_name);
|
||
|
||
// Prefer multi-planar capture when available, as it is required for some
|
||
// devices/pixel formats (e.g. NV12 via VIDEO_CAPTURE_MPLANE).
|
||
let queue = if caps_flags.contains(Capabilities::VIDEO_CAPTURE_MPLANE) {
|
||
QueueType::VideoCaptureMplane
|
||
} else if caps_flags.contains(Capabilities::VIDEO_CAPTURE) {
|
||
QueueType::VideoCapture
|
||
} else {
|
||
return Err(AppError::VideoError(
|
||
"Device does not support capture queues".to_string(),
|
||
));
|
||
};
|
||
|
||
// CSI/HDMI bridge without a subdev pairing (tc358743 on uvcvideo,
|
||
// rk_hdmirx on RK3588): probe DV timings on the video node, with
|
||
// the same no-signal gate as the subdev path. When we *do* have
|
||
// a subdev, reuse its already-probed mode to drive S_FMT.
|
||
let dv_mode = if let Some(mode) = subdev_dv_mode.as_ref() {
|
||
Some(DvTimingsMode {
|
||
width: mode.width,
|
||
height: mode.height,
|
||
fps: mode.fps,
|
||
})
|
||
} else if is_csi_bridge {
|
||
Some(probe_and_apply_dv_timings(&fd)?)
|
||
} else {
|
||
None
|
||
};
|
||
|
||
// rkcif + RK628: G_FMT is often 0×0 until the first S_FMT; G_FMT may
|
||
// also fail. With DV timings from the subdev, build the format (same as
|
||
// `v4l2-ctl --set-fmt-video=width=…,height=…`).
|
||
let mut fmt: V4l2rFormat = match (
|
||
ioctl::g_fmt::<V4l2rFormat>(&fd, queue),
|
||
is_csi_bridge,
|
||
dv_mode.as_ref(),
|
||
) {
|
||
(Ok(f), _, _) if f.width > 0 && f.height > 0 => f,
|
||
(_, true, Some(m)) => {
|
||
let fourcc = format.to_fourcc();
|
||
V4l2rFormat::from((&fourcc, (m.width as usize, m.height as usize)))
|
||
}
|
||
(Ok(f), _, _) => f,
|
||
(Err(e), _, _) => {
|
||
return Err(AppError::VideoError(format!(
|
||
"Failed to get device format: {}",
|
||
e
|
||
)));
|
||
}
|
||
};
|
||
|
||
// Prefer the DV-timings-reported geometry for CSI bridges — the
|
||
// source, not the user config, dictates what the capture hardware
|
||
// will actually deliver.
|
||
let (target_w, target_h) = match dv_mode {
|
||
Some(DvTimingsMode { width, height, .. }) => (width, height),
|
||
None => (resolution.width, resolution.height),
|
||
};
|
||
fmt.width = target_w;
|
||
fmt.height = target_h;
|
||
fmt.pixelformat = V4l2rPixelFormat::from(&format.to_fourcc());
|
||
|
||
let actual_fmt: V4l2rFormat = ioctl::s_fmt(&mut fd, (queue, &fmt))
|
||
.map_err(|e| AppError::VideoError(format!("Failed to set device format: {}", e)))?;
|
||
|
||
let actual_resolution = Resolution::new(actual_fmt.width, actual_fmt.height);
|
||
let actual_format = PixelFormat::from_v4l2r(actual_fmt.pixelformat).unwrap_or(format);
|
||
|
||
let stride = actual_fmt
|
||
.plane_fmt
|
||
.first()
|
||
.map(|p| p.bytesperline)
|
||
.unwrap_or_else(|| match actual_format.bytes_per_pixel() {
|
||
Some(bpp) => actual_resolution.width * bpp as u32,
|
||
None => actual_resolution.width,
|
||
});
|
||
|
||
if fps > 0 {
|
||
match set_fps(&fd, queue, fps) {
|
||
Ok(()) => {}
|
||
Err(ioctl::GParmError::IoctlError(err))
|
||
if matches!(err, Errno::ENOTTY | Errno::ENOSYS | Errno::EOPNOTSUPP) => {}
|
||
Err(e) => warn!("Failed to set hardware FPS: {}", e),
|
||
}
|
||
}
|
||
|
||
let req: v4l2_requestbuffers = ioctl::reqbufs(
|
||
&fd,
|
||
queue,
|
||
MemoryType::Mmap,
|
||
buffer_count,
|
||
MemoryConsistency::empty(),
|
||
)
|
||
.map_err(|e| AppError::VideoError(format!("Failed to request buffers: {}", e)))?;
|
||
let allocated = req.count as usize;
|
||
if allocated == 0 {
|
||
return Err(AppError::VideoError(
|
||
"Driver returned zero capture buffers".to_string(),
|
||
));
|
||
}
|
||
|
||
let mut mappings = Vec::with_capacity(allocated);
|
||
for index in 0..allocated as u32 {
|
||
let query: QueryBuffer = ioctl::querybuf(&fd, queue, index as usize).map_err(|e| {
|
||
AppError::VideoError(format!("Failed to query buffer {}: {}", index, e))
|
||
})?;
|
||
|
||
if query.planes.is_empty() {
|
||
return Err(AppError::VideoError(format!(
|
||
"Driver returned zero planes for buffer {}",
|
||
index
|
||
)));
|
||
}
|
||
|
||
let mut plane_maps = Vec::with_capacity(query.planes.len());
|
||
for plane in &query.planes {
|
||
let mapping = ioctl::mmap(&fd, plane.mem_offset, plane.length).map_err(|e| {
|
||
AppError::VideoError(format!("Failed to mmap buffer {}: {}", index, e))
|
||
})?;
|
||
plane_maps.push(mapping);
|
||
}
|
||
mappings.push(plane_maps);
|
||
}
|
||
|
||
let mut stream = Self {
|
||
fd,
|
||
queue,
|
||
resolution: actual_resolution,
|
||
format: actual_format,
|
||
stride,
|
||
timeout,
|
||
mappings,
|
||
subdev_fd: subdev_fd_opt,
|
||
bridge_kind: bridge.kind,
|
||
};
|
||
|
||
stream.queue_all_buffers()?;
|
||
ioctl::streamon(&stream.fd, stream.queue)
|
||
.map_err(|e| AppError::VideoError(format!("Failed to start capture stream: {}", e)))?;
|
||
|
||
// When the subdev path was used, SOURCE_CHANGE was already
|
||
// subscribed *there* (the rkcif video node returns ENOTTY).
|
||
// Otherwise try on the video node as a best-effort fallback for
|
||
// drivers that do honour it (tc358743/uvcvideo, rk_hdmirx).
|
||
if stream.subdev_fd.is_none() {
|
||
match ioctl::subscribe_event(
|
||
&stream.fd,
|
||
EventType::SourceChange(0),
|
||
SubscribeEventFlags::empty(),
|
||
) {
|
||
Ok(()) => debug!("Subscribed to V4L2_EVENT_SOURCE_CHANGE on video node"),
|
||
Err(e) => debug!(
|
||
"V4L2_EVENT_SOURCE_CHANGE subscription unavailable on video node \
|
||
({}), falling back to timeout-based restart",
|
||
e
|
||
),
|
||
}
|
||
}
|
||
|
||
Ok(stream)
|
||
}
|
||
|
||
pub fn resolution(&self) -> Resolution {
|
||
self.resolution
|
||
}
|
||
|
||
pub fn format(&self) -> PixelFormat {
|
||
self.format
|
||
}
|
||
|
||
pub fn stride(&self) -> u32 {
|
||
self.stride
|
||
}
|
||
|
||
/// Re-probe DV timings on the persistent subdev handle (no extra `open`).
|
||
pub fn probe_bridge_signal(&self) -> Option<ProbeResult> {
|
||
let subdev_fd = self.subdev_fd.as_ref()?;
|
||
Some(csi_bridge::probe_signal(
|
||
subdev_fd,
|
||
self.bridge_kind.unwrap_or(CsiBridgeKind::Unknown),
|
||
))
|
||
}
|
||
|
||
/// Like [`Self::probe_bridge_signal`] but isolates the ioctl on a dup'd
|
||
/// fd with a wall-clock cap — see [`csi_bridge::probe_signal_thread_timeout`].
|
||
pub fn probe_bridge_signal_with_timeout(&self, limit: Duration) -> Option<ProbeResult> {
|
||
let subdev_fd = self.subdev_fd.as_ref()?;
|
||
csi_bridge::probe_signal_thread_timeout(
|
||
subdev_fd,
|
||
self.bridge_kind.unwrap_or(CsiBridgeKind::Unknown),
|
||
limit,
|
||
)
|
||
}
|
||
|
||
fn expected_capture_bytes(&self) -> Option<usize> {
|
||
if self.format.is_compressed() {
|
||
return None;
|
||
}
|
||
// Stride is bytesperline; packed formats use stride × height (not × bpp).
|
||
if self.format.bytes_per_pixel().is_some() {
|
||
return (self.stride as usize).checked_mul(self.resolution.height as usize);
|
||
}
|
||
match self.format {
|
||
PixelFormat::Nv12 | PixelFormat::Nv21 | PixelFormat::Yuv420 | PixelFormat::Yvu420 => {
|
||
(self.stride as usize)
|
||
.checked_mul(self.resolution.height as usize)?
|
||
.checked_mul(3)?
|
||
.checked_div(2)
|
||
}
|
||
PixelFormat::Nv16 => (self.stride as usize)
|
||
.checked_mul(self.resolution.height as usize)?
|
||
.checked_mul(2),
|
||
PixelFormat::Nv24 => (self.stride as usize)
|
||
.checked_mul(self.resolution.height as usize)?
|
||
.checked_mul(3),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
pub fn next_into(&mut self, dst: &mut Vec<u8>) -> io::Result<CaptureMeta> {
|
||
self.wait_ready()?;
|
||
|
||
let dqbuf: V4l2Buffer = ioctl::dqbuf(&self.fd, self.queue)
|
||
.map_err(|e| io::Error::other(format!("dqbuf failed: {}", e)))?;
|
||
let index = dqbuf.as_v4l2_buffer().index as usize;
|
||
let sequence = dqbuf.as_v4l2_buffer().sequence as u64;
|
||
|
||
let mut total = 0usize;
|
||
for (plane_idx, plane) in dqbuf.planes_iter().enumerate() {
|
||
let bytes_used = *plane.bytesused as usize;
|
||
let data_offset = plane.data_offset.copied().unwrap_or(0) as usize;
|
||
if bytes_used == 0 {
|
||
continue;
|
||
}
|
||
let mapping = &self.mappings[index][plane_idx];
|
||
let start = data_offset.min(mapping.len());
|
||
let end = (data_offset + bytes_used).min(mapping.len());
|
||
total += end.saturating_sub(start);
|
||
}
|
||
|
||
dst.resize(total, 0);
|
||
let mut cursor = 0usize;
|
||
for (plane_idx, plane) in dqbuf.planes_iter().enumerate() {
|
||
let bytes_used = *plane.bytesused as usize;
|
||
let data_offset = plane.data_offset.copied().unwrap_or(0) as usize;
|
||
if bytes_used == 0 {
|
||
continue;
|
||
}
|
||
let mapping = &self.mappings[index][plane_idx];
|
||
let start = data_offset.min(mapping.len());
|
||
let end = (data_offset + bytes_used).min(mapping.len());
|
||
let len = end.saturating_sub(start);
|
||
if len == 0 {
|
||
continue;
|
||
}
|
||
dst[cursor..cursor + len].copy_from_slice(&mapping[start..end]);
|
||
cursor += len;
|
||
}
|
||
|
||
self.queue_buffer(index as u32)
|
||
.map_err(|e| io::Error::other(e.to_string()))?;
|
||
|
||
if let Some(expected) = self.expected_capture_bytes() {
|
||
if total > 0 && total != expected {
|
||
warn!(
|
||
"DQBUF bytes_used ({}) != expected ({}) for {:?} {}x{} stride={} — requesting stream re-open",
|
||
total,
|
||
expected,
|
||
self.format,
|
||
self.resolution.width,
|
||
self.resolution.height,
|
||
self.stride
|
||
);
|
||
return Err(io::Error::other(SOURCE_CHANGED_MARKER));
|
||
}
|
||
}
|
||
|
||
Ok(CaptureMeta {
|
||
bytes_used: total,
|
||
sequence,
|
||
})
|
||
}
|
||
|
||
fn wait_ready(&self) -> io::Result<()> {
|
||
if self.timeout.is_zero() {
|
||
return Ok(());
|
||
}
|
||
// Multiplex video fd (POLLIN for DQBUF, POLLPRI as fallback for
|
||
// drivers that deliver events here) and the optional subdev fd
|
||
// (POLLPRI only — SOURCE_CHANGE on RK628 / rkcif).
|
||
let mut poll_fds: Vec<PollFd> = Vec::with_capacity(2);
|
||
poll_fds.push(PollFd::new(
|
||
self.fd.as_fd(),
|
||
PollFlags::POLLIN | PollFlags::POLLPRI | PollFlags::POLLERR | PollFlags::POLLHUP,
|
||
));
|
||
if let Some(subdev_fd) = self.subdev_fd.as_ref() {
|
||
poll_fds.push(PollFd::new(subdev_fd.as_fd(), PollFlags::POLLPRI));
|
||
}
|
||
let timeout_ms = self.timeout.as_millis().min(u16::MAX as u128) as u16;
|
||
let ready = poll(&mut poll_fds, PollTimeout::from(timeout_ms))?;
|
||
if ready == 0 {
|
||
return Err(io::Error::new(io::ErrorKind::TimedOut, "capture timeout"));
|
||
}
|
||
|
||
// Subdev POLLPRI fires first on rkcif/RK628 when the source-side
|
||
// HDMI timings changed. Drain all pending events and bubble up
|
||
// the `source_changed` marker so the upper layer re-opens with a
|
||
// fresh DV_TIMINGS probe.
|
||
if let Some(subdev_fd) = self.subdev_fd.as_ref() {
|
||
if let Some(revents) = poll_fds.get(1).and_then(|f| f.revents()) {
|
||
if revents.contains(PollFlags::POLLPRI) {
|
||
let drained = drain_events(subdev_fd);
|
||
info!(
|
||
"Subdev SOURCE_CHANGE detected (drained {} event(s)), \
|
||
requesting stream re-open",
|
||
drained
|
||
);
|
||
return Err(io::Error::other(SOURCE_CHANGED_MARKER));
|
||
}
|
||
}
|
||
}
|
||
|
||
if let Some(revents) = poll_fds[0].revents() {
|
||
if revents.contains(PollFlags::POLLERR) || revents.contains(PollFlags::POLLHUP) {
|
||
debug!(
|
||
"capture poll: video revents={:?} (ERR/HUP) — requesting stream re-open",
|
||
revents
|
||
);
|
||
return Err(io::Error::other(SOURCE_CHANGED_MARKER));
|
||
}
|
||
if revents.contains(PollFlags::POLLPRI) {
|
||
let drained = drain_events(&self.fd);
|
||
info!(
|
||
"Video-node SOURCE_CHANGE detected (drained {} event(s)), \
|
||
requesting stream re-open",
|
||
drained
|
||
);
|
||
return Err(io::Error::other(SOURCE_CHANGED_MARKER));
|
||
}
|
||
if !revents.contains(PollFlags::POLLIN) {
|
||
// rkcif + RK628: the driver may wake `poll` after internally
|
||
// invalidating queued buffers without queueing a V4L2 event.
|
||
// Treat like SOURCE_CHANGE so we STREAMOFF / re-S_FMT.
|
||
debug!(
|
||
"capture poll: ready={} video revents={:?} (no POLLIN) — requesting stream re-open",
|
||
ready, revents
|
||
);
|
||
return Err(io::Error::other(SOURCE_CHANGED_MARKER));
|
||
}
|
||
return Ok(());
|
||
}
|
||
|
||
debug!(
|
||
"capture poll: ready={} but video revents unavailable — requesting stream re-open",
|
||
ready
|
||
);
|
||
Err(io::Error::other(SOURCE_CHANGED_MARKER))
|
||
}
|
||
|
||
fn queue_all_buffers(&mut self) -> Result<()> {
|
||
for index in 0..self.mappings.len() as u32 {
|
||
self.queue_buffer(index)?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn queue_buffer(&mut self, index: u32) -> Result<()> {
|
||
let handle = MmapHandle;
|
||
let planes = self.mappings[index as usize]
|
||
.iter()
|
||
.map(|mapping| {
|
||
let mut plane = QBufPlane::new_from_handle(&handle, 0);
|
||
plane.0.length = mapping.len() as u32;
|
||
plane
|
||
})
|
||
.collect();
|
||
let mut qbuf: QBuffer<MmapHandle> = QBuffer::new(self.queue, index);
|
||
qbuf.planes = planes;
|
||
ioctl::qbuf::<_, ()>(&self.fd, qbuf)
|
||
.map_err(|e| AppError::VideoError(format!("Failed to queue buffer: {}", e)))?;
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl Drop for CaptureStream {
|
||
fn drop(&mut self) {
|
||
// Release ordering matters on rkcif: a subsequent open()/S_FMT from a
|
||
// freshly-constructed stream returns EBUSY if the previous capture has
|
||
// not fully relinquished its buffers. Mirror the ustreamer teardown
|
||
// order:
|
||
// 1. STREAMOFF (stop DMA)
|
||
// 2. unsubscribe_all (no further DQEVENT paths)
|
||
// 3. munmap via Drop (release buffer mappings)
|
||
// 4. REQBUFS count=0 (free kernel buffer list)
|
||
// 5. close(fd) (implicit on File Drop)
|
||
if let Err(e) = ioctl::streamoff(&self.fd, self.queue) {
|
||
debug!("Failed to stop capture stream: {}", e);
|
||
}
|
||
if let Err(e) = ioctl::unsubscribe_all_events(&self.fd) {
|
||
debug!("Failed to unsubscribe V4L2 events: {}", e);
|
||
}
|
||
// Explicit munmap *before* REQBUFS(0) — the kernel refuses to free the
|
||
// buffer list while mappings are outstanding.
|
||
self.mappings.clear();
|
||
if let Err(e) = ioctl::reqbufs::<v4l2_requestbuffers>(
|
||
&self.fd,
|
||
self.queue,
|
||
MemoryType::Mmap,
|
||
0,
|
||
MemoryConsistency::empty(),
|
||
) {
|
||
debug!("Failed to release capture buffers: {}", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Driver-name check for CSI/HDMI bridge devices (rk_hdmirx, rkcif, tc358743,
|
||
/// …) that expose DV timings. Kept in sync with `video::device::is_csi_hdmi_bridge`
|
||
/// but queries the raw V4L2 driver string so we don't need a full
|
||
/// `VideoDeviceInfo` at `CaptureStream::open` time.
|
||
fn is_csi_bridge_driver(driver: &str) -> bool {
|
||
let d = driver.to_ascii_lowercase();
|
||
d == "rk_hdmirx" || d == "rkcif" || d == "tc358743" || d.starts_with("rkcif")
|
||
}
|
||
|
||
/// Drain any pending `V4L2_EVENT_*` events on `fd`. Used after POLLPRI to
|
||
/// clear the queue so the next poll doesn't immediately wake up on stale
|
||
/// state. Capped at 16 events per call.
|
||
fn drain_events(fd: &File) -> u32 {
|
||
let mut drained = 0u32;
|
||
while let Ok(_ev) = ioctl::dqevent::<V4l2Event>(fd) {
|
||
drained = drained.saturating_add(1);
|
||
if drained >= 16 {
|
||
break;
|
||
}
|
||
}
|
||
drained
|
||
}
|
||
|
||
/// Result of a successful `VIDIOC_QUERY_DV_TIMINGS` + `VIDIOC_S_DV_TIMINGS`
|
||
/// probe. Used by the CSI bridge path to override the requested resolution
|
||
/// with the source-reported geometry before `S_FMT`.
|
||
#[derive(Debug, Clone, Copy)]
|
||
struct DvTimingsMode {
|
||
width: u32,
|
||
height: u32,
|
||
#[allow(dead_code)]
|
||
fps: Option<f64>,
|
||
}
|
||
|
||
/// Probe DV timings from the source and latch them into the driver.
|
||
///
|
||
/// Mirrors PiKVM/ustreamer's `src_hdmi_open_sequence`:
|
||
/// 1. `VIDIOC_QUERY_DV_TIMINGS` — active-probe the source.
|
||
/// 2. On success, `VIDIOC_S_DV_TIMINGS` — commit so that subsequent
|
||
/// `S_FMT` is accepted at the matching geometry.
|
||
/// 3. Return the timings for the caller to feed into `S_FMT`.
|
||
///
|
||
/// Errno mapping (see `V4L2_CID_DV_RX_POWER_PRESENT` semantics):
|
||
/// * `ENOLINK` → `NoCable` (TMDS clock absent, cable unplugged)
|
||
/// * `ENOLCK` → `NoSync` (TMDS present, timings unstable)
|
||
/// * `ERANGE` → `OutOfRange` (timings outside hardware caps)
|
||
/// * `ENODATA` → `NoSignal` (driver says "no DV timings support on
|
||
/// this input", e.g. EDID not applied yet)
|
||
/// * anything else → `NoSignal` (fallback, keeps the retry loop going)
|
||
fn probe_and_apply_dv_timings(fd: &File) -> Result<DvTimingsMode> {
|
||
let timings: v4l2_dv_timings = match ioctl::query_dv_timings(fd) {
|
||
Ok(t) => t,
|
||
Err(err) => {
|
||
let status = match &err {
|
||
QueryDvTimingsError::NoLink => SignalStatus::NoCable,
|
||
QueryDvTimingsError::UnstableSignal => SignalStatus::NoSync,
|
||
QueryDvTimingsError::IoctlError(Errno::ERANGE) => SignalStatus::OutOfRange,
|
||
QueryDvTimingsError::Unsupported => SignalStatus::NoSignal,
|
||
// I2C-layer failures between rkcif and the RK628 bridge
|
||
// (`ret=-110`/-121/-5) typically mean the bridge is in the
|
||
// middle of a PHY re-lock, not that the source is gone.
|
||
// Classify them as `NoSync` so the upper layer keeps retrying
|
||
// on the short end of the back-off ladder.
|
||
QueryDvTimingsError::IoctlError(Errno::EIO)
|
||
| QueryDvTimingsError::IoctlError(Errno::EREMOTEIO)
|
||
| QueryDvTimingsError::IoctlError(Errno::ETIMEDOUT) => SignalStatus::NoSync,
|
||
QueryDvTimingsError::IoctlError(_) => SignalStatus::NoSignal,
|
||
};
|
||
info!(
|
||
"VIDIOC_QUERY_DV_TIMINGS failed: {} -> SignalStatus::{:?}",
|
||
err, status
|
||
);
|
||
return Err(AppError::CaptureNoSignal {
|
||
kind: status.as_str().to_string(),
|
||
});
|
||
}
|
||
};
|
||
|
||
// `v4l2_dv_timings` is a packed union; copy the scalar fields out to
|
||
// aligned locals before formatting / comparing to avoid UB (and the
|
||
// rustc E0793 "reference to field of packed struct is unaligned" error).
|
||
let timings_type: u32 = timings.type_;
|
||
if timings_type != V4L2_DV_BT_656_1120 {
|
||
warn!(
|
||
"QUERY_DV_TIMINGS returned unknown type {}, treating as NoSignal",
|
||
timings_type
|
||
);
|
||
return Err(AppError::CaptureNoSignal {
|
||
kind: SignalStatus::NoSignal.as_str().to_string(),
|
||
});
|
||
}
|
||
|
||
let bt = unsafe { timings.__bindgen_anon_1.bt };
|
||
let bt_width: u32 = bt.width;
|
||
let bt_height: u32 = bt.height;
|
||
let bt_pixelclock: u64 = bt.pixelclock;
|
||
let bt_hfrontporch: u32 = bt.hfrontporch;
|
||
let bt_hsync: u32 = bt.hsync;
|
||
let bt_hbackporch: u32 = bt.hbackporch;
|
||
let bt_vfrontporch: u32 = bt.vfrontporch;
|
||
let bt_vsync: u32 = bt.vsync;
|
||
let bt_vbackporch: u32 = bt.vbackporch;
|
||
|
||
if bt_width == 0 || bt_height == 0 || bt_width <= 64 || bt_height <= 64 {
|
||
warn!(
|
||
"QUERY_DV_TIMINGS returned degenerate {}x{}, treating as NoSignal",
|
||
bt_width, bt_height
|
||
);
|
||
return Err(AppError::CaptureNoSignal {
|
||
kind: SignalStatus::NoSignal.as_str().to_string(),
|
||
});
|
||
}
|
||
|
||
// Latch the detected timings so subsequent S_FMT / STREAMON use the
|
||
// right pixel clock + blanking. Failure here is *not* fatal on some
|
||
// drivers (rkcif doesn't implement S_DV_TIMINGS per-output-device, only
|
||
// on the bridging subdev), so degrade to a warning and keep going.
|
||
if let Err(e) = ioctl::s_dv_timings::<_, v4l2_dv_timings>(fd, timings) {
|
||
debug!(
|
||
"VIDIOC_S_DV_TIMINGS failed ({}), continuing with queried timings for S_FMT",
|
||
e
|
||
);
|
||
}
|
||
|
||
let fps = dv_timings_fps_from_scalars(
|
||
bt_width,
|
||
bt_height,
|
||
bt_hfrontporch + bt_hsync + bt_hbackporch,
|
||
bt_vfrontporch + bt_vsync + bt_vbackporch,
|
||
bt_pixelclock,
|
||
);
|
||
info!(
|
||
"DV timings locked: {}x{} @ {} (pix_clk={})",
|
||
bt_width,
|
||
bt_height,
|
||
fps.map(|f| format!("{:.2} fps", f))
|
||
.unwrap_or_else(|| "?fps".to_string()),
|
||
bt_pixelclock
|
||
);
|
||
|
||
Ok(DvTimingsMode {
|
||
width: bt_width,
|
||
height: bt_height,
|
||
fps,
|
||
})
|
||
}
|
||
|
||
fn dv_timings_fps_from_scalars(
|
||
width: u32,
|
||
height: u32,
|
||
h_blanking: u32,
|
||
v_blanking: u32,
|
||
pixelclock: u64,
|
||
) -> Option<f64> {
|
||
let total_h = (width + h_blanking) as u64;
|
||
let total_v = (height + v_blanking) as u64;
|
||
let denom = total_h.checked_mul(total_v)?;
|
||
if denom == 0 || pixelclock == 0 {
|
||
return None;
|
||
}
|
||
Some(pixelclock as f64 / denom as f64)
|
||
}
|
||
|
||
fn set_fps(fd: &File, queue: QueueType, fps: u32) -> std::result::Result<(), ioctl::GParmError> {
|
||
let mut params = unsafe { std::mem::zeroed::<v4l2_streamparm>() };
|
||
params.type_ = queue as u32;
|
||
params.parm = v4l2_streamparm__bindgen_ty_1 {
|
||
capture: v4l2r::bindings::v4l2_captureparm {
|
||
timeperframe: v4l2r::bindings::v4l2_fract {
|
||
numerator: 1,
|
||
denominator: fps,
|
||
},
|
||
..unsafe { std::mem::zeroed() }
|
||
},
|
||
};
|
||
|
||
let _actual: v4l2_streamparm = ioctl::s_parm(fd, params)?;
|
||
Ok(())
|
||
}
|