mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 00:10:50 +08:00
fix: 完善 RK3588 HDMI RX 信号检测与自动恢复
- 统一使用 QUERY_DV_TIMINGS 判断 HDMI RX 输入状态 - 增加 source-following 设备状态 API 与前端只读展示 - 支持长时间无信号后的 MJPEG/WebRTC 自动恢复 - 修复模式切换时采集设备尚未释放导致的 EBUSY - 取消陈旧 WebRTC 重连并统一采集恢复策略 - 移除视频输入状态区域的冗余标题
This commit is contained in:
@@ -32,10 +32,7 @@ use super::encoder_state::{build_encoder_state, EncoderThreadState};
|
||||
const AUTO_STOP_GRACE_PERIOD_SECS: u64 = 3;
|
||||
/// After this many consecutive timeouts, log a prominent warning.
|
||||
const CAPTURE_TIMEOUT_RESTART_THRESHOLD: u32 = 5;
|
||||
const CAPTURE_TIMEOUT_STOP_THRESHOLD: u32 = 60;
|
||||
const CAPTURE_TIMEOUT_SOFT_RESTART_THRESHOLD: u32 = 3;
|
||||
const CSI_BRIDGE_NOSIGNAL_INTERVAL_MS: u64 = 500;
|
||||
const NOSIGNAL_POLL_MAX: Duration = Duration::from_secs(20);
|
||||
/// Throttle repeated encoding errors to avoid log flooding
|
||||
const ENCODE_ERROR_THROTTLE_SECS: u64 = 5;
|
||||
|
||||
@@ -53,10 +50,11 @@ use crate::video::capture::status::{
|
||||
use crate::video::capture::{is_source_changed_error, BridgeContext, CaptureStream};
|
||||
use crate::video::codec::h264_bitstream;
|
||||
use crate::video::codec::registry::{EncoderBackend, VideoEncoderType};
|
||||
use crate::video::device::bridge::{self as csi_bridge, ProbeResult};
|
||||
use crate::video::device::parse_bridge_kind;
|
||||
use crate::video::device::VideoControlMode;
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
use crate::video::frame::{FrameBuffer, FrameBufferPool, VideoFrame};
|
||||
use crate::video::recovery::{wait_for_source_change, CaptureRecoveryPolicy};
|
||||
use crate::video::signal::SignalStatus;
|
||||
|
||||
const MIN_CAPTURE_FRAME_SIZE: usize = 128;
|
||||
@@ -90,14 +88,27 @@ pub struct PipelineStateNotification {
|
||||
pub state: &'static str,
|
||||
pub reason: Option<&'static str>,
|
||||
pub next_retry_ms: Option<u64>,
|
||||
pub applied_config: Option<PipelineAppliedConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PipelineAppliedConfig {
|
||||
pub resolution: Resolution,
|
||||
pub format: PixelFormat,
|
||||
pub fps: u32,
|
||||
}
|
||||
|
||||
impl PipelineStateNotification {
|
||||
fn streaming() -> Self {
|
||||
fn streaming(resolution: Resolution, format: PixelFormat, fps: u32) -> Self {
|
||||
Self {
|
||||
state: "streaming",
|
||||
reason: None,
|
||||
next_retry_ms: None,
|
||||
applied_config: Some(PipelineAppliedConfig {
|
||||
resolution,
|
||||
format,
|
||||
fps,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,14 +117,7 @@ impl PipelineStateNotification {
|
||||
state: "no_signal",
|
||||
reason: Some(status.as_str()),
|
||||
next_retry_ms,
|
||||
}
|
||||
}
|
||||
|
||||
fn device_busy(reason: &'static str) -> Self {
|
||||
Self {
|
||||
state: "device_busy",
|
||||
reason: Some(reason),
|
||||
next_retry_ms: None,
|
||||
applied_config: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,6 +125,8 @@ impl PipelineStateNotification {
|
||||
/// Shared video pipeline configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SharedVideoPipelineConfig {
|
||||
/// Whether the capture mode is configured by the client or follows HDMI.
|
||||
pub control_mode: VideoControlMode,
|
||||
/// Input resolution
|
||||
pub resolution: Resolution,
|
||||
/// Input pixel format
|
||||
@@ -138,6 +144,7 @@ pub struct SharedVideoPipelineConfig {
|
||||
impl Default for SharedVideoPipelineConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
control_mode: VideoControlMode::Configurable,
|
||||
resolution: Resolution::HD720,
|
||||
input_format: PixelFormat::Yuyv,
|
||||
output_codec: VideoEncoderType::H264,
|
||||
@@ -285,81 +292,6 @@ pub struct SharedVideoPipeline {
|
||||
last_state_notification: ParkingMutex<Option<PipelineStateNotification>>,
|
||||
}
|
||||
|
||||
fn poll_bridge_subdev_after_no_signal(bridge_ctx: &BridgeContext, pipeline: &SharedVideoPipeline) {
|
||||
let Some(subdev_path) = bridge_ctx.subdev_path.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let kind = bridge_ctx
|
||||
.kind
|
||||
.unwrap_or(csi_bridge::CsiBridgeKind::Unknown);
|
||||
let deadline = Instant::now() + NOSIGNAL_POLL_MAX;
|
||||
let mut poll_count: u32 = 0;
|
||||
info!(
|
||||
"No-signal poll: scanning subdev {:?} every {} ms (max {:?})",
|
||||
subdev_path, CSI_BRIDGE_NOSIGNAL_INTERVAL_MS, NOSIGNAL_POLL_MAX
|
||||
);
|
||||
loop {
|
||||
if !pipeline.running_flag.load(Ordering::Acquire) {
|
||||
return;
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
info!(
|
||||
"No-signal poll: stopped after {:?} ({} attempts)",
|
||||
NOSIGNAL_POLL_MAX, poll_count
|
||||
);
|
||||
return;
|
||||
}
|
||||
let fd = match csi_bridge::open_subdev(subdev_path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"No-signal poll: open subdev {:?} failed: {}",
|
||||
subdev_path, e
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(CSI_BRIDGE_NOSIGNAL_INTERVAL_MS));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match csi_bridge::probe_signal_thread_timeout(
|
||||
&fd,
|
||||
kind,
|
||||
csi_bridge::RK628_SUBDEV_PROBE_TIMEOUT,
|
||||
) {
|
||||
Some(ProbeResult::Locked(mode)) => {
|
||||
info!(
|
||||
"No-signal poll: locked {}x{} @ {} Hz — proceeding to capture re-open",
|
||||
mode.width, mode.height, mode.pixelclock
|
||||
);
|
||||
return;
|
||||
}
|
||||
Some(other) => {
|
||||
poll_count = poll_count.saturating_add(1);
|
||||
if poll_count == 1 || poll_count.is_multiple_of(8) {
|
||||
debug!(
|
||||
"No-signal poll: attempt {} — still {:?}",
|
||||
poll_count,
|
||||
other.as_status()
|
||||
);
|
||||
}
|
||||
if let Some(st) = other.as_status() {
|
||||
pipeline.notify_state(PipelineStateNotification::no_signal(
|
||||
st,
|
||||
Some(CSI_BRIDGE_NOSIGNAL_INTERVAL_MS.saturating_add(50)),
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
poll_count = poll_count.saturating_add(1);
|
||||
debug!(
|
||||
"No-signal poll: attempt {} — probe ioctl timed out",
|
||||
poll_count
|
||||
);
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(CSI_BRIDGE_NOSIGNAL_INTERVAL_MS));
|
||||
}
|
||||
}
|
||||
|
||||
impl SharedVideoPipeline {
|
||||
/// Create a new shared video pipeline
|
||||
pub fn new(config: SharedVideoPipelineConfig) -> Result<Arc<Self>> {
|
||||
@@ -575,7 +507,6 @@ impl SharedVideoPipeline {
|
||||
_jpeg_quality: u8,
|
||||
subdev_path: Option<std::path::PathBuf>,
|
||||
bridge_kind: Option<String>,
|
||||
_v4l2_driver: Option<String>,
|
||||
) -> Result<()> {
|
||||
if *self.running_rx.borrow() {
|
||||
warn!("Pipeline already running");
|
||||
@@ -601,24 +532,33 @@ impl SharedVideoPipeline {
|
||||
buffer_count.max(1),
|
||||
Duration::from_secs(2),
|
||||
bridge_ctx_probe,
|
||||
config.control_mode,
|
||||
) {
|
||||
Ok(s) => {
|
||||
let negotiated_res = s.resolution();
|
||||
let negotiated_fmt = s.format();
|
||||
if negotiated_res != config.resolution || negotiated_fmt != config.input_format {
|
||||
let previous = (config.resolution, config.input_format, config.fps);
|
||||
if config.control_mode == VideoControlMode::SourceFollowing {
|
||||
if let Some(source_fps) = s.source_fps() {
|
||||
config.fps = source_fps.round().clamp(1.0, 120.0) as u32;
|
||||
}
|
||||
}
|
||||
config.resolution = negotiated_res;
|
||||
config.input_format = negotiated_fmt;
|
||||
if previous != (config.resolution, config.input_format, config.fps) {
|
||||
info!(
|
||||
"Negotiated capture {}x{} {:?} (configured {}x{} {:?}) — aligning encoder to source",
|
||||
"Negotiated capture {}x{} {:?} @ {} fps (configured {}x{} {:?} @ {} fps) — aligning encoder to source",
|
||||
negotiated_res.width,
|
||||
negotiated_res.height,
|
||||
negotiated_fmt,
|
||||
config.resolution.width,
|
||||
config.resolution.height,
|
||||
config.input_format
|
||||
config.fps,
|
||||
previous.0.width,
|
||||
previous.0.height,
|
||||
previous.1,
|
||||
previous.2,
|
||||
);
|
||||
config.resolution = negotiated_res;
|
||||
config.input_format = negotiated_fmt;
|
||||
*self.config.write().await = config.clone();
|
||||
}
|
||||
*self.config.write().await = config.clone();
|
||||
Some(s)
|
||||
}
|
||||
Err(AppError::CaptureNoSignal { kind }) => {
|
||||
@@ -628,7 +568,11 @@ impl SharedVideoPipeline {
|
||||
let status = signal_status_from_capture_kind(&kind);
|
||||
self.notify_state(PipelineStateNotification::no_signal(
|
||||
status,
|
||||
Some(Duration::from_secs(2).as_millis() as u64),
|
||||
Some(
|
||||
CaptureRecoveryPolicy::new(config.control_mode)
|
||||
.retry_delay(1)
|
||||
.as_millis() as u64,
|
||||
),
|
||||
));
|
||||
None
|
||||
}
|
||||
@@ -754,74 +698,19 @@ impl SharedVideoPipeline {
|
||||
let mut initial_geometry: Option<(Resolution, PixelFormat)> = None;
|
||||
let mut resolution = config.resolution;
|
||||
let mut pixel_format = config.input_format;
|
||||
let mut active_fps = config.fps;
|
||||
let mut stride: u32 = 0;
|
||||
|
||||
match preopened {
|
||||
Some(s) => {
|
||||
resolution = s.resolution();
|
||||
pixel_format = s.format();
|
||||
stride = s.stride();
|
||||
initial_geometry = Some((resolution, pixel_format));
|
||||
stream = Some(s);
|
||||
}
|
||||
None => {
|
||||
match open_capture_stream(
|
||||
&device_path,
|
||||
config.resolution,
|
||||
config.input_format,
|
||||
config.fps,
|
||||
buffer_count.max(1),
|
||||
Duration::from_secs(2),
|
||||
bridge_ctx.clone(),
|
||||
) {
|
||||
Ok(s) => {
|
||||
resolution = s.resolution();
|
||||
pixel_format = s.format();
|
||||
stride = s.stride();
|
||||
if resolution != config.resolution
|
||||
|| pixel_format != config.input_format
|
||||
{
|
||||
info!(
|
||||
"First capture open negotiated {}x{} {:?} but encoder expects {}x{} {:?} — stopping for dimension resync",
|
||||
resolution.width,
|
||||
resolution.height,
|
||||
pixel_format,
|
||||
config.resolution.width,
|
||||
config.resolution.height,
|
||||
config.input_format
|
||||
);
|
||||
pipeline.notify_state(PipelineStateNotification::device_busy(
|
||||
"config_changing",
|
||||
));
|
||||
*pipeline.pending_sync_geometry.lock() =
|
||||
Some((resolution, pixel_format));
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(1);
|
||||
return;
|
||||
}
|
||||
initial_geometry = Some((resolution, pixel_format));
|
||||
stream = Some(s);
|
||||
}
|
||||
Err(AppError::CaptureNoSignal { kind }) => {
|
||||
warn!(
|
||||
"Capture stream open reports no signal ({}) — pipeline will retry",
|
||||
kind
|
||||
);
|
||||
pipeline.notify_state(PipelineStateNotification::no_signal(
|
||||
signal_status_from_capture_kind(&kind),
|
||||
Some(CSI_BRIDGE_NOSIGNAL_INTERVAL_MS),
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to open capture stream: {}", e);
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(s) = preopened {
|
||||
resolution = s.resolution();
|
||||
pixel_format = s.format();
|
||||
active_fps = s
|
||||
.source_fps()
|
||||
.map(|fps| fps.round().clamp(1.0, 120.0) as u32)
|
||||
.unwrap_or(config.fps);
|
||||
stride = s.stride();
|
||||
initial_geometry = Some((resolution, pixel_format));
|
||||
stream = Some(s);
|
||||
}
|
||||
|
||||
fn open_or_retry(
|
||||
@@ -838,6 +727,7 @@ impl SharedVideoPipeline {
|
||||
buffer_count.max(1),
|
||||
Duration::from_secs(2),
|
||||
bridge_ctx,
|
||||
config.control_mode,
|
||||
is_device_lost_message,
|
||||
) {
|
||||
CaptureOpenResult::NoSignal(status) => {
|
||||
@@ -860,6 +750,7 @@ impl SharedVideoPipeline {
|
||||
let grace_period = Duration::from_secs(AUTO_STOP_GRACE_PERIOD_SECS);
|
||||
let mut sequence: u64 = 0;
|
||||
let mut consecutive_timeouts: u32 = 0;
|
||||
let recovery_policy = CaptureRecoveryPolicy::new(config.control_mode);
|
||||
let capture_error_throttler = LogThrottler::with_secs(5);
|
||||
let mut suppressed_capture_errors: HashMap<String, u64> = HashMap::new();
|
||||
|
||||
@@ -877,9 +768,7 @@ impl SharedVideoPipeline {
|
||||
"No subscribers for {}s, auto-stopping video pipeline",
|
||||
grace_period.as_secs()
|
||||
);
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -899,6 +788,10 @@ impl SharedVideoPipeline {
|
||||
let new_res = new_stream.resolution();
|
||||
let new_fmt = new_stream.format();
|
||||
let new_stride = new_stream.stride();
|
||||
let new_fps = new_stream
|
||||
.source_fps()
|
||||
.map(|fps| fps.round().clamp(1.0, 120.0) as u32)
|
||||
.unwrap_or(config.fps);
|
||||
|
||||
// Pre-probe was skipped (no signal at pipeline start) but the
|
||||
// encoder was sized to saved settings — if DV timings now
|
||||
@@ -916,14 +809,13 @@ impl SharedVideoPipeline {
|
||||
config.resolution.height,
|
||||
config.input_format
|
||||
);
|
||||
pipeline.notify_state(PipelineStateNotification::device_busy(
|
||||
"config_changing",
|
||||
pipeline.notify_state(PipelineStateNotification::no_signal(
|
||||
SignalStatus::NoSignal,
|
||||
Some(recovery_policy.retry_delay(1).as_millis() as u64),
|
||||
));
|
||||
*pipeline.pending_sync_geometry.lock() =
|
||||
Some((new_res, new_fmt));
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -944,15 +836,15 @@ impl SharedVideoPipeline {
|
||||
orig_res, orig_fmt, new_res, new_fmt
|
||||
);
|
||||
pipeline.notify_state(
|
||||
PipelineStateNotification::device_busy(
|
||||
"config_changing",
|
||||
PipelineStateNotification::no_signal(
|
||||
SignalStatus::NoSignal,
|
||||
Some(recovery_policy.retry_delay(1).as_millis()
|
||||
as u64),
|
||||
),
|
||||
);
|
||||
*pipeline.pending_sync_geometry.lock() =
|
||||
Some((new_res, new_fmt));
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
@@ -963,6 +855,7 @@ impl SharedVideoPipeline {
|
||||
}
|
||||
resolution = new_res;
|
||||
pixel_format = new_fmt;
|
||||
active_fps = new_fps;
|
||||
stride = new_stride;
|
||||
stream = Some(new_stream);
|
||||
consecutive_timeouts = 0;
|
||||
@@ -973,36 +866,34 @@ impl SharedVideoPipeline {
|
||||
}
|
||||
CaptureOpenResult::NoSignal(status) => {
|
||||
consecutive_timeouts = consecutive_timeouts.saturating_add(1);
|
||||
if consecutive_timeouts >= CAPTURE_TIMEOUT_STOP_THRESHOLD {
|
||||
if !recovery_policy.should_retry(consecutive_timeouts) {
|
||||
warn!(
|
||||
"Capture soft-restart gave up after {} attempts, \
|
||||
stopping pipeline",
|
||||
consecutive_timeouts
|
||||
);
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
|
||||
break;
|
||||
}
|
||||
let wait_ms = CSI_BRIDGE_NOSIGNAL_INTERVAL_MS;
|
||||
let delay = recovery_policy.retry_delay(consecutive_timeouts);
|
||||
pipeline.notify_state(PipelineStateNotification::no_signal(
|
||||
status,
|
||||
Some(wait_ms),
|
||||
Some(delay.as_millis() as u64),
|
||||
));
|
||||
std::thread::sleep(Duration::from_millis(wait_ms));
|
||||
if wait_for_source_change(&bridge_ctx, delay, || {
|
||||
pipeline.running_flag.load(Ordering::Acquire)
|
||||
}) {
|
||||
info!("SOURCE_CHANGE woke capture retry");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
CaptureOpenResult::DeviceLost(reason) => {
|
||||
pipeline.mark_device_lost(reason);
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
|
||||
break;
|
||||
}
|
||||
CaptureOpenResult::Fatal => {
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1024,100 +915,37 @@ impl SharedVideoPipeline {
|
||||
// drop the stream so the next iteration re-opens
|
||||
// via a fresh DV_TIMINGS probe. This is the fast
|
||||
// path for source-side resolution switches on
|
||||
// RK628 / rkcif — sub-second recovery vs. the ~8 s
|
||||
// timeout fallback.
|
||||
// RK628 / rkcif; the retry policy is only a fallback
|
||||
// when a driver does not provide usable events.
|
||||
if is_source_changed_error(&e) {
|
||||
info!(
|
||||
"Capture reported SOURCE_CHANGE — \
|
||||
dropping stream for immediate re-open"
|
||||
);
|
||||
if recovery_policy.control_mode()
|
||||
== VideoControlMode::SourceFollowing
|
||||
{
|
||||
pipeline.notify_state(PipelineStateNotification::no_signal(
|
||||
SignalStatus::NoSignal,
|
||||
Some(recovery_policy.retry_delay(1).as_millis() as u64),
|
||||
));
|
||||
}
|
||||
consecutive_timeouts = 0;
|
||||
stream = None;
|
||||
continue;
|
||||
}
|
||||
if e.kind() == std::io::ErrorKind::TimedOut {
|
||||
consecutive_timeouts = consecutive_timeouts.saturating_add(1);
|
||||
let probe_result = {
|
||||
let sr = stream.as_mut().expect("stream is Some above");
|
||||
sr.probe_bridge_signal_with_timeout(
|
||||
csi_bridge::RK628_SUBDEV_PROBE_TIMEOUT,
|
||||
)
|
||||
};
|
||||
match probe_result {
|
||||
Some(ProbeResult::Locked(mode)) => {
|
||||
let probed_resolution =
|
||||
Resolution::new(mode.width, mode.height);
|
||||
if probed_resolution == resolution {
|
||||
info!(
|
||||
"Capture timeout but bridge is locked at {}x{} — soft-restarting capture without encoder rebuild",
|
||||
probed_resolution.width,
|
||||
probed_resolution.height
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"Capture timeout probe detected geometry change {}x{} -> {}x{} — soft-restarting capture for encoder rebuild",
|
||||
resolution.width,
|
||||
resolution.height,
|
||||
probed_resolution.width,
|
||||
probed_resolution.height
|
||||
);
|
||||
pipeline.notify_state(
|
||||
PipelineStateNotification::device_busy(
|
||||
"config_changing",
|
||||
),
|
||||
);
|
||||
}
|
||||
consecutive_timeouts = 0;
|
||||
stream = None;
|
||||
continue;
|
||||
}
|
||||
Some(other) => {
|
||||
let status =
|
||||
other.as_status().unwrap_or(SignalStatus::NoSignal);
|
||||
warn!(
|
||||
"Capture timeout probe reports no signal ({})",
|
||||
status.as_str()
|
||||
);
|
||||
pipeline.notify_state(
|
||||
PipelineStateNotification::no_signal(
|
||||
status,
|
||||
Some(Duration::from_secs(2).as_millis() as u64),
|
||||
),
|
||||
);
|
||||
// Drop capture so RK628 / rkcif can release the queue,
|
||||
// then poll subdev on a fresh fd until timings lock (or
|
||||
// timeout). Avoids sitting on DQBUF 2s × N with a dead
|
||||
// stream while `v4l2-ctl --query-dv-timings` already shows
|
||||
// a real mode.
|
||||
stream = None;
|
||||
consecutive_timeouts = 0;
|
||||
if bridge_ctx.has_subdev()
|
||||
&& matches!(
|
||||
other,
|
||||
ProbeResult::NoSignal
|
||||
| ProbeResult::NoSync
|
||||
| ProbeResult::OutOfRange
|
||||
)
|
||||
{
|
||||
poll_bridge_subdev_after_no_signal(
|
||||
&bridge_ctx,
|
||||
&pipeline,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
None if bridge_ctx.has_subdev() => {
|
||||
warn!(
|
||||
"DV-timings probe timed out or failed — forcing stream re-open (RK628 / rkcif)"
|
||||
);
|
||||
consecutive_timeouts = 0;
|
||||
stream = None;
|
||||
poll_bridge_subdev_after_no_signal(&bridge_ctx, &pipeline);
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
warn!("Capture timeout - no signal?");
|
||||
}
|
||||
if recovery_policy.control_mode()
|
||||
== VideoControlMode::SourceFollowing
|
||||
{
|
||||
let delay = recovery_policy.retry_delay(consecutive_timeouts);
|
||||
pipeline.notify_state(PipelineStateNotification::no_signal(
|
||||
SignalStatus::NoSignal,
|
||||
Some(delay.as_millis() as u64),
|
||||
));
|
||||
stream = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
if consecutive_timeouts >= CAPTURE_TIMEOUT_SOFT_RESTART_THRESHOLD {
|
||||
@@ -1145,17 +973,6 @@ impl SharedVideoPipeline {
|
||||
consecutive_timeouts
|
||||
);
|
||||
}
|
||||
|
||||
if consecutive_timeouts >= CAPTURE_TIMEOUT_STOP_THRESHOLD {
|
||||
warn!(
|
||||
"Capture timed out {} consecutive times, stopping video pipeline",
|
||||
consecutive_timeouts
|
||||
);
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
consecutive_timeouts = 0;
|
||||
// EIO (5) / EPIPE (32) / EPROTO (71) in next_into generally
|
||||
@@ -1187,9 +1004,7 @@ impl SharedVideoPipeline {
|
||||
CaptureIoErrorKind::DeviceLost => {
|
||||
error!("Capture device lost: {}", e);
|
||||
pipeline.mark_device_lost(e.to_string());
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
|
||||
break;
|
||||
}
|
||||
CaptureIoErrorKind::Other => {}
|
||||
@@ -1223,7 +1038,11 @@ impl SharedVideoPipeline {
|
||||
owned.truncate(frame_size);
|
||||
|
||||
// Notify streaming only after the short-frame guard passes.
|
||||
pipeline.notify_state(PipelineStateNotification::streaming());
|
||||
pipeline.notify_state(PipelineStateNotification::streaming(
|
||||
resolution,
|
||||
pixel_format,
|
||||
active_fps,
|
||||
));
|
||||
let frame = Arc::new(VideoFrame::from_pooled(
|
||||
Arc::new(FrameBuffer::new(owned, Some(buffer_pool.clone()))),
|
||||
resolution,
|
||||
@@ -1240,10 +1059,14 @@ impl SharedVideoPipeline {
|
||||
let _ = frame_seq_tx.send(sequence);
|
||||
}
|
||||
|
||||
// `running` represents completed lifecycle state, not a stop request.
|
||||
// Drop the V4L2 stream first so STREAMOFF, buffer teardown and FD close
|
||||
// have all completed before another consumer is told the device is free.
|
||||
drop(stream);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = pipeline.running.send(false);
|
||||
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
|
||||
info!("Video pipeline stopped");
|
||||
let _ = pipeline.running.send(false);
|
||||
info!("Video pipeline stopped and capture device released");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1458,9 +1281,7 @@ impl SharedVideoPipeline {
|
||||
|
||||
/// Stop the pipeline (non-blocking, does not wait for capture thread to exit)
|
||||
pub fn stop(&self) {
|
||||
if *self.running_rx.borrow() {
|
||||
let _ = self.running.send(false);
|
||||
self.running_flag.store(false, Ordering::Release);
|
||||
if self.running_flag.swap(false, Ordering::AcqRel) {
|
||||
self.clear_cmd_tx();
|
||||
info!("Stopping video pipeline");
|
||||
}
|
||||
@@ -1471,32 +1292,38 @@ impl SharedVideoPipeline {
|
||||
/// This ensures the V4L2 device is released before returning, which is
|
||||
/// necessary when another consumer (e.g. MJPEG streamer) needs to open
|
||||
/// the same device immediately after.
|
||||
pub async fn stop_and_wait(&self, timeout: std::time::Duration) {
|
||||
pub async fn stop_and_wait(&self, timeout: std::time::Duration) -> Result<()> {
|
||||
self.stop();
|
||||
let mut rx = self.running_watch();
|
||||
if !*rx.borrow() {
|
||||
// Capture thread may still be running from a previous `stop()` call.
|
||||
// Wait for the "Video pipeline stopped" log (thread sets running=false
|
||||
// at exit), unless it already happened.
|
||||
}
|
||||
let deadline = tokio::time::Instant::now() + timeout;
|
||||
loop {
|
||||
if !self.running_flag.load(Ordering::Acquire) {
|
||||
// Flag is cleared, but the capture thread may still be unwinding
|
||||
// (dropping the V4L2 stream). Give it a brief moment.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
break;
|
||||
}
|
||||
|
||||
while *rx.borrow() {
|
||||
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
|
||||
if remaining.is_zero() {
|
||||
warn!(
|
||||
"Timed out waiting for video pipeline to stop after {:?}",
|
||||
return Err(AppError::VideoError(format!(
|
||||
"Timed out waiting {:?} for video pipeline to release capture device",
|
||||
timeout
|
||||
);
|
||||
break;
|
||||
)));
|
||||
}
|
||||
match tokio::time::timeout(remaining, rx.changed()).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(_)) if !*rx.borrow() => break,
|
||||
Ok(Err(_)) => {
|
||||
return Err(AppError::VideoError(
|
||||
"Video pipeline lifecycle channel closed before capture device release"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(AppError::VideoError(format!(
|
||||
"Timed out waiting {:?} for video pipeline to release capture device",
|
||||
timeout
|
||||
)));
|
||||
}
|
||||
}
|
||||
let _ = tokio::time::timeout(remaining, rx.changed()).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set bitrate using preset
|
||||
@@ -1696,7 +1523,7 @@ fn copy_rows(
|
||||
|
||||
impl Drop for SharedVideoPipeline {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.running.send(false);
|
||||
self.running_flag.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1765,4 +1592,49 @@ mod tests {
|
||||
let h265 = SharedVideoPipelineConfig::h265(Resolution::HD720, BitratePreset::Speed);
|
||||
assert_eq!(h265.output_codec, VideoEncoderType::H265);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_request_does_not_publish_worker_exit() {
|
||||
let pipeline = SharedVideoPipeline::new(SharedVideoPipelineConfig::h264(
|
||||
Resolution::HD720,
|
||||
BitratePreset::Balanced,
|
||||
))
|
||||
.unwrap();
|
||||
let _ = pipeline.running.send(true);
|
||||
pipeline.running_flag.store(true, Ordering::Release);
|
||||
|
||||
pipeline.stop();
|
||||
|
||||
assert!(!pipeline.running_flag.load(Ordering::Acquire));
|
||||
assert!(pipeline.is_running());
|
||||
|
||||
// Simulate the capture thread's common cleanup tail.
|
||||
let _ = pipeline.running.send(false);
|
||||
assert!(!pipeline.is_running());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stop_and_wait_observes_completed_worker_cleanup() {
|
||||
let pipeline = SharedVideoPipeline::new(SharedVideoPipelineConfig::h264(
|
||||
Resolution::HD720,
|
||||
BitratePreset::Balanced,
|
||||
))
|
||||
.unwrap();
|
||||
let _ = pipeline.running.send(true);
|
||||
pipeline.running_flag.store(true, Ordering::Release);
|
||||
|
||||
let worker = pipeline.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(30)).await;
|
||||
let _ = worker.running.send(false);
|
||||
});
|
||||
|
||||
let started = Instant::now();
|
||||
pipeline
|
||||
.stop_and_wait(Duration::from_secs(1))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(started.elapsed() >= Duration::from_millis(20));
|
||||
assert!(!pipeline.is_running());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user