refactor: 删除部分多余的代码和注释

This commit is contained in:
mofeng-git
2026-05-01 17:31:04 +08:00
parent 74035f8e12
commit d8e7de74a6
165 changed files with 2960 additions and 9917 deletions

View File

@@ -0,0 +1,30 @@
//! Shared tuning for V4L2 MJPEG capture paths (`Streamer` + `SharedVideoPipeline`).
/// Frames smaller than this are treated as incomplete / noise.
pub(crate) const MIN_CAPTURE_FRAME_SIZE: usize = 128;
/// After startup, validate JPEG header every N frames to limit CPU use.
pub(crate) const JPEG_VALIDATE_INTERVAL: u64 = 30;
/// Validate every MJPEG frame for the first N frames (UVC warm-up / bad headers).
pub(crate) const STARTUP_JPEG_VALIDATE_FRAMES: u64 = 3;
#[inline]
pub(crate) fn should_validate_jpeg_frame(validate_counter: u64) -> bool {
validate_counter <= STARTUP_JPEG_VALIDATE_FRAMES
|| validate_counter.is_multiple_of(JPEG_VALIDATE_INTERVAL)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jpeg_validation_policy_startup_then_interval() {
assert!(should_validate_jpeg_frame(1));
assert!(should_validate_jpeg_frame(2));
assert!(should_validate_jpeg_frame(3));
assert!(!should_validate_jpeg_frame(4));
assert!(should_validate_jpeg_frame(30));
}
}

View File

