fix: 补齐 ATX 控制器缺失接口并完成全项目 clippy -D warnings 修复

This commit is contained in:
mofeng-git
2026-02-10 21:37:33 +08:00
parent 72eb2c450d
commit 394baca938
64 changed files with 474 additions and 760 deletions

View File

@@ -31,8 +31,10 @@ unsafe impl Send for HwMjpegH26xPipeline {}
impl HwMjpegH26xPipeline {
pub fn new(config: HwMjpegH26xConfig) -> Result<Self, String> {
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(),

View File

@@ -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::{

View File

@@ -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<PowerStatus> {
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);
}
}

View File

@@ -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]

View File

@@ -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<String>,
@@ -187,15 +138,6 @@ pub struct AtxDevices {
pub usb_relays: Vec<String>,
}
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);
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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()));

View File

@@ -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

View File

@@ -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

View File

@@ -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<String>,
}
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]

View File

@@ -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

View File

@@ -219,8 +219,10 @@ impl From<u8> 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)]

View File

@@ -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()))
}
}
}
}

View File

@@ -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 {

View File

@@ -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<()> {

View File

@@ -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! {

View File

@@ -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<OtgService>,
msd_dir: impl Into<PathBuf>,
) -> Self {
pub fn new(otg_service: Arc<OtgService>, msd_dir: impl Into<PathBuf>) -> Self {
let msd_dir = msd_dir.into();
let images_path = msd_dir.join("images");
let ventoy_dir = msd_dir.join("ventoy");

View File

@@ -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)

View File

@@ -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)]

View File

@@ -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)]

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -36,6 +36,7 @@ const FLAG_MSD: u8 = 0b10;
/// HID device paths
#[derive(Debug, Clone)]
#[derive(Default)]
pub struct HidDevicePaths {
pub keyboard: Option<PathBuf>,
pub mouse_relative: Option<PathBuf>,
@@ -43,16 +44,6 @@ pub struct HidDevicePaths {
pub consumer: Option<PathBuf>,
}
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<PathBuf> {
@@ -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]

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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),

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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};

View File

@@ -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::<v4l2_frmivalenum>(
&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 {

View File

@@ -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)]

View File

@@ -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<EncoderBackend> for H265EncoderType {
fn from(backend: EncoderBackend) -> Self {
@@ -87,10 +84,12 @@ impl From<EncoderBackend> 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
};

View File

@@ -145,6 +145,7 @@ impl EncoderBackend {
}
/// Parse from string (case-insensitive)
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"vaapi" => Some(EncoderBackend::Vaapi),

View File

@@ -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 {

View File

@@ -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<EncoderBackend> for VP8EncoderType {
fn from(backend: EncoderBackend) -> Self {
@@ -68,18 +65,15 @@ impl From<EncoderBackend> 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
};

View File

@@ -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<EncoderBackend> for VP9EncoderType {
fn from(backend: EncoderBackend) -> Self {
@@ -68,18 +65,15 @@ impl From<EncoderBackend> 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
};

View File

@@ -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 {

View File

@@ -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)))?;

View File

@@ -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<tokio::sync::mpsc::Receiver<std::sync::Arc<
crate::video::shared_video_pipeline::EncodedVideoFrame,
>>> {
) -> Option<
tokio::sync::mpsc::Receiver<
std::sync::Arc<crate::video::shared_video_pipeline::EncodedVideoFrame>,
>,
> {
// 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);

View File

@@ -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(),

View File

@@ -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<u8>) -> io::Result<CaptureMeta> {
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| {

View File

@@ -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

View File

@@ -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",

View File

@@ -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()))

View File

@@ -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::<bytes::Bytes, std::io::Error>(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! {

View File

@@ -127,14 +127,14 @@ fn try_serve_file(path: &str) -> Option<Response<Body>> {
.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<Response<Body>> {
file_path.display(),
e
);
return None;
None
}
}
}

View File

@@ -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 {

View File

@@ -93,7 +93,6 @@ impl PeerConnection {
urls: turn.urls.clone(),
username: turn.username.clone(),
credential: turn.credential.clone(),
..Default::default()
});
}

View File

@@ -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))
})
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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(())

View File

@@ -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;
});
});