mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 11:42:02 +08:00
refactor: 删除部分多余的代码和注释
This commit is contained in:
30
src/video/capture_limits.rs
Normal file
30
src/video/capture_limits.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
47
src/video/traits.rs
Normal 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
22
src/video/types.rs
Normal 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,
|
||||
};
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user