@@ -135,7 +135,7 @@ pub async fn enforce_constraints_with_stream_manager(
}
if current_mode == StreamMode::WebRTC {
let current_codec = stream_manager.webrtc_streamer().current_video_codec().await;
let current_codec = stream_manager.current_video_codec().await;
if !constraints.is_webrtc_codec_allowed(current_codec) {
let target_codec = constraints.preferred_webrtc_codec();
stream_manager.set_video_codec(target_codec).await?;

View File

@@ -14,9 +14,7 @@ use tracing::{debug, info, warn};
use v4l2r::bindings::{
v4l2_bt_timings, v4l2_dv_timings, V4L2_DV_BT_656_1120, V4L2_DV_FL_HAS_CEA861_VIC,
};
use v4l2r::ioctl::{
self, Event as V4l2Event, EventType, QueryDvTimingsError, SubscribeEventFlags,
};
use v4l2r::ioctl::{self, Event as V4l2Event, EventType, QueryDvTimingsError, SubscribeEventFlags};
use v4l2r::nix::errno::Errno;
use crate::video::SignalStatus;
@@ -143,9 +141,9 @@ pub fn probe_signal(subdev_fd: &impl AsRawFd, kind: CsiBridgeKind) -> ProbeResul
Err(QueryDvTimingsError::NoLink) => ProbeResult::NoCable,
Err(QueryDvTimingsError::UnstableSignal) => ProbeResult::NoSync,
Err(QueryDvTimingsError::IoctlError(Errno::ERANGE)) => ProbeResult::OutOfRange,
Err(QueryDvTimingsError::IoctlError(
Errno::EIO | Errno::EREMOTEIO | Errno::ETIMEDOUT,
)) => ProbeResult::NoSync,
Err(QueryDvTimingsError::IoctlError(Errno::EIO | Errno::EREMOTEIO | Errno::ETIMEDOUT)) => {
ProbeResult::NoSync
}
Err(QueryDvTimingsError::Unsupported) | Err(QueryDvTimingsError::IoctlError(_)) => {
ProbeResult::NoSignal
}
@@ -222,14 +220,8 @@ fn classify_timings(timings: v4l2_dv_timings, kind: CsiBridgeKind) -> ProbeResul
return ProbeResult::NoSignal;
}
let total_h: u64 = (width
+ bt.hfrontporch
+ bt.hsync
+ bt.hbackporch) as u64;
let total_v: u64 = (height
+ bt.vfrontporch
+ bt.vsync
+ bt.vbackporch) as u64;
let total_h: u64 = (width + bt.hfrontporch + bt.hsync + bt.hbackporch) as u64;
let total_v: u64 = (height + bt.vfrontporch + bt.vsync + bt.vbackporch) as u64;
let fps = if total_h > 0 && total_v > 0 && pixelclock > 0 {
Some(pixelclock as f64 / (total_h as f64 * total_v as f64))
} else {

View File

@@ -168,16 +168,15 @@ impl VideoDevice {
// subdev (the video node returns ENOTTY). Tc358743 and rk_hdmirx
// typically expose DV ioctls on the video node itself, but having
// the subdev handle for EDID/event subscription doesn't hurt.
let (subdev_path, bridge_kind) = if is_rkcif_driver(&caps.driver)
|| is_rk_hdmirx_driver(&caps.driver, &caps.card)
{
match csi_bridge::discover_subdev_for_video(&self.path) {
Some((path, kind)) => (Some(path), Some(format!("{:?}", kind).to_lowercase())),
None => (None, None),
}
} else {
(None, None)
};
let (subdev_path, bridge_kind) =
if is_rkcif_driver(&caps.driver) || is_rk_hdmirx_driver(&caps.driver, &caps.card) {
match csi_bridge::discover_subdev_for_video(&self.path) {
Some((path, kind)) => (Some(path), Some(format!("{:?}", kind).to_lowercase())),
None => (None, None),
}
} else {
(None, None)
};
// Probe the HDMI source for both signal presence *and* the live
// frame-rate. rkcif's `VIDIOC_ENUM_FRAMEINTERVALS` returns a
@@ -225,9 +224,7 @@ impl VideoDevice {
(false, None)
}
}
} else if is_rk_hdmirx_driver(&caps.driver, &caps.card)
|| is_rkcif_driver(&caps.driver)
{
} else if is_rk_hdmirx_driver(&caps.driver, &caps.card) || is_rkcif_driver(&caps.driver) {
let dv = self.current_dv_timings_mode();
debug!(
"has_signal via video node {:?} (driver={}): dv_timings={:?}",
@@ -247,21 +244,20 @@ impl VideoDevice {
(true, None)
};
let mut formats = if is_rk_hdmirx_driver(&caps.driver, &caps.card)
|| is_rkcif_driver(&caps.driver)
{
// CSI/HDMI bridge drivers (rk_hdmirx, rkcif) expose multiple pixel
// formats via ENUM_FMT (e.g. rk_hdmirx: BGR3/NV24/NV16/NV12) but
// `ENUM_FRAMESIZES` is fiction for these drivers (rkcif reports a
// degenerate `64x64 StepWise 8/8` that only describes its DMA
// engine, rk_hdmirx returns ENOTTY). The only authoritative
// resolution is whatever the bridge subdev's DV timings report,
// so we treat the HDMI source mode as the single allowed
// resolution for every pixel format.
self.enumerate_bridge_formats(subdev_hdmi_mode)?
} else {
self.enumerate_formats()?
};
let mut formats =
if is_rk_hdmirx_driver(&caps.driver, &caps.card) || is_rkcif_driver(&caps.driver) {
// CSI/HDMI bridge drivers (rk_hdmirx, rkcif) expose multiple pixel
// formats via ENUM_FMT (e.g. rk_hdmirx: BGR3/NV24/NV16/NV12) but
// `ENUM_FRAMESIZES` is fiction for these drivers (rkcif reports a
// degenerate `64x64 StepWise 8/8` that only describes its DMA
// engine, rk_hdmirx returns ENOTTY). The only authoritative
// resolution is whatever the bridge subdev's DV timings report,
// so we treat the HDMI source mode as the single allowed
// resolution for every pixel format.
self.enumerate_bridge_formats(subdev_hdmi_mode)?
} else {
self.enumerate_formats()?
};
// For CSI/HDMI bridges, the driver-enumerated fps list is fiction
// (rkcif: always `1..30`; rk_hdmirx: typically `ENOTTY`). Replace
@@ -923,7 +919,11 @@ pub fn enumerate_devices() -> Result<Vec<VideoDeviceInfo>> {
// The path tiebreaker ensures deterministic ordering when multiple sub-devices
// share the same priority (e.g. rkcif nodes), so that /dev/video0 is preferred
// over /dev/video10 after deduplication.
devices.sort_by(|a, b| b.priority.cmp(&a.priority).then_with(|| a.path.cmp(&b.path)));
devices.sort_by(|a, b| {
b.priority
.cmp(&a.priority)
.then_with(|| a.path.cmp(&b.path))
});
// Deduplicate rkcif sub-devices: the driver exposes many /dev/video* nodes
// for a single MIPI CSI pipeline. Keep only the highest-priority node per
@@ -976,8 +976,11 @@ fn collapse_rkcif_probe_candidates(candidates: &mut Vec<PathBuf>) {
fn sysfs_uevent_driver(path: &Path) -> Option<String> {
let name = path.file_name()?.to_str()?;
let uevent =
read_sysfs_string(&Path::new("/sys/class/video4linux").join(name).join("device/uevent"))?;
let uevent = read_sysfs_string(
&Path::new("/sys/class/video4linux")
.join(name)
.join("device/uevent"),
)?;
extract_uevent_value(&uevent, "driver")
}
@@ -1037,7 +1040,10 @@ fn sysfs_maybe_capture(path: &Path) -> bool {
// kernel driver that created them has been unloaded but the device nodes
// were never cleaned up. Opening them returns ENODEV; skip the probe.
if !sysfs_base.exists() {
debug!("Skipping {:?}: no matching /sys/class/video4linux entry", path);
debug!(
"Skipping {:?}: no matching /sys/class/video4linux entry",
path
);
return false;
}
@@ -1081,7 +1087,13 @@ fn sysfs_maybe_capture(path: &Path) -> bool {
// succeed QUERYCAP but expose only VIDEO_M2M / STATS / PARAMS and get
// filtered later — skipping here saves an open() + ioctl() per node.
let driver_skip = [
"rkvenc", "rkvdec", "vepu", "vdpu", "hantro", "mpp_", "rockchip-vpu",
"rkvenc",
"rkvdec",
"vepu",
"vdpu",
"hantro",
"mpp_",
"rockchip-vpu",
];
if let Some(driver) = &driver {
if driver_skip.iter().any(|hint| driver.contains(hint)) {

View File

@@ -1,91 +1,13 @@
//! Encoder traits and common types
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use std::time::Instant;
use typeshare::typeshare;
use crate::error::Result;
use crate::video::format::{PixelFormat, Resolution};
/// Bitrate preset for video encoding
///
/// Simplifies bitrate configuration by providing three intuitive presets
/// plus a custom option for advanced users.
#[typeshare]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
#[derive(Default)]
pub enum BitratePreset {
/// Speed priority: 1 Mbps, lowest latency, smaller GOP
/// Best for: slow networks, remote management, low-bandwidth scenarios
Speed,
/// Balanced: 4 Mbps, good quality/latency tradeoff
/// Best for: typical usage, recommended default
#[default]
Balanced,
/// Quality priority: 8 Mbps, best visual quality
/// Best for: local network, high-bandwidth scenarios, detailed work
Quality,
/// Custom bitrate in kbps (for advanced users)
Custom(u32),
}
impl BitratePreset {
/// Get bitrate value in kbps
pub fn bitrate_kbps(&self) -> u32 {
match self {
Self::Speed => 1000,
Self::Balanced => 4000,
Self::Quality => 8000,
Self::Custom(kbps) => *kbps,
}
}
/// Get recommended GOP size based on preset
///
/// Speed preset uses shorter GOP for faster recovery from packet loss.
/// Quality preset uses longer GOP for better compression efficiency.
pub fn gop_size(&self, fps: u32) -> u32 {
match self {
Self::Speed => (fps / 2).max(15), // 0.5 second, minimum 15 frames
Self::Balanced => fps, // 1 second
Self::Quality => fps * 2, // 2 seconds
Self::Custom(_) => fps, // Default 1 second for custom
}
}
/// Get quality preset name for encoder configuration
pub fn quality_level(&self) -> &'static str {
match self {
Self::Speed => "low", // ultrafast/veryfast preset
Self::Balanced => "medium", // medium preset
Self::Quality => "high", // slower preset, better quality
Self::Custom(_) => "medium",
}
}
/// Create from kbps value, mapping to nearest preset or Custom
pub fn from_kbps(kbps: u32) -> Self {
match kbps {
0..=1500 => Self::Speed,
1501..=6000 => Self::Balanced,
6001..=10000 => Self::Quality,
_ => Self::Custom(kbps),
}
}
}
impl std::fmt::Display for BitratePreset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Speed => write!(f, "Speed (1 Mbps)"),
Self::Balanced => write!(f, "Balanced (4 Mbps)"),
Self::Quality => write!(f, "Quality (8 Mbps)"),
Self::Custom(kbps) => write!(f, "Custom ({} kbps)", kbps),
}
}
}
/// Defined in `config::schema` (typeshare + serde). Re-export for encoder users.
pub use crate::config::BitratePreset;
/// Encoder configuration
#[derive(Debug, Clone)]

View File

@@ -2,6 +2,7 @@
//!
//! This module provides V4L2 video capture, encoding, and streaming functionality.
pub(crate) mod capture_limits;
pub mod codec_constraints;
pub mod convert;
pub mod csi_bridge;
@@ -13,6 +14,8 @@ pub mod frame;
pub mod shared_video_pipeline;
pub mod stream_manager;
pub mod streamer;
pub mod traits;
pub mod types;
pub mod usb_reset;
pub mod v4l2r_capture;

View File

@@ -38,25 +38,19 @@ 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);
/// Minimum valid frame size for capture
const MIN_CAPTURE_FRAME_SIZE: usize = 128;
/// Validate every JPEG frame during startup to avoid poisoning HW decoders
/// with incomplete UVC warm-up frames.
const STARTUP_JPEG_VALIDATE_FRAMES: u64 = 3;
/// Validate JPEG header every N frames to reduce overhead
const JPEG_VALIDATE_INTERVAL: u64 = 30;
/// Throttle repeated encoding errors to avoid log flooding
const ENCODE_ERROR_THROTTLE_SECS: u64 = 5;
use crate::error::{AppError, Result};
use crate::utils::LogThrottler;
use crate::video::capture_limits::{should_validate_jpeg_frame, MIN_CAPTURE_FRAME_SIZE};
use crate::video::csi_bridge::{self, ProbeResult};
use crate::video::device::parse_bridge_kind;
use crate::video::encoder::registry::{EncoderBackend, VideoEncoderType};
use crate::video::format::{PixelFormat, Resolution};
use crate::video::frame::{FrameBuffer, FrameBufferPool, VideoFrame};
use crate::video::device::parse_bridge_kind;
use crate::video::SignalStatus;
use crate::video::v4l2r_capture::{is_source_changed_error, BridgeContext, V4l2rCaptureStream};
use crate::video::SignalStatus;
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
use hwcodec::ffmpeg_hw::last_error_message as ffmpeg_hw_last_error;
@@ -250,11 +244,6 @@ fn log_encoding_error(
}
}
fn should_validate_jpeg_frame(validate_counter: u64) -> bool {
validate_counter <= STARTUP_JPEG_VALIDATE_FRAMES
|| validate_counter.is_multiple_of(JPEG_VALIDATE_INTERVAL)
}
/// Pipeline statistics
#[derive(Debug, Clone, Default)]
pub struct SharedVideoPipelineStats {
@@ -283,10 +272,7 @@ pub struct SharedVideoPipeline {
last_state_notification: ParkingMutex<Option<PipelineStateNotification>>,
}
fn poll_bridge_subdev_after_no_signal(
bridge_ctx: &BridgeContext,
pipeline: &SharedVideoPipeline,
) {
fn poll_bridge_subdev_after_no_signal(bridge_ctx: &BridgeContext, pipeline: &SharedVideoPipeline) {
let Some(subdev_path) = bridge_ctx.subdev_path.as_ref() else {
return;
};
@@ -313,7 +299,10 @@ fn poll_bridge_subdev_after_no_signal(
let fd = match csi_bridge::open_subdev(subdev_path) {
Ok(f) => f,
Err(e) => {
debug!("No-signal poll: open subdev {:?} failed: {}", subdev_path, e);
debug!(
"No-signal poll: open subdev {:?} failed: {}",
subdev_path, e
);
std::thread::sleep(Duration::from_millis(CSI_BRIDGE_NOSIGNAL_INTERVAL_MS));
continue;
}
@@ -565,21 +554,20 @@ impl SharedVideoPipeline {
subdev_path.clone(),
parse_bridge_kind(bridge_kind.as_deref()),
);
let preopened: Option<V4l2rCaptureStream> =
match V4l2rCaptureStream::open_with_bridge(
&device_path,
config.resolution,
config.input_format,
config.fps,
buffer_count.max(1),
Duration::from_secs(2),
bridge_ctx_probe,
) {
Ok(s) => {
let negotiated_res = s.resolution();
let negotiated_fmt = s.format();
if negotiated_res != config.resolution || negotiated_fmt != config.input_format {
info!(
let preopened: Option<V4l2rCaptureStream> = match V4l2rCaptureStream::open_with_bridge(
&device_path,
config.resolution,
config.input_format,
config.fps,
buffer_count.max(1),
Duration::from_secs(2),
bridge_ctx_probe,
) {
Ok(s) => {
let negotiated_res = s.resolution();
let negotiated_fmt = s.format();
if negotiated_res != config.resolution || negotiated_fmt != config.input_format {
info!(
"Negotiated capture {}x{} {:?} (configured {}x{} {:?}) — aligning encoder to source",
negotiated_res.width,
negotiated_res.height,
@@ -588,25 +576,25 @@ impl SharedVideoPipeline {
config.resolution.height,
config.input_format
);
config.resolution = negotiated_res;
config.input_format = negotiated_fmt;
*self.config.write().await = config.clone();
}
Some(s)
config.resolution = negotiated_res;
config.input_format = negotiated_fmt;
*self.config.write().await = config.clone();
}
Err(AppError::CaptureNoSignal { kind }) => {
debug!(
"Pre-probe: no signal — encoder uses configured geometry until capture opens"
);
let status = SignalStatus::from_str(&kind).unwrap_or(SignalStatus::NoSignal);
self.notify_state(PipelineStateNotification::no_signal(
status,
Some(Duration::from_secs(2).as_millis() as u64),
));
None
}
Err(e) => return Err(e),
};
Some(s)
}
Err(AppError::CaptureNoSignal { kind }) => {
debug!(
"Pre-probe: no signal — encoder uses configured geometry until capture opens"
);
let status = SignalStatus::from_str(&kind).unwrap_or(SignalStatus::NoSignal);
self.notify_state(PipelineStateNotification::no_signal(
status,
Some(Duration::from_secs(2).as_millis() as u64),
));
None
}
Err(e) => return Err(e),
};
let mut encoder_state = build_encoder_state(&config)?;
let _ = self.running.send(true);
@@ -707,10 +695,8 @@ impl SharedVideoPipeline {
let latest_frame = latest_frame.clone();
let frame_seq_tx = frame_seq_tx.clone();
let buffer_pool = buffer_pool.clone();
let bridge_ctx = BridgeContext::from_parts(
subdev_path,
parse_bridge_kind(bridge_kind.as_deref()),
);
let bridge_ctx =
BridgeContext::from_parts(subdev_path, parse_bridge_kind(bridge_kind.as_deref()));
std::thread::spawn(move || {
let mut stream: Option<V4l2rCaptureStream> = None;
let mut initial_geometry: Option<(Resolution, PixelFormat)> = None;
@@ -874,7 +860,8 @@ impl SharedVideoPipeline {
// ── No usable stream? Try to (re)open, back off on failure. ──
if stream.is_none() {
match open_or_retry(&device_path, &config, buffer_count, bridge_ctx.clone()) {
match open_or_retry(&device_path, &config, buffer_count, bridge_ctx.clone())
{
OpenResult::Opened(new_stream) => {
let new_res = new_stream.resolution();
let new_fmt = new_stream.format();
@@ -884,7 +871,8 @@ impl SharedVideoPipeline {
// encoder was sized to saved settings — if DV timings now
// disagree, we cannot encode until WebRTC resyncs dimensions.
if initial_geometry.is_none()
&& (new_res != config.resolution || new_fmt != config.input_format)
&& (new_res != config.resolution
|| new_fmt != config.input_format)
{
info!(
"Deferred capture open is {}x{} {:?} but encoder expects {}x{} {:?} — stopping for dimension resync",
@@ -898,7 +886,8 @@ impl SharedVideoPipeline {
pipeline.notify_state(PipelineStateNotification::device_busy(
"config_changing",
));
*pipeline.pending_sync_geometry.lock() = Some((new_res, new_fmt));
*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));
@@ -950,8 +939,7 @@ impl SharedVideoPipeline {
);
}
OpenResult::NoSignal(status) => {
consecutive_timeouts =
consecutive_timeouts.saturating_add(1);
consecutive_timeouts = consecutive_timeouts.saturating_add(1);
if consecutive_timeouts >= CAPTURE_TIMEOUT_STOP_THRESHOLD {
warn!(
"Capture soft-restart gave up after {} attempts, \
@@ -1092,9 +1080,7 @@ impl SharedVideoPipeline {
}
}
if consecutive_timeouts
>= CAPTURE_TIMEOUT_SOFT_RESTART_THRESHOLD
{
if consecutive_timeouts >= CAPTURE_TIMEOUT_SOFT_RESTART_THRESHOLD {
// Drop the stream so the next loop
// iteration re-opens via the DV-timings
// probe. This catches source-side
@@ -1105,12 +1091,10 @@ impl SharedVideoPipeline {
closing stream for soft-restart",
consecutive_timeouts
);
pipeline.notify_state(
PipelineStateNotification::no_signal(
SignalStatus::UvcCaptureStall,
Some(Duration::from_secs(2).as_millis() as u64),
),
);
pipeline.notify_state(PipelineStateNotification::no_signal(
SignalStatus::UvcCaptureStall,
Some(Duration::from_secs(2).as_millis() as u64),
));
stream = None;
continue;
}
@@ -1551,13 +1535,4 @@ mod tests {
let h265 = SharedVideoPipelineConfig::h265(Resolution::HD720, BitratePreset::Speed);
assert_eq!(h265.output_codec, VideoEncoderType::H265);
}
#[test]
fn test_startup_jpeg_validation_policy() {
assert!(should_validate_jpeg_frame(1));
assert!(should_validate_jpeg_frame(2));
assert!(should_validate_jpeg_frame(3));
assert!(!should_validate_jpeg_frame(4));
assert!(should_validate_jpeg_frame(30));
}
}

View File

@@ -39,8 +39,8 @@ use crate::stream::MjpegStreamHandler;
use crate::video::codec_constraints::StreamCodecConstraints;
use crate::video::format::{PixelFormat, Resolution};
use crate::video::is_csi_hdmi_bridge;
use crate::video::streamer::{Streamer, StreamerStats, StreamerState};
use crate::webrtc::WebRtcStreamer;
use crate::video::streamer::{Streamer, StreamerState, StreamerStats};
use crate::video::traits::VideoOutput;
/// Video stream manager configuration
#[derive(Debug, Clone)]
@@ -95,8 +95,8 @@ pub struct VideoStreamManager {
mode: RwLock<StreamMode>,
/// MJPEG streamer (handles video capture and MJPEG distribution)
streamer: Arc<Streamer>,
/// WebRTC streamer (unified WebRTC manager with multi-codec support)
webrtc_streamer: Arc<WebRtcStreamer>,
/// WebRTC output (unified WebRTC manager with multi-codec support)
webrtc_streamer: Arc<dyn VideoOutput>,
/// Event bus for notifications
events: RwLock<Option<Arc<EventBus>>>,
/// Configuration store
@@ -111,7 +111,7 @@ impl VideoStreamManager {
/// Create a new video stream manager with WebRtcStreamer
pub fn with_webrtc_streamer(
streamer: Arc<Streamer>,
webrtc_streamer: Arc<WebRtcStreamer>,
webrtc_streamer: Arc<dyn VideoOutput>,
) -> Arc<Self> {
Arc::new(Self {
mode: RwLock::new(StreamMode::Mjpeg),
@@ -175,11 +175,6 @@ impl VideoStreamManager {
self.streamer.clone()
}
/// Get the WebRTC streamer (unified interface with multi-codec support)
pub fn webrtc_streamer(&self) -> Arc<WebRtcStreamer> {
self.webrtc_streamer.clone()
}
/// Get the MJPEG stream handler
pub fn mjpeg_handler(&self) -> Arc<MjpegStreamHandler> {
self.streamer.mjpeg_handler()
@@ -812,6 +807,16 @@ impl VideoStreamManager {
self.webrtc_streamer.get_pipeline_config().await
}
/// Get current video codec type
pub async fn current_video_codec(&self) -> crate::video::encoder::VideoCodecType {
self.webrtc_streamer.current_video_codec().await
}
/// Check if hardware encoding is in use
pub async fn is_hardware_encoding(&self) -> bool {
self.webrtc_streamer.is_hardware_encoding().await
}
/// Set video codec for the shared video pipeline
///
/// This allows external consumers (like RustDesk) to set the video codec

View File

@@ -12,7 +12,9 @@ use tokio::sync::RwLock;
use tracing::{debug, error, info, trace, warn};
use super::csi_bridge;
use super::device::{enumerate_devices, find_best_device, parse_bridge_kind, VideoDevice, VideoDeviceInfo};
use super::device::{
enumerate_devices, find_best_device, parse_bridge_kind, VideoDevice, VideoDeviceInfo,
};
use super::format::{PixelFormat, Resolution};
use super::frame::{FrameBuffer, FrameBufferPool, VideoFrame};
use super::is_csi_hdmi_bridge;
@@ -20,13 +22,9 @@ use crate::error::{AppError, Result};
use crate::events::{EventBus, SystemEvent};
use crate::stream::MjpegStreamHandler;
use crate::utils::LogThrottler;
use crate::video::capture_limits::{should_validate_jpeg_frame, MIN_CAPTURE_FRAME_SIZE};
use crate::video::v4l2r_capture::{is_source_changed_error, BridgeContext, V4l2rCaptureStream};
/// Minimum valid frame size for capture
const MIN_CAPTURE_FRAME_SIZE: usize = 128;
/// Validate JPEG header every N frames to reduce overhead
const JPEG_VALIDATE_INTERVAL: u64 = 30;
/// Streamer configuration
#[derive(Debug, Clone)]
pub struct StreamerConfig {
@@ -477,11 +475,10 @@ impl Streamer {
);
return Ok(preferred);
}
let fmt = device
.formats
.first()
.map(|f| f.format)
.ok_or_else(|| AppError::VideoError("No supported formats found".to_string()))?;
let fmt =
device.formats.first().map(|f| f.format).ok_or_else(|| {
AppError::VideoError("No supported formats found".to_string())
})?;
info!(
"select_format: CSI bridge with signal, preferred {:?} unavailable, selected {:?} from {:?}",
preferred,
@@ -916,9 +913,7 @@ impl Streamer {
"CSI open probe reports no signal ({:?}), will soft-restart",
status
);
set_retry(
backoff_secs(no_signal_restart_count).saturating_mul(1000),
);
set_retry(backoff_secs(no_signal_restart_count).saturating_mul(1000));
go_offline();
set_state(status.into());
last_error = Some(format!("CaptureNoSignal({})", kind));
@@ -952,8 +947,9 @@ impl Streamer {
// restart path. This lets CSI bridges recover on their
// own when the source comes back (resolution change,
// host reboot, HDMI cable re-plug).
let was_no_signal =
handle.block_on(async { self.state().await }).is_no_signal_like();
let was_no_signal = handle
.block_on(async { self.state().await })
.is_no_signal_like();
if !was_no_signal {
error!(
"Failed to open device {:?}: {}",
@@ -965,9 +961,7 @@ impl Streamer {
break 'session;
}
debug!(
"Open failed in NoSignal-like state, backing off before soft-restart"
);
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));
@@ -1040,9 +1034,7 @@ 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),
);
set_retry(backoff_secs(no_signal_restart_count).saturating_mul(1000));
go_offline();
set_state(StreamerState::NoSignal);
need_soft_restart = true;
@@ -1112,15 +1104,13 @@ impl Streamer {
if is_transient_signal_error {
if os_err == Some(71) {
warn!(
"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"),
)
});
warn!("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 {
go_offline();
set_state(StreamerState::UvcUsbError);
@@ -1133,9 +1123,7 @@ impl Streamer {
e
);
}
set_retry(
backoff_secs(no_signal_restart_count).saturating_mul(1000),
);
set_retry(backoff_secs(no_signal_restart_count).saturating_mul(1000));
go_offline();
set_state(StreamerState::NoSignal);
need_soft_restart = true;
@@ -1165,7 +1153,7 @@ impl Streamer {
validate_counter = validate_counter.wrapping_add(1);
if pixel_format.is_compressed()
&& validate_counter.is_multiple_of(JPEG_VALIDATE_INTERVAL)
&& should_validate_jpeg_frame(validate_counter)
&& !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size])
{
continue 'capture;
@@ -1567,7 +1555,10 @@ fn probe_subdev_signal(
let fd = match csi_bridge::open_subdev(subdev_path) {
Ok(f) => f,
Err(e) => {
debug!("probe_subdev_signal: failed to open {:?}: {}", subdev_path, e);
debug!(
"probe_subdev_signal: failed to open {:?}: {}",
subdev_path, e
);
return Some(crate::video::SignalStatus::NoSignal);
}
};
@@ -1608,9 +1599,7 @@ fn wait_subdev_for_source_change(
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"
);
info!("Subdev SOURCE_CHANGE during no-signal wait, retrying open immediately");
return;
}
Ok(false) => continue,

47
src/video/traits.rs Normal file
View File

@@ -0,0 +1,47 @@
//! Traits for video output consumers (WebRTC, RTSP, RustDesk, etc.)
use std::path::PathBuf;
use std::sync::Arc;
use super::types::{
BitratePreset, PixelFormat, Resolution, SharedVideoPipeline, SharedVideoPipelineConfig,
SharedVideoPipelineStats, VideoCodecType,
};
use crate::error::Result;
use crate::events::EventBus;
use crate::hid::HidController;
/// Trait for video output consumers that receive encoded video frames.
///
/// Implemented by `WebRtcStreamer`. `VideoStreamManager` depends on this
/// trait instead of the concrete type, breaking the video <-> webrtc
/// circular import.
#[async_trait::async_trait]
pub trait VideoOutput: Send + Sync {
async fn set_event_bus(&self, events: Arc<EventBus>);
async fn update_video_config(&self, resolution: Resolution, format: PixelFormat, fps: u32);
async fn set_capture_device(
&self,
device_path: PathBuf,
jpeg_quality: u8,
subdev_path: Option<PathBuf>,
bridge_kind: Option<String>,
v4l2_driver: Option<String>,
);
async fn current_video_codec(&self) -> VideoCodecType;
async fn is_hardware_encoding(&self) -> bool;
async fn close_all_sessions(&self);
async fn close_all_sessions_and_release_device(&self) -> usize;
async fn session_count(&self) -> usize;
async fn set_hid_controller(&self, hid: Arc<HidController>);
async fn set_audio_enabled(&self, enabled: bool) -> Result<()>;
async fn is_audio_enabled(&self) -> bool;
async fn reconnect_audio_sources(&self);
async fn ensure_video_pipeline_for_external(&self) -> Result<Arc<SharedVideoPipeline>>;
async fn get_pipeline_config(&self) -> Option<SharedVideoPipelineConfig>;
async fn set_video_codec(&self, codec: VideoCodecType) -> Result<()>;
async fn set_bitrate_preset(&self, preset: BitratePreset) -> Result<()>;
async fn request_keyframe(&self) -> Result<()>;
async fn current_video_geometry(&self) -> (Resolution, PixelFormat, u32);
async fn pipeline_stats(&self) -> Option<SharedVideoPipelineStats>;
}

22
src/video/types.rs Normal file
View File

@@ -0,0 +1,22 @@
//! Re-exports of shared video types used by other modules (e.g., webrtc)
//!
//! External modules should import from `crate::video::types` instead of
//! reaching into internal submodules directly.
// From video::format
pub use super::format::{PixelFormat, Resolution};
// From video::frame
pub use super::frame::VideoFrame;
// From video::encoder (codec-level types)
pub use super::encoder::{BitratePreset, VideoCodecType};
// From video::encoder::registry
pub use super::encoder::registry::{EncoderBackend, VideoEncoderType};
// From video::shared_video_pipeline
pub use super::shared_video_pipeline::{
EncodedVideoFrame, PipelineStateNotification, SharedVideoPipeline, SharedVideoPipelineConfig,
SharedVideoPipelineStats,
};

View File

@@ -129,9 +129,7 @@ impl V4l2rCaptureStream {
subdev_dv_mode = Some(mode);
}
other => {
let status = other
.as_status()
.unwrap_or(SignalStatus::NoSignal);
let status = other.as_status().unwrap_or(SignalStatus::NoSignal);
debug!(
"Subdev {:?} reports no signal ({:?}) — refusing STREAMON",
subdev_path, status
@@ -200,7 +198,10 @@ impl V4l2rCaptureStream {
}
(Ok(f), _, _) => f,
(Err(e), _, _) => {
return Err(AppError::VideoError(format!("Failed to get device format: {}", e)));
return Err(AppError::VideoError(format!(
"Failed to get device format: {}",
e
)));
}
};
@@ -446,10 +447,7 @@ impl V4l2rCaptureStream {
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,
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));