mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 08:20:49 +08:00
fix: 完善 RK3588 HDMI RX 信号检测与自动恢复
- 统一使用 QUERY_DV_TIMINGS 判断 HDMI RX 输入状态 - 增加 source-following 设备状态 API 与前端只读展示 - 支持长时间无信号后的 MJPEG/WebRTC 自动恢复 - 修复模式切换时采集设备尚未释放导致的 EBUSY - 取消陈旧 WebRTC 重连并统一采集恢复策略 - 移除视频输入状态区域的冗余标题
This commit is contained in:
@@ -13,8 +13,8 @@ use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use super::device::{
|
||||
bridge as csi_bridge, enumerate_devices, find_best_device, is_csi_hdmi_bridge,
|
||||
parse_bridge_kind, select_recovery_device, VideoDevice, VideoDeviceInfo,
|
||||
VideoDeviceRecoveryHint,
|
||||
parse_bridge_kind, resolve_video_input_config, select_recovery_device, VideoControlMode,
|
||||
VideoDevice, VideoDeviceInfo, VideoDeviceRecoveryHint,
|
||||
};
|
||||
use super::format::{PixelFormat, Resolution};
|
||||
use super::frame::{FrameBuffer, FrameBufferPool, VideoFrame};
|
||||
@@ -30,6 +30,7 @@ use crate::video::capture::status::{
|
||||
use crate::video::capture::{
|
||||
is_source_changed_error, BridgeContext, CaptureStream, DEFAULT_CAPTURE_BUFFER_COUNT,
|
||||
};
|
||||
use crate::video::recovery::{wait_for_source_change, CaptureRecoveryPolicy};
|
||||
|
||||
const MIN_CAPTURE_FRAME_SIZE: usize = 128;
|
||||
|
||||
@@ -373,7 +374,10 @@ impl Streamer {
|
||||
.ok_or_else(|| AppError::VideoError("Video device not found".to_string()))?
|
||||
};
|
||||
|
||||
let (format, resolution) = self.resolve_capture_config(&device, format, resolution)?;
|
||||
let resolved = self.resolve_capture_config(&device, format, resolution, fps)?;
|
||||
let format = resolved.format;
|
||||
let resolution = resolved.resolution;
|
||||
let fps = resolved.fps;
|
||||
|
||||
// IMPORTANT: Disconnect all MJPEG clients FIRST before stopping capture
|
||||
// This prevents race conditions where clients try to reconnect and reopen the device
|
||||
@@ -442,19 +446,18 @@ impl Streamer {
|
||||
device.path.display()
|
||||
);
|
||||
|
||||
// Determine best format for this device
|
||||
let config = self.config.read().await;
|
||||
let format = self.select_format(&device, config.format)?;
|
||||
let resolution = self.select_resolution(&device, &format, config.resolution)?;
|
||||
|
||||
let resolved =
|
||||
self.resolve_capture_config(&device, config.format, config.resolution, config.fps)?;
|
||||
drop(config);
|
||||
|
||||
// Update config with actual values
|
||||
{
|
||||
let mut config = self.config.write().await;
|
||||
config.device_path = Some(device.path.clone());
|
||||
config.format = format;
|
||||
config.resolution = resolution;
|
||||
config.format = resolved.format;
|
||||
config.resolution = resolved.resolution;
|
||||
config.fps = resolved.fps;
|
||||
}
|
||||
|
||||
// Store device info
|
||||
@@ -462,7 +465,10 @@ impl Streamer {
|
||||
|
||||
*self.state.write().await = StreamerState::Ready;
|
||||
|
||||
info!("Streamer initialized: {} @ {}", format, resolution);
|
||||
info!(
|
||||
"Streamer initialized: {} @ {} {} fps",
|
||||
resolved.format, resolved.resolution, resolved.fps
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -574,10 +580,20 @@ impl Streamer {
|
||||
device: &VideoDeviceInfo,
|
||||
requested_format: PixelFormat,
|
||||
requested_resolution: Resolution,
|
||||
) -> Result<(PixelFormat, Resolution)> {
|
||||
let format = self.select_format(device, requested_format)?;
|
||||
let resolution = self.select_resolution(device, &format, requested_resolution)?;
|
||||
Ok((format, resolution))
|
||||
requested_fps: u32,
|
||||
) -> Result<super::device::ResolvedVideoInputConfig> {
|
||||
let mut resolved = resolve_video_input_config(
|
||||
device,
|
||||
requested_format,
|
||||
requested_resolution,
|
||||
requested_fps,
|
||||
);
|
||||
if device.control_mode == VideoControlMode::Configurable {
|
||||
resolved.format = self.select_format(device, resolved.format)?;
|
||||
resolved.resolution =
|
||||
self.select_resolution(device, &resolved.format, resolved.resolution)?;
|
||||
}
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
/// Restart capture for recovery (direct capture path)
|
||||
@@ -783,26 +799,9 @@ impl Streamer {
|
||||
const RETRY_DELAY_MS: u64 = 200;
|
||||
const IDLE_STOP_DELAY_SECS: u64 = 5;
|
||||
const BUFFER_COUNT: u32 = DEFAULT_CAPTURE_BUFFER_COUNT;
|
||||
/// Initial back-off after signal loss before the first soft restart.
|
||||
///
|
||||
/// PiKVM/ustreamer drops to sub-second recovery because it subscribes to
|
||||
/// `V4L2_EVENT_SOURCE_CHANGE`; lacking that (for now), we bound how long
|
||||
/// the user has to stare at a placeholder after a source-side resolution
|
||||
/// change by driving a soft-restart at 1 s, then 2 s, 4 s, …, 8 s.
|
||||
const NOSIGNAL_SOFT_RESTART_INITIAL_SECS: u64 = 1;
|
||||
const NOSIGNAL_SOFT_RESTART_MAX_SECS: u64 = 8;
|
||||
|
||||
let handle = tokio::runtime::Handle::current();
|
||||
let mut last_state = StreamerState::Streaming;
|
||||
|
||||
// Compute the current soft-restart back-off window (in seconds)
|
||||
// for the exponential ladder 1 s → 2 s → 4 s → 8 s (capped).
|
||||
let backoff_secs = |count: u32| -> u64 {
|
||||
NOSIGNAL_SOFT_RESTART_INITIAL_SECS
|
||||
.saturating_mul(2u64.pow(count.min(3)))
|
||||
.min(NOSIGNAL_SOFT_RESTART_MAX_SECS)
|
||||
};
|
||||
|
||||
let mut set_state = |new_state: StreamerState| {
|
||||
if new_state != last_state {
|
||||
handle.block_on(async {
|
||||
@@ -829,20 +828,23 @@ impl Streamer {
|
||||
self.next_retry_ms.store(ms, Ordering::Relaxed);
|
||||
};
|
||||
|
||||
// How many soft-restart cycles have been attempted (for exponential back-off).
|
||||
// Consecutive recovery attempts, shared with the common retry policy.
|
||||
let mut no_signal_restart_count: u32 = 0;
|
||||
|
||||
// Last (resolution, format, fps) combination for which we emitted a
|
||||
// `StreamConfigApplied` event. Used to de-duplicate the event across
|
||||
// soft-restarts that produce the exact same geometry (e.g. a spurious
|
||||
// single-frame timeout on a stable source) — the frontend would
|
||||
// otherwise re-layout the `<img>` on every glitch.
|
||||
let mut last_applied: Option<(u32, u32, PixelFormat, u32)> = None;
|
||||
let mut no_consumers_since: Option<std::time::Instant> = None;
|
||||
|
||||
'session: loop {
|
||||
if self.direct_stop.load(Ordering::Relaxed) {
|
||||
break 'session;
|
||||
}
|
||||
if self.mjpeg_handler.client_count() == 0 {
|
||||
let since = no_consumers_since.get_or_insert_with(std::time::Instant::now);
|
||||
if since.elapsed() >= Duration::from_secs(IDLE_STOP_DELAY_SECS) {
|
||||
info!("No MJPEG consumers during recovery; stopping capture");
|
||||
break 'session;
|
||||
}
|
||||
} else {
|
||||
no_consumers_since = None;
|
||||
}
|
||||
|
||||
// Re-read config at the start of each session so that a re_init_device()
|
||||
// call (from a previous soft-restart or recovery) is reflected here.
|
||||
@@ -855,52 +857,44 @@ impl Streamer {
|
||||
// `VideoDeviceInfo` during enumeration; we re-read it here
|
||||
// rather than caching on Streamer so a hot-plug recovery picks
|
||||
// up a possibly-different subdev path.
|
||||
let bridge_ctx = handle.block_on(async {
|
||||
let (bridge_ctx, control_mode) = handle.block_on(async {
|
||||
self.current_device
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.map(|info| {
|
||||
BridgeContext::from_parts(
|
||||
info.subdev_path.clone(),
|
||||
parse_bridge_kind(info.bridge_kind.as_deref()),
|
||||
(
|
||||
BridgeContext::from_parts(
|
||||
info.subdev_path.clone(),
|
||||
parse_bridge_kind(info.bridge_kind.as_deref()),
|
||||
),
|
||||
info.control_mode,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.unwrap_or((BridgeContext::default(), VideoControlMode::Configurable))
|
||||
});
|
||||
let recovery_policy = CaptureRecoveryPolicy::new(control_mode);
|
||||
|
||||
// ── STREAMON gate: for CSI bridges with a subdev, refuse to
|
||||
// open the video node when the subdev reports no signal.
|
||||
// On RK628 this prevents a kernel null-pointer deref.
|
||||
if let Some(subdev_path) = bridge_ctx.subdev_path.as_ref() {
|
||||
match probe_subdev_signal(subdev_path, bridge_ctx.kind) {
|
||||
Some(crate::video::signal::SignalStatus::NoCable)
|
||||
| Some(crate::video::signal::SignalStatus::NoSync)
|
||||
| Some(crate::video::signal::SignalStatus::NoSignal)
|
||||
| Some(crate::video::signal::SignalStatus::OutOfRange) => {
|
||||
let status = probe_subdev_signal(subdev_path, bridge_ctx.kind)
|
||||
.unwrap_or(crate::video::signal::SignalStatus::NoSignal);
|
||||
let wait_secs = backoff_secs(no_signal_restart_count);
|
||||
debug!(
|
||||
"Pre-STREAMON gate: subdev {:?} reports {:?} — \
|
||||
waiting for SOURCE_CHANGE (<= {}s) before opening {:?}",
|
||||
subdev_path, status, wait_secs, device_path
|
||||
);
|
||||
set_retry(wait_secs.saturating_mul(1000));
|
||||
go_offline();
|
||||
set_state(status.into());
|
||||
// Wait for SOURCE_CHANGE or timeout before retrying.
|
||||
// Opens the subdev just for the poll — cheap and
|
||||
// does NOT touch the video node.
|
||||
wait_subdev_for_source_change(
|
||||
subdev_path,
|
||||
&self.direct_stop,
|
||||
Duration::from_secs(wait_secs),
|
||||
);
|
||||
no_signal_restart_count = no_signal_restart_count.saturating_add(1);
|
||||
continue 'session;
|
||||
}
|
||||
_ => {} // Locked (None from as_status) or unknown — proceed
|
||||
if let Some(status) = probe_subdev_signal(subdev_path, bridge_ctx.kind) {
|
||||
let delay =
|
||||
recovery_policy.retry_delay(no_signal_restart_count.saturating_add(1));
|
||||
debug!(
|
||||
"Pre-STREAMON gate: subdev {:?} reports {:?} — \
|
||||
waiting for SOURCE_CHANGE (<= {:?}) before opening {:?}",
|
||||
subdev_path, status, delay, device_path
|
||||
);
|
||||
set_retry(delay.as_millis() as u64);
|
||||
go_offline();
|
||||
set_state(status.into());
|
||||
wait_for_source_change(&bridge_ctx, delay, || {
|
||||
!self.direct_stop.load(Ordering::Relaxed)
|
||||
});
|
||||
no_signal_restart_count = no_signal_restart_count.saturating_add(1);
|
||||
continue 'session;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -922,6 +916,7 @@ impl Streamer {
|
||||
BUFFER_COUNT,
|
||||
Duration::from_secs(2),
|
||||
bridge_ctx.clone(),
|
||||
control_mode,
|
||||
) {
|
||||
Ok(stream) => {
|
||||
stream_opt = Some(stream);
|
||||
@@ -938,7 +933,9 @@ impl Streamer {
|
||||
"CSI open probe reports no signal ({:?}), will soft-restart",
|
||||
status
|
||||
);
|
||||
set_retry(backoff_secs(no_signal_restart_count).saturating_mul(1000));
|
||||
let delay =
|
||||
recovery_policy.retry_delay(no_signal_restart_count.saturating_add(1));
|
||||
set_retry(delay.as_millis() as u64);
|
||||
go_offline();
|
||||
set_state(status.into());
|
||||
last_error = Some(format!("CaptureNoSignal({})", kind));
|
||||
@@ -987,9 +984,16 @@ impl Streamer {
|
||||
}
|
||||
|
||||
debug!("Open failed in NoSignal-like state, backing off before soft-restart");
|
||||
let wait = backoff_secs(no_signal_restart_count);
|
||||
set_retry(wait.saturating_mul(1000));
|
||||
std::thread::sleep(Duration::from_secs(wait));
|
||||
if !recovery_policy.should_retry(no_signal_restart_count.saturating_add(1)) {
|
||||
set_state(StreamerState::Error);
|
||||
break 'session;
|
||||
}
|
||||
let delay =
|
||||
recovery_policy.retry_delay(no_signal_restart_count.saturating_add(1));
|
||||
set_retry(delay.as_millis() as u64);
|
||||
wait_for_source_change(&bridge_ctx, delay, || {
|
||||
!self.direct_stop.load(Ordering::Relaxed)
|
||||
});
|
||||
no_signal_restart_count = no_signal_restart_count.saturating_add(1);
|
||||
continue 'session;
|
||||
}
|
||||
@@ -997,8 +1001,21 @@ impl Streamer {
|
||||
|
||||
let resolution = stream.resolution();
|
||||
let pixel_format = stream.format();
|
||||
let source_fps = stream
|
||||
.source_fps()
|
||||
.map(|fps| fps.round().clamp(1.0, 120.0) as u32)
|
||||
.unwrap_or(config.fps);
|
||||
let stride = stream.stride();
|
||||
|
||||
if control_mode == VideoControlMode::SourceFollowing {
|
||||
handle.block_on(async {
|
||||
let mut current = self.config.write().await;
|
||||
current.resolution = resolution;
|
||||
current.format = pixel_format;
|
||||
current.fps = source_fps;
|
||||
});
|
||||
}
|
||||
|
||||
info!(
|
||||
"Capture format: {}x{} {:?} stride={}",
|
||||
resolution.width, resolution.height, pixel_format, stride
|
||||
@@ -1053,7 +1070,9 @@ impl Streamer {
|
||||
Err(e) => {
|
||||
if is_source_changed_error(&e) {
|
||||
info!("Capture SOURCE_CHANGE — soft-restart for DV re-probe");
|
||||
set_retry(backoff_secs(no_signal_restart_count).saturating_mul(1000));
|
||||
let delay = recovery_policy
|
||||
.retry_delay(no_signal_restart_count.saturating_add(1));
|
||||
set_retry(delay.as_millis() as u64);
|
||||
go_offline();
|
||||
set_state(StreamerState::NoSignal);
|
||||
need_soft_restart = true;
|
||||
@@ -1062,8 +1081,9 @@ impl Streamer {
|
||||
if e.kind() == std::io::ErrorKind::TimedOut {
|
||||
if signal_present {
|
||||
signal_present = false;
|
||||
let wait = backoff_secs(no_signal_restart_count);
|
||||
set_retry(wait.saturating_mul(1000));
|
||||
let delay = recovery_policy
|
||||
.retry_delay(no_signal_restart_count.saturating_add(1));
|
||||
set_retry(delay.as_millis() as u64);
|
||||
go_offline();
|
||||
set_state(StreamerState::NoSignal);
|
||||
no_signal_since = Some(std::time::Instant::now());
|
||||
@@ -1071,11 +1091,12 @@ impl Streamer {
|
||||
fps_frame_count = 0;
|
||||
last_fps_time = std::time::Instant::now();
|
||||
} else if let Some(since) = no_signal_since {
|
||||
let wait = backoff_secs(no_signal_restart_count);
|
||||
if since.elapsed().as_secs() >= wait {
|
||||
let delay = recovery_policy
|
||||
.retry_delay(no_signal_restart_count.saturating_add(1));
|
||||
if since.elapsed() >= delay {
|
||||
info!(
|
||||
"NoSignal for {}s, attempting soft restart (attempt {})",
|
||||
wait,
|
||||
"NoSignal for {:?}, attempting soft restart (attempt {})",
|
||||
delay,
|
||||
no_signal_restart_count + 1
|
||||
);
|
||||
need_soft_restart = true;
|
||||
@@ -1122,12 +1143,7 @@ impl Streamer {
|
||||
"Capture transient error (EPROTO/-71, often UVC USB): {}",
|
||||
e
|
||||
);
|
||||
let is_uvc = handle.block_on(async {
|
||||
self.current_device.read().await.as_ref().is_some_and(|d| {
|
||||
d.driver.eq_ignore_ascii_case("uvcvideo")
|
||||
})
|
||||
});
|
||||
if is_uvc {
|
||||
if control_mode == VideoControlMode::Configurable {
|
||||
go_offline();
|
||||
set_state(StreamerState::UvcUsbError);
|
||||
need_soft_restart = true;
|
||||
@@ -1139,9 +1155,9 @@ impl Streamer {
|
||||
e
|
||||
);
|
||||
}
|
||||
set_retry(
|
||||
backoff_secs(no_signal_restart_count).saturating_mul(1000),
|
||||
);
|
||||
let delay = recovery_policy
|
||||
.retry_delay(no_signal_restart_count.saturating_add(1));
|
||||
set_retry(delay.as_millis() as u64);
|
||||
go_offline();
|
||||
set_state(StreamerState::NoSignal);
|
||||
need_soft_restart = true;
|
||||
@@ -1192,25 +1208,28 @@ impl Streamer {
|
||||
self.mjpeg_handler.set_online();
|
||||
set_state(StreamerState::Streaming);
|
||||
|
||||
let fps_val = config.fps;
|
||||
let current = (resolution.width, resolution.height, pixel_format, fps_val);
|
||||
if last_applied != Some(current) {
|
||||
last_applied = Some(current);
|
||||
let dp = device_path.display().to_string();
|
||||
let fmt = format!("{:?}", pixel_format);
|
||||
let w = resolution.width;
|
||||
let h = resolution.height;
|
||||
handle.block_on(async {
|
||||
self.publish_event(SystemEvent::StreamConfigApplied {
|
||||
transition_id: None,
|
||||
device: dp,
|
||||
resolution: (w, h),
|
||||
format: fmt,
|
||||
fps: fps_val,
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
let fps_val = source_fps;
|
||||
let recovered_device = device_path.display().to_string();
|
||||
handle.block_on(async {
|
||||
self.publish_event(SystemEvent::StreamRecovered {
|
||||
device: recovered_device,
|
||||
})
|
||||
.await;
|
||||
});
|
||||
let dp = device_path.display().to_string();
|
||||
let fmt = pixel_format.to_string();
|
||||
let w = resolution.width;
|
||||
let h = resolution.height;
|
||||
handle.block_on(async {
|
||||
self.publish_event(SystemEvent::StreamConfigApplied {
|
||||
transition_id: None,
|
||||
device: dp,
|
||||
resolution: (w, h),
|
||||
format: fmt,
|
||||
fps: fps_val,
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
self.mjpeg_handler.update_frame(frame);
|
||||
@@ -1240,65 +1259,8 @@ impl Streamer {
|
||||
}
|
||||
|
||||
no_signal_restart_count = no_signal_restart_count.saturating_add(1);
|
||||
|
||||
match VideoDevice::open_readonly(&device_path).and_then(|d| d.info()) {
|
||||
Ok(device_info) => {
|
||||
// Skip re-open while rkcif still reports placeholder (≤64²) geometry.
|
||||
let probed_res = device_info
|
||||
.formats
|
||||
.first()
|
||||
.and_then(|f| f.resolutions.first())
|
||||
.map(|r| (r.width, r.height));
|
||||
|
||||
if matches!(probed_res, Some((w, h)) if w <= 64 || h <= 64)
|
||||
|| probed_res.is_none()
|
||||
{
|
||||
warn!(
|
||||
"Soft restart: probed resolution too small ({:?}), still no signal",
|
||||
probed_res
|
||||
);
|
||||
set_retry(2_000);
|
||||
go_offline();
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
continue 'session;
|
||||
}
|
||||
|
||||
handle.block_on(async {
|
||||
let fmt;
|
||||
let res;
|
||||
{
|
||||
let cfg = self.config.read().await;
|
||||
fmt = self
|
||||
.select_format(&device_info, cfg.format)
|
||||
.unwrap_or(cfg.format);
|
||||
res = self
|
||||
.select_resolution(&device_info, &fmt, cfg.resolution)
|
||||
.unwrap_or(cfg.resolution);
|
||||
}
|
||||
{
|
||||
let mut cfg = self.config.write().await;
|
||||
cfg.format = fmt;
|
||||
cfg.resolution = res;
|
||||
}
|
||||
*self.current_device.write().await = Some(device_info);
|
||||
info!(
|
||||
"Soft restart: re-probed device → {}x{} {:?}",
|
||||
res.width, res.height, fmt
|
||||
);
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Soft restart: failed to re-probe device: {}", e);
|
||||
// Brief wait before retrying to avoid spinning.
|
||||
let wait = 2u64.pow(no_signal_restart_count.min(3));
|
||||
std::thread::sleep(Duration::from_secs(wait));
|
||||
}
|
||||
}
|
||||
|
||||
// Reset no_signal_since so the back-off timer is fresh for the new session.
|
||||
// no_signal_since will be re-set if the new session immediately times out.
|
||||
|
||||
// Continue 'session → re-open CaptureStream with updated config.
|
||||
// Continue 'session: the single open path performs QUERY_DV_TIMINGS,
|
||||
// applies the source mode, and owns the retry delay.
|
||||
} // 'session
|
||||
|
||||
self.direct_active.store(false, Ordering::SeqCst);
|
||||
@@ -1316,28 +1278,28 @@ impl Streamer {
|
||||
.map_err(|e| AppError::VideoError(format!("Cannot open device for re-init: {}", e)))?;
|
||||
let device_info = device.info()?;
|
||||
|
||||
let (format, resolution) = {
|
||||
let resolved = {
|
||||
let config = self.config.read().await;
|
||||
let fmt = self
|
||||
.select_format(&device_info, config.format)
|
||||
.unwrap_or(config.format);
|
||||
let res = self
|
||||
.select_resolution(&device_info, &fmt, config.resolution)
|
||||
.unwrap_or(config.resolution);
|
||||
(fmt, res)
|
||||
self.resolve_capture_config(&device_info, config.format, config.resolution, config.fps)
|
||||
.unwrap_or(super::device::ResolvedVideoInputConfig {
|
||||
format: config.format,
|
||||
resolution: config.resolution,
|
||||
fps: config.fps,
|
||||
})
|
||||
};
|
||||
|
||||
{
|
||||
let mut cfg = self.config.write().await;
|
||||
cfg.device_path = Some(device_info.path.clone());
|
||||
cfg.format = format;
|
||||
cfg.resolution = resolution;
|
||||
cfg.format = resolved.format;
|
||||
cfg.resolution = resolved.resolution;
|
||||
cfg.fps = resolved.fps;
|
||||
}
|
||||
*self.current_device.write().await = Some(device_info);
|
||||
|
||||
info!(
|
||||
"Device re-initialized: {}x{} {:?}",
|
||||
resolution.width, resolution.height, format
|
||||
resolved.resolution.width, resolved.resolution.height, resolved.format
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1619,50 +1581,6 @@ fn probe_subdev_signal(
|
||||
probe.as_status()
|
||||
}
|
||||
|
||||
fn wait_subdev_for_source_change(
|
||||
subdev_path: &std::path::Path,
|
||||
direct_stop: &AtomicBool,
|
||||
max_wait: Duration,
|
||||
) {
|
||||
let fd = match csi_bridge::open_subdev(subdev_path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"wait_subdev_for_source_change: failed to open {:?}: {}",
|
||||
subdev_path, e
|
||||
);
|
||||
std::thread::sleep(max_wait.min(Duration::from_secs(1)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = csi_bridge::subscribe_source_change(&fd) {
|
||||
debug!(
|
||||
"wait_subdev_for_source_change: subscribe failed on {:?}: {}",
|
||||
subdev_path, e
|
||||
);
|
||||
}
|
||||
let slice = Duration::from_millis(250);
|
||||
let deadline = std::time::Instant::now() + max_wait;
|
||||
while std::time::Instant::now() < deadline {
|
||||
if direct_stop.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let wait = remaining.min(slice);
|
||||
match csi_bridge::wait_source_change(&fd, wait) {
|
||||
Ok(true) => {
|
||||
info!("Subdev SOURCE_CHANGE during no-signal wait, retrying open immediately");
|
||||
return;
|
||||
}
|
||||
Ok(false) => continue,
|
||||
Err(e) => {
|
||||
debug!("wait_source_change error on {:?}: {}", subdev_path, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for StreamerState {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
|
||||
Reference in New Issue
Block a user