From f8a031c90c1e3920e7c76d37af41ebc7e8848a3f Mon Sep 17 00:00:00 2001 From: mofeng Date: Mon, 9 Feb 2026 14:54:46 +0800 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=A0=91?= =?UTF-8?q?=E8=8E=93=E6=B4=BE=20v4l2m2m=20=E7=BC=96=E7=A0=81=E5=99=A8?= =?UTF-8?q?=E6=8E=A2=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 3 +- .../cpp/common/platform/linux/linux.cpp | 15 ++++++-- libs/hwcodec/cpp/common/util.cpp | 6 ++-- libs/hwcodec/src/ffmpeg.rs | 32 ++++++++++++++++- libs/hwcodec/src/ffmpeg_ram/encode.rs | 34 +++++++++++++++++-- src/main.rs | 7 ++-- 6 files changed, 85 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c4f7dc1a..23687974 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,8 @@ serde_json = "1" # Logging tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "tracing-log"] } +tracing-log = "0.2" # Error handling thiserror = "2" diff --git a/libs/hwcodec/cpp/common/platform/linux/linux.cpp b/libs/hwcodec/cpp/common/platform/linux/linux.cpp index 92c5ff7e..9574de02 100644 --- a/libs/hwcodec/cpp/common/platform/linux/linux.cpp +++ b/libs/hwcodec/cpp/common/platform/linux/linux.cpp @@ -162,10 +162,19 @@ int linux_support_v4l2m2m() { }; // Check common V4L2 M2M device paths used by various ARM SoCs + // /dev/video10 - Standard on many SoCs + // /dev/video11 - Standard on many SoCs (often decoder) + // /dev/video0 - Some platforms (like RPi) might use this + // /dev/video1 - Alternate RPi path + // /dev/video2 - Alternate path + // /dev/video32 - Some Allwinner/Rockchip legacy const char *m2m_devices[] = { - "/dev/video10", // Common M2M encoder device - "/dev/video11", // Common M2M decoder device - "/dev/video0", // Some SoCs use video0 for M2M + "/dev/video10", + "/dev/video11", + "/dev/video0", + "/dev/video1", + "/dev/video2", + "/dev/video32", }; for (size_t i = 0; i < sizeof(m2m_devices) / sizeof(m2m_devices[0]); i++) { diff --git a/libs/hwcodec/cpp/common/util.cpp b/libs/hwcodec/cpp/common/util.cpp index a65d5a7f..9661bc2c 100644 --- a/libs/hwcodec/cpp/common/util.cpp +++ b/libs/hwcodec/cpp/common/util.cpp @@ -147,11 +147,11 @@ bool set_lantency_free(void *priv_data, const std::string &name) { // V4L2 M2M hardware encoder - minimize buffer latency if (name.find("v4l2m2m") != std::string::npos) { // Minimize number of output buffers for lower latency - if ((ret = av_opt_set_int(priv_data, "num_output_buffers", 2, 0)) < 0) { + if ((ret = av_opt_set_int(priv_data, "num_output_buffers", 4, 0)) < 0) { LOG_WARN(std::string("v4l2m2m set num_output_buffers failed, ret = ") + av_err2str(ret)); // Not fatal } - if ((ret = av_opt_set_int(priv_data, "num_capture_buffers", 2, 0)) < 0) { + if ((ret = av_opt_set_int(priv_data, "num_capture_buffers", 4, 0)) < 0) { LOG_WARN(std::string("v4l2m2m set num_capture_buffers failed, ret = ") + av_err2str(ret)); // Not fatal } @@ -500,4 +500,4 @@ bool has_flag_could_not_find_ref_with_poc() { extern "C" void hwcodec_set_flag_could_not_find_ref_with_poc() { util_decode::g_flag_could_not_find_ref_with_poc = true; -} \ No newline at end of file +} diff --git a/libs/hwcodec/src/ffmpeg.rs b/libs/hwcodec/src/ffmpeg.rs index bb57dd7f..0ae134d1 100644 --- a/libs/hwcodec/src/ffmpeg.rs +++ b/libs/hwcodec/src/ffmpeg.rs @@ -6,6 +6,7 @@ include!(concat!(env!("OUT_DIR"), "/ffmpeg_ffi.rs")); use serde_derive::{Deserialize, Serialize}; +use std::env; #[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] pub enum AVHWDeviceType { @@ -53,7 +54,36 @@ pub extern "C" fn hwcodec_av_log_callback(level: i32, message: *const std::os::r pub(crate) fn init_av_log() { static INIT: std::sync::Once = std::sync::Once::new(); INIT.call_once(|| unsafe { - av_log_set_level(AV_LOG_ERROR as i32); + av_log_set_level(parse_ffmpeg_log_level()); hwcodec_set_av_log_callback(); }); } + +fn parse_ffmpeg_log_level() -> i32 { + let raw = match env::var("ONE_KVM_FFMPEG_LOG") { + Ok(value) => value, + Err(_) => return AV_LOG_ERROR as i32, + }; + + let value = raw.trim().to_ascii_lowercase(); + if value.is_empty() { + return AV_LOG_ERROR as i32; + } + + if let Ok(level) = value.parse::() { + return level; + } + + match value.as_str() { + "quiet" => AV_LOG_QUIET as i32, + "panic" => AV_LOG_PANIC as i32, + "fatal" => AV_LOG_FATAL as i32, + "error" => AV_LOG_ERROR as i32, + "warn" | "warning" => AV_LOG_WARNING as i32, + "info" => AV_LOG_INFO as i32, + "verbose" => AV_LOG_VERBOSE as i32, + "debug" => AV_LOG_DEBUG as i32, + "trace" => AV_LOG_TRACE as i32, + _ => AV_LOG_ERROR as i32, + } +} diff --git a/libs/hwcodec/src/ffmpeg_ram/encode.rs b/libs/hwcodec/src/ffmpeg_ram/encode.rs index dff0a135..263b04fc 100644 --- a/libs/hwcodec/src/ffmpeg_ram/encode.rs +++ b/libs/hwcodec/src/ffmpeg_ram/encode.rs @@ -352,16 +352,46 @@ impl Encoder { debug!("Encoder {} created successfully", codec.name); let mut passed = false; let mut last_err: Option = None; + let is_v4l2m2m = codec.name.contains("v4l2m2m"); - let max_attempts = 1; + let max_attempts = if is_v4l2m2m { 5 } else { 1 }; for attempt in 0..max_attempts { + if is_v4l2m2m { + encoder.request_keyframe(); + } let pts = (attempt as i64) * 33; // 33ms is an approximation for 30 FPS (1000 / 30) let start = std::time::Instant::now(); match encoder.encode(&yuv, pts) { Ok(frames) => { let elapsed = start.elapsed().as_millis(); - if frames.len() == 1 { + if is_v4l2m2m { + if !frames.is_empty() && elapsed < TEST_TIMEOUT_MS as _ { + debug!( + "Encoder {} test passed on attempt {} (frames: {})", + codec.name, + attempt + 1, + frames.len() + ); + res.push(codec.clone()); + passed = true; + break; + } else if frames.is_empty() { + debug!( + "Encoder {} test produced no output on attempt {}", + codec.name, + attempt + 1 + ); + } else { + debug!( + "Encoder {} test failed on attempt {} - frames: {}, timeout: {}ms", + codec.name, + attempt + 1, + frames.len(), + elapsed + ); + } + } else if frames.len() == 1 { if frames[0].key == 1 && elapsed < TEST_TIMEOUT_MS as _ { debug!( "Encoder {} test passed on attempt {}", diff --git a/src/main.rs b/src/main.rs index d360ae80..43f04907 100644 --- a/src/main.rs +++ b/src/main.rs @@ -712,10 +712,13 @@ fn init_logging(level: LogLevel, verbose_count: u8) { let env_filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into()); - tracing_subscriber::registry() + if let Err(err) = tracing_subscriber::registry() .with(env_filter) .with(tracing_subscriber::fmt::layer()) - .init(); + .try_init() + { + eprintln!("failed to initialize tracing: {}", err); + } } /// Get the application data directory From 72eb2c450dc3535e3f7a8cd7508ddee72bf9001c Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Tue, 10 Feb 2026 13:52:52 +0800 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20=E8=BF=81=E7=A7=BB=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E9=87=87=E9=9B=86=E5=88=B0=20v4l2r=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=9A=E5=B9=B3=E9=9D=A2=E8=AE=BE=E5=A4=87=E5=B9=B6?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=9E=84=E5=BB=BA=E5=A4=B4=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 V4L2 采集依赖从 v4l 切换到 v4l2r - 新增基于 v4l2r 的 mmap 采集实现,优先使用 VIDEO_CAPTURE_MPLANE - 更新像素格式转换与设备枚举逻辑,探测阶段改为只读打开 - 增加采集错误日志节流,避免 dqbuf EINVAL 日志风暴 - 交叉编译镜像安装更新的 Linux 内核头文件供 bindgen 使用 --- Cargo.toml | 2 +- build/cross/Dockerfile.arm64 | 21 ++- build/cross/Dockerfile.armv7 | 21 ++- build/cross/Dockerfile.x86_64 | 23 ++- src/stream/mjpeg_streamer.rs | 135 ++++++-------- src/video/capture.rs | 175 ++++++++---------- src/video/device.rs | 266 ++++++++++++++++----------- src/video/format.rs | 50 +++-- src/video/mod.rs | 1 + src/video/shared_video_pipeline.rs | 113 ++++++------ src/video/streamer.rs | 137 ++++++-------- src/video/v4l2r_capture.rs | 284 +++++++++++++++++++++++++++++ 12 files changed, 779 insertions(+), 449 deletions(-) create mode 100644 src/video/v4l2r_capture.rs diff --git a/Cargo.toml b/Cargo.toml index 23687974..1ac350c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ clap = { version = "4", features = ["derive"] } time = "0.3" # Video capture (V4L2) -v4l = "0.14" +v4l2r = "0.0.7" # JPEG encoding (libjpeg-turbo, SIMD accelerated) turbojpeg = "1.3" diff --git a/build/cross/Dockerfile.arm64 b/build/cross/Dockerfile.arm64 index d0542c1b..9b6e849a 100644 --- a/build/cross/Dockerfile.arm64 +++ b/build/cross/Dockerfile.arm64 @@ -3,9 +3,13 @@ FROM debian:11 +# Linux headers used by v4l2r bindgen +ARG LINUX_HEADERS_VERSION=6.6 +ARG LINUX_HEADERS_SHA256= + # Set Rustup mirrors (Aliyun) -ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \ - RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup +#ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \ +# RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup # Install Rust toolchain RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -31,6 +35,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ meson \ ninja-build \ wget \ + xz-utils \ file \ gcc-aarch64-linux-gnu \ g++-aarch64-linux-gnu \ @@ -47,10 +52,22 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libv4l-dev:arm64 \ libudev-dev:arm64 \ zlib1g-dev:arm64 \ + linux-libc-dev:arm64 \ # Note: libjpeg-turbo, libyuv, libvpx, libx264, libx265, libopus are built from source below for static linking libdrm-dev:arm64 \ && rm -rf /var/lib/apt/lists/* +# Install newer V4L2 headers for v4l2r bindgen +RUN mkdir -p /opt/v4l2-headers \ + && wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \ + && if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \ + && tar -xf /tmp/linux-headers.tar.xz -C /tmp \ + && cd /tmp/linux-${LINUX_HEADERS_VERSION} \ + && make ARCH=arm64 headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \ + && rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz + +ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include + # Build static libjpeg-turbo from source (cross-compile for ARM64) RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \ && cd /tmp/libjpeg-turbo \ diff --git a/build/cross/Dockerfile.armv7 b/build/cross/Dockerfile.armv7 index 3bebfc6f..fc908f82 100644 --- a/build/cross/Dockerfile.armv7 +++ b/build/cross/Dockerfile.armv7 @@ -3,9 +3,13 @@ FROM debian:11 +# Linux headers used by v4l2r bindgen +ARG LINUX_HEADERS_VERSION=6.6 +ARG LINUX_HEADERS_SHA256= + # Set Rustup mirrors (Aliyun) -ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \ - RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup +#ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \ +# RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup # Install Rust toolchain RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -31,6 +35,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ meson \ ninja-build \ wget \ + xz-utils \ file \ gcc-arm-linux-gnueabihf \ g++-arm-linux-gnueabihf \ @@ -46,10 +51,22 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libasound2-dev:armhf \ libv4l-dev:armhf \ libudev-dev:armhf \ + linux-libc-dev:armhf \ zlib1g-dev:armhf \ libdrm-dev:armhf \ && rm -rf /var/lib/apt/lists/* +# Install newer V4L2 headers for v4l2r bindgen +RUN mkdir -p /opt/v4l2-headers \ + && wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \ + && if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \ + && tar -xf /tmp/linux-headers.tar.xz -C /tmp \ + && cd /tmp/linux-${LINUX_HEADERS_VERSION} \ + && make ARCH=arm headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \ + && rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz + +ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include + # Build static libjpeg-turbo from source (cross-compile for ARMv7) RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \ && cd /tmp/libjpeg-turbo \ diff --git a/build/cross/Dockerfile.x86_64 b/build/cross/Dockerfile.x86_64 index 779b1b02..ebaf9a82 100644 --- a/build/cross/Dockerfile.x86_64 +++ b/build/cross/Dockerfile.x86_64 @@ -3,9 +3,13 @@ FROM debian:11 +# Linux headers used by v4l2r bindgen +ARG LINUX_HEADERS_VERSION=6.6 +ARG LINUX_HEADERS_SHA256= + # Set Rustup mirrors (Aliyun) -ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \ - RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup +#ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \ +# RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup # Install Rust toolchain RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -29,6 +33,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libclang-dev \ llvm \ wget \ + xz-utils \ # Autotools for libopus (requires autoreconf) autoconf \ automake \ @@ -37,6 +42,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libasound2-dev \ libv4l-dev \ libudev-dev \ + linux-libc-dev \ zlib1g-dev \ # Note: libjpeg-turbo, libx264, libx265, libopus are built from source below for static linking libva-dev \ @@ -49,6 +55,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libxdmcp-dev \ && rm -rf /var/lib/apt/lists/* +# Install newer V4L2 headers for v4l2r bindgen +RUN mkdir -p /opt/v4l2-headers \ + && wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \ + && if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \ + && tar -xf /tmp/linux-headers.tar.xz -C /tmp \ + && cd /tmp/linux-${LINUX_HEADERS_VERSION} \ + && make ARCH=x86 headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \ + && rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz + +ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include + # Build static libjpeg-turbo from source (needed by libyuv) RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \ && cd /tmp/libjpeg-turbo \ @@ -208,4 +225,4 @@ RUN rustup target add x86_64-unknown-linux-gnu # Configure environment for static linking ENV PKG_CONFIG_ALLOW_CROSS=1\ FFMPEG_STATIC=1 \ - LIBYUV_STATIC=1 \ No newline at end of file + LIBYUV_STATIC=1 diff --git a/src/stream/mjpeg_streamer.rs b/src/stream/mjpeg_streamer.rs index 79e0fb38..d9219123 100644 --- a/src/stream/mjpeg_streamer.rs +++ b/src/stream/mjpeg_streamer.rs @@ -16,17 +16,15 @@ //! Note: Audio WebSocket is handled separately by audio_ws.rs (/api/ws/audio) use std::io; +use std::collections::HashMap; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::time::Duration; use tokio::sync::{Mutex, RwLock}; use tracing::{error, info, warn}; -use v4l::buffer::Type as BufferType; -use v4l::io::traits::CaptureStream; -use v4l::prelude::*; -use v4l::video::Capture; -use v4l::video::capture::Parameters; -use v4l::Format; +use crate::video::v4l2r_capture::V4l2rCaptureStream; +use crate::utils::LogThrottler; use crate::audio::AudioController; use crate::error::{AppError, Result}; @@ -491,8 +489,7 @@ impl MjpegStreamer { } }; - let mut device_opt: Option = None; - let mut format_opt: Option = None; + let mut stream_opt: Option = None; let mut last_error: Option = None; for attempt in 0..MAX_RETRIES { @@ -501,8 +498,18 @@ impl MjpegStreamer { return; } - let device = match Device::with_path(&device_path) { - Ok(d) => d, + match V4l2rCaptureStream::open( + &device_path, + config.resolution, + config.format, + config.fps, + 4, + Duration::from_secs(2), + ) { + Ok(stream) => { + stream_opt = Some(stream); + break; + } Err(e) => { let err_str = e.to_string(); if err_str.contains("busy") || err_str.contains("resource") { @@ -519,42 +526,12 @@ impl MjpegStreamer { last_error = Some(err_str); break; } - }; - - let requested = Format::new( - config.resolution.width, - config.resolution.height, - config.format.to_fourcc(), - ); - - match device.set_format(&requested) { - Ok(actual) => { - device_opt = Some(device); - format_opt = Some(actual); - break; - } - Err(e) => { - let err_str = e.to_string(); - if err_str.contains("busy") || err_str.contains("resource") { - warn!( - "Device busy on set_format attempt {}/{}, retrying in {}ms...", - attempt + 1, - MAX_RETRIES, - RETRY_DELAY_MS - ); - std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS)); - last_error = Some(err_str); - continue; - } - last_error = Some(err_str); - break; - } } } - let (device, actual_format) = match (device_opt, format_opt) { - (Some(d), Some(f)) => (d, f), - _ => { + let mut stream = match stream_opt { + Some(stream) => stream, + None => { error!( "Failed to open device {:?}: {}", device_path, @@ -567,40 +544,36 @@ impl MjpegStreamer { } }; + let resolution = stream.resolution(); + let pixel_format = stream.format(); + let stride = stream.stride(); + info!( "Capture format: {}x{} {:?} stride={}", - actual_format.width, actual_format.height, actual_format.fourcc, actual_format.stride + resolution.width, resolution.height, pixel_format, stride ); - let resolution = Resolution::new(actual_format.width, actual_format.height); - let pixel_format = - PixelFormat::from_fourcc(actual_format.fourcc).unwrap_or(config.format); - - if config.fps > 0 { - if let Err(e) = device.set_params(&Parameters::with_fps(config.fps)) { - warn!("Failed to set hardware FPS: {}", e); - } - } - - let mut stream = match MmapStream::with_buffers(&device, BufferType::VideoCapture, 4) { - Ok(s) => s, - Err(e) => { - error!("Failed to create capture stream: {}", e); - set_state(MjpegStreamerState::Error); - self.mjpeg_handler.set_offline(); - self.direct_active.store(false, Ordering::SeqCst); - return; - } - }; - let buffer_pool = Arc::new(FrameBufferPool::new(8)); let mut signal_present = true; - let mut sequence: u64 = 0; let mut validate_counter: u64 = 0; + let capture_error_throttler = LogThrottler::with_secs(5); + let mut suppressed_capture_errors: HashMap = HashMap::new(); + + let classify_capture_error = |err: &std::io::Error| -> String { + let message = err.to_string(); + if message.contains("dqbuf failed") && message.contains("EINVAL") { + "capture_dqbuf_einval".to_string() + } else if message.contains("dqbuf failed") { + "capture_dqbuf".to_string() + } else { + format!("capture_{:?}", err.kind()) + } + }; while !self.direct_stop.load(Ordering::Relaxed) { - let (buf, meta) = match stream.next() { - Ok(frame_data) => frame_data, + let mut owned = buffer_pool.take(MIN_CAPTURE_FRAME_SIZE); + let meta = match stream.next_into(&mut owned) { + Ok(meta) => meta, Err(e) => { if e.kind() == io::ErrorKind::TimedOut { if signal_present { @@ -628,12 +601,23 @@ impl MjpegStreamer { return; } - error!("Capture error: {}", e); + let key = classify_capture_error(&e); + if capture_error_throttler.should_log(&key) { + let suppressed = suppressed_capture_errors.remove(&key).unwrap_or(0); + if suppressed > 0 { + error!("Capture error: {} (suppressed {} repeats)", e, suppressed); + } else { + error!("Capture error: {}", e); + } + } else { + let counter = suppressed_capture_errors.entry(key).or_insert(0); + *counter = counter.saturating_add(1); + } continue; } }; - let frame_size = meta.bytesused as usize; + let frame_size = meta.bytes_used; if frame_size < MIN_CAPTURE_FRAME_SIZE { continue; } @@ -641,22 +625,19 @@ impl MjpegStreamer { validate_counter = validate_counter.wrapping_add(1); if pixel_format.is_compressed() && validate_counter % JPEG_VALIDATE_INTERVAL == 0 - && !VideoFrame::is_valid_jpeg_bytes(&buf[..frame_size]) + && !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size]) { continue; } - let mut owned = buffer_pool.take(frame_size); - owned.resize(frame_size, 0); - owned[..frame_size].copy_from_slice(&buf[..frame_size]); + owned.truncate(frame_size); let frame = VideoFrame::from_pooled( Arc::new(FrameBuffer::new(owned, Some(buffer_pool.clone()))), resolution, pixel_format, - actual_format.stride, - sequence, + stride, + meta.sequence, ); - sequence = sequence.wrapping_add(1); if !signal_present { signal_present = true; diff --git a/src/video/capture.rs b/src/video/capture.rs index 8701521f..e2218ac9 100644 --- a/src/video/capture.rs +++ b/src/video/capture.rs @@ -2,6 +2,7 @@ //! //! Provides async video capture using memory-mapped buffers. +use std::collections::HashMap; use std::io; use std::path::{Path, PathBuf}; use bytes::Bytes; @@ -10,16 +11,12 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::{watch, Mutex}; use tracing::{debug, error, info, warn}; -use v4l::buffer::Type as BufferType; -use v4l::io::traits::CaptureStream; -use v4l::prelude::*; -use v4l::video::capture::Parameters; -use v4l::video::Capture; -use v4l::Format; use super::format::{PixelFormat, Resolution}; use super::frame::VideoFrame; use crate::error::{AppError, Result}; +use crate::utils::LogThrottler; +use crate::video::v4l2r_capture::V4l2rCaptureStream; /// Default number of capture buffers (reduced from 4 to 2 for lower latency) const DEFAULT_BUFFER_COUNT: u32 = 2; @@ -280,9 +277,15 @@ fn run_capture( return Ok(()); } - // Open device - let device = match Device::with_path(&config.device_path) { - Ok(d) => d, + let stream = match V4l2rCaptureStream::open( + &config.device_path, + config.resolution, + config.format, + config.fps, + config.buffer_count, + config.timeout, + ) { + Ok(stream) => stream, Err(e) => { let err_str = e.to_string(); if err_str.contains("busy") || err_str.contains("resource") { @@ -306,34 +309,7 @@ fn run_capture( } }; - // Set format - let format = Format::new( - config.resolution.width, - config.resolution.height, - config.format.to_fourcc(), - ); - - let actual_format = match device.set_format(&format) { - Ok(f) => f, - Err(e) => { - let err_str = e.to_string(); - if err_str.contains("busy") || err_str.contains("resource") { - warn!( - "Device busy on set_format attempt {}/{}, retrying in {}ms...", - attempt + 1, - MAX_RETRIES, - RETRY_DELAY_MS - ); - std::thread::sleep(Duration::from_millis(RETRY_DELAY_MS)); - last_error = Some(AppError::VideoError(format!("Failed to set format: {}", e))); - continue; - } - return Err(AppError::VideoError(format!("Failed to set format: {}", e))); - } - }; - - // Device opened and format set successfully - proceed with capture - return run_capture_inner(config, state, stats, stop_flag, device, actual_format); + return run_capture_inner(config, state, stats, stop_flag, stream); } // All retries exhausted @@ -348,48 +324,16 @@ fn run_capture_inner( state: &watch::Sender, stats: &Arc>, stop_flag: &AtomicBool, - device: Device, - actual_format: Format, + mut stream: V4l2rCaptureStream, ) -> Result<()> { + let resolution = stream.resolution(); + let pixel_format = stream.format(); + let stride = stream.stride(); info!( "Capture format: {}x{} {:?} stride={}", - actual_format.width, actual_format.height, actual_format.fourcc, actual_format.stride + resolution.width, resolution.height, pixel_format, stride ); - - // Try to set hardware FPS (V4L2 VIDIOC_S_PARM) - if config.fps > 0 { - match device.set_params(&Parameters::with_fps(config.fps)) { - Ok(actual_params) => { - // Extract actual FPS from returned interval (numerator/denominator) - let actual_hw_fps = if actual_params.interval.numerator > 0 { - actual_params.interval.denominator / actual_params.interval.numerator - } else { - 0 - }; - - if actual_hw_fps == config.fps { - info!("Hardware FPS set successfully: {} fps", actual_hw_fps); - } else if actual_hw_fps > 0 { - info!( - "Hardware FPS coerced: requested {} fps, got {} fps", - config.fps, actual_hw_fps - ); - } else { - warn!("Hardware FPS setting returned invalid interval"); - } - } - Err(e) => { - warn!("Failed to set hardware FPS: {}", e); - } - } - } - - // Create stream with mmap buffers - let mut stream = - MmapStream::with_buffers(&device, BufferType::VideoCapture, config.buffer_count) - .map_err(|e| AppError::VideoError(format!("Failed to create stream: {}", e)))?; - let _ = state.send(CaptureState::Running); info!("Capture started"); @@ -397,12 +341,25 @@ fn run_capture_inner( let mut fps_frame_count = 0u64; let mut fps_window_start = Instant::now(); let fps_window_duration = Duration::from_secs(1); + let mut scratch = Vec::new(); + let capture_error_throttler = LogThrottler::with_secs(5); + let mut suppressed_capture_errors: HashMap = HashMap::new(); + + let classify_capture_error = |err: &std::io::Error| -> String { + let message = err.to_string(); + if message.contains("dqbuf failed") && message.contains("EINVAL") { + "capture_dqbuf_einval".to_string() + } else if message.contains("dqbuf failed") { + "capture_dqbuf".to_string() + } else { + format!("capture_{:?}", err.kind()) + } + }; // Main capture loop while !stop_flag.load(Ordering::Relaxed) { - // Try to capture a frame - let (_buf, meta) = match stream.next() { - Ok(frame_data) => frame_data, + let meta = match stream.next_into(&mut scratch) { + Ok(meta) => meta, Err(e) => { if e.kind() == io::ErrorKind::TimedOut { warn!("Capture timeout - no signal?"); @@ -432,19 +389,30 @@ fn run_capture_inner( }); } - error!("Capture error: {}", e); + let key = classify_capture_error(&e); + if capture_error_throttler.should_log(&key) { + let suppressed = suppressed_capture_errors.remove(&key).unwrap_or(0); + if suppressed > 0 { + error!("Capture error: {} (suppressed {} repeats)", e, suppressed); + } else { + error!("Capture error: {}", e); + } + } else { + let counter = suppressed_capture_errors.entry(key).or_insert(0); + *counter = counter.saturating_add(1); + } continue; } }; // Use actual bytes used, not buffer size - let frame_size = meta.bytesused as usize; + let frame_size = meta.bytes_used; // Validate frame if frame_size < MIN_FRAME_SIZE { debug!( "Dropping small frame: {} bytes (bytesused={})", - frame_size, meta.bytesused + frame_size, meta.bytes_used ); continue; } @@ -470,6 +438,10 @@ fn run_capture_inner( s.current_fps = (fps_frame_count as f32 / elapsed.as_secs_f32()).max(0.0); } } + + if *state.borrow() == CaptureState::NoSignal { + let _ = state.send(CaptureState::Running); + } } info!("Capture stopped"); @@ -525,38 +497,37 @@ fn grab_single_frame( resolution: Resolution, format: PixelFormat, ) -> Result { - let device = Device::with_path(device_path) - .map_err(|e| AppError::VideoError(format!("Failed to open device: {}", e)))?; - - let fmt = Format::new(resolution.width, resolution.height, format.to_fourcc()); - let actual = device - .set_format(&fmt) - .map_err(|e| AppError::VideoError(format!("Failed to set format: {}", e)))?; - - let mut stream = MmapStream::with_buffers(&device, BufferType::VideoCapture, 2) - .map_err(|e| AppError::VideoError(format!("Failed to create stream: {}", e)))?; + let mut stream = V4l2rCaptureStream::open( + device_path, + resolution, + format, + 0, + 2, + Duration::from_secs(DEFAULT_TIMEOUT), + )?; + let actual_resolution = stream.resolution(); + let actual_format = stream.format(); + let actual_stride = stream.stride(); + let mut scratch = Vec::new(); // Try to get a valid frame (skip first few which might be bad) for attempt in 0..5 { - match stream.next() { - Ok((buf, _meta)) => { - if buf.len() >= MIN_FRAME_SIZE { - let actual_format = PixelFormat::from_fourcc(actual.fourcc).unwrap_or(format); - + match stream.next_into(&mut scratch) { + Ok(meta) => { + if meta.bytes_used >= MIN_FRAME_SIZE { return Ok(VideoFrame::new( - Bytes::copy_from_slice(buf), - Resolution::new(actual.width, actual.height), + Bytes::copy_from_slice(&scratch[..meta.bytes_used]), + actual_resolution, actual_format, - actual.stride, + actual_stride, 0, )); } } - Err(e) => { - if attempt == 4 { - return Err(AppError::VideoError(format!("Failed to grab frame: {}", e))); - } + Err(e) if attempt == 4 => { + return Err(AppError::VideoError(format!("Failed to grab frame: {}", e))); } + Err(_) => {} } } diff --git a/src/video/device.rs b/src/video/device.rs index c99b4786..543340f6 100644 --- a/src/video/device.rs +++ b/src/video/device.rs @@ -1,15 +1,17 @@ //! V4L2 device enumeration and capability query use serde::{Deserialize, Serialize}; +use std::fs::File; use std::path::{Path, PathBuf}; use std::sync::mpsc; use std::time::Duration; use tracing::{debug, info, warn}; -use v4l::capability::Flags; -use v4l::prelude::*; -use v4l::video::Capture; -use v4l::Format; -use v4l::FourCC; +use v4l2r::nix::errno::Errno; +use v4l2r::bindings::{v4l2_frmivalenum, v4l2_frmsizeenum}; +use v4l2r::ioctl::{ + self, Capabilities, Capability as V4l2rCapability, FormatIterator, FrmIvalTypes, FrmSizeTypes, +}; +use v4l2r::{Format as V4l2rFormat, QueueType}; use super::format::{PixelFormat, Resolution}; use crate::error::{AppError, Result}; @@ -81,7 +83,7 @@ pub struct DeviceCapabilities { /// Wrapper around a V4L2 video device pub struct VideoDevice { pub path: PathBuf, - device: Device, + fd: File, } impl VideoDevice { @@ -90,42 +92,54 @@ impl VideoDevice { let path = path.as_ref().to_path_buf(); debug!("Opening video device: {:?}", path); - let device = Device::with_path(&path).map_err(|e| { - AppError::VideoError(format!("Failed to open device {:?}: {}", path, e)) - })?; + let fd = File::options() + .read(true) + .write(true) + .open(&path) + .map_err(|e| AppError::VideoError(format!("Failed to open device {:?}: {}", path, e)))?; - Ok(Self { path, device }) + Ok(Self { path, fd }) + } + + /// Open a video device read-only (for probing/enumeration) + pub fn open_readonly(path: impl AsRef) -> Result { + let path = path.as_ref().to_path_buf(); + debug!("Opening video device (read-only): {:?}", path); + + let fd = File::options() + .read(true) + .open(&path) + .map_err(|e| AppError::VideoError(format!("Failed to open device {:?}: {}", path, e)))?; + + Ok(Self { path, fd }) } /// Get device capabilities pub fn capabilities(&self) -> Result { - let caps = self - .device - .query_caps() + let caps: V4l2rCapability = ioctl::querycap(&self.fd) .map_err(|e| AppError::VideoError(format!("Failed to query capabilities: {}", e)))?; + let flags = caps.device_caps(); Ok(DeviceCapabilities { - video_capture: caps.capabilities.contains(Flags::VIDEO_CAPTURE), - video_capture_mplane: caps.capabilities.contains(Flags::VIDEO_CAPTURE_MPLANE), - video_output: caps.capabilities.contains(Flags::VIDEO_OUTPUT), - streaming: caps.capabilities.contains(Flags::STREAMING), - read_write: caps.capabilities.contains(Flags::READ_WRITE), + video_capture: flags.contains(Capabilities::VIDEO_CAPTURE), + video_capture_mplane: flags.contains(Capabilities::VIDEO_CAPTURE_MPLANE), + video_output: flags.contains(Capabilities::VIDEO_OUTPUT), + streaming: flags.contains(Capabilities::STREAMING), + read_write: flags.contains(Capabilities::READWRITE), }) } /// Get detailed device information pub fn info(&self) -> Result { - let caps = self - .device - .query_caps() + let caps: V4l2rCapability = ioctl::querycap(&self.fd) .map_err(|e| AppError::VideoError(format!("Failed to query capabilities: {}", e)))?; - + let flags = caps.device_caps(); let capabilities = DeviceCapabilities { - video_capture: caps.capabilities.contains(Flags::VIDEO_CAPTURE), - video_capture_mplane: caps.capabilities.contains(Flags::VIDEO_CAPTURE_MPLANE), - video_output: caps.capabilities.contains(Flags::VIDEO_OUTPUT), - streaming: caps.capabilities.contains(Flags::STREAMING), - read_write: caps.capabilities.contains(Flags::READ_WRITE), + video_capture: flags.contains(Capabilities::VIDEO_CAPTURE), + video_capture_mplane: flags.contains(Capabilities::VIDEO_CAPTURE_MPLANE), + video_output: flags.contains(Capabilities::VIDEO_OUTPUT), + streaming: flags.contains(Capabilities::STREAMING), + read_write: flags.contains(Capabilities::READWRITE), }; let formats = self.enumerate_formats()?; @@ -141,7 +155,7 @@ impl VideoDevice { path: self.path.clone(), name: caps.card.clone(), driver: caps.driver.clone(), - bus_info: caps.bus.clone(), + bus_info: caps.bus_info.clone(), card: caps.card, formats, capabilities, @@ -154,16 +168,13 @@ impl VideoDevice { pub fn enumerate_formats(&self) -> Result> { let mut formats = Vec::new(); - // Get supported formats - let format_descs = self - .device - .enum_formats() - .map_err(|e| AppError::VideoError(format!("Failed to enumerate formats: {}", e)))?; + let queue = self.capture_queue_type()?; + let format_descs = FormatIterator::new(&self.fd, queue); for desc in format_descs { // Try to convert FourCC to our PixelFormat - if let Some(format) = PixelFormat::from_fourcc(desc.fourcc) { - let resolutions = self.enumerate_resolutions(desc.fourcc)?; + if let Some(format) = PixelFormat::from_v4l2r(desc.pixelformat) { + let resolutions = self.enumerate_resolutions(desc.pixelformat)?; formats.push(FormatInfo { format, @@ -173,7 +184,7 @@ impl VideoDevice { } else { debug!( "Skipping unsupported format: {:?} ({})", - desc.fourcc, desc.description + desc.pixelformat, desc.description ); } } @@ -185,46 +196,53 @@ impl VideoDevice { } /// Enumerate resolutions for a specific format - fn enumerate_resolutions(&self, fourcc: FourCC) -> Result> { + fn enumerate_resolutions(&self, fourcc: v4l2r::PixelFormat) -> Result> { let mut resolutions = Vec::new(); - // Try to enumerate frame sizes - match self.device.enum_framesizes(fourcc) { - Ok(sizes) => { - for size in sizes { - match size.size { - v4l::framesize::FrameSizeEnum::Discrete(d) => { - let fps = self - .enumerate_fps(fourcc, d.width, d.height) - .unwrap_or_default(); - resolutions.push(ResolutionInfo::new(d.width, d.height, fps)); - } - v4l::framesize::FrameSizeEnum::Stepwise(s) => { - // For stepwise, add some common resolutions - for res in [ - Resolution::VGA, - Resolution::HD720, - Resolution::HD1080, - Resolution::UHD4K, - ] { - if res.width >= s.min_width - && res.width <= s.max_width - && res.height >= s.min_height - && res.height <= s.max_height - { - let fps = self - .enumerate_fps(fourcc, res.width, res.height) - .unwrap_or_default(); - resolutions - .push(ResolutionInfo::new(res.width, res.height, fps)); + let mut index = 0u32; + loop { + match ioctl::enum_frame_sizes::(&self.fd, index, fourcc) { + Ok(size) => { + if let Some(size) = size.size() { + match size { + FrmSizeTypes::Discrete(d) => { + let fps = + self.enumerate_fps(fourcc, d.width, d.height).unwrap_or_default(); + resolutions.push(ResolutionInfo::new(d.width, d.height, fps)); + } + FrmSizeTypes::StepWise(s) => { + for res in [ + Resolution::VGA, + Resolution::HD720, + Resolution::HD1080, + Resolution::UHD4K, + ] { + if res.width >= s.min_width + && res.width <= s.max_width + && res.height >= s.min_height + && res.height <= s.max_height + { + let fps = self + .enumerate_fps(fourcc, res.width, res.height) + .unwrap_or_default(); + resolutions.push(ResolutionInfo::new(res.width, res.height, fps)); + } } } } } + index += 1; + } + Err(e) => { + let is_einval = matches!( + e, + v4l2r::ioctl::FrameSizeError::IoctlError(err) if err == Errno::EINVAL + ); + if !is_einval { + debug!("Failed to enumerate frame sizes for {:?}: {}", fourcc, e); + } + break; } - } - Err(e) => { - debug!("Failed to enumerate frame sizes for {:?}: {}", fourcc, e); } } @@ -236,36 +254,59 @@ impl VideoDevice { } /// Enumerate FPS for a specific resolution - fn enumerate_fps(&self, fourcc: FourCC, width: u32, height: u32) -> Result> { + fn enumerate_fps( + &self, + fourcc: v4l2r::PixelFormat, + width: u32, + height: u32, + ) -> Result> { let mut fps_list = Vec::new(); - match self.device.enum_frameintervals(fourcc, width, height) { - Ok(intervals) => { - for interval in intervals { - match interval.interval { - v4l::frameinterval::FrameIntervalEnum::Discrete(fraction) => { - if fraction.numerator > 0 { - let fps = fraction.denominator / fraction.numerator; - fps_list.push(fps); + let mut index = 0u32; + loop { + match ioctl::enum_frame_intervals::( + &self.fd, + index, + fourcc, + width, + height, + ) { + Ok(interval) => { + if let Some(interval) = interval.intervals() { + match interval { + FrmIvalTypes::Discrete(fraction) => { + if fraction.numerator > 0 { + let fps = fraction.denominator / fraction.numerator; + fps_list.push(fps); + } } - } - v4l::frameinterval::FrameIntervalEnum::Stepwise(step) => { - // Just pick max/min/step - if step.max.numerator > 0 { - let min_fps = step.max.denominator / step.max.numerator; - let max_fps = step.min.denominator / step.min.numerator; - fps_list.push(min_fps); - if max_fps != min_fps { - fps_list.push(max_fps); + FrmIvalTypes::StepWise(step) => { + if step.max.numerator > 0 { + let min_fps = step.max.denominator / step.max.numerator; + let max_fps = step.min.denominator / step.min.numerator; + fps_list.push(min_fps); + if max_fps != min_fps { + fps_list.push(max_fps); + } } } } } + index += 1; + } + Err(e) => { + let is_einval = matches!( + e, + v4l2r::ioctl::FrameIntervalsError::IoctlError(err) if err == Errno::EINVAL + ); + if !is_einval { + debug!( + "Failed to enumerate frame intervals for {:?} {}x{}: {}", + fourcc, width, height, e + ); + } + break; } - } - Err(_) => { - // If enumeration fails, assume 30fps - fps_list.push(30); } } @@ -275,20 +316,26 @@ impl VideoDevice { } /// Get current format - pub fn get_format(&self) -> Result { - self.device - .format() + pub fn get_format(&self) -> Result { + let queue = self.capture_queue_type()?; + ioctl::g_fmt(&self.fd, queue) .map_err(|e| AppError::VideoError(format!("Failed to get format: {}", e))) } /// Set capture format - pub fn set_format(&self, width: u32, height: u32, format: PixelFormat) -> Result { - let fmt = Format::new(width, height, format.to_fourcc()); + pub fn set_format(&self, width: u32, height: u32, format: PixelFormat) -> Result { + let queue = self.capture_queue_type()?; + let mut fmt: V4l2rFormat = ioctl::g_fmt(&self.fd, queue) + .map_err(|e| AppError::VideoError(format!("Failed to get format: {}", e)))?; + fmt.width = width; + fmt.height = height; + fmt.pixelformat = format.to_v4l2r(); - // Request the format - let actual = self - .device - .set_format(&fmt) + let mut fd = self + .fd + .try_clone() + .map_err(|e| AppError::VideoError(format!("Failed to clone device fd: {}", e)))?; + let actual: V4l2rFormat = ioctl::s_fmt(&mut fd, (queue, &fmt)) .map_err(|e| AppError::VideoError(format!("Failed to set format: {}", e)))?; if actual.width != width || actual.height != height { @@ -376,8 +423,21 @@ impl VideoDevice { } /// Get the inner device reference (for advanced usage) - pub fn inner(&self) -> &Device { - &self.device + pub fn inner(&self) -> &File { + &self.fd + } + + fn capture_queue_type(&self) -> Result { + let caps = self.capabilities()?; + if caps.video_capture { + Ok(QueueType::VideoCapture) + } else if caps.video_capture_mplane { + Ok(QueueType::VideoCaptureMplane) + } else { + Err(AppError::VideoError( + "Device does not expose a capture queue".to_string(), + )) + } } } @@ -446,7 +506,7 @@ fn probe_device_with_timeout(path: &Path, timeout: Duration) -> Option Result { - let device = VideoDevice::open(&path_for_thread)?; + let device = VideoDevice::open_readonly(&path_for_thread)?; device.info() })(); let _ = tx.send(result); diff --git a/src/video/format.rs b/src/video/format.rs index 4097ae6f..f794dcfe 100644 --- a/src/video/format.rs +++ b/src/video/format.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; -use v4l::format::fourcc; +use v4l2r::PixelFormat as V4l2rPixelFormat; /// Supported pixel formats #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -41,30 +41,29 @@ pub enum PixelFormat { } impl PixelFormat { - /// Convert to V4L2 FourCC - pub fn to_fourcc(&self) -> fourcc::FourCC { + /// Convert to V4L2 FourCC bytes + pub fn to_fourcc(&self) -> [u8; 4] { match self { - PixelFormat::Mjpeg => fourcc::FourCC::new(b"MJPG"), - PixelFormat::Jpeg => fourcc::FourCC::new(b"JPEG"), - PixelFormat::Yuyv => fourcc::FourCC::new(b"YUYV"), - PixelFormat::Yvyu => fourcc::FourCC::new(b"YVYU"), - PixelFormat::Uyvy => fourcc::FourCC::new(b"UYVY"), - PixelFormat::Nv12 => fourcc::FourCC::new(b"NV12"), - PixelFormat::Nv21 => fourcc::FourCC::new(b"NV21"), - PixelFormat::Nv16 => fourcc::FourCC::new(b"NV16"), - PixelFormat::Nv24 => fourcc::FourCC::new(b"NV24"), - PixelFormat::Yuv420 => fourcc::FourCC::new(b"YU12"), - PixelFormat::Yvu420 => fourcc::FourCC::new(b"YV12"), - PixelFormat::Rgb565 => fourcc::FourCC::new(b"RGBP"), - PixelFormat::Rgb24 => fourcc::FourCC::new(b"RGB3"), - PixelFormat::Bgr24 => fourcc::FourCC::new(b"BGR3"), - PixelFormat::Grey => fourcc::FourCC::new(b"GREY"), + PixelFormat::Mjpeg => *b"MJPG", + PixelFormat::Jpeg => *b"JPEG", + PixelFormat::Yuyv => *b"YUYV", + PixelFormat::Yvyu => *b"YVYU", + PixelFormat::Uyvy => *b"UYVY", + PixelFormat::Nv12 => *b"NV12", + PixelFormat::Nv21 => *b"NV21", + PixelFormat::Nv16 => *b"NV16", + PixelFormat::Nv24 => *b"NV24", + PixelFormat::Yuv420 => *b"YU12", + PixelFormat::Yvu420 => *b"YV12", + PixelFormat::Rgb565 => *b"RGBP", + PixelFormat::Rgb24 => *b"RGB3", + PixelFormat::Bgr24 => *b"BGR3", + PixelFormat::Grey => *b"GREY", } } /// Try to convert from V4L2 FourCC - pub fn from_fourcc(fourcc: fourcc::FourCC) -> Option { - let repr = fourcc.repr; + pub fn from_fourcc(repr: [u8; 4]) -> Option { match &repr { b"MJPG" => Some(PixelFormat::Mjpeg), b"JPEG" => Some(PixelFormat::Jpeg), @@ -85,6 +84,17 @@ impl PixelFormat { } } + /// Convert to v4l2r PixelFormat + pub fn to_v4l2r(&self) -> V4l2rPixelFormat { + V4l2rPixelFormat::from(&self.to_fourcc()) + } + + /// Convert from v4l2r PixelFormat + pub fn from_v4l2r(format: V4l2rPixelFormat) -> Option { + let repr: [u8; 4] = format.into(); + Self::from_fourcc(repr) + } + /// Check if format is compressed (JPEG/MJPEG) pub fn is_compressed(&self) -> bool { matches!(self, PixelFormat::Mjpeg | PixelFormat::Jpeg) diff --git a/src/video/mod.rs b/src/video/mod.rs index b5664f48..f13385a9 100644 --- a/src/video/mod.rs +++ b/src/video/mod.rs @@ -13,6 +13,7 @@ pub mod h264_pipeline; pub mod shared_video_pipeline; pub mod stream_manager; pub mod streamer; +pub mod v4l2r_capture; pub mod video_session; pub use capture::VideoCapturer; diff --git a/src/video/shared_video_pipeline.rs b/src/video/shared_video_pipeline.rs index 6ab721ff..55fd660f 100644 --- a/src/video/shared_video_pipeline.rs +++ b/src/video/shared_video_pipeline.rs @@ -18,6 +18,7 @@ use bytes::Bytes; use parking_lot::RwLock as ParkingRwLock; +use std::collections::HashMap; use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -32,16 +33,12 @@ const MIN_CAPTURE_FRAME_SIZE: usize = 128; const JPEG_VALIDATE_INTERVAL: u64 = 30; use crate::error::{AppError, Result}; +use crate::utils::LogThrottler; use crate::video::convert::{Nv12Converter, PixelConverter}; use crate::video::decoder::MjpegTurboDecoder; #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] use hwcodec::ffmpeg_hw::{last_error_message as ffmpeg_hw_last_error, HwMjpegH26xConfig, HwMjpegH26xPipeline}; -use v4l::buffer::Type as BufferType; -use v4l::io::traits::CaptureStream; -use v4l::prelude::*; -use v4l::video::Capture; -use v4l::video::capture::Parameters; -use v4l::Format; +use crate::video::v4l2r_capture::V4l2rCaptureStream; use crate::video::encoder::h264::{detect_best_encoder, H264Config, H264Encoder, H264InputFormat}; use crate::video::encoder::h265::{ detect_best_h265_encoder, H265Config, H265Encoder, H265InputFormat, @@ -1279,53 +1276,17 @@ impl SharedVideoPipeline { let frame_seq_tx = frame_seq_tx.clone(); let buffer_pool = buffer_pool.clone(); std::thread::spawn(move || { - let device = match Device::with_path(&device_path) { - Ok(d) => d, - Err(e) => { - error!("Failed to open device {:?}: {}", device_path, e); - let _ = pipeline.running.send(false); - pipeline.running_flag.store(false, Ordering::Release); - let _ = frame_seq_tx.send(1); - return; - } - }; - - let requested_format = Format::new( - config.resolution.width, - config.resolution.height, - config.input_format.to_fourcc(), - ); - - let actual_format = match device.set_format(&requested_format) { - Ok(f) => f, - Err(e) => { - error!("Failed to set capture format: {}", e); - let _ = pipeline.running.send(false); - pipeline.running_flag.store(false, Ordering::Release); - let _ = frame_seq_tx.send(1); - return; - } - }; - - let resolution = Resolution::new(actual_format.width, actual_format.height); - let pixel_format = - PixelFormat::from_fourcc(actual_format.fourcc).unwrap_or(config.input_format); - let stride = actual_format.stride; - - if config.fps > 0 { - if let Err(e) = device.set_params(&Parameters::with_fps(config.fps)) { - warn!("Failed to set hardware FPS: {}", e); - } - } - - let mut stream = match MmapStream::with_buffers( - &device, - BufferType::VideoCapture, + let mut stream = match V4l2rCaptureStream::open( + &device_path, + config.resolution, + config.input_format, + config.fps, buffer_count.max(1), + Duration::from_secs(2), ) { - Ok(s) => s, + Ok(stream) => stream, Err(e) => { - error!("Failed to create capture stream: {}", 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); @@ -1333,10 +1294,27 @@ impl SharedVideoPipeline { } }; + let resolution = stream.resolution(); + let pixel_format = stream.format(); + let stride = stream.stride(); + let mut no_subscribers_since: Option = None; let grace_period = Duration::from_secs(AUTO_STOP_GRACE_PERIOD_SECS); let mut sequence: u64 = 0; let mut validate_counter: u64 = 0; + let capture_error_throttler = LogThrottler::with_secs(5); + let mut suppressed_capture_errors: HashMap = HashMap::new(); + + let classify_capture_error = |err: &std::io::Error| -> String { + let message = err.to_string(); + if message.contains("dqbuf failed") && message.contains("EINVAL") { + "capture_dqbuf_einval".to_string() + } else if message.contains("dqbuf failed") { + "capture_dqbuf".to_string() + } else { + format!("capture_{:?}", err.kind()) + } + }; while pipeline.running_flag.load(Ordering::Acquire) { let subscriber_count = pipeline.subscriber_count(); @@ -1366,19 +1344,36 @@ impl SharedVideoPipeline { no_subscribers_since = None; } - let (buf, meta) = match stream.next() { - Ok(frame_data) => frame_data, + let mut owned = buffer_pool.take(MIN_CAPTURE_FRAME_SIZE); + let meta = match stream.next_into(&mut owned) { + Ok(meta) => meta, Err(e) => { if e.kind() == std::io::ErrorKind::TimedOut { warn!("Capture timeout - no signal?"); } else { - error!("Capture error: {}", e); + let key = classify_capture_error(&e); + if capture_error_throttler.should_log(&key) { + let suppressed = + suppressed_capture_errors.remove(&key).unwrap_or(0); + if suppressed > 0 { + error!( + "Capture error: {} (suppressed {} repeats)", + e, suppressed + ); + } else { + error!("Capture error: {}", e); + } + } else { + let counter = + suppressed_capture_errors.entry(key).or_insert(0); + *counter = counter.saturating_add(1); + } } continue; } }; - let frame_size = meta.bytesused as usize; + let frame_size = meta.bytes_used; if frame_size < MIN_CAPTURE_FRAME_SIZE { continue; } @@ -1386,22 +1381,20 @@ impl SharedVideoPipeline { validate_counter = validate_counter.wrapping_add(1); if pixel_format.is_compressed() && validate_counter % JPEG_VALIDATE_INTERVAL == 0 - && !VideoFrame::is_valid_jpeg_bytes(&buf[..frame_size]) + && !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size]) { continue; } - let mut owned = buffer_pool.take(frame_size); - owned.resize(frame_size, 0); - owned[..frame_size].copy_from_slice(&buf[..frame_size]); + owned.truncate(frame_size); let frame = Arc::new(VideoFrame::from_pooled( Arc::new(FrameBuffer::new(owned, Some(buffer_pool.clone()))), resolution, pixel_format, stride, - sequence, + meta.sequence, )); - sequence = sequence.wrapping_add(1); + sequence = meta.sequence.wrapping_add(1); { let mut guard = latest_frame.write(); diff --git a/src/video/streamer.rs b/src/video/streamer.rs index 2b11e744..af2e073f 100644 --- a/src/video/streamer.rs +++ b/src/video/streamer.rs @@ -3,9 +3,11 @@ //! This module provides a high-level interface for video capture and streaming, //! managing the lifecycle of the capture thread and MJPEG/WebRTC distribution. +use std::collections::HashMap; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; +use std::time::Duration; use tokio::sync::RwLock; use tracing::{debug, error, info, trace, warn}; @@ -15,12 +17,8 @@ use super::frame::{FrameBuffer, FrameBufferPool, VideoFrame}; use crate::error::{AppError, Result}; use crate::events::{EventBus, SystemEvent}; use crate::stream::MjpegStreamHandler; -use v4l::buffer::Type as BufferType; -use v4l::io::traits::CaptureStream; -use v4l::prelude::*; -use v4l::video::capture::Parameters; -use v4l::video::Capture; -use v4l::Format; +use crate::utils::LogThrottler; +use crate::video::v4l2r_capture::V4l2rCaptureStream; /// Minimum valid frame size for capture const MIN_CAPTURE_FRAME_SIZE: usize = 128; @@ -632,8 +630,7 @@ impl Streamer { } }; - let mut device_opt: Option = None; - let mut format_opt: Option = None; + let mut stream_opt: Option = None; let mut last_error: Option = None; for attempt in 0..MAX_RETRIES { @@ -642,8 +639,18 @@ impl Streamer { return; } - let device = match Device::with_path(&device_path) { - Ok(d) => d, + match V4l2rCaptureStream::open( + &device_path, + config.resolution, + config.format, + config.fps, + BUFFER_COUNT, + Duration::from_secs(2), + ) { + Ok(stream) => { + stream_opt = Some(stream); + break; + } Err(e) => { let err_str = e.to_string(); if err_str.contains("busy") || err_str.contains("resource") { @@ -660,42 +667,12 @@ impl Streamer { last_error = Some(err_str); break; } - }; - - let requested = Format::new( - config.resolution.width, - config.resolution.height, - config.format.to_fourcc(), - ); - - match device.set_format(&requested) { - Ok(actual) => { - device_opt = Some(device); - format_opt = Some(actual); - break; - } - Err(e) => { - let err_str = e.to_string(); - if err_str.contains("busy") || err_str.contains("resource") { - warn!( - "Device busy on set_format attempt {}/{}, retrying in {}ms...", - attempt + 1, - MAX_RETRIES, - RETRY_DELAY_MS - ); - std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS)); - last_error = Some(err_str); - continue; - } - last_error = Some(err_str); - break; - } } } - let (device, actual_format) = match (device_opt, format_opt) { - (Some(d), Some(f)) => (d, f), - _ => { + let mut stream = match stream_opt { + Some(stream) => stream, + None => { error!( "Failed to open device {:?}: {}", device_path, @@ -709,42 +686,35 @@ impl Streamer { } }; + let resolution = stream.resolution(); + let pixel_format = stream.format(); + let stride = stream.stride(); + info!( "Capture format: {}x{} {:?} stride={}", - actual_format.width, actual_format.height, actual_format.fourcc, actual_format.stride + resolution.width, resolution.height, pixel_format, stride ); - let resolution = Resolution::new(actual_format.width, actual_format.height); - let pixel_format = - PixelFormat::from_fourcc(actual_format.fourcc).unwrap_or(config.format); - - if config.fps > 0 { - if let Err(e) = device.set_params(&Parameters::with_fps(config.fps)) { - warn!("Failed to set hardware FPS: {}", e); - } - } - - let mut stream = - match MmapStream::with_buffers(&device, BufferType::VideoCapture, BUFFER_COUNT) { - Ok(s) => s, - Err(e) => { - error!("Failed to create capture stream: {}", e); - self.mjpeg_handler.set_offline(); - set_state(StreamerState::Error); - self.direct_active.store(false, Ordering::SeqCst); - self.current_fps.store(0, Ordering::Relaxed); - return; - } - }; - let buffer_pool = Arc::new(FrameBufferPool::new(BUFFER_COUNT.max(4) as usize)); let mut signal_present = true; - let mut sequence: u64 = 0; let mut validate_counter: u64 = 0; let mut idle_since: Option = None; let mut fps_frame_count: u64 = 0; let mut last_fps_time = std::time::Instant::now(); + let capture_error_throttler = LogThrottler::with_secs(5); + let mut suppressed_capture_errors: HashMap = HashMap::new(); + + let classify_capture_error = |err: &std::io::Error| -> String { + let message = err.to_string(); + if message.contains("dqbuf failed") && message.contains("EINVAL") { + "capture_dqbuf_einval".to_string() + } else if message.contains("dqbuf failed") { + "capture_dqbuf".to_string() + } else { + format!("capture_{:?}", err.kind()) + } + }; while !self.direct_stop.load(Ordering::Relaxed) { let mjpeg_clients = self.mjpeg_handler.client_count(); @@ -768,8 +738,9 @@ impl Streamer { idle_since = None; } - let (buf, meta) = match stream.next() { - Ok(frame_data) => frame_data, + let mut owned = buffer_pool.take(MIN_CAPTURE_FRAME_SIZE); + let meta = match stream.next_into(&mut owned) { + Ok(meta) => meta, Err(e) => { if e.kind() == std::io::ErrorKind::TimedOut { if signal_present { @@ -811,12 +782,23 @@ impl Streamer { break; } - error!("Capture error: {}", e); + let key = classify_capture_error(&e); + if capture_error_throttler.should_log(&key) { + let suppressed = suppressed_capture_errors.remove(&key).unwrap_or(0); + if suppressed > 0 { + error!("Capture error: {} (suppressed {} repeats)", e, suppressed); + } else { + error!("Capture error: {}", e); + } + } else { + let counter = suppressed_capture_errors.entry(key).or_insert(0); + *counter = counter.saturating_add(1); + } continue; } }; - let frame_size = meta.bytesused as usize; + let frame_size = meta.bytes_used; if frame_size < MIN_CAPTURE_FRAME_SIZE { continue; } @@ -824,22 +806,19 @@ impl Streamer { validate_counter = validate_counter.wrapping_add(1); if pixel_format.is_compressed() && validate_counter % JPEG_VALIDATE_INTERVAL == 0 - && !VideoFrame::is_valid_jpeg_bytes(&buf[..frame_size]) + && !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size]) { continue; } - let mut owned = buffer_pool.take(frame_size); - owned.resize(frame_size, 0); - owned[..frame_size].copy_from_slice(&buf[..frame_size]); + owned.truncate(frame_size); let frame = VideoFrame::from_pooled( Arc::new(FrameBuffer::new(owned, Some(buffer_pool.clone()))), resolution, pixel_format, - actual_format.stride, - sequence, + stride, + meta.sequence, ); - sequence = sequence.wrapping_add(1); if !signal_present { signal_present = true; diff --git a/src/video/v4l2r_capture.rs b/src/video/v4l2r_capture.rs new file mode 100644 index 00000000..cd23f263 --- /dev/null +++ b/src/video/v4l2r_capture.rs @@ -0,0 +1,284 @@ +//! V4L2 capture implementation using v4l2r (ioctl layer). + +use std::fs::File; +use std::io; +use std::os::fd::AsFd; +use std::path::Path; +use std::time::Duration; + +use nix::poll::{poll, PollFd, PollFlags, PollTimeout}; +use tracing::{debug, warn}; +use v4l2r::bindings::{v4l2_requestbuffers, v4l2_streamparm, v4l2_streamparm__bindgen_ty_1}; +use v4l2r::ioctl::{ + self, Capabilities, Capability as V4l2rCapability, MemoryConsistency, PlaneMapping, + QBufPlane, QBuffer, QueryBuffer, V4l2Buffer, +}; +use v4l2r::memory::{MemoryType, MmapHandle}; +use v4l2r::{Format as V4l2rFormat, PixelFormat as V4l2rPixelFormat, QueueType}; + +use crate::error::{AppError, Result}; +use crate::video::format::{PixelFormat, Resolution}; + +/// Metadata for a captured frame. +#[derive(Debug, Clone, Copy)] +pub struct CaptureMeta { + pub bytes_used: usize, + pub sequence: u64, +} + +/// V4L2 capture stream backed by v4l2r ioctl. +pub struct V4l2rCaptureStream { + fd: File, + queue: QueueType, + resolution: Resolution, + format: PixelFormat, + stride: u32, + timeout: Duration, + mappings: Vec>, +} + +impl V4l2rCaptureStream { + pub fn open( + device_path: impl AsRef, + resolution: Resolution, + format: PixelFormat, + fps: u32, + buffer_count: u32, + timeout: Duration, + ) -> Result { + let mut fd = File::options() + .read(true) + .write(true) + .open(device_path.as_ref()) + .map_err(|e| AppError::VideoError(format!("Failed to open device: {}", e)))?; + + let caps: V4l2rCapability = ioctl::querycap(&fd) + .map_err(|e| AppError::VideoError(format!("Failed to query capabilities: {}", e)))?; + let caps_flags = caps.device_caps(); + + // Prefer multi-planar capture when available, as it is required for some + // devices/pixel formats (e.g. NV12 via VIDEO_CAPTURE_MPLANE). + let queue = if caps_flags.contains(Capabilities::VIDEO_CAPTURE_MPLANE) { + QueueType::VideoCaptureMplane + } else if caps_flags.contains(Capabilities::VIDEO_CAPTURE) { + QueueType::VideoCapture + } else { + return Err(AppError::VideoError( + "Device does not support capture queues".to_string(), + )); + }; + + let mut fmt: V4l2rFormat = ioctl::g_fmt(&fd, queue).map_err(|e| { + AppError::VideoError(format!("Failed to get device format: {}", e)) + })?; + + fmt.width = resolution.width; + fmt.height = resolution.height; + fmt.pixelformat = V4l2rPixelFormat::from(&format.to_fourcc()); + + let actual_fmt: V4l2rFormat = ioctl::s_fmt(&mut fd, (queue, &fmt)).map_err(|e| { + AppError::VideoError(format!("Failed to set device format: {}", e)) + })?; + + let actual_resolution = Resolution::new(actual_fmt.width, actual_fmt.height); + let actual_format = PixelFormat::from_v4l2r(actual_fmt.pixelformat).unwrap_or(format); + + let stride = actual_fmt + .plane_fmt + .get(0) + .map(|p| p.bytesperline) + .unwrap_or_else(|| match actual_format.bytes_per_pixel() { + Some(bpp) => actual_resolution.width * bpp as u32, + None => actual_resolution.width, + }); + + if fps > 0 { + if let Err(e) = set_fps(&fd, queue, fps) { + warn!("Failed to set hardware FPS: {}", e); + } + } + + let req: v4l2_requestbuffers = ioctl::reqbufs( + &fd, + queue, + MemoryType::Mmap, + buffer_count, + MemoryConsistency::empty(), + ) + .map_err(|e| AppError::VideoError(format!("Failed to request buffers: {}", e)))?; + let allocated = req.count as usize; + if allocated == 0 { + return Err(AppError::VideoError( + "Driver returned zero capture buffers".to_string(), + )); + } + + let mut mappings = Vec::with_capacity(allocated); + for index in 0..allocated as u32 { + let query: QueryBuffer = ioctl::querybuf(&fd, queue, index as usize).map_err(|e| { + AppError::VideoError(format!("Failed to query buffer {}: {}", index, e)) + })?; + + if query.planes.is_empty() { + return Err(AppError::VideoError(format!( + "Driver returned zero planes for buffer {}", + index + ))); + } + + let mut plane_maps = Vec::with_capacity(query.planes.len()); + for plane in &query.planes { + let mapping = ioctl::mmap(&fd, plane.mem_offset, plane.length).map_err(|e| { + AppError::VideoError(format!( + "Failed to mmap buffer {}: {}", + index, e + )) + })?; + plane_maps.push(mapping); + } + mappings.push(plane_maps); + } + + let mut stream = Self { + fd, + queue, + resolution: actual_resolution, + format: actual_format, + stride, + timeout, + mappings, + }; + + stream.queue_all_buffers()?; + ioctl::streamon(&stream.fd, stream.queue).map_err(|e| { + AppError::VideoError(format!("Failed to start capture stream: {}", e)) + })?; + + Ok(stream) + } + + pub fn resolution(&self) -> Resolution { + self.resolution + } + + pub fn format(&self) -> PixelFormat { + self.format + } + + pub fn stride(&self) -> u32 { + self.stride + } + + pub fn next_into(&mut self, dst: &mut Vec) -> io::Result { + self.wait_ready()?; + + let dqbuf: V4l2Buffer = ioctl::dqbuf(&self.fd, self.queue).map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("dqbuf failed: {}", e)) + })?; + let index = dqbuf.as_v4l2_buffer().index as usize; + let sequence = dqbuf.as_v4l2_buffer().sequence as u64; + + let mut total = 0usize; + for (plane_idx, plane) in dqbuf.planes_iter().enumerate() { + let bytes_used = *plane.bytesused as usize; + let data_offset = plane.data_offset.copied().unwrap_or(0) as usize; + if bytes_used == 0 { + continue; + } + let mapping = &self.mappings[index][plane_idx]; + let start = data_offset.min(mapping.len()); + let end = (data_offset + bytes_used).min(mapping.len()); + total += end.saturating_sub(start); + } + + dst.resize(total, 0); + let mut cursor = 0usize; + for (plane_idx, plane) in dqbuf.planes_iter().enumerate() { + let bytes_used = *plane.bytesused as usize; + let data_offset = plane.data_offset.copied().unwrap_or(0) as usize; + if bytes_used == 0 { + continue; + } + let mapping = &self.mappings[index][plane_idx]; + let start = data_offset.min(mapping.len()); + let end = (data_offset + bytes_used).min(mapping.len()); + let len = end.saturating_sub(start); + if len == 0 { + continue; + } + dst[cursor..cursor + len].copy_from_slice(&mapping[start..end]); + cursor += len; + } + + self.queue_buffer(index as u32) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + + Ok(CaptureMeta { + bytes_used: total, + sequence, + }) + } + + fn wait_ready(&self) -> io::Result<()> { + if self.timeout.is_zero() { + return Ok(()); + } + let mut fds = [PollFd::new(self.fd.as_fd(), PollFlags::POLLIN)]; + let timeout_ms = self.timeout.as_millis().min(u16::MAX as u128) as u16; + let ready = poll(&mut fds, PollTimeout::from(timeout_ms))?; + if ready == 0 { + return Err(io::Error::new(io::ErrorKind::TimedOut, "capture timeout")); + } + Ok(()) + } + + fn queue_all_buffers(&mut self) -> Result<()> { + for index in 0..self.mappings.len() as u32 { + self.queue_buffer(index)?; + } + Ok(()) + } + + fn queue_buffer(&mut self, index: u32) -> Result<()> { + let handle = MmapHandle::default(); + let planes = self.mappings[index as usize] + .iter() + .map(|mapping| { + let mut plane = QBufPlane::new_from_handle(&handle, 0); + plane.0.length = mapping.len() as u32; + plane + }) + .collect(); + let mut qbuf: QBuffer = QBuffer::new(self.queue, index); + qbuf.planes = planes; + ioctl::qbuf::<_, ()>(&self.fd, qbuf) + .map_err(|e| AppError::VideoError(format!("Failed to queue buffer: {}", e)))?; + Ok(()) + } +} + +impl Drop for V4l2rCaptureStream { + fn drop(&mut self) { + if let Err(e) = ioctl::streamoff(&self.fd, self.queue) { + debug!("Failed to stop capture stream: {}", e); + } + } +} + +fn set_fps(fd: &File, queue: QueueType, fps: u32) -> Result<()> { + let mut params = unsafe { std::mem::zeroed::() }; + params.type_ = queue as u32; + params.parm = v4l2_streamparm__bindgen_ty_1 { + capture: v4l2r::bindings::v4l2_captureparm { + timeperframe: v4l2r::bindings::v4l2_fract { + numerator: 1, + denominator: fps, + }, + ..unsafe { std::mem::zeroed() } + }, + }; + + let _actual: v4l2_streamparm = ioctl::s_parm(fd, params) + .map_err(|e| AppError::VideoError(format!("Failed to set FPS: {}", e)))?; + Ok(()) +} From 394baca9381c3fbb83fd61497bbbd49fd9a4fbf0 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Tue, 10 Feb 2026 21:37:33 +0800 Subject: [PATCH 03/19] =?UTF-8?q?fix:=20=E8=A1=A5=E9=BD=90=20ATX=20?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=99=A8=E7=BC=BA=E5=A4=B1=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=B9=B6=E5=AE=8C=E6=88=90=E5=85=A8=E9=A1=B9=E7=9B=AE=20clippy?= =?UTF-8?q?=20-D=20warnings=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/hwcodec/src/ffmpeg_hw/mod.rs | 6 +- libs/hwcodec/src/ffmpeg_ram/decode.rs | 3 +- src/atx/controller.rs | 406 ++++++++++---------------- src/atx/mod.rs | 5 +- src/atx/types.rs | 79 +---- src/atx/wol.rs | 6 +- src/audio/capture.rs | 9 +- src/audio/controller.rs | 2 + src/audio/device.rs | 4 +- src/audio/monitor.rs | 9 +- src/audio/streamer.rs | 22 +- src/auth/middleware.rs | 6 +- src/auth/user.rs | 13 +- src/config/schema.rs | 58 +--- src/events/types.rs | 1 + src/extensions/types.rs | 12 +- src/hid/backend.rs | 7 +- src/hid/ch9329.rs | 14 +- src/hid/mod.rs | 37 ++- src/hid/monitor.rs | 9 +- src/hid/otg.rs | 25 +- src/main.rs | 8 +- src/msd/controller.rs | 5 +- src/msd/image.rs | 4 +- src/msd/monitor.rs | 7 +- src/msd/types.rs | 7 +- src/msd/ventoy_drive.rs | 11 +- src/otg/manager.rs | 6 +- src/otg/service.rs | 18 +- src/rustdesk/bytes_codec.rs | 2 +- src/rustdesk/config.rs | 18 +- src/rustdesk/connection.rs | 15 +- src/rustdesk/crypto.rs | 2 +- src/rustdesk/frame_adapters.rs | 3 +- src/rustdesk/mod.rs | 3 +- src/rustdesk/rendezvous.rs | 2 +- src/state.rs | 1 + src/stream/mjpeg_streamer.rs | 8 +- src/utils/mod.rs | 4 +- src/video/capture.rs | 2 +- src/video/device.rs | 39 +-- src/video/encoder/h264.rs | 14 +- src/video/encoder/h265.rs | 16 +- src/video/encoder/registry.rs | 1 + src/video/encoder/traits.rs | 7 +- src/video/encoder/vp8.rs | 16 +- src/video/encoder/vp9.rs | 16 +- src/video/frame.rs | 5 + src/video/shared_video_pipeline.rs | 68 +++-- src/video/stream_manager.rs | 14 +- src/video/streamer.rs | 12 +- src/video/v4l2r_capture.rs | 36 +-- src/video/video_session.rs | 1 - src/web/handlers/config/apply.rs | 4 +- src/web/handlers/extensions.rs | 40 +-- src/web/handlers/mod.rs | 21 +- src/web/static_files.rs | 6 +- src/webrtc/config.rs | 7 +- src/webrtc/peer.rs | 1 - src/webrtc/rtp.rs | 15 +- src/webrtc/track.rs | 2 +- src/webrtc/universal_session.rs | 19 +- src/webrtc/video_track.rs | 3 +- src/webrtc/webrtc_streamer.rs | 12 +- 64 files changed, 474 insertions(+), 760 deletions(-) diff --git a/libs/hwcodec/src/ffmpeg_hw/mod.rs b/libs/hwcodec/src/ffmpeg_hw/mod.rs index 222c9d14..a80e4ba4 100644 --- a/libs/hwcodec/src/ffmpeg_hw/mod.rs +++ b/libs/hwcodec/src/ffmpeg_hw/mod.rs @@ -31,8 +31,10 @@ unsafe impl Send for HwMjpegH26xPipeline {} impl HwMjpegH26xPipeline { pub fn new(config: HwMjpegH26xConfig) -> Result { unsafe { - let dec = CString::new(config.decoder.as_str()).map_err(|_| "decoder name invalid".to_string())?; - let enc = CString::new(config.encoder.as_str()).map_err(|_| "encoder name invalid".to_string())?; + let dec = CString::new(config.decoder.as_str()) + .map_err(|_| "decoder name invalid".to_string())?; + let enc = CString::new(config.encoder.as_str()) + .map_err(|_| "encoder name invalid".to_string())?; let ctx = ffmpeg_hw_mjpeg_h26x_new( dec.as_ptr(), enc.as_ptr(), diff --git a/libs/hwcodec/src/ffmpeg_ram/decode.rs b/libs/hwcodec/src/ffmpeg_ram/decode.rs index df0512a3..0cd9caf4 100644 --- a/libs/hwcodec/src/ffmpeg_ram/decode.rs +++ b/libs/hwcodec/src/ffmpeg_ram/decode.rs @@ -1,8 +1,7 @@ use crate::{ ffmpeg::{init_av_log, AVPixelFormat}, ffmpeg_ram::{ - ffmpeg_ram_decode, ffmpeg_ram_free_decoder, ffmpeg_ram_last_error, - ffmpeg_ram_new_decoder, + ffmpeg_ram_decode, ffmpeg_ram_free_decoder, ffmpeg_ram_last_error, ffmpeg_ram_new_decoder, }, }; use std::{ diff --git a/src/atx/controller.rs b/src/atx/controller.rs index 16e8aa07..31dd3841 100644 --- a/src/atx/controller.rs +++ b/src/atx/controller.rs @@ -8,11 +8,11 @@ use tracing::{debug, info, warn}; use super::executor::{timing, AtxKeyExecutor}; use super::led::LedSensor; -use super::types::{AtxKeyConfig, AtxLedConfig, AtxState, PowerStatus}; +use super::types::{AtxKeyConfig, AtxLedConfig, AtxState, AtxAction, PowerStatus}; use crate::error::{AppError, Result}; /// ATX power control configuration -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct AtxControllerConfig { /// Whether ATX is enabled pub enabled: bool, @@ -24,17 +24,6 @@ pub struct AtxControllerConfig { pub led: AtxLedConfig, } -impl Default for AtxControllerConfig { - fn default() -> Self { - Self { - enabled: false, - power: AtxKeyConfig::default(), - reset: AtxKeyConfig::default(), - led: AtxLedConfig::default(), - } - } -} - /// Internal state holding all ATX components /// Grouped together to reduce lock acquisitions struct AtxInner { @@ -54,34 +43,7 @@ pub struct AtxController { } impl AtxController { - /// Create a new ATX controller with the specified configuration - pub fn new(config: AtxControllerConfig) -> Self { - Self { - inner: RwLock::new(AtxInner { - config, - power_executor: None, - reset_executor: None, - led_sensor: None, - }), - } - } - - /// Create a disabled ATX controller - pub fn disabled() -> Self { - Self::new(AtxControllerConfig::default()) - } - - /// Initialize the ATX controller and its executors - pub async fn init(&self) -> Result<()> { - let mut inner = self.inner.write().await; - - if !inner.config.enabled { - info!("ATX disabled in configuration"); - return Ok(()); - } - - info!("Initializing ATX controller"); - + async fn init_components(inner: &mut AtxInner) { // Initialize power executor if inner.config.power.is_configured() { let mut executor = AtxKeyExecutor::new(inner.config.power.clone()); @@ -123,234 +85,180 @@ impl AtxController { inner.led_sensor = Some(sensor); } } - - info!("ATX controller initialized successfully"); - Ok(()) } - /// Reload the ATX controller with new configuration - /// - /// This is called when configuration changes and supports hot-reload. - pub async fn reload(&self, new_config: AtxControllerConfig) -> Result<()> { - info!("Reloading ATX controller with new configuration"); + async fn shutdown_components(inner: &mut AtxInner) { + if let Some(executor) = inner.power_executor.as_mut() { + if let Err(e) = executor.shutdown().await { + warn!("Failed to shutdown power executor: {}", e); + } + } + inner.power_executor = None; - // Shutdown existing executors - self.shutdown_internal().await?; + if let Some(executor) = inner.reset_executor.as_mut() { + if let Err(e) = executor.shutdown().await { + warn!("Failed to shutdown reset executor: {}", e); + } + } + inner.reset_executor = None; - // Update configuration and re-initialize - { - let mut inner = self.inner.write().await; - inner.config = new_config; + if let Some(sensor) = inner.led_sensor.as_mut() { + if let Err(e) = sensor.shutdown().await { + warn!("Failed to shutdown LED sensor: {}", e); + } + } + inner.led_sensor = None; + } + + /// Create a new ATX controller with the specified configuration + pub fn new(config: AtxControllerConfig) -> Self { + Self { + inner: RwLock::new(AtxInner { + config, + power_executor: None, + reset_executor: None, + led_sensor: None, + }), + } + } + + /// Create a disabled ATX controller + pub fn disabled() -> Self { + Self::new(AtxControllerConfig::default()) + } + + /// Initialize the ATX controller and its executors + pub async fn init(&self) -> Result<()> { + let mut inner = self.inner.write().await; + + if !inner.config.enabled { + info!("ATX disabled in configuration"); + return Ok(()); } - // Re-initialize - self.init().await?; + info!("Initializing ATX controller"); + + Self::init_components(&mut inner).await; - info!("ATX controller reloaded successfully"); Ok(()) } - /// Get current ATX state (single lock acquisition) + /// Reload ATX controller configuration + pub async fn reload(&self, config: AtxControllerConfig) -> Result<()> { + let mut inner = self.inner.write().await; + + info!("Reloading ATX controller configuration"); + + // Shutdown existing components first, then rebuild with new config. + Self::shutdown_components(&mut inner).await; + inner.config = config; + + if !inner.config.enabled { + info!("ATX disabled after reload"); + return Ok(()); + } + + Self::init_components(&mut inner).await; + info!("ATX controller reloaded"); + + Ok(()) + } + + /// Shutdown ATX controller and release all resources + pub async fn shutdown(&self) -> Result<()> { + let mut inner = self.inner.write().await; + Self::shutdown_components(&mut inner).await; + info!("ATX controller shutdown complete"); + Ok(()) + } + + /// Trigger a power action (short/long/reset) + pub async fn trigger_power_action(&self, action: AtxAction) -> Result<()> { + let inner = self.inner.read().await; + + match action { + AtxAction::Short | AtxAction::Long => { + if let Some(executor) = &inner.power_executor { + let duration = match action { + AtxAction::Short => timing::SHORT_PRESS, + AtxAction::Long => timing::LONG_PRESS, + _ => unreachable!(), + }; + executor.pulse(duration).await?; + } else { + return Err(AppError::Config( + "Power button not configured for ATX controller".to_string(), + )); + } + } + AtxAction::Reset => { + if let Some(executor) = &inner.reset_executor { + executor.pulse(timing::RESET_PRESS).await?; + } else { + return Err(AppError::Config( + "Reset button not configured for ATX controller".to_string(), + )); + } + } + } + + Ok(()) + } + + /// Trigger a short power button press + pub async fn power_short(&self) -> Result<()> { + self.trigger_power_action(AtxAction::Short).await + } + + /// Trigger a long power button press + pub async fn power_long(&self) -> Result<()> { + self.trigger_power_action(AtxAction::Long).await + } + + /// Trigger a reset button press + pub async fn reset(&self) -> Result<()> { + self.trigger_power_action(AtxAction::Reset).await + } + + /// Get the current power status using the LED sensor (if configured) + pub async fn power_status(&self) -> PowerStatus { + let inner = self.inner.read().await; + + if let Some(sensor) = &inner.led_sensor { + match sensor.read().await { + Ok(status) => status, + Err(e) => { + debug!("Failed to read ATX LED sensor: {}", e); + PowerStatus::Unknown + } + } + } else { + PowerStatus::Unknown + } + } + + /// Get a snapshot of the ATX state for API responses pub async fn state(&self) -> AtxState { let inner = self.inner.read().await; - let power_status = if let Some(sensor) = inner.led_sensor.as_ref() { - sensor.read().await.unwrap_or(PowerStatus::Unknown) + let power_status = if let Some(sensor) = &inner.led_sensor { + match sensor.read().await { + Ok(status) => status, + Err(e) => { + debug!("Failed to read ATX LED sensor: {}", e); + PowerStatus::Unknown + } + } } else { PowerStatus::Unknown }; AtxState { available: inner.config.enabled, - power_configured: inner - .power_executor - .as_ref() - .map(|e| e.is_initialized()) - .unwrap_or(false), - reset_configured: inner - .reset_executor - .as_ref() - .map(|e| e.is_initialized()) - .unwrap_or(false), + power_configured: inner.power_executor.is_some(), + reset_configured: inner.reset_executor.is_some(), power_status, - led_supported: inner - .led_sensor - .as_ref() - .map(|s| s.is_initialized()) - .unwrap_or(false), + led_supported: inner.led_sensor.is_some(), } } - - /// Get current state as SystemEvent - pub async fn current_state_event(&self) -> crate::events::SystemEvent { - let state = self.state().await; - crate::events::SystemEvent::AtxStateChanged { - power_status: state.power_status, - } - } - - /// Check if ATX is available - pub async fn is_available(&self) -> bool { - let inner = self.inner.read().await; - inner.config.enabled - } - - /// Check if power button is configured and initialized - pub async fn is_power_ready(&self) -> bool { - let inner = self.inner.read().await; - inner - .power_executor - .as_ref() - .map(|e| e.is_initialized()) - .unwrap_or(false) - } - - /// Check if reset button is configured and initialized - pub async fn is_reset_ready(&self) -> bool { - let inner = self.inner.read().await; - inner - .reset_executor - .as_ref() - .map(|e| e.is_initialized()) - .unwrap_or(false) - } - - /// Short press power button (turn on or graceful shutdown) - pub async fn power_short(&self) -> Result<()> { - let inner = self.inner.read().await; - let executor = inner - .power_executor - .as_ref() - .ok_or_else(|| AppError::Internal("Power button not configured".to_string()))?; - - info!( - "ATX: Short press power button ({}ms)", - timing::SHORT_PRESS.as_millis() - ); - executor.pulse(timing::SHORT_PRESS).await - } - - /// Long press power button (force power off) - pub async fn power_long(&self) -> Result<()> { - let inner = self.inner.read().await; - let executor = inner - .power_executor - .as_ref() - .ok_or_else(|| AppError::Internal("Power button not configured".to_string()))?; - - info!( - "ATX: Long press power button ({}ms)", - timing::LONG_PRESS.as_millis() - ); - executor.pulse(timing::LONG_PRESS).await - } - - /// Press reset button - pub async fn reset(&self) -> Result<()> { - let inner = self.inner.read().await; - let executor = inner - .reset_executor - .as_ref() - .ok_or_else(|| AppError::Internal("Reset button not configured".to_string()))?; - - info!( - "ATX: Press reset button ({}ms)", - timing::RESET_PRESS.as_millis() - ); - executor.pulse(timing::RESET_PRESS).await - } - - /// Get current power status from LED sensor - pub async fn power_status(&self) -> Result { - let inner = self.inner.read().await; - match inner.led_sensor.as_ref() { - Some(sensor) => sensor.read().await, - None => Ok(PowerStatus::Unknown), - } - } - - /// Shutdown the ATX controller - pub async fn shutdown(&self) -> Result<()> { - info!("Shutting down ATX controller"); - self.shutdown_internal().await?; - info!("ATX controller shutdown complete"); - Ok(()) - } - - /// Internal shutdown helper - async fn shutdown_internal(&self) -> Result<()> { - let mut inner = self.inner.write().await; - - // Shutdown power executor - if let Some(mut executor) = inner.power_executor.take() { - executor.shutdown().await.ok(); - } - - // Shutdown reset executor - if let Some(mut executor) = inner.reset_executor.take() { - executor.shutdown().await.ok(); - } - - // Shutdown LED sensor - if let Some(mut sensor) = inner.led_sensor.take() { - sensor.shutdown().await.ok(); - } - - Ok(()) - } -} - -impl Drop for AtxController { - fn drop(&mut self) { - debug!("ATX controller dropped"); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_controller_config_default() { - let config = AtxControllerConfig::default(); - assert!(!config.enabled); - assert!(!config.power.is_configured()); - assert!(!config.reset.is_configured()); - assert!(!config.led.is_configured()); - } - - #[test] - fn test_controller_creation() { - let controller = AtxController::disabled(); - assert!(controller.inner.try_read().is_ok()); - } - - #[tokio::test] - async fn test_controller_disabled_state() { - let controller = AtxController::disabled(); - let state = controller.state().await; - assert!(!state.available); - assert!(!state.power_configured); - assert!(!state.reset_configured); - } - - #[tokio::test] - async fn test_controller_init_disabled() { - let controller = AtxController::disabled(); - let result = controller.init().await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_controller_is_available() { - let controller = AtxController::disabled(); - assert!(!controller.is_available().await); - - let config = AtxControllerConfig { - enabled: true, - ..Default::default() - }; - let controller = AtxController::new(config); - assert!(controller.is_available().await); - } } diff --git a/src/atx/mod.rs b/src/atx/mod.rs index dd7c90b4..1f28a509 100644 --- a/src/atx/mod.rs +++ b/src/atx/mod.rs @@ -88,10 +88,7 @@ mod tests { #[test] fn test_discover_devices() { - let devices = discover_devices(); - // Just verify the function runs without error - assert!(devices.gpio_chips.len() >= 0); - assert!(devices.usb_relays.len() >= 0); + let _devices = discover_devices(); } #[test] diff --git a/src/atx/types.rs b/src/atx/types.rs index cea0e176..4523bdc6 100644 --- a/src/atx/types.rs +++ b/src/atx/types.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use typeshare::typeshare; /// Power status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum PowerStatus { /// Power is on @@ -15,18 +15,13 @@ pub enum PowerStatus { /// Power is off Off, /// Power status unknown (no LED connected) + #[default] Unknown, } -impl Default for PowerStatus { - fn default() -> Self { - Self::Unknown - } -} - /// Driver type for ATX key operations #[typeshare] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum AtxDriverType { /// GPIO control via Linux character device @@ -34,36 +29,26 @@ pub enum AtxDriverType { /// USB HID relay module UsbRelay, /// Disabled / Not configured + #[default] None, } -impl Default for AtxDriverType { - fn default() -> Self { - Self::None - } -} - /// Active level for GPIO pins #[typeshare] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum ActiveLevel { /// Active high (default for most cases) + #[default] High, /// Active low (inverted) Low, } -impl Default for ActiveLevel { - fn default() -> Self { - Self::High - } -} - /// Configuration for a single ATX key (power or reset) /// This is the "four-tuple" configuration: (driver, device, pin/channel, level) #[typeshare] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(default)] pub struct AtxKeyConfig { /// Driver type (GPIO or USB Relay) @@ -80,17 +65,6 @@ pub struct AtxKeyConfig { pub active_level: ActiveLevel, } -impl Default for AtxKeyConfig { - fn default() -> Self { - Self { - driver: AtxDriverType::None, - device: String::new(), - pin: 0, - active_level: ActiveLevel::High, - } - } -} - impl AtxKeyConfig { /// Check if this key is configured pub fn is_configured(&self) -> bool { @@ -100,7 +74,7 @@ impl AtxKeyConfig { /// LED sensing configuration (optional) #[typeshare] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(default)] pub struct AtxLedConfig { /// Whether LED sensing is enabled @@ -113,17 +87,6 @@ pub struct AtxLedConfig { pub inverted: bool, } -impl Default for AtxLedConfig { - fn default() -> Self { - Self { - enabled: false, - gpio_chip: String::new(), - gpio_pin: 0, - inverted: false, - } - } -} - impl AtxLedConfig { /// Check if LED sensing is configured pub fn is_configured(&self) -> bool { @@ -132,7 +95,7 @@ impl AtxLedConfig { } /// ATX state information -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AtxState { /// Whether ATX feature is available/enabled pub available: bool, @@ -146,18 +109,6 @@ pub struct AtxState { pub led_supported: bool, } -impl Default for AtxState { - fn default() -> Self { - Self { - available: false, - power_configured: false, - reset_configured: false, - power_status: PowerStatus::Unknown, - led_supported: false, - } - } -} - /// ATX power action request #[derive(Debug, Clone, Deserialize)] pub struct AtxPowerRequest { @@ -179,7 +130,7 @@ pub enum AtxAction { /// Available ATX devices for discovery #[typeshare] -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AtxDevices { /// Available GPIO chips (/dev/gpiochip*) pub gpio_chips: Vec, @@ -187,15 +138,6 @@ pub struct AtxDevices { pub usb_relays: Vec, } -impl Default for AtxDevices { - fn default() -> Self { - Self { - gpio_chips: Vec::new(), - usb_relays: Vec::new(), - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -266,5 +208,6 @@ mod tests { assert!(!state.power_configured); assert!(!state.reset_configured); assert_eq!(state.power_status, PowerStatus::Unknown); + assert!(!state.led_supported); } } diff --git a/src/atx/wol.rs b/src/atx/wol.rs index 958e3a97..9da93cd7 100644 --- a/src/atx/wol.rs +++ b/src/atx/wol.rs @@ -10,7 +10,7 @@ use crate::error::{AppError, Result}; /// WOL magic packet structure: /// - 6 bytes of 0xFF /// - 16 repetitions of the target MAC address (6 bytes each) -/// Total: 6 + 16 * 6 = 102 bytes +/// Total: 6 + 16 * 6 = 102 bytes const MAGIC_PACKET_SIZE: usize = 102; /// Parse MAC address string into bytes @@ -160,8 +160,8 @@ mod tests { let packet = build_magic_packet(&mac); // Check header (6 bytes of 0xFF) - for i in 0..6 { - assert_eq!(packet[i], 0xFF); + for byte in packet.iter().take(6) { + assert_eq!(*byte, 0xFF); } // Check MAC repetitions diff --git a/src/audio/capture.rs b/src/audio/capture.rs index 33fcc673..aef64344 100644 --- a/src/audio/capture.rs +++ b/src/audio/capture.rs @@ -184,14 +184,7 @@ impl AudioCapturer { let log_throttler = self.log_throttler.clone(); let handle = tokio::task::spawn_blocking(move || { - capture_loop( - config, - state, - frame_tx, - stop_flag, - sequence, - log_throttler, - ); + capture_loop(config, state, frame_tx, stop_flag, sequence, log_throttler); }); *self.capture_handle.lock().await = Some(handle); diff --git a/src/audio/controller.rs b/src/audio/controller.rs index ea3621d0..f7ebf82d 100644 --- a/src/audio/controller.rs +++ b/src/audio/controller.rs @@ -39,7 +39,9 @@ impl AudioQuality { } /// Parse from string + #[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Self { + match s.to_lowercase().as_str() { "voice" | "low" => AudioQuality::Voice, "high" | "music" => AudioQuality::High, diff --git a/src/audio/device.rs b/src/audio/device.rs index ed42726c..77536680 100644 --- a/src/audio/device.rs +++ b/src/audio/device.rs @@ -85,9 +85,7 @@ pub fn enumerate_audio_devices_with_current( let mut devices = Vec::new(); // Try to enumerate cards - let cards = match alsa::card::Iter::new() { - i => i, - }; + let cards = alsa::card::Iter::new(); for card_result in cards { let card = match card_result { diff --git a/src/audio/monitor.rs b/src/audio/monitor.rs index d29b747a..6abfb010 100644 --- a/src/audio/monitor.rs +++ b/src/audio/monitor.rs @@ -17,8 +17,10 @@ use crate::utils::LogThrottler; /// Audio health status #[derive(Debug, Clone, PartialEq)] +#[derive(Default)] pub enum AudioHealthStatus { /// Device is healthy and operational + #[default] Healthy, /// Device has an error, attempting recovery Error { @@ -33,11 +35,6 @@ pub enum AudioHealthStatus { Disconnected, } -impl Default for AudioHealthStatus { - fn default() -> Self { - Self::Healthy - } -} /// Audio health monitor configuration #[derive(Debug, Clone)] @@ -166,7 +163,7 @@ impl AudioHealthMonitor { let attempt = self.retry_count.load(Ordering::Relaxed); // Only publish every 5 attempts to avoid event spam - if attempt == 1 || attempt % 5 == 0 { + if attempt == 1 || attempt.is_multiple_of(5) { debug!("Audio reconnecting, attempt {}", attempt); if let Some(ref events) = *self.events.read().await { diff --git a/src/audio/streamer.rs b/src/audio/streamer.rs index 0d843e05..462a3c7c 100644 --- a/src/audio/streamer.rs +++ b/src/audio/streamer.rs @@ -15,8 +15,10 @@ use crate::error::{AppError, Result}; /// Audio stream state #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default)] pub enum AudioStreamState { /// Stream is stopped + #[default] Stopped, /// Stream is starting up Starting, @@ -26,14 +28,10 @@ pub enum AudioStreamState { Error, } -impl Default for AudioStreamState { - fn default() -> Self { - Self::Stopped - } -} /// Audio streamer configuration #[derive(Debug, Clone)] +#[derive(Default)] pub struct AudioStreamerConfig { /// Audio capture configuration pub capture: AudioConfig, @@ -41,14 +39,6 @@ pub struct AudioStreamerConfig { pub opus: OpusConfig, } -impl Default for AudioStreamerConfig { - fn default() -> Self { - Self { - capture: AudioConfig::default(), - opus: OpusConfig::default(), - } - } -} impl AudioStreamerConfig { /// Create config for a specific device with default quality @@ -290,11 +280,7 @@ impl AudioStreamer { // Encode to Opus let opus_result = { let mut enc_guard = encoder.lock().await; - if let Some(ref mut enc) = *enc_guard { - Some(enc.encode_frame(&audio_frame)) - } else { - None - } + (*enc_guard).as_mut().map(|enc| enc.encode_frame(&audio_frame)) }; match opus_result { diff --git a/src/auth/middleware.rs b/src/auth/middleware.rs index 5bbbd2f0..80f40f3f 100644 --- a/src/auth/middleware.rs +++ b/src/auth/middleware.rs @@ -92,11 +92,7 @@ fn is_public_endpoint(path: &str) -> bool { // Note: paths here are relative to /api since middleware is applied within the nested router matches!( path, - "/" - | "/auth/login" - | "/health" - | "/setup" - | "/setup/init" + "/" | "/auth/login" | "/health" | "/setup" | "/setup/init" ) || path.starts_with("/assets/") || path.starts_with("/static/") || path.ends_with(".js") diff --git a/src/auth/user.rs b/src/auth/user.rs index f731f52f..0854a5ab 100644 --- a/src/auth/user.rs +++ b/src/auth/user.rs @@ -161,13 +161,12 @@ impl UserStore { } let now = Utc::now(); - let result = - sqlx::query("UPDATE users SET username = ?1, updated_at = ?2 WHERE id = ?3") - .bind(new_username) - .bind(now.to_rfc3339()) - .bind(user_id) - .execute(&self.pool) - .await?; + let result = sqlx::query("UPDATE users SET username = ?1, updated_at = ?2 WHERE id = ?3") + .bind(new_username) + .bind(now.to_rfc3339()) + .bind(user_id) + .execute(&self.pool) + .await?; if result.rows_affected() == 0 { return Err(AppError::NotFound("User not found".to_string())); diff --git a/src/config/schema.rs b/src/config/schema.rs index 63abe268..bb5d06a6 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -11,6 +11,7 @@ pub use crate::rustdesk::config::RustDeskConfig; #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] +#[derive(Default)] pub struct AppConfig { /// Whether initial setup has been completed pub initialized: bool, @@ -36,23 +37,6 @@ pub struct AppConfig { pub rustdesk: RustDeskConfig, } -impl Default for AppConfig { - fn default() -> Self { - Self { - initialized: false, - auth: AuthConfig::default(), - video: VideoConfig::default(), - hid: HidConfig::default(), - msd: MsdConfig::default(), - atx: AtxConfig::default(), - audio: AudioConfig::default(), - stream: StreamConfig::default(), - web: WebConfig::default(), - extensions: ExtensionsConfig::default(), - rustdesk: RustDeskConfig::default(), - } - } -} /// Authentication configuration #[typeshare] @@ -116,20 +100,17 @@ impl Default for VideoConfig { #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] +#[derive(Default)] pub enum HidBackend { /// USB OTG HID gadget Otg, /// CH9329 serial HID controller Ch9329, /// Disabled + #[default] None, } -impl Default for HidBackend { - fn default() -> Self { - Self::None - } -} /// OTG USB device descriptor configuration #[typeshare] @@ -163,8 +144,10 @@ impl Default for OtgDescriptorConfig { #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum OtgHidProfile { /// Full HID device set (keyboard + relative mouse + absolute mouse + consumer control) + #[default] Full, /// Full HID device set without MSD FullNoMsd, @@ -180,11 +163,6 @@ pub enum OtgHidProfile { Custom, } -impl Default for OtgHidProfile { - fn default() -> Self { - Self::Full - } -} /// OTG HID function selection (used when profile is Custom) #[typeshare] @@ -360,6 +338,7 @@ pub use crate::atx::{ActiveLevel, AtxDriverType, AtxKeyConfig, AtxLedConfig}; #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] +#[derive(Default)] pub struct AtxConfig { /// Enable ATX functionality pub enabled: bool, @@ -373,17 +352,6 @@ pub struct AtxConfig { pub wol_interface: String, } -impl Default for AtxConfig { - fn default() -> Self { - Self { - enabled: false, - power: AtxKeyConfig::default(), - reset: AtxKeyConfig::default(), - led: AtxLedConfig::default(), - wol_interface: String::new(), - } - } -} impl AtxConfig { /// Convert to AtxControllerConfig for the controller @@ -427,25 +395,24 @@ impl Default for AudioConfig { #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] +#[derive(Default)] pub enum StreamMode { /// WebRTC with H264/H265 WebRTC, /// MJPEG over HTTP + #[default] Mjpeg, } -impl Default for StreamMode { - fn default() -> Self { - Self::Mjpeg - } -} /// Encoder type #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] +#[derive(Default)] pub enum EncoderType { /// Auto-detect best encoder + #[default] Auto, /// Software encoder (libx264) Software, @@ -463,11 +430,6 @@ pub enum EncoderType { V4l2m2m, } -impl Default for EncoderType { - fn default() -> Self { - Self::Auto - } -} impl EncoderType { /// Convert to EncoderBackend for registry queries diff --git a/src/events/types.rs b/src/events/types.rs index 44a69b2d..ab3ebfd4 100644 --- a/src/events/types.rs +++ b/src/events/types.rs @@ -124,6 +124,7 @@ pub struct ClientStats { /// ``` #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "event", content = "data")] +#[allow(clippy::large_enum_variant)] pub enum SystemEvent { // ============================================================================ // Video Stream Events diff --git a/src/extensions/types.rs b/src/extensions/types.rs index c6a3a70d..b8c69c77 100644 --- a/src/extensions/types.rs +++ b/src/extensions/types.rs @@ -149,6 +149,7 @@ impl Default for GostcConfig { #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] +#[derive(Default)] pub struct EasytierConfig { /// Enable auto-start pub enabled: bool, @@ -165,17 +166,6 @@ pub struct EasytierConfig { pub virtual_ip: Option, } -impl Default for EasytierConfig { - fn default() -> Self { - Self { - enabled: false, - network_name: String::new(), - network_secret: String::new(), - peer_urls: Vec::new(), - virtual_ip: None, - } - } -} /// Combined extensions configuration #[typeshare] diff --git a/src/hid/backend.rs b/src/hid/backend.rs index 95de431f..dbb8dcbb 100644 --- a/src/hid/backend.rs +++ b/src/hid/backend.rs @@ -14,6 +14,7 @@ fn default_ch9329_baud_rate() -> u32 { /// HID backend type #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] +#[derive(Default)] pub enum HidBackendType { /// USB OTG gadget mode Otg, @@ -26,14 +27,10 @@ pub enum HidBackendType { baud_rate: u32, }, /// No HID backend (disabled) + #[default] None, } -impl Default for HidBackendType { - fn default() -> Self { - Self::None - } -} impl HidBackendType { /// Check if OTG backend is available on this system diff --git a/src/hid/ch9329.rs b/src/hid/ch9329.rs index 0893a49e..85196ce8 100644 --- a/src/hid/ch9329.rs +++ b/src/hid/ch9329.rs @@ -219,8 +219,10 @@ impl From for LedStatus { /// CH9329 work mode #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u8)] +#[derive(Default)] pub enum WorkMode { /// Mode 0: Standard USB Keyboard + Mouse (default) + #[default] KeyboardMouse = 0x00, /// Mode 1: Standard USB Keyboard only KeyboardOnly = 0x01, @@ -230,17 +232,14 @@ pub enum WorkMode { CustomHid = 0x03, } -impl Default for WorkMode { - fn default() -> Self { - Self::KeyboardMouse - } -} /// CH9329 serial communication mode #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u8)] +#[derive(Default)] pub enum SerialMode { /// Mode 0: Protocol transmission mode (default) + #[default] Protocol = 0x00, /// Mode 1: ASCII mode Ascii = 0x01, @@ -248,11 +247,6 @@ pub enum SerialMode { Transparent = 0x02, } -impl Default for SerialMode { - fn default() -> Self { - Self::Protocol - } -} /// CH9329 configuration parameters #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/hid/mod.rs b/src/hid/mod.rs index 611bdea8..b73dc916 100644 --- a/src/hid/mod.rs +++ b/src/hid/mod.rs @@ -42,17 +42,17 @@ pub struct HidInfo { pub screen_resolution: Option<(u32, u32)>, } -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use tokio::sync::RwLock; use tracing::{info, warn}; use crate::error::{AppError, Result}; use crate::otg::OtgService; +use std::time::Duration; use tokio::sync::mpsc; use tokio::sync::Mutex; use tokio::task::JoinHandle; -use std::time::Duration; const HID_EVENT_QUEUE_CAPACITY: usize = 64; const HID_EVENT_SEND_TIMEOUT_MS: u64 = 30; @@ -203,7 +203,10 @@ impl HidController { )); } - if matches!(event.event_type, MouseEventType::Move | MouseEventType::MoveAbs) { + if matches!( + event.event_type, + MouseEventType::Move | MouseEventType::MoveAbs + ) { // Best-effort: drop/merge move events if queue is full self.enqueue_mouse_move(event) } else { @@ -470,13 +473,7 @@ impl HidController { None => break, }; - process_hid_event( - event, - &backend, - &monitor, - &backend_type, - ) - .await; + process_hid_event(event, &backend, &monitor, &backend_type).await; // After each event, flush latest move if pending if pending_move_flag.swap(false, Ordering::AcqRel) { @@ -505,9 +502,9 @@ impl HidController { self.pending_move_flag.store(true, Ordering::Release); Ok(()) } - Err(mpsc::error::TrySendError::Closed(_)) => Err(AppError::BadRequest( - "HID event queue closed".to_string(), - )), + Err(mpsc::error::TrySendError::Closed(_)) => { + Err(AppError::BadRequest("HID event queue closed".to_string())) + } } } @@ -517,9 +514,11 @@ impl HidController { Err(mpsc::error::TrySendError::Full(ev)) => { // For non-move events, wait briefly to avoid dropping critical input let tx = self.hid_tx.clone(); - let send_result = - tokio::time::timeout(Duration::from_millis(HID_EVENT_SEND_TIMEOUT_MS), tx.send(ev)) - .await; + let send_result = tokio::time::timeout( + Duration::from_millis(HID_EVENT_SEND_TIMEOUT_MS), + tx.send(ev), + ) + .await; if send_result.is_ok() { Ok(()) } else { @@ -527,9 +526,9 @@ impl HidController { Ok(()) } } - Err(mpsc::error::TrySendError::Closed(_)) => Err(AppError::BadRequest( - "HID event queue closed".to_string(), - )), + Err(mpsc::error::TrySendError::Closed(_)) => { + Err(AppError::BadRequest("HID event queue closed".to_string())) + } } } } diff --git a/src/hid/monitor.rs b/src/hid/monitor.rs index 0ce84d88..1fc8834f 100644 --- a/src/hid/monitor.rs +++ b/src/hid/monitor.rs @@ -17,8 +17,10 @@ use crate::utils::LogThrottler; /// HID health status #[derive(Debug, Clone, PartialEq)] +#[derive(Default)] pub enum HidHealthStatus { /// Device is healthy and operational + #[default] Healthy, /// Device has an error, attempting recovery Error { @@ -33,11 +35,6 @@ pub enum HidHealthStatus { Disconnected, } -impl Default for HidHealthStatus { - fn default() -> Self { - Self::Healthy - } -} /// HID health monitor configuration #[derive(Debug, Clone)] @@ -196,7 +193,7 @@ impl HidHealthMonitor { let attempt = self.retry_count.load(Ordering::Relaxed); // Only publish every 5 attempts to avoid event spam - if attempt == 1 || attempt % 5 == 0 { + if attempt == 1 || attempt.is_multiple_of(5) { debug!("HID {} reconnecting, attempt {}", backend, attempt); if let Some(ref events) = *self.events.read().await { diff --git a/src/hid/otg.rs b/src/hid/otg.rs index b21917d4..3c04bea0 100644 --- a/src/hid/otg.rs +++ b/src/hid/otg.rs @@ -228,7 +228,7 @@ impl OtgBackend { Ok(false) } Ok(_) => Ok(false), - Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)), + Err(e) => Err(std::io::Error::other(e)), } } @@ -393,21 +393,10 @@ impl OtgBackend { /// Check if all HID device files exist pub fn check_devices_exist(&self) -> bool { - self.keyboard_path - .as_ref() - .map_or(true, |p| p.exists()) - && self - .mouse_rel_path - .as_ref() - .map_or(true, |p| p.exists()) - && self - .mouse_abs_path - .as_ref() - .map_or(true, |p| p.exists()) - && self - .consumer_path - .as_ref() - .map_or(true, |p| p.exists()) + self.keyboard_path.as_ref().is_none_or(|p| p.exists()) + && self.mouse_rel_path.as_ref().is_none_or(|p| p.exists()) + && self.mouse_abs_path.as_ref().is_none_or(|p| p.exists()) + && self.consumer_path.as_ref().is_none_or(|p| p.exists()) } /// Get list of missing device paths @@ -952,9 +941,7 @@ impl HidBackend for OtgBackend { } fn supports_absolute_mouse(&self) -> bool { - self.mouse_abs_path - .as_ref() - .map_or(false, |p| p.exists()) + self.mouse_abs_path.as_ref().is_some_and(|p| p.exists()) } async fn send_consumer(&self, event: ConsumerEvent) -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 43f04907..1b264448 100644 --- a/src/main.rs +++ b/src/main.rs @@ -158,7 +158,11 @@ async fn main() -> anyhow::Result<()> { } let bind_ips = resolve_bind_addresses(&config.web)?; - let scheme = if config.web.https_enabled { "https" } else { "http" }; + let scheme = if config.web.https_enabled { + "https" + } else { + "http" + }; let bind_port = if config.web.https_enabled { config.web.https_port } else { @@ -646,7 +650,7 @@ async fn main() -> anyhow::Result<()> { let server = axum_server::from_tcp_rustls(listener, tls_config.clone())? .serve(app.clone().into_make_service()); - servers.push(async move { server.await }); + servers.push(server); } tokio::select! { diff --git a/src/msd/controller.rs b/src/msd/controller.rs index 5641d3c7..5d38396d 100644 --- a/src/msd/controller.rs +++ b/src/msd/controller.rs @@ -52,10 +52,7 @@ impl MsdController { /// # Parameters /// * `otg_service` - OTG service for gadget management /// * `msd_dir` - Base directory for MSD storage - pub fn new( - otg_service: Arc, - msd_dir: impl Into, - ) -> Self { + pub fn new(otg_service: Arc, msd_dir: impl Into) -> Self { let msd_dir = msd_dir.into(); let images_path = msd_dir.join("images"); let ventoy_dir = msd_dir.join("ventoy"); diff --git a/src/msd/image.rs b/src/msd/image.rs index d08b7a18..8e83425c 100644 --- a/src/msd/image.rs +++ b/src/msd/image.rs @@ -88,7 +88,7 @@ impl ImageManager { .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) .map(|d| { chrono::DateTime::from_timestamp(d.as_secs() as i64, 0) - .unwrap_or_else(|| Utc::now().into()) + .unwrap_or_else(Utc::now) }) .unwrap_or_else(Utc::now); @@ -400,7 +400,7 @@ impl ImageManager { .headers() .get(reqwest::header::CONTENT_DISPOSITION) .and_then(|v| v.to_str().ok()) - .and_then(|s| extract_filename_from_content_disposition(s)); + .and_then(extract_filename_from_content_disposition); if let Some(name) = from_header { sanitize_filename(&name) diff --git a/src/msd/monitor.rs b/src/msd/monitor.rs index a9f80109..077dadaf 100644 --- a/src/msd/monitor.rs +++ b/src/msd/monitor.rs @@ -16,8 +16,10 @@ use crate::utils::LogThrottler; /// MSD health status #[derive(Debug, Clone, PartialEq)] +#[derive(Default)] pub enum MsdHealthStatus { /// Device is healthy and operational + #[default] Healthy, /// Device has an error Error { @@ -28,11 +30,6 @@ pub enum MsdHealthStatus { }, } -impl Default for MsdHealthStatus { - fn default() -> Self { - Self::Healthy - } -} /// MSD health monitor configuration #[derive(Debug, Clone)] diff --git a/src/msd/types.rs b/src/msd/types.rs index a658db16..5a061c59 100644 --- a/src/msd/types.rs +++ b/src/msd/types.rs @@ -7,8 +7,10 @@ use std::path::PathBuf; /// MSD operating mode #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum MsdMode { /// No storage connected + #[default] None, /// Image file mounted (ISO/IMG) Image, @@ -16,11 +18,6 @@ pub enum MsdMode { Drive, } -impl Default for MsdMode { - fn default() -> Self { - Self::None - } -} /// Image file metadata #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/msd/ventoy_drive.rs b/src/msd/ventoy_drive.rs index 8839a6e1..57d8f4a2 100644 --- a/src/msd/ventoy_drive.rs +++ b/src/msd/ventoy_drive.rs @@ -328,8 +328,7 @@ impl VentoyDrive { let image = match VentoyImage::open(&path) { Ok(img) => img, Err(e) => { - let _ = rt.block_on(tx.send(Err(std::io::Error::new( - std::io::ErrorKind::Other, + let _ = rt.block_on(tx.send(Err(std::io::Error::other( e.to_string(), )))); return; @@ -341,8 +340,7 @@ impl VentoyDrive { // Stream the file through the writer if let Err(e) = image.read_file_to_writer(&file_path_owned, &mut chunk_writer) { - let _ = rt.block_on(tx.send(Err(std::io::Error::new( - std::io::ErrorKind::Other, + let _ = rt.block_on(tx.send(Err(std::io::Error::other( e.to_string(), )))); } @@ -543,12 +541,11 @@ mod tests { /// Decompress xz file using system command fn decompress_xz(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> { let output = Command::new("xz") - .args(&["-d", "-k", "-c", src.to_str().unwrap()]) + .args(["-d", "-k", "-c", src.to_str().unwrap()]) .output()?; if !output.status.success() { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, + return Err(std::io::Error::other( format!( "xz decompress failed: {}", String::from_utf8_lossy(&output.stderr) diff --git a/src/otg/manager.rs b/src/otg/manager.rs index 64773d2b..9996238a 100644 --- a/src/otg/manager.rs +++ b/src/otg/manager.rs @@ -422,7 +422,11 @@ impl OtgGadgetManager { if dest.exists() { if let Err(e) = remove_file(&dest) { - warn!("Failed to remove existing config link {}: {}", dest.display(), e); + warn!( + "Failed to remove existing config link {}: {}", + dest.display(), + e + ); continue; } } diff --git a/src/otg/service.rs b/src/otg/service.rs index 2be3c944..e0eeb37a 100644 --- a/src/otg/service.rs +++ b/src/otg/service.rs @@ -36,6 +36,7 @@ const FLAG_MSD: u8 = 0b10; /// HID device paths #[derive(Debug, Clone)] +#[derive(Default)] pub struct HidDevicePaths { pub keyboard: Option, pub mouse_relative: Option, @@ -43,16 +44,6 @@ pub struct HidDevicePaths { pub consumer: Option, } -impl Default for HidDevicePaths { - fn default() -> Self { - Self { - keyboard: None, - mouse_relative: None, - mouse_absolute: None, - consumer: None, - } - } -} impl HidDevicePaths { pub fn existing_paths(&self) -> Vec { @@ -239,14 +230,13 @@ impl OtgService { let requested_functions = self.hid_functions.read().await.clone(); { let state = self.state.read().await; - if state.hid_enabled { - if state.hid_functions.as_ref() == Some(&requested_functions) { + if state.hid_enabled + && state.hid_functions.as_ref() == Some(&requested_functions) { if let Some(ref paths) = state.hid_paths { info!("HID already enabled, returning existing paths"); return Ok(paths.clone()); } } - } } // Recreate gadget with both HID and MSD if needed @@ -671,7 +661,7 @@ mod tests { fn test_service_creation() { let _service = OtgService::new(); // Just test that creation doesn't panic - assert!(!OtgService::is_available() || true); // Depends on environment + let _ = OtgService::is_available(); // Depends on environment } #[tokio::test] diff --git a/src/rustdesk/bytes_codec.rs b/src/rustdesk/bytes_codec.rs index a592f7f5..18f163ca 100644 --- a/src/rustdesk/bytes_codec.rs +++ b/src/rustdesk/bytes_codec.rs @@ -50,7 +50,7 @@ fn decode_header(first_byte: u8, header_bytes: &[u8]) -> (usize, usize) { let head_len = ((first_byte & 0x3) + 1) as usize; let mut n = first_byte as usize; - if head_len > 1 && header_bytes.len() >= 1 { + if head_len > 1 && !header_bytes.is_empty() { n |= (header_bytes[0] as usize) << 8; } if head_len > 2 && header_bytes.len() >= 2 { diff --git a/src/rustdesk/config.rs b/src/rustdesk/config.rs index 3a72e792..c8c64f29 100644 --- a/src/rustdesk/config.rs +++ b/src/rustdesk/config.rs @@ -202,9 +202,11 @@ mod tests { #[test] fn test_rendezvous_addr() { - let mut config = RustDeskConfig::default(); + let mut config = RustDeskConfig { + rendezvous_server: "example.com".to_string(), + ..Default::default() + }; - config.rendezvous_server = "example.com".to_string(); assert_eq!(config.rendezvous_addr(), "example.com:21116"); config.rendezvous_server = "example.com:21116".to_string(); @@ -217,10 +219,12 @@ mod tests { #[test] fn test_relay_addr() { - let mut config = RustDeskConfig::default(); + let mut config = RustDeskConfig { + rendezvous_server: "example.com".to_string(), + ..Default::default() + }; // Rendezvous server configured, relay defaults to same host - config.rendezvous_server = "example.com".to_string(); assert_eq!(config.relay_addr(), Some("example.com:21117".to_string())); // Explicit relay server @@ -238,10 +242,12 @@ mod tests { #[test] fn test_effective_rendezvous_server() { - let mut config = RustDeskConfig::default(); + let mut config = RustDeskConfig { + rendezvous_server: "custom.example.com".to_string(), + ..Default::default() + }; // When user sets a server, use it - config.rendezvous_server = "custom.example.com".to_string(); assert_eq!(config.effective_rendezvous_server(), "custom.example.com"); // When empty, returns empty diff --git a/src/rustdesk/connection.rs b/src/rustdesk/connection.rs index b408ed12..9581bd91 100644 --- a/src/rustdesk/connection.rs +++ b/src/rustdesk/connection.rs @@ -729,7 +729,7 @@ impl Connection { } // Check if client sent supported_decoding with a codec preference - if let Some(ref supported_decoding) = opt.supported_decoding.as_ref() { + if let Some(supported_decoding) = opt.supported_decoding.as_ref() { let prefer = supported_decoding.prefer.value(); debug!("Client codec preference: prefer={}", prefer); @@ -1352,8 +1352,12 @@ impl Connection { debug!("Mouse event: x={}, y={}, mask={}", me.x, me.y, me.mask); // Convert RustDesk mouse event to One-KVM mouse events - let mouse_events = - convert_mouse_event(me, self.screen_width, self.screen_height, self.relative_mouse_active); + let mouse_events = convert_mouse_event( + me, + self.screen_width, + self.screen_height, + self.relative_mouse_active, + ); // Send to HID controller if available if let Some(ref hid) = self.hid { @@ -1616,7 +1620,10 @@ async fn run_video_streaming( ); } if let Err(e) = video_manager.request_keyframe().await { - debug!("Failed to request keyframe for connection {}: {}", conn_id, e); + debug!( + "Failed to request keyframe for connection {}: {}", + conn_id, e + ); } // Inner loop: receives frames from current subscription diff --git a/src/rustdesk/crypto.rs b/src/rustdesk/crypto.rs index 10860402..88b1257f 100644 --- a/src/rustdesk/crypto.rs +++ b/src/rustdesk/crypto.rs @@ -189,7 +189,7 @@ pub fn hash_password_double(password: &str, salt: &str, challenge: &str) -> Vec< // Second hash: SHA256(first_hash + challenge) let mut hasher2 = Sha256::new(); - hasher2.update(&first_hash); + hasher2.update(first_hash); hasher2.update(challenge.as_bytes()); hasher2.finalize().to_vec() } diff --git a/src/rustdesk/frame_adapters.rs b/src/rustdesk/frame_adapters.rs index fbee2c1e..3fa8c8e4 100644 --- a/src/rustdesk/frame_adapters.rs +++ b/src/rustdesk/frame_adapters.rs @@ -127,7 +127,8 @@ impl VideoFrameAdapter { // Inject cached SPS/PPS before IDR when missing if is_keyframe && (!has_sps || !has_pps) { - if let (Some(ref sps), Some(ref pps)) = (self.h264_sps.as_ref(), self.h264_pps.as_ref()) { + if let (Some(sps), Some(pps)) = (self.h264_sps.as_ref(), self.h264_pps.as_ref()) + { let mut out = Vec::with_capacity(8 + sps.len() + pps.len() + data.len()); out.extend_from_slice(&[0, 0, 0, 1]); out.extend_from_slice(sps); diff --git a/src/rustdesk/mod.rs b/src/rustdesk/mod.rs index 5b636497..128337e1 100644 --- a/src/rustdesk/mod.rs +++ b/src/rustdesk/mod.rs @@ -36,8 +36,8 @@ use tracing::{debug, error, info, warn}; use crate::audio::AudioController; use crate::hid::HidController; -use crate::video::stream_manager::VideoStreamManager; use crate::utils::bind_tcp_listener; +use crate::video::stream_manager::VideoStreamManager; use self::config::RustDeskConfig; use self::connection::ConnectionManager; @@ -559,6 +559,7 @@ impl RustDeskService { /// 2. Send RelayResponse with client's socket_addr /// 3. Connect to RELAY server /// 4. Accept connection without waiting for response +#[allow(clippy::too_many_arguments)] async fn handle_relay_request( rendezvous_addr: &str, relay_server: &str, diff --git a/src/rustdesk/rendezvous.rs b/src/rustdesk/rendezvous.rs index d347f81f..87c9ecd4 100644 --- a/src/rustdesk/rendezvous.rs +++ b/src/rustdesk/rendezvous.rs @@ -559,7 +559,7 @@ impl RendezvousMediator { ); let msg = make_punch_hole_sent( - &ph.socket_addr.to_vec(), // Use peer's socket_addr, not ours + &ph.socket_addr, // Use peer's socket_addr, not ours &id, &ph.relay_server, ph.nat_type.enum_value().unwrap_or(NatType::UNKNOWN_NAT), diff --git a/src/state.rs b/src/state.rs index b322f7ed..83d8237f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -64,6 +64,7 @@ pub struct AppState { impl AppState { /// Create new application state + #[allow(clippy::too_many_arguments)] pub fn new( config: ConfigStore, sessions: SessionStore, diff --git a/src/stream/mjpeg_streamer.rs b/src/stream/mjpeg_streamer.rs index d9219123..6fbd7379 100644 --- a/src/stream/mjpeg_streamer.rs +++ b/src/stream/mjpeg_streamer.rs @@ -15,16 +15,16 @@ //! //! Note: Audio WebSocket is handled separately by audio_ws.rs (/api/ws/audio) -use std::io; +use crate::utils::LogThrottler; +use crate::video::v4l2r_capture::V4l2rCaptureStream; use std::collections::HashMap; +use std::io; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::sync::{Mutex, RwLock}; use tracing::{error, info, warn}; -use crate::video::v4l2r_capture::V4l2rCaptureStream; -use crate::utils::LogThrottler; use crate::audio::AudioController; use crate::error::{AppError, Result}; @@ -624,7 +624,7 @@ impl MjpegStreamer { validate_counter = validate_counter.wrapping_add(1); if pixel_format.is_compressed() - && validate_counter % JPEG_VALIDATE_INTERVAL == 0 + && validate_counter.is_multiple_of(JPEG_VALIDATE_INTERVAL) && !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size]) { continue; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 12bf372a..c31db32d 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,8 +2,8 @@ //! //! This module contains common utilities used across the codebase. -pub mod throttle; pub mod net; +pub mod throttle; -pub use throttle::LogThrottler; pub use net::{bind_tcp_listener, bind_udp_socket}; +pub use throttle::LogThrottler; diff --git a/src/video/capture.rs b/src/video/capture.rs index e2218ac9..464227fd 100644 --- a/src/video/capture.rs +++ b/src/video/capture.rs @@ -2,10 +2,10 @@ //! //! Provides async video capture using memory-mapped buffers. +use bytes::Bytes; use std::collections::HashMap; use std::io; use std::path::{Path, PathBuf}; -use bytes::Bytes; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; diff --git a/src/video/device.rs b/src/video/device.rs index 543340f6..b35bee8e 100644 --- a/src/video/device.rs +++ b/src/video/device.rs @@ -6,11 +6,11 @@ use std::path::{Path, PathBuf}; use std::sync::mpsc; use std::time::Duration; use tracing::{debug, info, warn}; -use v4l2r::nix::errno::Errno; use v4l2r::bindings::{v4l2_frmivalenum, v4l2_frmsizeenum}; use v4l2r::ioctl::{ self, Capabilities, Capability as V4l2rCapability, FormatIterator, FrmIvalTypes, FrmSizeTypes, }; +use v4l2r::nix::errno::Errno; use v4l2r::{Format as V4l2rFormat, QueueType}; use super::format::{PixelFormat, Resolution}; @@ -96,7 +96,9 @@ impl VideoDevice { .read(true) .write(true) .open(&path) - .map_err(|e| AppError::VideoError(format!("Failed to open device {:?}: {}", path, e)))?; + .map_err(|e| { + AppError::VideoError(format!("Failed to open device {:?}: {}", path, e)) + })?; Ok(Self { path, fd }) } @@ -106,10 +108,9 @@ impl VideoDevice { let path = path.as_ref().to_path_buf(); debug!("Opening video device (read-only): {:?}", path); - let fd = File::options() - .read(true) - .open(&path) - .map_err(|e| AppError::VideoError(format!("Failed to open device {:?}: {}", path, e)))?; + let fd = File::options().read(true).open(&path).map_err(|e| { + AppError::VideoError(format!("Failed to open device {:?}: {}", path, e)) + })?; Ok(Self { path, fd }) } @@ -206,8 +207,9 @@ impl VideoDevice { if let Some(size) = size.size() { match size { FrmSizeTypes::Discrete(d) => { - let fps = - self.enumerate_fps(fourcc, d.width, d.height).unwrap_or_default(); + let fps = self + .enumerate_fps(fourcc, d.width, d.height) + .unwrap_or_default(); resolutions.push(ResolutionInfo::new(d.width, d.height, fps)); } FrmSizeTypes::StepWise(s) => { @@ -225,7 +227,8 @@ impl VideoDevice { let fps = self .enumerate_fps(fourcc, res.width, res.height) .unwrap_or_default(); - resolutions.push(ResolutionInfo::new(res.width, res.height, fps)); + resolutions + .push(ResolutionInfo::new(res.width, res.height, fps)); } } } @@ -265,11 +268,7 @@ impl VideoDevice { let mut index = 0u32; loop { match ioctl::enum_frame_intervals::( - &self.fd, - index, - fourcc, - width, - height, + &self.fd, index, fourcc, width, height, ) { Ok(interval) => { if let Some(interval) = interval.intervals() { @@ -411,7 +410,7 @@ impl VideoDevice { .max() .unwrap_or(0); - priority += (max_resolution / 100000) as u32; + priority += max_resolution / 100000; // Known good drivers get bonus let good_drivers = ["uvcvideo", "tc358743"]; @@ -563,15 +562,7 @@ fn sysfs_maybe_capture(path: &Path) -> bool { } let skip_hints = [ - "codec", - "decoder", - "encoder", - "isp", - "mem2mem", - "m2m", - "vbi", - "radio", - "metadata", + "codec", "decoder", "encoder", "isp", "mem2mem", "m2m", "vbi", "radio", "metadata", "output", ]; if skip_hints.iter().any(|hint| sysfs_name.contains(hint)) && !maybe_capture { diff --git a/src/video/encoder/h264.rs b/src/video/encoder/h264.rs index 65c2512b..f59a27b9 100644 --- a/src/video/encoder/h264.rs +++ b/src/video/encoder/h264.rs @@ -33,6 +33,7 @@ fn init_hwcodec_logging() { /// H.264 encoder type (detected from hwcodec) #[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Default)] pub enum H264EncoderType { /// NVIDIA NVENC Nvenc, @@ -49,6 +50,7 @@ pub enum H264EncoderType { /// Software encoding (libx264/openh264) Software, /// No encoder available + #[default] None, } @@ -67,11 +69,6 @@ impl std::fmt::Display for H264EncoderType { } } -impl Default for H264EncoderType { - fn default() -> Self { - Self::None - } -} /// Map codec name to encoder type fn codec_name_to_type(name: &str) -> H264EncoderType { @@ -94,10 +91,12 @@ fn codec_name_to_type(name: &str) -> H264EncoderType { /// Input pixel format for H264 encoder #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default)] pub enum H264InputFormat { /// YUV420P (I420) - planar Y, U, V Yuv420p, /// NV12 - Y plane + interleaved UV plane (optimal for VAAPI) + #[default] Nv12, /// NV21 - Y plane + interleaved VU plane Nv21, @@ -113,11 +112,6 @@ pub enum H264InputFormat { Bgr24, } -impl Default for H264InputFormat { - fn default() -> Self { - Self::Nv12 // Default to NV12 for VAAPI compatibility - } -} /// H.264 encoder configuration #[derive(Debug, Clone)] diff --git a/src/video/encoder/h265.rs b/src/video/encoder/h265.rs index 8a89015d..f96c6b62 100644 --- a/src/video/encoder/h265.rs +++ b/src/video/encoder/h265.rs @@ -31,6 +31,7 @@ fn init_hwcodec_logging() { /// H.265 encoder type (detected from hwcodec) #[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Default)] pub enum H265EncoderType { /// NVIDIA NVENC Nvenc, @@ -47,6 +48,7 @@ pub enum H265EncoderType { /// Software encoder (libx265) Software, /// No encoder available + #[default] None, } @@ -65,11 +67,6 @@ impl std::fmt::Display for H265EncoderType { } } -impl Default for H265EncoderType { - fn default() -> Self { - Self::None - } -} impl From for H265EncoderType { fn from(backend: EncoderBackend) -> Self { @@ -87,10 +84,12 @@ impl From for H265EncoderType { /// Input pixel format for H265 encoder #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default)] pub enum H265InputFormat { /// YUV420P (I420) - planar Y, U, V Yuv420p, /// NV12 - Y plane + interleaved UV plane (optimal for hardware encoders) + #[default] Nv12, /// NV21 - Y plane + interleaved VU plane Nv21, @@ -106,11 +105,6 @@ pub enum H265InputFormat { Bgr24, } -impl Default for H265InputFormat { - fn default() -> Self { - Self::Nv12 // Default to NV12 for hardware encoder compatibility - } -} /// H.265 encoder configuration #[derive(Debug, Clone)] @@ -256,8 +250,6 @@ pub fn detect_best_h265_encoder(width: u32, height: u32) -> (H265EncoderType, Op H265EncoderType::Rkmpp } else if codec.name.contains("v4l2m2m") { H265EncoderType::V4l2M2m - } else if codec.name.contains("libx265") { - H265EncoderType::Software } else { H265EncoderType::Software // Default to software for unknown }; diff --git a/src/video/encoder/registry.rs b/src/video/encoder/registry.rs index 1f9dd1a9..5a9658dc 100644 --- a/src/video/encoder/registry.rs +++ b/src/video/encoder/registry.rs @@ -145,6 +145,7 @@ impl EncoderBackend { } /// Parse from string (case-insensitive) + #[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Option { match s.to_lowercase().as_str() { "vaapi" => Some(EncoderBackend::Vaapi), diff --git a/src/video/encoder/traits.rs b/src/video/encoder/traits.rs index 940ec245..d4b5bd88 100644 --- a/src/video/encoder/traits.rs +++ b/src/video/encoder/traits.rs @@ -15,12 +15,14 @@ use crate::video::format::{PixelFormat, Resolution}; #[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 @@ -74,11 +76,6 @@ impl BitratePreset { } } -impl Default for BitratePreset { - fn default() -> Self { - Self::Balanced - } -} impl std::fmt::Display for BitratePreset { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/src/video/encoder/vp8.rs b/src/video/encoder/vp8.rs index 868af8ee..302fe7ab 100644 --- a/src/video/encoder/vp8.rs +++ b/src/video/encoder/vp8.rs @@ -31,12 +31,14 @@ fn init_hwcodec_logging() { /// VP8 encoder type (detected from hwcodec) #[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Default)] pub enum VP8EncoderType { /// VAAPI (Intel on Linux) Vaapi, /// Software encoder (libvpx) Software, /// No encoder available + #[default] None, } @@ -50,11 +52,6 @@ impl std::fmt::Display for VP8EncoderType { } } -impl Default for VP8EncoderType { - fn default() -> Self { - Self::None - } -} impl From for VP8EncoderType { fn from(backend: EncoderBackend) -> Self { @@ -68,18 +65,15 @@ impl From for VP8EncoderType { /// Input pixel format for VP8 encoder #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default)] pub enum VP8InputFormat { /// YUV420P (I420) - planar Y, U, V Yuv420p, /// NV12 - Y plane + interleaved UV plane + #[default] Nv12, } -impl Default for VP8InputFormat { - fn default() -> Self { - Self::Nv12 // Default to NV12 for VAAPI compatibility - } -} /// VP8 encoder configuration #[derive(Debug, Clone)] @@ -180,8 +174,6 @@ pub fn detect_best_vp8_encoder(width: u32, height: u32) -> (VP8EncoderType, Opti let encoder_type = if codec.name.contains("vaapi") { VP8EncoderType::Vaapi - } else if codec.name.contains("libvpx") { - VP8EncoderType::Software } else { VP8EncoderType::Software // Default to software for unknown }; diff --git a/src/video/encoder/vp9.rs b/src/video/encoder/vp9.rs index 6995db5d..6ff4c589 100644 --- a/src/video/encoder/vp9.rs +++ b/src/video/encoder/vp9.rs @@ -31,12 +31,14 @@ fn init_hwcodec_logging() { /// VP9 encoder type (detected from hwcodec) #[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Default)] pub enum VP9EncoderType { /// VAAPI (Intel on Linux) Vaapi, /// Software encoder (libvpx-vp9) Software, /// No encoder available + #[default] None, } @@ -50,11 +52,6 @@ impl std::fmt::Display for VP9EncoderType { } } -impl Default for VP9EncoderType { - fn default() -> Self { - Self::None - } -} impl From for VP9EncoderType { fn from(backend: EncoderBackend) -> Self { @@ -68,18 +65,15 @@ impl From for VP9EncoderType { /// Input pixel format for VP9 encoder #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default)] pub enum VP9InputFormat { /// YUV420P (I420) - planar Y, U, V Yuv420p, /// NV12 - Y plane + interleaved UV plane + #[default] Nv12, } -impl Default for VP9InputFormat { - fn default() -> Self { - Self::Nv12 // Default to NV12 for VAAPI compatibility - } -} /// VP9 encoder configuration #[derive(Debug, Clone)] @@ -180,8 +174,6 @@ pub fn detect_best_vp9_encoder(width: u32, height: u32) -> (VP9EncoderType, Opti let encoder_type = if codec.name.contains("vaapi") { VP9EncoderType::Vaapi - } else if codec.name.contains("libvpx") { - VP9EncoderType::Software } else { VP9EncoderType::Software // Default to software for unknown }; diff --git a/src/video/frame.rs b/src/video/frame.rs index dc8f4c92..9d0b43b0 100644 --- a/src/video/frame.rs +++ b/src/video/frame.rs @@ -81,6 +81,11 @@ impl FrameBuffer { pub fn len(&self) -> usize { self.data.len() } + + /// Check if the frame buffer has no data + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } } impl std::fmt::Debug for FrameBuffer { diff --git a/src/video/shared_video_pipeline.rs b/src/video/shared_video_pipeline.rs index 55fd660f..bfb2acc9 100644 --- a/src/video/shared_video_pipeline.rs +++ b/src/video/shared_video_pipeline.rs @@ -36,9 +36,6 @@ use crate::error::{AppError, Result}; use crate::utils::LogThrottler; use crate::video::convert::{Nv12Converter, PixelConverter}; use crate::video::decoder::MjpegTurboDecoder; -#[cfg(any(target_arch = "aarch64", target_arch = "arm"))] -use hwcodec::ffmpeg_hw::{last_error_message as ffmpeg_hw_last_error, HwMjpegH26xConfig, HwMjpegH26xPipeline}; -use crate::video::v4l2r_capture::V4l2rCaptureStream; use crate::video::encoder::h264::{detect_best_encoder, H264Config, H264Encoder, H264InputFormat}; use crate::video::encoder::h265::{ detect_best_h265_encoder, H265Config, H265Encoder, H265InputFormat, @@ -49,6 +46,11 @@ use crate::video::encoder::vp8::{detect_best_vp8_encoder, VP8Config, VP8Encoder} use crate::video::encoder::vp9::{detect_best_vp9_encoder, VP9Config, VP9Encoder}; use crate::video::format::{PixelFormat, Resolution}; use crate::video::frame::{FrameBuffer, FrameBufferPool, VideoFrame}; +use crate::video::v4l2r_capture::V4l2rCaptureStream; +#[cfg(any(target_arch = "aarch64", target_arch = "arm"))] +use hwcodec::ffmpeg_hw::{ + last_error_message as ffmpeg_hw_last_error, HwMjpegH26xConfig, HwMjpegH26xPipeline, +}; /// Encoded video frame for distribution #[derive(Debug, Clone)] @@ -508,7 +510,10 @@ impl SharedVideoPipeline { #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] if needs_mjpeg_decode && is_rkmpp_encoder - && matches!(config.output_codec, VideoEncoderType::H264 | VideoEncoderType::H265) + && matches!( + config.output_codec, + VideoEncoderType::H264 | VideoEncoderType::H265 + ) { info!( "Initializing FFmpeg HW MJPEG->{} pipeline (no fallback)", @@ -525,7 +530,11 @@ impl SharedVideoPipeline { thread_count: 1, }; let pipeline = HwMjpegH26xPipeline::new(hw_config).map_err(|e| { - let detail = if e.is_empty() { ffmpeg_hw_last_error() } else { e }; + let detail = if e.is_empty() { + ffmpeg_hw_last_error() + } else { + e + }; AppError::VideoError(format!( "FFmpeg HW MJPEG->{} init failed: {}", config.output_codec, detail @@ -899,7 +908,11 @@ impl SharedVideoPipeline { /// Get subscriber count pub fn subscriber_count(&self) -> usize { - self.subscribers.read().iter().filter(|tx| !tx.is_closed()).count() + self.subscribers + .read() + .iter() + .filter(|tx| !tx.is_closed()) + .count() } /// Report that a receiver has lagged behind @@ -948,7 +961,11 @@ impl SharedVideoPipeline { pipeline .reconfigure(bitrate_kbps as i32, gop as i32) .map_err(|e| { - let detail = if e.is_empty() { ffmpeg_hw_last_error() } else { e }; + let detail = if e.is_empty() { + ffmpeg_hw_last_error() + } else { + e + }; AppError::VideoError(format!( "FFmpeg HW reconfigure failed: {}", detail @@ -1364,8 +1381,7 @@ impl SharedVideoPipeline { error!("Capture error: {}", e); } } else { - let counter = - suppressed_capture_errors.entry(key).or_insert(0); + let counter = suppressed_capture_errors.entry(key).or_insert(0); *counter = counter.saturating_add(1); } } @@ -1380,7 +1396,7 @@ impl SharedVideoPipeline { validate_counter = validate_counter.wrapping_add(1); if pixel_format.is_compressed() - && validate_counter % JPEG_VALIDATE_INTERVAL == 0 + && validate_counter.is_multiple_of(JPEG_VALIDATE_INTERVAL) && !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size]) { continue; @@ -1401,7 +1417,6 @@ impl SharedVideoPipeline { *guard = Some(frame); } let _ = frame_seq_tx.send(sequence); - } pipeline.running_flag.store(false, Ordering::Release); @@ -1466,7 +1481,11 @@ impl SharedVideoPipeline { } let packet = pipeline.encode(raw_frame, pts_ms).map_err(|e| { - let detail = if e.is_empty() { ffmpeg_hw_last_error() } else { e }; + let detail = if e.is_empty() { + ffmpeg_hw_last_error() + } else { + e + }; AppError::VideoError(format!("FFmpeg HW encode failed: {}", detail)) })?; @@ -1486,9 +1505,10 @@ impl SharedVideoPipeline { } let decoded_buf = if input_format.is_compressed() { - let decoder = state.mjpeg_decoder.as_mut().ok_or_else(|| { - AppError::VideoError("MJPEG decoder not initialized".to_string()) - })?; + let decoder = state + .mjpeg_decoder + .as_mut() + .ok_or_else(|| AppError::VideoError("MJPEG decoder not initialized".to_string()))?; let decoded = decoder.decode(raw_frame)?; Some(decoded) } else { @@ -1518,16 +1538,18 @@ impl SharedVideoPipeline { debug!("[Pipeline] Keyframe will be generated for this frame"); } - let encode_result = if needs_yuv420p && state.yuv420p_converter.is_some() { + let encode_result = if needs_yuv420p { // Software encoder with direct input conversion to YUV420P - let conv = state.yuv420p_converter.as_mut().unwrap(); - let yuv420p_data = conv - .convert(raw_frame) - .map_err(|e| AppError::VideoError(format!("YUV420P conversion failed: {}", e)))?; - encoder.encode_raw(yuv420p_data, pts_ms) - } else if state.nv12_converter.is_some() { + if let Some(conv) = state.yuv420p_converter.as_mut() { + let yuv420p_data = conv.convert(raw_frame).map_err(|e| { + AppError::VideoError(format!("YUV420P conversion failed: {}", e)) + })?; + encoder.encode_raw(yuv420p_data, pts_ms) + } else { + encoder.encode_raw(raw_frame, pts_ms) + } + } else if let Some(conv) = state.nv12_converter.as_mut() { // Hardware encoder with input conversion to NV12 - let conv = state.nv12_converter.as_mut().unwrap(); let nv12_data = conv .convert(raw_frame) .map_err(|e| AppError::VideoError(format!("NV12 conversion failed: {}", e)))?; diff --git a/src/video/stream_manager.rs b/src/video/stream_manager.rs index cabe553a..a2734a5f 100644 --- a/src/video/stream_manager.rs +++ b/src/video/stream_manager.rs @@ -718,9 +718,11 @@ impl VideoStreamManager { /// Returns None if video capture cannot be started or pipeline creation fails. pub async fn subscribe_encoded_frames( &self, - ) -> Option>> { + ) -> Option< + tokio::sync::mpsc::Receiver< + std::sync::Arc, + >, + > { // 1. Ensure video capture is initialized (for config discovery) if self.streamer.state().await == StreamerState::Uninitialized { tracing::info!("Initializing video capture for encoded frame subscription"); @@ -756,7 +758,11 @@ impl VideoStreamManager { } // 3. Use WebRtcStreamer to ensure the shared video pipeline is running - match self.webrtc_streamer.ensure_video_pipeline_for_external().await { + match self + .webrtc_streamer + .ensure_video_pipeline_for_external() + .await + { Ok(pipeline) => Some(pipeline.subscribe()), Err(e) => { tracing::error!("Failed to start shared video pipeline: {}", e); diff --git a/src/video/streamer.rs b/src/video/streamer.rs index af2e073f..fdca1d7c 100644 --- a/src/video/streamer.rs +++ b/src/video/streamer.rs @@ -571,11 +571,9 @@ impl Streamer { break; } } - } else { - if zero_since.is_some() { - info!("Clients reconnected, canceling auto-pause"); - zero_since = None; - } + } else if zero_since.is_some() { + info!("Clients reconnected, canceling auto-pause"); + zero_since = None; } } }); @@ -805,7 +803,7 @@ impl Streamer { validate_counter = validate_counter.wrapping_add(1); if pixel_format.is_compressed() - && validate_counter % JPEG_VALIDATE_INTERVAL == 0 + && validate_counter.is_multiple_of(JPEG_VALIDATE_INTERVAL) && !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size]) { continue; @@ -964,7 +962,7 @@ impl Streamer { *streamer.state.write().await = StreamerState::Recovering; // Publish reconnecting event (every 5 attempts to avoid spam) - if attempt == 1 || attempt % 5 == 0 { + if attempt == 1 || attempt.is_multiple_of(5) { streamer .publish_event(SystemEvent::StreamReconnecting { device: device_path.clone(), diff --git a/src/video/v4l2r_capture.rs b/src/video/v4l2r_capture.rs index cd23f263..0027896c 100644 --- a/src/video/v4l2r_capture.rs +++ b/src/video/v4l2r_capture.rs @@ -10,8 +10,8 @@ use nix::poll::{poll, PollFd, PollFlags, PollTimeout}; use tracing::{debug, warn}; use v4l2r::bindings::{v4l2_requestbuffers, v4l2_streamparm, v4l2_streamparm__bindgen_ty_1}; use v4l2r::ioctl::{ - self, Capabilities, Capability as V4l2rCapability, MemoryConsistency, PlaneMapping, - QBufPlane, QBuffer, QueryBuffer, V4l2Buffer, + self, Capabilities, Capability as V4l2rCapability, MemoryConsistency, PlaneMapping, QBufPlane, + QBuffer, QueryBuffer, V4l2Buffer, }; use v4l2r::memory::{MemoryType, MmapHandle}; use v4l2r::{Format as V4l2rFormat, PixelFormat as V4l2rPixelFormat, QueueType}; @@ -68,24 +68,21 @@ impl V4l2rCaptureStream { )); }; - let mut fmt: V4l2rFormat = ioctl::g_fmt(&fd, queue).map_err(|e| { - AppError::VideoError(format!("Failed to get device format: {}", e)) - })?; + let mut fmt: V4l2rFormat = ioctl::g_fmt(&fd, queue) + .map_err(|e| AppError::VideoError(format!("Failed to get device format: {}", e)))?; fmt.width = resolution.width; fmt.height = resolution.height; fmt.pixelformat = V4l2rPixelFormat::from(&format.to_fourcc()); - let actual_fmt: V4l2rFormat = ioctl::s_fmt(&mut fd, (queue, &fmt)).map_err(|e| { - AppError::VideoError(format!("Failed to set device format: {}", e)) - })?; + let actual_fmt: V4l2rFormat = ioctl::s_fmt(&mut fd, (queue, &fmt)) + .map_err(|e| AppError::VideoError(format!("Failed to set device format: {}", e)))?; let actual_resolution = Resolution::new(actual_fmt.width, actual_fmt.height); let actual_format = PixelFormat::from_v4l2r(actual_fmt.pixelformat).unwrap_or(format); let stride = actual_fmt - .plane_fmt - .get(0) + .plane_fmt.first() .map(|p| p.bytesperline) .unwrap_or_else(|| match actual_format.bytes_per_pixel() { Some(bpp) => actual_resolution.width * bpp as u32, @@ -129,10 +126,7 @@ impl V4l2rCaptureStream { let mut plane_maps = Vec::with_capacity(query.planes.len()); for plane in &query.planes { let mapping = ioctl::mmap(&fd, plane.mem_offset, plane.length).map_err(|e| { - AppError::VideoError(format!( - "Failed to mmap buffer {}: {}", - index, e - )) + AppError::VideoError(format!("Failed to mmap buffer {}: {}", index, e)) })?; plane_maps.push(mapping); } @@ -150,9 +144,8 @@ impl V4l2rCaptureStream { }; stream.queue_all_buffers()?; - ioctl::streamon(&stream.fd, stream.queue).map_err(|e| { - AppError::VideoError(format!("Failed to start capture stream: {}", e)) - })?; + ioctl::streamon(&stream.fd, stream.queue) + .map_err(|e| AppError::VideoError(format!("Failed to start capture stream: {}", e)))?; Ok(stream) } @@ -172,9 +165,8 @@ impl V4l2rCaptureStream { pub fn next_into(&mut self, dst: &mut Vec) -> io::Result { self.wait_ready()?; - let dqbuf: V4l2Buffer = ioctl::dqbuf(&self.fd, self.queue).map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("dqbuf failed: {}", e)) - })?; + let dqbuf: V4l2Buffer = ioctl::dqbuf(&self.fd, self.queue) + .map_err(|e| io::Error::other(format!("dqbuf failed: {}", e)))?; let index = dqbuf.as_v4l2_buffer().index as usize; let sequence = dqbuf.as_v4l2_buffer().sequence as u64; @@ -211,7 +203,7 @@ impl V4l2rCaptureStream { } self.queue_buffer(index as u32) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| io::Error::other(e.to_string()))?; Ok(CaptureMeta { bytes_used: total, @@ -240,7 +232,7 @@ impl V4l2rCaptureStream { } fn queue_buffer(&mut self, index: u32) -> Result<()> { - let handle = MmapHandle::default(); + let handle = MmapHandle; let planes = self.mappings[index as usize] .iter() .map(|mapping| { diff --git a/src/video/video_session.rs b/src/video/video_session.rs index 7b51f725..07af5f01 100644 --- a/src/video/video_session.rs +++ b/src/video/video_session.rs @@ -326,7 +326,6 @@ impl VideoSessionManager { bitrate_preset: self.config.bitrate_preset, fps: self.config.fps, encoder_backend: self.config.encoder_backend, - ..Default::default() }; // Create new pipeline diff --git a/src/web/handlers/config/apply.rs b/src/web/handlers/config/apply.rs index d7dd88e0..7af5d15a 100644 --- a/src/web/handlers/config/apply.rs +++ b/src/web/handlers/config/apply.rs @@ -191,9 +191,7 @@ pub async fn apply_hid_config( // Low-endpoint UDCs (e.g., musb) cannot handle consumer control endpoints reliably if new_config.backend == HidBackend::Otg { - if let Some(udc) = - crate::otg::configfs::resolve_udc_name(new_config.otg_udc.as_deref()) - { + if let Some(udc) = crate::otg::configfs::resolve_udc_name(new_config.otg_udc.as_deref()) { if crate::otg::configfs::is_low_endpoint_udc(&udc) && new_hid_functions.consumer { tracing::warn!( "UDC {} has low endpoint resources, disabling consumer control", diff --git a/src/web/handlers/extensions.rs b/src/web/handlers/extensions.rs index f91cdb2b..a90c8962 100644 --- a/src/web/handlers/extensions.rs +++ b/src/web/handlers/extensions.rs @@ -86,7 +86,7 @@ pub async fn start_extension( // Start the extension mgr.start(ext_id, &config.extensions) .await - .map_err(|e| AppError::Internal(e))?; + .map_err(AppError::Internal)?; // Return updated status Ok(Json(ExtensionInfo { @@ -108,7 +108,7 @@ pub async fn stop_extension( let mgr = &state.extensions; // Stop the extension - mgr.stop(ext_id).await.map_err(|e| AppError::Internal(e))?; + mgr.stop(ext_id).await.map_err(AppError::Internal)?; // Return updated status Ok(Json(ExtensionInfo { @@ -263,14 +263,16 @@ pub async fn update_gostc_config( if was_enabled && !is_enabled { state.extensions.stop(ExtensionId::Gostc).await.ok(); - } else if !was_enabled && is_enabled && has_key { - if state.extensions.check_available(ExtensionId::Gostc) { - state - .extensions - .start(ExtensionId::Gostc, &new_config.extensions) - .await - .ok(); - } + } else if !was_enabled + && is_enabled + && has_key + && state.extensions.check_available(ExtensionId::Gostc) + { + state + .extensions + .start(ExtensionId::Gostc, &new_config.extensions) + .await + .ok(); } Ok(Json(new_config.extensions.gostc.clone())) @@ -312,14 +314,16 @@ pub async fn update_easytier_config( if was_enabled && !is_enabled { state.extensions.stop(ExtensionId::Easytier).await.ok(); - } else if !was_enabled && is_enabled && has_name { - if state.extensions.check_available(ExtensionId::Easytier) { - state - .extensions - .start(ExtensionId::Easytier, &new_config.extensions) - .await - .ok(); - } + } else if !was_enabled + && is_enabled + && has_name + && state.extensions.check_available(ExtensionId::Easytier) + { + state + .extensions + .start(ExtensionId::Easytier, &new_config.extensions) + .await + .ok(); } Ok(Json(new_config.extensions.easytier.clone())) diff --git a/src/web/handlers/mod.rs b/src/web/handlers/mod.rs index ea0d077f..5a7a16af 100644 --- a/src/web/handlers/mod.rs +++ b/src/web/handlers/mod.rs @@ -205,7 +205,7 @@ fn get_cpu_model() -> String { .count(); Some(format!("{} {}C", std::env::consts::ARCH, cores)) }) - .unwrap_or_else(|| format!("{}", std::env::consts::ARCH)) + .unwrap_or_else(|| std::env::consts::ARCH.to_string()) } /// CPU usage state for calculating usage between samples @@ -686,8 +686,7 @@ pub async fn setup_init( if matches!(new_config.hid.backend, crate::config::HidBackend::Otg) { let mut hid_functions = new_config.hid.effective_otg_functions(); - if let Some(udc) = - crate::otg::configfs::resolve_udc_name(new_config.hid.otg_udc.as_deref()) + if let Some(udc) = crate::otg::configfs::resolve_udc_name(new_config.hid.otg_udc.as_deref()) { if crate::otg::configfs::is_low_endpoint_udc(&udc) && hid_functions.consumer { tracing::warn!( @@ -1842,12 +1841,12 @@ pub async fn mjpeg_stream( break; } // Send last frame again to keep connection alive - if let Some(frame) = handler_clone.current_frame() { - if frame.is_valid_jpeg() { - if tx.send(create_mjpeg_part(frame.data())).await.is_err() { - break; - } - } + let Some(frame) = handler_clone.current_frame() else { + continue; + }; + + if frame.is_valid_jpeg() && tx.send(create_mjpeg_part(frame.data())).await.is_err() { + break; } } } @@ -1866,7 +1865,7 @@ pub async fn mjpeg_stream( yield Ok::(data); // Record FPS after yield - data has been handed to Axum/hyper // This is closer to actual TCP send than recording at tx.send() - handler_for_stream.record_frame_sent(&guard_for_stream.id()); + handler_for_stream.record_frame_sent(guard_for_stream.id()); } }; @@ -2516,7 +2515,7 @@ pub async fn msd_drive_download( let (file_size, mut rx) = drive.read_file_stream(&file_path).await?; // Extract filename for Content-Disposition - let filename = file_path.split('/').last().unwrap_or("download"); + let filename = file_path.split('/').next_back().unwrap_or("download"); // Create a stream from the channel receiver let body_stream = async_stream::stream! { diff --git a/src/web/static_files.rs b/src/web/static_files.rs index 3fb84bbf..44af069b 100644 --- a/src/web/static_files.rs +++ b/src/web/static_files.rs @@ -127,14 +127,14 @@ fn try_serve_file(path: &str) -> Option> { .first_or_octet_stream() .to_string(); - return Some( + Some( Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, mime) .header(header::CACHE_CONTROL, "public, max-age=86400") .body(Body::from(data)) .unwrap(), - ); + ) } Err(e) => { tracing::debug!( @@ -143,7 +143,7 @@ fn try_serve_file(path: &str) -> Option> { file_path.display(), e ); - return None; + None } } } diff --git a/src/webrtc/config.rs b/src/webrtc/config.rs index 3b5c79c3..3bcf3c1c 100644 --- a/src/webrtc/config.rs +++ b/src/webrtc/config.rs @@ -108,18 +108,15 @@ impl TurnServer { /// Video codec preference #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] +#[derive(Default)] pub enum VideoCodec { + #[default] H264, VP8, VP9, AV1, } -impl Default for VideoCodec { - fn default() -> Self { - Self::H264 - } -} impl std::fmt::Display for VideoCodec { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/src/webrtc/peer.rs b/src/webrtc/peer.rs index 767a0ba3..396b3782 100644 --- a/src/webrtc/peer.rs +++ b/src/webrtc/peer.rs @@ -93,7 +93,6 @@ impl PeerConnection { urls: turn.urls.clone(), username: turn.username.clone(), credential: turn.credential.clone(), - ..Default::default() }); } diff --git a/src/webrtc/rtp.rs b/src/webrtc/rtp.rs index e8dac17b..f576f4e9 100644 --- a/src/webrtc/rtp.rs +++ b/src/webrtc/rtp.rs @@ -330,9 +330,7 @@ impl OpusAudioTrack { stream_id.to_string(), )); - Self { - track, - } + Self { track } } /// Get the underlying WebRTC track @@ -365,13 +363,10 @@ impl OpusAudioTrack { ..Default::default() }; - self.track - .write_sample(&sample) - .await - .map_err(|e| { - error!("Failed to write Opus sample: {}", e); - AppError::WebRtcError(format!("Failed to write audio sample: {}", e)) - }) + self.track.write_sample(&sample).await.map_err(|e| { + error!("Failed to write Opus sample: {}", e); + AppError::WebRtcError(format!("Failed to write audio sample: {}", e)) + }) } } diff --git a/src/webrtc/track.rs b/src/webrtc/track.rs index f9617df2..d3787c63 100644 --- a/src/webrtc/track.rs +++ b/src/webrtc/track.rs @@ -199,7 +199,7 @@ impl VideoTrack { let data = frame.data(); let max_payload_size = 1200; // MTU - headers - let packet_count = (data.len() + max_payload_size - 1) / max_payload_size; + let packet_count = data.len().div_ceil(max_payload_size); let mut bytes_sent = 0u64; for i in 0..packet_count { diff --git a/src/webrtc/universal_session.rs b/src/webrtc/universal_session.rs index b62bc89f..30129954 100644 --- a/src/webrtc/universal_session.rs +++ b/src/webrtc/universal_session.rs @@ -292,7 +292,6 @@ impl UniversalSession { urls: turn.urls.clone(), username: turn.username.clone(), credential: turn.credential.clone(), - ..Default::default() }); } @@ -430,7 +429,9 @@ impl UniversalSession { let candidate = IceCandidate { candidate: candidate_str, sdp_mid: candidate_json.as_ref().and_then(|j| j.sdp_mid.clone()), - sdp_mline_index: candidate_json.as_ref().and_then(|j| j.sdp_mline_index), + sdp_mline_index: candidate_json + .as_ref() + .and_then(|j| j.sdp_mline_index), username_fragment: candidate_json .as_ref() .and_then(|j| j.username_fragment.clone()), @@ -615,20 +616,15 @@ impl UniversalSession { }; // Verify codec matches - let frame_codec = match encoded_frame.codec { - VideoEncoderType::H264 => VideoEncoderType::H264, - VideoEncoderType::H265 => VideoEncoderType::H265, - VideoEncoderType::VP8 => VideoEncoderType::VP8, - VideoEncoderType::VP9 => VideoEncoderType::VP9, - }; + let frame_codec = encoded_frame.codec; if frame_codec != expected_codec { continue; } // Debug log for H265 frames - if expected_codec == VideoEncoderType::H265 { - if encoded_frame.is_keyframe || frames_sent % 30 == 0 { + if expected_codec == VideoEncoderType::H265 + && (encoded_frame.is_keyframe || frames_sent.is_multiple_of(30)) { debug!( "[Session-H265] Received frame #{}: size={}, keyframe={}, seq={}", frames_sent, @@ -637,7 +633,6 @@ impl UniversalSession { encoded_frame.sequence ); } - } // Ensure decoder starts from a keyframe and recover on gaps. let mut gap_detected = false; @@ -768,7 +763,7 @@ impl UniversalSession { // 20ms at 48kHz = 960 samples let samples = 960u32; if let Err(e) = audio_track.write_packet(&opus_frame.data, samples).await { - if packets_sent % 100 == 0 { + if packets_sent.is_multiple_of(100) { debug!("Failed to write audio packet: {}", e); } } else { diff --git a/src/webrtc/video_track.rs b/src/webrtc/video_track.rs index 7ad4c99c..7fe5a8b3 100644 --- a/src/webrtc/video_track.rs +++ b/src/webrtc/video_track.rs @@ -285,7 +285,7 @@ impl UniversalVideoTrack { } /// Get current statistics - + /// /// Write an encoded frame to the track /// /// Handles codec-specific processing: @@ -464,7 +464,6 @@ impl UniversalVideoTrack { if let Err(e) = rtp_track.write_rtp(&packet).await { trace!("H265 write_rtp failed: {}", e); } - } Ok(()) diff --git a/src/webrtc/webrtc_streamer.rs b/src/webrtc/webrtc_streamer.rs index 44ed2b13..8b74e256 100644 --- a/src/webrtc/webrtc_streamer.rs +++ b/src/webrtc/webrtc_streamer.rs @@ -35,8 +35,8 @@ use tokio::sync::RwLock; use tracing::{debug, info, trace, warn}; use crate::audio::{AudioController, OpusFrame}; -use crate::events::EventBus; use crate::error::{AppError, Result}; +use crate::events::EventBus; use crate::hid::HidController; use crate::video::encoder::registry::EncoderBackend; use crate::video::encoder::registry::VideoEncoderType; @@ -270,7 +270,6 @@ impl WebRtcStreamer { bitrate_preset: config.bitrate_preset, fps: config.fps, encoder_backend: config.encoder_backend, - ..Default::default() }; info!("Creating shared video pipeline for {:?}", codec); @@ -311,7 +310,9 @@ impl WebRtcStreamer { } drop(pipeline_guard); - info!("Video pipeline stopped, but keeping capture config for new sessions"); + info!( + "Video pipeline stopped, but keeping capture config for new sessions" + ); } break; } @@ -926,10 +927,7 @@ impl WebRtcStreamer { let pipeline = pipeline_for_callback.clone(); let sid = sid.clone(); tokio::spawn(async move { - info!( - "Requesting keyframe for session {} after reconnect", - sid - ); + info!("Requesting keyframe for session {} after reconnect", sid); pipeline.request_keyframe().await; }); }); From 261deb1303d47acb68851fc1f778ee49990986ae Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Tue, 10 Feb 2026 22:30:52 +0800 Subject: [PATCH 04/19] =?UTF-8?q?refactor:=20=E6=94=B6=E6=95=9B=E5=8D=95?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=A8=A1=E5=9E=8B=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=8F=AF=E8=AE=BF=E9=97=AE=E6=80=A7=E4=B8=8E=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E5=BC=8F=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端移除 is_admin 权限字段与相关逻辑,统一为单用户系统模型 - 修复会话过期清理的时间比较方式(改为 RFC3339 参数比较) - /api/config 聚合配置增加敏感字段脱敏,避免暴露 TURN/RustDesk 密钥与密码 - 配置更新日志改为摘要,避免打印完整配置内容 - 前端修复可点击卡片语义与键盘可达,补齐图标按钮可访问名称 - 调整弹窗与抽屉的响应式尺寸,优化多端显示与交互 --- src/auth/session.rs | 4 +++- src/auth/user.rs | 20 ++++++++------------ src/config/store.rs | 1 - src/web/handlers/config/mod.rs | 18 +++++++++++++++++- src/web/handlers/config/rustdesk.rs | 2 +- src/web/handlers/mod.rs | 16 ++++------------ web/src/components/AppLayout.vue | 10 +++++----- web/src/components/StatsSheet.vue | 4 ++-- web/src/components/StatusCard.vue | 16 ++++++++++------ web/src/views/ConsoleView.vue | 9 +++++---- web/src/views/LoginView.vue | 3 ++- web/src/views/SettingsView.vue | 27 +++++++++++++++++---------- web/src/views/SetupView.vue | 11 +++++++---- 13 files changed, 81 insertions(+), 60 deletions(-) diff --git a/src/auth/session.rs b/src/auth/session.rs index 9902acc8..6106dc16 100644 --- a/src/auth/session.rs +++ b/src/auth/session.rs @@ -110,7 +110,9 @@ impl SessionStore { /// Delete all expired sessions pub async fn cleanup_expired(&self) -> Result { - let result = sqlx::query("DELETE FROM sessions WHERE expires_at < datetime('now')") + let now = Utc::now().to_rfc3339(); + let result = sqlx::query("DELETE FROM sessions WHERE expires_at < ?1") + .bind(now) .execute(&self.pool) .await?; Ok(result.rows_affected()) diff --git a/src/auth/user.rs b/src/auth/user.rs index 0854a5ab..986fc41b 100644 --- a/src/auth/user.rs +++ b/src/auth/user.rs @@ -7,7 +7,7 @@ use super::password::{hash_password, verify_password}; use crate::error::{AppError, Result}; /// User row type from database -type UserRow = (String, String, String, i32, String, String); +type UserRow = (String, String, String, String, String); /// User data #[derive(Debug, Clone, Serialize, Deserialize)] @@ -16,7 +16,6 @@ pub struct User { pub username: String, #[serde(skip_serializing)] pub password_hash: String, - pub is_admin: bool, pub created_at: DateTime, pub updated_at: DateTime, } @@ -24,12 +23,11 @@ pub struct User { impl User { /// Convert from database row to User fn from_row(row: UserRow) -> Self { - let (id, username, password_hash, is_admin, created_at, updated_at) = row; + let (id, username, password_hash, created_at, updated_at) = row; Self { id, username, password_hash, - is_admin: is_admin != 0, created_at: DateTime::parse_from_rfc3339(&created_at) .map(|dt| dt.with_timezone(&Utc)) .unwrap_or_else(|_| Utc::now()), @@ -53,7 +51,7 @@ impl UserStore { } /// Create a new user - pub async fn create(&self, username: &str, password: &str, is_admin: bool) -> Result { + pub async fn create(&self, username: &str, password: &str) -> Result { // Check if username already exists if self.get_by_username(username).await?.is_some() { return Err(AppError::BadRequest(format!( @@ -68,21 +66,19 @@ impl UserStore { id: Uuid::new_v4().to_string(), username: username.to_string(), password_hash, - is_admin, created_at: now, updated_at: now, }; sqlx::query( r#" - INSERT INTO users (id, username, password_hash, is_admin, created_at, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6) + INSERT INTO users (id, username, password_hash, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5) "#, ) .bind(&user.id) .bind(&user.username) .bind(&user.password_hash) - .bind(user.is_admin as i32) .bind(user.created_at.to_rfc3339()) .bind(user.updated_at.to_rfc3339()) .execute(&self.pool) @@ -94,7 +90,7 @@ impl UserStore { /// Get user by ID pub async fn get(&self, user_id: &str) -> Result> { let row: Option = sqlx::query_as( - "SELECT id, username, password_hash, is_admin, created_at, updated_at FROM users WHERE id = ?1", + "SELECT id, username, password_hash, created_at, updated_at FROM users WHERE id = ?1", ) .bind(user_id) .fetch_optional(&self.pool) @@ -106,7 +102,7 @@ impl UserStore { /// Get user by username pub async fn get_by_username(&self, username: &str) -> Result> { let row: Option = sqlx::query_as( - "SELECT id, username, password_hash, is_admin, created_at, updated_at FROM users WHERE username = ?1", + "SELECT id, username, password_hash, created_at, updated_at FROM users WHERE username = ?1", ) .bind(username) .fetch_optional(&self.pool) @@ -178,7 +174,7 @@ impl UserStore { /// List all users pub async fn list(&self) -> Result> { let rows: Vec = sqlx::query_as( - "SELECT id, username, password_hash, is_admin, created_at, updated_at FROM users ORDER BY created_at", + "SELECT id, username, password_hash, created_at, updated_at FROM users ORDER BY created_at", ) .fetch_all(&self.pool) .await?; diff --git a/src/config/store.rs b/src/config/store.rs index 0e48be8d..ce7a1b5b 100644 --- a/src/config/store.rs +++ b/src/config/store.rs @@ -82,7 +82,6 @@ impl ConfigStore { id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, - is_admin INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) diff --git a/src/web/handlers/config/mod.rs b/src/web/handlers/config/mod.rs index 6748ac20..3a28bc10 100644 --- a/src/web/handlers/config/mod.rs +++ b/src/web/handlers/config/mod.rs @@ -50,10 +50,26 @@ use std::sync::Arc; use crate::config::AppConfig; use crate::state::AppState; +fn sanitize_config_for_api(config: &mut AppConfig) { + // Auth secrets + config.auth.totp_secret = None; + + // Stream secrets + config.stream.turn_password = None; + + // RustDesk secrets + config.rustdesk.device_password.clear(); + config.rustdesk.relay_key = None; + config.rustdesk.public_key = None; + config.rustdesk.private_key = None; + config.rustdesk.signing_public_key = None; + config.rustdesk.signing_private_key = None; +} + /// 获取完整配置 pub async fn get_all_config(State(state): State>) -> Json { let mut config = (*state.config.get()).clone(); // 不暴露敏感信息 - config.auth.totp_secret = None; + sanitize_config_for_api(&mut config); Json(config) } diff --git a/src/web/handlers/config/rustdesk.rs b/src/web/handlers/config/rustdesk.rs index 29dbf3c9..ae1a9648 100644 --- a/src/web/handlers/config/rustdesk.rs +++ b/src/web/handlers/config/rustdesk.rs @@ -139,7 +139,7 @@ pub async fn regenerate_device_password( Ok(Json(RustDeskConfigResponse::from(&new_config))) } -/// 获取设备密码(管理员专用) +/// 获取设备密码(已认证用户) pub async fn get_device_password(State(state): State>) -> Json { let config = state.config.get().rustdesk.clone(); Json(serde_json::json!({ diff --git a/src/web/handlers/mod.rs b/src/web/handlers/mod.rs index 5a7a16af..6c3d9f6c 100644 --- a/src/web/handlers/mod.rs +++ b/src/web/handlers/mod.rs @@ -589,11 +589,8 @@ pub async fn setup_init( )); } - // Create admin user - state - .users - .create(&req.username, &req.password, true) - .await?; + // Create single system user + state.users.create(&req.username, &req.password).await?; // Update config state @@ -771,10 +768,7 @@ pub async fn setup_init( } } - tracing::info!( - "System initialized successfully with admin user: {}", - req.username - ); + tracing::info!("System initialized successfully"); Ok(Json(LoginResponse { success: true, @@ -799,7 +793,7 @@ pub async fn update_config( // Keep old config for rollback let old_config = state.config.get(); - tracing::info!("Received config update: {:?}", req.updates); + tracing::info!("Received config update request"); // Validate and merge config first (outside the update closure) let config_json = serde_json::to_value(&old_config) @@ -808,8 +802,6 @@ pub async fn update_config( let merged = merge_json(config_json, req.updates.clone()) .map_err(|_| AppError::Internal("Failed to merge config".to_string()))?; - tracing::debug!("Merged config: {:?}", merged); - let new_config: AppConfig = serde_json::from_value(merged) .map_err(|e| AppError::BadRequest(format!("Invalid config format: {}", e)))?; diff --git a/web/src/components/AppLayout.vue b/web/src/components/AppLayout.vue index ec22a9a7..879c5123 100644 --- a/web/src/components/AppLayout.vue +++ b/web/src/components/AppLayout.vue @@ -52,7 +52,7 @@ async function handleLogout() {