fix: 修复无信号状态同步与采集管线阻塞

- 使用非阻塞 V4L2 句柄并类型化采集错误
- 完善视频管线生命周期,避免复用停止中的管线
- 区分音视频事件并支持 REST/WebSocket 状态快照
- 修复信号丢失后保留最后一帧及刷新后黑屏问题
- 优化无信号提示、状态展示优先级及相关界面细节
- 降低重复 OTG 错误日志级别
This commit is contained in:
mofeng-git
2026-07-30 22:43:59 +08:00
parent ce1712ff2e
commit 6fdcf5c7c9
24 changed files with 406 additions and 165 deletions

View File

@@ -47,7 +47,7 @@ use crate::video::capture::status::{
capture_error_log_key, classify_capture_io_error, is_device_lost_message,
signal_status_from_capture_kind, CaptureIoErrorKind,
};
use crate::video::capture::{is_source_changed_error, BridgeContext, CaptureStream};
use crate::video::capture::{BridgeContext, CaptureReadError, CaptureStream};
use crate::video::codec::h264_bitstream;
use crate::video::codec::registry::{EncoderBackend, VideoEncoderType};
use crate::video::device::parse_bridge_kind;
@@ -98,6 +98,13 @@ pub struct PipelineAppliedConfig {
pub fps: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PipelineLifecycle {
Running,
Stopping,
Stopped,
}
impl PipelineStateNotification {
fn streaming(resolution: Resolution, format: PixelFormat, fps: u32) -> Self {
Self {
@@ -453,6 +460,19 @@ impl SharedVideoPipeline {
*self.running_rx.borrow()
}
/// Lifecycle state derived from the stop-request flag and the capture
/// thread's completion signal. A stopping pipeline must never receive a
/// new subscriber or be replaced before it releases the V4L2 device.
pub fn lifecycle(&self) -> PipelineLifecycle {
if self.running_flag.load(Ordering::Acquire) {
PipelineLifecycle::Running
} else if *self.running_rx.borrow() {
PipelineLifecycle::Stopping
} else {
PipelineLifecycle::Stopped
}
}
/// Subscribe to running state changes
///
/// Returns a watch receiver that can be used to detect when the pipeline stops.
@@ -909,7 +929,7 @@ impl SharedVideoPipeline {
consecutive_timeouts = 0;
meta
}
Err(e) => {
Err(CaptureReadError::SourceChanged) => {
// V4L2 driver reported V4L2_EVENT_SOURCE_CHANGE.
// The current capture is effectively invalidated:
// drop the stream so the next iteration re-opens
@@ -917,21 +937,22 @@ impl SharedVideoPipeline {
// path for source-side resolution switches on
// 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;
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;
}
Err(CaptureReadError::Io(e)) => {
if e.kind() == std::io::ErrorKind::WouldBlock {
continue;
}
if e.kind() == std::io::ErrorKind::TimedOut {
@@ -1607,10 +1628,12 @@ mod tests {
assert!(!pipeline.running_flag.load(Ordering::Acquire));
assert!(pipeline.is_running());
assert_eq!(pipeline.lifecycle(), PipelineLifecycle::Stopping);
// Simulate the capture thread's common cleanup tail.
let _ = pipeline.running.send(false);
assert!(!pipeline.is_running());
assert_eq!(pipeline.lifecycle(), PipelineLifecycle::Stopped);
}
#[tokio::test]