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 { impl HwMjpegH26xPipeline {
pub fn new(config: HwMjpegH26xConfig) -> Result<Self, String> { pub fn new(config: HwMjpegH26xConfig) -> Result<Self, String> {
unsafe { unsafe {
let dec = CString::new(config.decoder.as_str()).map_err(|_| "decoder name invalid".to_string())?; let dec = CString::new(config.decoder.as_str())
let enc = CString::new(config.encoder.as_str()).map_err(|_| "encoder name invalid".to_string())?; .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( let ctx = ffmpeg_hw_mjpeg_h26x_new(
dec.as_ptr(), dec.as_ptr(),
enc.as_ptr(), enc.as_ptr(),

View File

@@ -1,8 +1,7 @@
use crate::{ use crate::{
ffmpeg::{init_av_log, AVPixelFormat}, ffmpeg::{init_av_log, AVPixelFormat},
ffmpeg_ram::{ ffmpeg_ram::{
ffmpeg_ram_decode, ffmpeg_ram_free_decoder, ffmpeg_ram_last_error, ffmpeg_ram_decode, ffmpeg_ram_free_decoder, ffmpeg_ram_last_error, ffmpeg_ram_new_decoder,
ffmpeg_ram_new_decoder,
}, },
}; };
use std::{ use std::{

View File

@@ -8,11 +8,11 @@ use tracing::{debug, info, warn};
use super::executor::{timing, AtxKeyExecutor}; use super::executor::{timing, AtxKeyExecutor};
use super::led::LedSensor; use super::led::LedSensor;
use super::types::{AtxKeyConfig, AtxLedConfig, AtxState, PowerStatus}; use super::types::{AtxKeyConfig, AtxLedConfig, AtxState, AtxAction, PowerStatus};
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
/// ATX power control configuration /// ATX power control configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct AtxControllerConfig { pub struct AtxControllerConfig {
/// Whether ATX is enabled /// Whether ATX is enabled
pub enabled: bool, pub enabled: bool,
@@ -24,17 +24,6 @@ pub struct AtxControllerConfig {
pub led: AtxLedConfig, 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 /// Internal state holding all ATX components
/// Grouped together to reduce lock acquisitions /// Grouped together to reduce lock acquisitions
struct AtxInner { struct AtxInner {
@@ -54,34 +43,7 @@ pub struct AtxController {
} }
impl AtxController { impl AtxController {
/// Create a new ATX controller with the specified configuration async fn init_components(inner: &mut AtxInner) {
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");
// Initialize power executor // Initialize power executor
if inner.config.power.is_configured() { if inner.config.power.is_configured() {
let mut executor = AtxKeyExecutor::new(inner.config.power.clone()); let mut executor = AtxKeyExecutor::new(inner.config.power.clone());
@@ -123,234 +85,180 @@ impl AtxController {
inner.led_sensor = Some(sensor); inner.led_sensor = Some(sensor);
} }
} }
info!("ATX controller initialized successfully");
Ok(())
} }
/// Reload the ATX controller with new configuration async fn shutdown_components(inner: &mut AtxInner) {
/// if let Some(executor) = inner.power_executor.as_mut() {
/// This is called when configuration changes and supports hot-reload. if let Err(e) = executor.shutdown().await {
pub async fn reload(&self, new_config: AtxControllerConfig) -> Result<()> { warn!("Failed to shutdown power executor: {}", e);
info!("Reloading ATX controller with new configuration"); }
}
inner.power_executor = None;
// Shutdown existing executors if let Some(executor) = inner.reset_executor.as_mut() {
self.shutdown_internal().await?; if let Err(e) = executor.shutdown().await {
warn!("Failed to shutdown reset executor: {}", e);
}
}
inner.reset_executor = None;
// Update configuration and re-initialize if let Some(sensor) = inner.led_sensor.as_mut() {
{ if let Err(e) = sensor.shutdown().await {
let mut inner = self.inner.write().await; warn!("Failed to shutdown LED sensor: {}", e);
inner.config = new_config; }
}
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 info!("Initializing ATX controller");
self.init().await?;
Self::init_components(&mut inner).await;
info!("ATX controller reloaded successfully");
Ok(()) 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 { pub async fn state(&self) -> AtxState {
let inner = self.inner.read().await; let inner = self.inner.read().await;
let power_status = if let Some(sensor) = inner.led_sensor.as_ref() { let power_status = if let Some(sensor) = &inner.led_sensor {
sensor.read().await.unwrap_or(PowerStatus::Unknown) match sensor.read().await {
Ok(status) => status,
Err(e) => {
debug!("Failed to read ATX LED sensor: {}", e);
PowerStatus::Unknown
}
}
} else { } else {
PowerStatus::Unknown PowerStatus::Unknown
}; };
AtxState { AtxState {
available: inner.config.enabled, available: inner.config.enabled,
power_configured: inner power_configured: inner.power_executor.is_some(),
.power_executor reset_configured: inner.reset_executor.is_some(),
.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_status, power_status,
led_supported: inner led_supported: inner.led_sensor.is_some(),
.led_sensor
.as_ref()
.map(|s| s.is_initialized())
.unwrap_or(false),
} }
} }
/// 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] #[test]
fn test_discover_devices() { fn test_discover_devices() {
let devices = 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);
} }
#[test] #[test]

View File

@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use typeshare::typeshare; use typeshare::typeshare;
/// Power status /// Power status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum PowerStatus { pub enum PowerStatus {
/// Power is on /// Power is on
@@ -15,18 +15,13 @@ pub enum PowerStatus {
/// Power is off /// Power is off
Off, Off,
/// Power status unknown (no LED connected) /// Power status unknown (no LED connected)
#[default]
Unknown, Unknown,
} }
impl Default for PowerStatus {
fn default() -> Self {
Self::Unknown
}
}
/// Driver type for ATX key operations /// Driver type for ATX key operations
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum AtxDriverType { pub enum AtxDriverType {
/// GPIO control via Linux character device /// GPIO control via Linux character device
@@ -34,36 +29,26 @@ pub enum AtxDriverType {
/// USB HID relay module /// USB HID relay module
UsbRelay, UsbRelay,
/// Disabled / Not configured /// Disabled / Not configured
#[default]
None, None,
} }
impl Default for AtxDriverType {
fn default() -> Self {
Self::None
}
}
/// Active level for GPIO pins /// Active level for GPIO pins
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum ActiveLevel { pub enum ActiveLevel {
/// Active high (default for most cases) /// Active high (default for most cases)
#[default]
High, High,
/// Active low (inverted) /// Active low (inverted)
Low, Low,
} }
impl Default for ActiveLevel {
fn default() -> Self {
Self::High
}
}
/// Configuration for a single ATX key (power or reset) /// Configuration for a single ATX key (power or reset)
/// This is the "four-tuple" configuration: (driver, device, pin/channel, level) /// This is the "four-tuple" configuration: (driver, device, pin/channel, level)
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(default)] #[serde(default)]
pub struct AtxKeyConfig { pub struct AtxKeyConfig {
/// Driver type (GPIO or USB Relay) /// Driver type (GPIO or USB Relay)
@@ -80,17 +65,6 @@ pub struct AtxKeyConfig {
pub active_level: ActiveLevel, 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 { impl AtxKeyConfig {
/// Check if this key is configured /// Check if this key is configured
pub fn is_configured(&self) -> bool { pub fn is_configured(&self) -> bool {
@@ -100,7 +74,7 @@ impl AtxKeyConfig {
/// LED sensing configuration (optional) /// LED sensing configuration (optional)
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(default)] #[serde(default)]
pub struct AtxLedConfig { pub struct AtxLedConfig {
/// Whether LED sensing is enabled /// Whether LED sensing is enabled
@@ -113,17 +87,6 @@ pub struct AtxLedConfig {
pub inverted: bool, pub inverted: bool,
} }
impl Default for AtxLedConfig {
fn default() -> Self {
Self {
enabled: false,
gpio_chip: String::new(),
gpio_pin: 0,
inverted: false,
}
}
}
impl AtxLedConfig { impl AtxLedConfig {
/// Check if LED sensing is configured /// Check if LED sensing is configured
pub fn is_configured(&self) -> bool { pub fn is_configured(&self) -> bool {
@@ -132,7 +95,7 @@ impl AtxLedConfig {
} }
/// ATX state information /// ATX state information
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AtxState { pub struct AtxState {
/// Whether ATX feature is available/enabled /// Whether ATX feature is available/enabled
pub available: bool, pub available: bool,
@@ -146,18 +109,6 @@ pub struct AtxState {
pub led_supported: bool, 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 /// ATX power action request
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct AtxPowerRequest { pub struct AtxPowerRequest {
@@ -179,7 +130,7 @@ pub enum AtxAction {
/// Available ATX devices for discovery /// Available ATX devices for discovery
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AtxDevices { pub struct AtxDevices {
/// Available GPIO chips (/dev/gpiochip*) /// Available GPIO chips (/dev/gpiochip*)
pub gpio_chips: Vec<String>, pub gpio_chips: Vec<String>,
@@ -187,15 +138,6 @@ pub struct AtxDevices {
pub usb_relays: Vec<String>, pub usb_relays: Vec<String>,
} }
impl Default for AtxDevices {
fn default() -> Self {
Self {
gpio_chips: Vec::new(),
usb_relays: Vec::new(),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -266,5 +208,6 @@ mod tests {
assert!(!state.power_configured); assert!(!state.power_configured);
assert!(!state.reset_configured); assert!(!state.reset_configured);
assert_eq!(state.power_status, PowerStatus::Unknown); 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: /// WOL magic packet structure:
/// - 6 bytes of 0xFF /// - 6 bytes of 0xFF
/// - 16 repetitions of the target MAC address (6 bytes each) /// - 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; const MAGIC_PACKET_SIZE: usize = 102;
/// Parse MAC address string into bytes /// Parse MAC address string into bytes
@@ -160,8 +160,8 @@ mod tests {
let packet = build_magic_packet(&mac); let packet = build_magic_packet(&mac);
// Check header (6 bytes of 0xFF) // Check header (6 bytes of 0xFF)
for i in 0..6 { for byte in packet.iter().take(6) {
assert_eq!(packet[i], 0xFF); assert_eq!(*byte, 0xFF);
} }
// Check MAC repetitions // Check MAC repetitions

View File

@@ -184,14 +184,7 @@ impl AudioCapturer {
let log_throttler = self.log_throttler.clone(); let log_throttler = self.log_throttler.clone();
let handle = tokio::task::spawn_blocking(move || { let handle = tokio::task::spawn_blocking(move || {
capture_loop( capture_loop(config, state, frame_tx, stop_flag, sequence, log_throttler);
config,
state,
frame_tx,
stop_flag,
sequence,
log_throttler,
);
}); });
*self.capture_handle.lock().await = Some(handle); *self.capture_handle.lock().await = Some(handle);

View File

@@ -39,7 +39,9 @@ impl AudioQuality {
} }
/// Parse from string /// Parse from string
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self { pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() { match s.to_lowercase().as_str() {
"voice" | "low" => AudioQuality::Voice, "voice" | "low" => AudioQuality::Voice,
"high" | "music" => AudioQuality::High, "high" | "music" => AudioQuality::High,

View File

@@ -85,9 +85,7 @@ pub fn enumerate_audio_devices_with_current(
let mut devices = Vec::new(); let mut devices = Vec::new();
// Try to enumerate cards // Try to enumerate cards
let cards = match alsa::card::Iter::new() { let cards = alsa::card::Iter::new();
i => i,
};
for card_result in cards { for card_result in cards {
let card = match card_result { let card = match card_result {

View File

@@ -17,8 +17,10 @@ use crate::utils::LogThrottler;
/// Audio health status /// Audio health status
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
#[derive(Default)]
pub enum AudioHealthStatus { pub enum AudioHealthStatus {
/// Device is healthy and operational /// Device is healthy and operational
#[default]
Healthy, Healthy,
/// Device has an error, attempting recovery /// Device has an error, attempting recovery
Error { Error {
@@ -33,11 +35,6 @@ pub enum AudioHealthStatus {
Disconnected, Disconnected,
} }
impl Default for AudioHealthStatus {
fn default() -> Self {
Self::Healthy
}
}
/// Audio health monitor configuration /// Audio health monitor configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -166,7 +163,7 @@ impl AudioHealthMonitor {
let attempt = self.retry_count.load(Ordering::Relaxed); let attempt = self.retry_count.load(Ordering::Relaxed);
// Only publish every 5 attempts to avoid event spam // 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); debug!("Audio reconnecting, attempt {}", attempt);
if let Some(ref events) = *self.events.read().await { if let Some(ref events) = *self.events.read().await {

View File

@@ -15,8 +15,10 @@ use crate::error::{AppError, Result};
/// Audio stream state /// Audio stream state
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Default)]
pub enum AudioStreamState { pub enum AudioStreamState {
/// Stream is stopped /// Stream is stopped
#[default]
Stopped, Stopped,
/// Stream is starting up /// Stream is starting up
Starting, Starting,
@@ -26,14 +28,10 @@ pub enum AudioStreamState {
Error, Error,
} }
impl Default for AudioStreamState {
fn default() -> Self {
Self::Stopped
}
}
/// Audio streamer configuration /// Audio streamer configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[derive(Default)]
pub struct AudioStreamerConfig { pub struct AudioStreamerConfig {
/// Audio capture configuration /// Audio capture configuration
pub capture: AudioConfig, pub capture: AudioConfig,
@@ -41,14 +39,6 @@ pub struct AudioStreamerConfig {
pub opus: OpusConfig, pub opus: OpusConfig,
} }
impl Default for AudioStreamerConfig {
fn default() -> Self {
Self {
capture: AudioConfig::default(),
opus: OpusConfig::default(),
}
}
}
impl AudioStreamerConfig { impl AudioStreamerConfig {
/// Create config for a specific device with default quality /// Create config for a specific device with default quality
@@ -290,11 +280,7 @@ impl AudioStreamer {
// Encode to Opus // Encode to Opus
let opus_result = { let opus_result = {
let mut enc_guard = encoder.lock().await; let mut enc_guard = encoder.lock().await;
if let Some(ref mut enc) = *enc_guard { (*enc_guard).as_mut().map(|enc| enc.encode_frame(&audio_frame))
Some(enc.encode_frame(&audio_frame))
} else {
None
}
}; };
match opus_result { 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 // Note: paths here are relative to /api since middleware is applied within the nested router
matches!( matches!(
path, path,
"/" "/" | "/auth/login" | "/health" | "/setup" | "/setup/init"
| "/auth/login"
| "/health"
| "/setup"
| "/setup/init"
) || path.starts_with("/assets/") ) || path.starts_with("/assets/")
|| path.starts_with("/static/") || path.starts_with("/static/")
|| path.ends_with(".js") || path.ends_with(".js")

View File

@@ -161,13 +161,12 @@ impl UserStore {
} }
let now = Utc::now(); let now = Utc::now();
let result = let result = sqlx::query("UPDATE users SET username = ?1, updated_at = ?2 WHERE id = ?3")
sqlx::query("UPDATE users SET username = ?1, updated_at = ?2 WHERE id = ?3") .bind(new_username)
.bind(new_username) .bind(now.to_rfc3339())
.bind(now.to_rfc3339()) .bind(user_id)
.bind(user_id) .execute(&self.pool)
.execute(&self.pool) .await?;
.await?;
if result.rows_affected() == 0 { if result.rows_affected() == 0 {
return Err(AppError::NotFound("User not found".to_string())); return Err(AppError::NotFound("User not found".to_string()));

View File

@@ -11,6 +11,7 @@ pub use crate::rustdesk::config::RustDeskConfig;
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
#[derive(Default)]
pub struct AppConfig { pub struct AppConfig {
/// Whether initial setup has been completed /// Whether initial setup has been completed
pub initialized: bool, pub initialized: bool,
@@ -36,23 +37,6 @@ pub struct AppConfig {
pub rustdesk: RustDeskConfig, 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 /// Authentication configuration
#[typeshare] #[typeshare]
@@ -116,20 +100,17 @@ impl Default for VideoConfig {
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum HidBackend { pub enum HidBackend {
/// USB OTG HID gadget /// USB OTG HID gadget
Otg, Otg,
/// CH9329 serial HID controller /// CH9329 serial HID controller
Ch9329, Ch9329,
/// Disabled /// Disabled
#[default]
None, None,
} }
impl Default for HidBackend {
fn default() -> Self {
Self::None
}
}
/// OTG USB device descriptor configuration /// OTG USB device descriptor configuration
#[typeshare] #[typeshare]
@@ -163,8 +144,10 @@ impl Default for OtgDescriptorConfig {
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum OtgHidProfile { pub enum OtgHidProfile {
/// Full HID device set (keyboard + relative mouse + absolute mouse + consumer control) /// Full HID device set (keyboard + relative mouse + absolute mouse + consumer control)
#[default]
Full, Full,
/// Full HID device set without MSD /// Full HID device set without MSD
FullNoMsd, FullNoMsd,
@@ -180,11 +163,6 @@ pub enum OtgHidProfile {
Custom, Custom,
} }
impl Default for OtgHidProfile {
fn default() -> Self {
Self::Full
}
}
/// OTG HID function selection (used when profile is Custom) /// OTG HID function selection (used when profile is Custom)
#[typeshare] #[typeshare]
@@ -360,6 +338,7 @@ pub use crate::atx::{ActiveLevel, AtxDriverType, AtxKeyConfig, AtxLedConfig};
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
#[derive(Default)]
pub struct AtxConfig { pub struct AtxConfig {
/// Enable ATX functionality /// Enable ATX functionality
pub enabled: bool, pub enabled: bool,
@@ -373,17 +352,6 @@ pub struct AtxConfig {
pub wol_interface: String, 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 { impl AtxConfig {
/// Convert to AtxControllerConfig for the controller /// Convert to AtxControllerConfig for the controller
@@ -427,25 +395,24 @@ impl Default for AudioConfig {
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum StreamMode { pub enum StreamMode {
/// WebRTC with H264/H265 /// WebRTC with H264/H265
WebRTC, WebRTC,
/// MJPEG over HTTP /// MJPEG over HTTP
#[default]
Mjpeg, Mjpeg,
} }
impl Default for StreamMode {
fn default() -> Self {
Self::Mjpeg
}
}
/// Encoder type /// Encoder type
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum EncoderType { pub enum EncoderType {
/// Auto-detect best encoder /// Auto-detect best encoder
#[default]
Auto, Auto,
/// Software encoder (libx264) /// Software encoder (libx264)
Software, Software,
@@ -463,11 +430,6 @@ pub enum EncoderType {
V4l2m2m, V4l2m2m,
} }
impl Default for EncoderType {
fn default() -> Self {
Self::Auto
}
}
impl EncoderType { impl EncoderType {
/// Convert to EncoderBackend for registry queries /// Convert to EncoderBackend for registry queries

View File

@@ -124,6 +124,7 @@ pub struct ClientStats {
/// ``` /// ```
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "event", content = "data")] #[serde(tag = "event", content = "data")]
#[allow(clippy::large_enum_variant)]
pub enum SystemEvent { pub enum SystemEvent {
// ============================================================================ // ============================================================================
// Video Stream Events // Video Stream Events

View File

@@ -149,6 +149,7 @@ impl Default for GostcConfig {
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
#[derive(Default)]
pub struct EasytierConfig { pub struct EasytierConfig {
/// Enable auto-start /// Enable auto-start
pub enabled: bool, pub enabled: bool,
@@ -165,17 +166,6 @@ pub struct EasytierConfig {
pub virtual_ip: Option<String>, 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 /// Combined extensions configuration
#[typeshare] #[typeshare]

View File

@@ -14,6 +14,7 @@ fn default_ch9329_baud_rate() -> u32 {
/// HID backend type /// HID backend type
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")] #[serde(tag = "type", rename_all = "lowercase")]
#[derive(Default)]
pub enum HidBackendType { pub enum HidBackendType {
/// USB OTG gadget mode /// USB OTG gadget mode
Otg, Otg,
@@ -26,14 +27,10 @@ pub enum HidBackendType {
baud_rate: u32, baud_rate: u32,
}, },
/// No HID backend (disabled) /// No HID backend (disabled)
#[default]
None, None,
} }
impl Default for HidBackendType {
fn default() -> Self {
Self::None
}
}
impl HidBackendType { impl HidBackendType {
/// Check if OTG backend is available on this system /// Check if OTG backend is available on this system

View File

@@ -219,8 +219,10 @@ impl From<u8> for LedStatus {
/// CH9329 work mode /// CH9329 work mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)] #[repr(u8)]
#[derive(Default)]
pub enum WorkMode { pub enum WorkMode {
/// Mode 0: Standard USB Keyboard + Mouse (default) /// Mode 0: Standard USB Keyboard + Mouse (default)
#[default]
KeyboardMouse = 0x00, KeyboardMouse = 0x00,
/// Mode 1: Standard USB Keyboard only /// Mode 1: Standard USB Keyboard only
KeyboardOnly = 0x01, KeyboardOnly = 0x01,
@@ -230,17 +232,14 @@ pub enum WorkMode {
CustomHid = 0x03, CustomHid = 0x03,
} }
impl Default for WorkMode {
fn default() -> Self {
Self::KeyboardMouse
}
}
/// CH9329 serial communication mode /// CH9329 serial communication mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)] #[repr(u8)]
#[derive(Default)]
pub enum SerialMode { pub enum SerialMode {
/// Mode 0: Protocol transmission mode (default) /// Mode 0: Protocol transmission mode (default)
#[default]
Protocol = 0x00, Protocol = 0x00,
/// Mode 1: ASCII mode /// Mode 1: ASCII mode
Ascii = 0x01, Ascii = 0x01,
@@ -248,11 +247,6 @@ pub enum SerialMode {
Transparent = 0x02, Transparent = 0x02,
} }
impl Default for SerialMode {
fn default() -> Self {
Self::Protocol
}
}
/// CH9329 configuration parameters /// CH9329 configuration parameters
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -42,17 +42,17 @@ pub struct HidInfo {
pub screen_resolution: Option<(u32, u32)>, pub screen_resolution: Option<(u32, u32)>,
} }
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing::{info, warn}; use tracing::{info, warn};
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
use crate::otg::OtgService; use crate::otg::OtgService;
use std::time::Duration;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use std::time::Duration;
const HID_EVENT_QUEUE_CAPACITY: usize = 64; const HID_EVENT_QUEUE_CAPACITY: usize = 64;
const HID_EVENT_SEND_TIMEOUT_MS: u64 = 30; 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 // Best-effort: drop/merge move events if queue is full
self.enqueue_mouse_move(event) self.enqueue_mouse_move(event)
} else { } else {
@@ -470,13 +473,7 @@ impl HidController {
None => break, None => break,
}; };
process_hid_event( process_hid_event(event, &backend, &monitor, &backend_type).await;
event,
&backend,
&monitor,
&backend_type,
)
.await;
// After each event, flush latest move if pending // After each event, flush latest move if pending
if pending_move_flag.swap(false, Ordering::AcqRel) { if pending_move_flag.swap(false, Ordering::AcqRel) {
@@ -505,9 +502,9 @@ impl HidController {
self.pending_move_flag.store(true, Ordering::Release); self.pending_move_flag.store(true, Ordering::Release);
Ok(()) Ok(())
} }
Err(mpsc::error::TrySendError::Closed(_)) => Err(AppError::BadRequest( Err(mpsc::error::TrySendError::Closed(_)) => {
"HID event queue closed".to_string(), Err(AppError::BadRequest("HID event queue closed".to_string()))
)), }
} }
} }
@@ -517,9 +514,11 @@ impl HidController {
Err(mpsc::error::TrySendError::Full(ev)) => { Err(mpsc::error::TrySendError::Full(ev)) => {
// For non-move events, wait briefly to avoid dropping critical input // For non-move events, wait briefly to avoid dropping critical input
let tx = self.hid_tx.clone(); let tx = self.hid_tx.clone();
let send_result = let send_result = tokio::time::timeout(
tokio::time::timeout(Duration::from_millis(HID_EVENT_SEND_TIMEOUT_MS), tx.send(ev)) Duration::from_millis(HID_EVENT_SEND_TIMEOUT_MS),
.await; tx.send(ev),
)
.await;
if send_result.is_ok() { if send_result.is_ok() {
Ok(()) Ok(())
} else { } else {
@@ -527,9 +526,9 @@ impl HidController {
Ok(()) Ok(())
} }
} }
Err(mpsc::error::TrySendError::Closed(_)) => Err(AppError::BadRequest( Err(mpsc::error::TrySendError::Closed(_)) => {
"HID event queue closed".to_string(), Err(AppError::BadRequest("HID event queue closed".to_string()))
)), }
} }
} }
} }

View File

@@ -17,8 +17,10 @@ use crate::utils::LogThrottler;
/// HID health status /// HID health status
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
#[derive(Default)]
pub enum HidHealthStatus { pub enum HidHealthStatus {
/// Device is healthy and operational /// Device is healthy and operational
#[default]
Healthy, Healthy,
/// Device has an error, attempting recovery /// Device has an error, attempting recovery
Error { Error {
@@ -33,11 +35,6 @@ pub enum HidHealthStatus {
Disconnected, Disconnected,
} }
impl Default for HidHealthStatus {
fn default() -> Self {
Self::Healthy
}
}
/// HID health monitor configuration /// HID health monitor configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -196,7 +193,7 @@ impl HidHealthMonitor {
let attempt = self.retry_count.load(Ordering::Relaxed); let attempt = self.retry_count.load(Ordering::Relaxed);
// Only publish every 5 attempts to avoid event spam // 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); debug!("HID {} reconnecting, attempt {}", backend, attempt);
if let Some(ref events) = *self.events.read().await { if let Some(ref events) = *self.events.read().await {

View File

@@ -228,7 +228,7 @@ impl OtgBackend {
Ok(false) Ok(false)
} }
Ok(_) => 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 /// Check if all HID device files exist
pub fn check_devices_exist(&self) -> bool { pub fn check_devices_exist(&self) -> bool {
self.keyboard_path self.keyboard_path.as_ref().is_none_or(|p| p.exists())
.as_ref() && self.mouse_rel_path.as_ref().is_none_or(|p| p.exists())
.map_or(true, |p| p.exists()) && self.mouse_abs_path.as_ref().is_none_or(|p| p.exists())
&& self && self.consumer_path.as_ref().is_none_or(|p| p.exists())
.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())
} }
/// Get list of missing device paths /// Get list of missing device paths
@@ -952,9 +941,7 @@ impl HidBackend for OtgBackend {
} }
fn supports_absolute_mouse(&self) -> bool { fn supports_absolute_mouse(&self) -> bool {
self.mouse_abs_path self.mouse_abs_path.as_ref().is_some_and(|p| p.exists())
.as_ref()
.map_or(false, |p| p.exists())
} }
async fn send_consumer(&self, event: ConsumerEvent) -> Result<()> { 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 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 { let bind_port = if config.web.https_enabled {
config.web.https_port config.web.https_port
} else { } else {
@@ -646,7 +650,7 @@ async fn main() -> anyhow::Result<()> {
let server = axum_server::from_tcp_rustls(listener, tls_config.clone())? let server = axum_server::from_tcp_rustls(listener, tls_config.clone())?
.serve(app.clone().into_make_service()); .serve(app.clone().into_make_service());
servers.push(async move { server.await }); servers.push(server);
} }
tokio::select! { tokio::select! {

View File

@@ -52,10 +52,7 @@ impl MsdController {
/// # Parameters /// # Parameters
/// * `otg_service` - OTG service for gadget management /// * `otg_service` - OTG service for gadget management
/// * `msd_dir` - Base directory for MSD storage /// * `msd_dir` - Base directory for MSD storage
pub fn new( pub fn new(otg_service: Arc<OtgService>, msd_dir: impl Into<PathBuf>) -> Self {
otg_service: Arc<OtgService>,
msd_dir: impl Into<PathBuf>,
) -> Self {
let msd_dir = msd_dir.into(); let msd_dir = msd_dir.into();
let images_path = msd_dir.join("images"); let images_path = msd_dir.join("images");
let ventoy_dir = msd_dir.join("ventoy"); 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()) .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| { .map(|d| {
chrono::DateTime::from_timestamp(d.as_secs() as i64, 0) 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); .unwrap_or_else(Utc::now);
@@ -400,7 +400,7 @@ impl ImageManager {
.headers() .headers()
.get(reqwest::header::CONTENT_DISPOSITION) .get(reqwest::header::CONTENT_DISPOSITION)
.and_then(|v| v.to_str().ok()) .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 { if let Some(name) = from_header {
sanitize_filename(&name) sanitize_filename(&name)

View File

@@ -16,8 +16,10 @@ use crate::utils::LogThrottler;
/// MSD health status /// MSD health status
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
#[derive(Default)]
pub enum MsdHealthStatus { pub enum MsdHealthStatus {
/// Device is healthy and operational /// Device is healthy and operational
#[default]
Healthy, Healthy,
/// Device has an error /// Device has an error
Error { Error {
@@ -28,11 +30,6 @@ pub enum MsdHealthStatus {
}, },
} }
impl Default for MsdHealthStatus {
fn default() -> Self {
Self::Healthy
}
}
/// MSD health monitor configuration /// MSD health monitor configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -7,8 +7,10 @@ use std::path::PathBuf;
/// MSD operating mode /// MSD operating mode
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum MsdMode { pub enum MsdMode {
/// No storage connected /// No storage connected
#[default]
None, None,
/// Image file mounted (ISO/IMG) /// Image file mounted (ISO/IMG)
Image, Image,
@@ -16,11 +18,6 @@ pub enum MsdMode {
Drive, Drive,
} }
impl Default for MsdMode {
fn default() -> Self {
Self::None
}
}
/// Image file metadata /// Image file metadata
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -328,8 +328,7 @@ impl VentoyDrive {
let image = match VentoyImage::open(&path) { let image = match VentoyImage::open(&path) {
Ok(img) => img, Ok(img) => img,
Err(e) => { Err(e) => {
let _ = rt.block_on(tx.send(Err(std::io::Error::new( let _ = rt.block_on(tx.send(Err(std::io::Error::other(
std::io::ErrorKind::Other,
e.to_string(), e.to_string(),
)))); ))));
return; return;
@@ -341,8 +340,7 @@ impl VentoyDrive {
// Stream the file through the writer // Stream the file through the writer
if let Err(e) = image.read_file_to_writer(&file_path_owned, &mut chunk_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( let _ = rt.block_on(tx.send(Err(std::io::Error::other(
std::io::ErrorKind::Other,
e.to_string(), e.to_string(),
)))); ))));
} }
@@ -543,12 +541,11 @@ mod tests {
/// Decompress xz file using system command /// Decompress xz file using system command
fn decompress_xz(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> { fn decompress_xz(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
let output = Command::new("xz") let output = Command::new("xz")
.args(&["-d", "-k", "-c", src.to_str().unwrap()]) .args(["-d", "-k", "-c", src.to_str().unwrap()])
.output()?; .output()?;
if !output.status.success() { if !output.status.success() {
return Err(std::io::Error::new( return Err(std::io::Error::other(
std::io::ErrorKind::Other,
format!( format!(
"xz decompress failed: {}", "xz decompress failed: {}",
String::from_utf8_lossy(&output.stderr) String::from_utf8_lossy(&output.stderr)

View File

@@ -422,7 +422,11 @@ impl OtgGadgetManager {
if dest.exists() { if dest.exists() {
if let Err(e) = remove_file(&dest) { 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; continue;
} }
} }

View File

@@ -36,6 +36,7 @@ const FLAG_MSD: u8 = 0b10;
/// HID device paths /// HID device paths
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[derive(Default)]
pub struct HidDevicePaths { pub struct HidDevicePaths {
pub keyboard: Option<PathBuf>, pub keyboard: Option<PathBuf>,
pub mouse_relative: Option<PathBuf>, pub mouse_relative: Option<PathBuf>,
@@ -43,16 +44,6 @@ pub struct HidDevicePaths {
pub consumer: Option<PathBuf>, pub consumer: Option<PathBuf>,
} }
impl Default for HidDevicePaths {
fn default() -> Self {
Self {
keyboard: None,
mouse_relative: None,
mouse_absolute: None,
consumer: None,
}
}
}
impl HidDevicePaths { impl HidDevicePaths {
pub fn existing_paths(&self) -> Vec<PathBuf> { pub fn existing_paths(&self) -> Vec<PathBuf> {
@@ -239,14 +230,13 @@ impl OtgService {
let requested_functions = self.hid_functions.read().await.clone(); let requested_functions = self.hid_functions.read().await.clone();
{ {
let state = self.state.read().await; let state = self.state.read().await;
if state.hid_enabled { if state.hid_enabled
if state.hid_functions.as_ref() == Some(&requested_functions) { && state.hid_functions.as_ref() == Some(&requested_functions) {
if let Some(ref paths) = state.hid_paths { if let Some(ref paths) = state.hid_paths {
info!("HID already enabled, returning existing paths"); info!("HID already enabled, returning existing paths");
return Ok(paths.clone()); return Ok(paths.clone());
} }
} }
}
} }
// Recreate gadget with both HID and MSD if needed // Recreate gadget with both HID and MSD if needed
@@ -671,7 +661,7 @@ mod tests {
fn test_service_creation() { fn test_service_creation() {
let _service = OtgService::new(); let _service = OtgService::new();
// Just test that creation doesn't panic // Just test that creation doesn't panic
assert!(!OtgService::is_available() || true); // Depends on environment let _ = OtgService::is_available(); // Depends on environment
} }
#[tokio::test] #[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 head_len = ((first_byte & 0x3) + 1) as usize;
let mut n = first_byte 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; n |= (header_bytes[0] as usize) << 8;
} }
if head_len > 2 && header_bytes.len() >= 2 { if head_len > 2 && header_bytes.len() >= 2 {

View File

@@ -202,9 +202,11 @@ mod tests {
#[test] #[test]
fn test_rendezvous_addr() { 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"); assert_eq!(config.rendezvous_addr(), "example.com:21116");
config.rendezvous_server = "example.com:21116".to_string(); config.rendezvous_server = "example.com:21116".to_string();
@@ -217,10 +219,12 @@ mod tests {
#[test] #[test]
fn test_relay_addr() { 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 // 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())); assert_eq!(config.relay_addr(), Some("example.com:21117".to_string()));
// Explicit relay server // Explicit relay server
@@ -238,10 +242,12 @@ mod tests {
#[test] #[test]
fn test_effective_rendezvous_server() { 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 // When user sets a server, use it
config.rendezvous_server = "custom.example.com".to_string();
assert_eq!(config.effective_rendezvous_server(), "custom.example.com"); assert_eq!(config.effective_rendezvous_server(), "custom.example.com");
// When empty, returns empty // When empty, returns empty

View File

@@ -729,7 +729,7 @@ impl Connection {
} }
// Check if client sent supported_decoding with a codec preference // 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(); let prefer = supported_decoding.prefer.value();
debug!("Client codec preference: prefer={}", prefer); debug!("Client codec preference: prefer={}", prefer);
@@ -1352,8 +1352,12 @@ impl Connection {
debug!("Mouse event: x={}, y={}, mask={}", me.x, me.y, me.mask); debug!("Mouse event: x={}, y={}, mask={}", me.x, me.y, me.mask);
// Convert RustDesk mouse event to One-KVM mouse events // Convert RustDesk mouse event to One-KVM mouse events
let mouse_events = let mouse_events = convert_mouse_event(
convert_mouse_event(me, self.screen_width, self.screen_height, self.relative_mouse_active); me,
self.screen_width,
self.screen_height,
self.relative_mouse_active,
);
// Send to HID controller if available // Send to HID controller if available
if let Some(ref hid) = self.hid { 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 { 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 // 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) // Second hash: SHA256(first_hash + challenge)
let mut hasher2 = Sha256::new(); let mut hasher2 = Sha256::new();
hasher2.update(&first_hash); hasher2.update(first_hash);
hasher2.update(challenge.as_bytes()); hasher2.update(challenge.as_bytes());
hasher2.finalize().to_vec() hasher2.finalize().to_vec()
} }

View File

@@ -127,7 +127,8 @@ impl VideoFrameAdapter {
// Inject cached SPS/PPS before IDR when missing // Inject cached SPS/PPS before IDR when missing
if is_keyframe && (!has_sps || !has_pps) { 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()); 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(&[0, 0, 0, 1]);
out.extend_from_slice(sps); out.extend_from_slice(sps);

View File

@@ -36,8 +36,8 @@ use tracing::{debug, error, info, warn};
use crate::audio::AudioController; use crate::audio::AudioController;
use crate::hid::HidController; use crate::hid::HidController;
use crate::video::stream_manager::VideoStreamManager;
use crate::utils::bind_tcp_listener; use crate::utils::bind_tcp_listener;
use crate::video::stream_manager::VideoStreamManager;
use self::config::RustDeskConfig; use self::config::RustDeskConfig;
use self::connection::ConnectionManager; use self::connection::ConnectionManager;
@@ -559,6 +559,7 @@ impl RustDeskService {
/// 2. Send RelayResponse with client's socket_addr /// 2. Send RelayResponse with client's socket_addr
/// 3. Connect to RELAY server /// 3. Connect to RELAY server
/// 4. Accept connection without waiting for response /// 4. Accept connection without waiting for response
#[allow(clippy::too_many_arguments)]
async fn handle_relay_request( async fn handle_relay_request(
rendezvous_addr: &str, rendezvous_addr: &str,
relay_server: &str, relay_server: &str,

View File

@@ -559,7 +559,7 @@ impl RendezvousMediator {
); );
let msg = make_punch_hole_sent( 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, &id,
&ph.relay_server, &ph.relay_server,
ph.nat_type.enum_value().unwrap_or(NatType::UNKNOWN_NAT), ph.nat_type.enum_value().unwrap_or(NatType::UNKNOWN_NAT),

View File

@@ -64,6 +64,7 @@ pub struct AppState {
impl AppState { impl AppState {
/// Create new application state /// Create new application state
#[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
config: ConfigStore, config: ConfigStore,
sessions: SessionStore, sessions: SessionStore,

View File

@@ -15,16 +15,16 @@
//! //!
//! Note: Audio WebSocket is handled separately by audio_ws.rs (/api/ws/audio) //! 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::collections::HashMap;
use std::io;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use crate::video::v4l2r_capture::V4l2rCaptureStream;
use crate::utils::LogThrottler;
use crate::audio::AudioController; use crate::audio::AudioController;
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
@@ -624,7 +624,7 @@ impl MjpegStreamer {
validate_counter = validate_counter.wrapping_add(1); validate_counter = validate_counter.wrapping_add(1);
if pixel_format.is_compressed() 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]) && !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size])
{ {
continue; continue;

View File

@@ -2,8 +2,8 @@
//! //!
//! This module contains common utilities used across the codebase. //! This module contains common utilities used across the codebase.
pub mod throttle;
pub mod net; pub mod net;
pub mod throttle;
pub use throttle::LogThrottler;
pub use net::{bind_tcp_listener, bind_udp_socket}; 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. //! Provides async video capture using memory-mapped buffers.
use bytes::Bytes;
use std::collections::HashMap; use std::collections::HashMap;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use bytes::Bytes;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};

View File

@@ -6,11 +6,11 @@ use std::path::{Path, PathBuf};
use std::sync::mpsc; use std::sync::mpsc;
use std::time::Duration; use std::time::Duration;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use v4l2r::nix::errno::Errno;
use v4l2r::bindings::{v4l2_frmivalenum, v4l2_frmsizeenum}; use v4l2r::bindings::{v4l2_frmivalenum, v4l2_frmsizeenum};
use v4l2r::ioctl::{ use v4l2r::ioctl::{
self, Capabilities, Capability as V4l2rCapability, FormatIterator, FrmIvalTypes, FrmSizeTypes, self, Capabilities, Capability as V4l2rCapability, FormatIterator, FrmIvalTypes, FrmSizeTypes,
}; };
use v4l2r::nix::errno::Errno;
use v4l2r::{Format as V4l2rFormat, QueueType}; use v4l2r::{Format as V4l2rFormat, QueueType};
use super::format::{PixelFormat, Resolution}; use super::format::{PixelFormat, Resolution};
@@ -96,7 +96,9 @@ impl VideoDevice {
.read(true) .read(true)
.write(true) .write(true)
.open(&path) .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 }) Ok(Self { path, fd })
} }
@@ -106,10 +108,9 @@ impl VideoDevice {
let path = path.as_ref().to_path_buf(); let path = path.as_ref().to_path_buf();
debug!("Opening video device (read-only): {:?}", path); debug!("Opening video device (read-only): {:?}", path);
let fd = File::options() let fd = File::options().read(true).open(&path).map_err(|e| {
.read(true) AppError::VideoError(format!("Failed to open device {:?}: {}", path, e))
.open(&path) })?;
.map_err(|e| AppError::VideoError(format!("Failed to open device {:?}: {}", path, e)))?;
Ok(Self { path, fd }) Ok(Self { path, fd })
} }
@@ -206,8 +207,9 @@ impl VideoDevice {
if let Some(size) = size.size() { if let Some(size) = size.size() {
match size { match size {
FrmSizeTypes::Discrete(d) => { FrmSizeTypes::Discrete(d) => {
let fps = let fps = self
self.enumerate_fps(fourcc, d.width, d.height).unwrap_or_default(); .enumerate_fps(fourcc, d.width, d.height)
.unwrap_or_default();
resolutions.push(ResolutionInfo::new(d.width, d.height, fps)); resolutions.push(ResolutionInfo::new(d.width, d.height, fps));
} }
FrmSizeTypes::StepWise(s) => { FrmSizeTypes::StepWise(s) => {
@@ -225,7 +227,8 @@ impl VideoDevice {
let fps = self let fps = self
.enumerate_fps(fourcc, res.width, res.height) .enumerate_fps(fourcc, res.width, res.height)
.unwrap_or_default(); .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; let mut index = 0u32;
loop { loop {
match ioctl::enum_frame_intervals::<v4l2_frmivalenum>( match ioctl::enum_frame_intervals::<v4l2_frmivalenum>(
&self.fd, &self.fd, index, fourcc, width, height,
index,
fourcc,
width,
height,
) { ) {
Ok(interval) => { Ok(interval) => {
if let Some(interval) = interval.intervals() { if let Some(interval) = interval.intervals() {
@@ -411,7 +410,7 @@ impl VideoDevice {
.max() .max()
.unwrap_or(0); .unwrap_or(0);
priority += (max_resolution / 100000) as u32; priority += max_resolution / 100000;
// Known good drivers get bonus // Known good drivers get bonus
let good_drivers = ["uvcvideo", "tc358743"]; let good_drivers = ["uvcvideo", "tc358743"];
@@ -563,15 +562,7 @@ fn sysfs_maybe_capture(path: &Path) -> bool {
} }
let skip_hints = [ let skip_hints = [
"codec", "codec", "decoder", "encoder", "isp", "mem2mem", "m2m", "vbi", "radio", "metadata",
"decoder",
"encoder",
"isp",
"mem2mem",
"m2m",
"vbi",
"radio",
"metadata",
"output", "output",
]; ];
if skip_hints.iter().any(|hint| sysfs_name.contains(hint)) && !maybe_capture { 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) /// H.264 encoder type (detected from hwcodec)
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Default)]
pub enum H264EncoderType { pub enum H264EncoderType {
/// NVIDIA NVENC /// NVIDIA NVENC
Nvenc, Nvenc,
@@ -49,6 +50,7 @@ pub enum H264EncoderType {
/// Software encoding (libx264/openh264) /// Software encoding (libx264/openh264)
Software, Software,
/// No encoder available /// No encoder available
#[default]
None, 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 /// Map codec name to encoder type
fn codec_name_to_type(name: &str) -> H264EncoderType { 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 /// Input pixel format for H264 encoder
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Default)]
pub enum H264InputFormat { pub enum H264InputFormat {
/// YUV420P (I420) - planar Y, U, V /// YUV420P (I420) - planar Y, U, V
Yuv420p, Yuv420p,
/// NV12 - Y plane + interleaved UV plane (optimal for VAAPI) /// NV12 - Y plane + interleaved UV plane (optimal for VAAPI)
#[default]
Nv12, Nv12,
/// NV21 - Y plane + interleaved VU plane /// NV21 - Y plane + interleaved VU plane
Nv21, Nv21,
@@ -113,11 +112,6 @@ pub enum H264InputFormat {
Bgr24, Bgr24,
} }
impl Default for H264InputFormat {
fn default() -> Self {
Self::Nv12 // Default to NV12 for VAAPI compatibility
}
}
/// H.264 encoder configuration /// H.264 encoder configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -31,6 +31,7 @@ fn init_hwcodec_logging() {
/// H.265 encoder type (detected from hwcodec) /// H.265 encoder type (detected from hwcodec)
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Default)]
pub enum H265EncoderType { pub enum H265EncoderType {
/// NVIDIA NVENC /// NVIDIA NVENC
Nvenc, Nvenc,
@@ -47,6 +48,7 @@ pub enum H265EncoderType {
/// Software encoder (libx265) /// Software encoder (libx265)
Software, Software,
/// No encoder available /// No encoder available
#[default]
None, 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 { impl From<EncoderBackend> for H265EncoderType {
fn from(backend: EncoderBackend) -> Self { fn from(backend: EncoderBackend) -> Self {
@@ -87,10 +84,12 @@ impl From<EncoderBackend> for H265EncoderType {
/// Input pixel format for H265 encoder /// Input pixel format for H265 encoder
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Default)]
pub enum H265InputFormat { pub enum H265InputFormat {
/// YUV420P (I420) - planar Y, U, V /// YUV420P (I420) - planar Y, U, V
Yuv420p, Yuv420p,
/// NV12 - Y plane + interleaved UV plane (optimal for hardware encoders) /// NV12 - Y plane + interleaved UV plane (optimal for hardware encoders)
#[default]
Nv12, Nv12,
/// NV21 - Y plane + interleaved VU plane /// NV21 - Y plane + interleaved VU plane
Nv21, Nv21,
@@ -106,11 +105,6 @@ pub enum H265InputFormat {
Bgr24, Bgr24,
} }
impl Default for H265InputFormat {
fn default() -> Self {
Self::Nv12 // Default to NV12 for hardware encoder compatibility
}
}
/// H.265 encoder configuration /// H.265 encoder configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -256,8 +250,6 @@ pub fn detect_best_h265_encoder(width: u32, height: u32) -> (H265EncoderType, Op
H265EncoderType::Rkmpp H265EncoderType::Rkmpp
} else if codec.name.contains("v4l2m2m") { } else if codec.name.contains("v4l2m2m") {
H265EncoderType::V4l2M2m H265EncoderType::V4l2M2m
} else if codec.name.contains("libx265") {
H265EncoderType::Software
} else { } else {
H265EncoderType::Software // Default to software for unknown H265EncoderType::Software // Default to software for unknown
}; };

View File

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

View File

@@ -15,12 +15,14 @@ use crate::video::format::{PixelFormat, Resolution};
#[typeshare] #[typeshare]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")] #[serde(tag = "type", content = "value")]
#[derive(Default)]
pub enum BitratePreset { pub enum BitratePreset {
/// Speed priority: 1 Mbps, lowest latency, smaller GOP /// Speed priority: 1 Mbps, lowest latency, smaller GOP
/// Best for: slow networks, remote management, low-bandwidth scenarios /// Best for: slow networks, remote management, low-bandwidth scenarios
Speed, Speed,
/// Balanced: 4 Mbps, good quality/latency tradeoff /// Balanced: 4 Mbps, good quality/latency tradeoff
/// Best for: typical usage, recommended default /// Best for: typical usage, recommended default
#[default]
Balanced, Balanced,
/// Quality priority: 8 Mbps, best visual quality /// Quality priority: 8 Mbps, best visual quality
/// Best for: local network, high-bandwidth scenarios, detailed work /// 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 { impl std::fmt::Display for BitratePreset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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) /// VP8 encoder type (detected from hwcodec)
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Default)]
pub enum VP8EncoderType { pub enum VP8EncoderType {
/// VAAPI (Intel on Linux) /// VAAPI (Intel on Linux)
Vaapi, Vaapi,
/// Software encoder (libvpx) /// Software encoder (libvpx)
Software, Software,
/// No encoder available /// No encoder available
#[default]
None, 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 { impl From<EncoderBackend> for VP8EncoderType {
fn from(backend: EncoderBackend) -> Self { fn from(backend: EncoderBackend) -> Self {
@@ -68,18 +65,15 @@ impl From<EncoderBackend> for VP8EncoderType {
/// Input pixel format for VP8 encoder /// Input pixel format for VP8 encoder
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Default)]
pub enum VP8InputFormat { pub enum VP8InputFormat {
/// YUV420P (I420) - planar Y, U, V /// YUV420P (I420) - planar Y, U, V
Yuv420p, Yuv420p,
/// NV12 - Y plane + interleaved UV plane /// NV12 - Y plane + interleaved UV plane
#[default]
Nv12, Nv12,
} }
impl Default for VP8InputFormat {
fn default() -> Self {
Self::Nv12 // Default to NV12 for VAAPI compatibility
}
}
/// VP8 encoder configuration /// VP8 encoder configuration
#[derive(Debug, Clone)] #[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") { let encoder_type = if codec.name.contains("vaapi") {
VP8EncoderType::Vaapi VP8EncoderType::Vaapi
} else if codec.name.contains("libvpx") {
VP8EncoderType::Software
} else { } else {
VP8EncoderType::Software // Default to software for unknown VP8EncoderType::Software // Default to software for unknown
}; };

View File

@@ -31,12 +31,14 @@ fn init_hwcodec_logging() {
/// VP9 encoder type (detected from hwcodec) /// VP9 encoder type (detected from hwcodec)
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Default)]
pub enum VP9EncoderType { pub enum VP9EncoderType {
/// VAAPI (Intel on Linux) /// VAAPI (Intel on Linux)
Vaapi, Vaapi,
/// Software encoder (libvpx-vp9) /// Software encoder (libvpx-vp9)
Software, Software,
/// No encoder available /// No encoder available
#[default]
None, 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 { impl From<EncoderBackend> for VP9EncoderType {
fn from(backend: EncoderBackend) -> Self { fn from(backend: EncoderBackend) -> Self {
@@ -68,18 +65,15 @@ impl From<EncoderBackend> for VP9EncoderType {
/// Input pixel format for VP9 encoder /// Input pixel format for VP9 encoder
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Default)]
pub enum VP9InputFormat { pub enum VP9InputFormat {
/// YUV420P (I420) - planar Y, U, V /// YUV420P (I420) - planar Y, U, V
Yuv420p, Yuv420p,
/// NV12 - Y plane + interleaved UV plane /// NV12 - Y plane + interleaved UV plane
#[default]
Nv12, Nv12,
} }
impl Default for VP9InputFormat {
fn default() -> Self {
Self::Nv12 // Default to NV12 for VAAPI compatibility
}
}
/// VP9 encoder configuration /// VP9 encoder configuration
#[derive(Debug, Clone)] #[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") { let encoder_type = if codec.name.contains("vaapi") {
VP9EncoderType::Vaapi VP9EncoderType::Vaapi
} else if codec.name.contains("libvpx") {
VP9EncoderType::Software
} else { } else {
VP9EncoderType::Software // Default to software for unknown VP9EncoderType::Software // Default to software for unknown
}; };

View File

@@ -81,6 +81,11 @@ impl FrameBuffer {
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.data.len() 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 { impl std::fmt::Debug for FrameBuffer {

View File

@@ -36,9 +36,6 @@ use crate::error::{AppError, Result};
use crate::utils::LogThrottler; use crate::utils::LogThrottler;
use crate::video::convert::{Nv12Converter, PixelConverter}; use crate::video::convert::{Nv12Converter, PixelConverter};
use crate::video::decoder::MjpegTurboDecoder; 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::h264::{detect_best_encoder, H264Config, H264Encoder, H264InputFormat};
use crate::video::encoder::h265::{ use crate::video::encoder::h265::{
detect_best_h265_encoder, H265Config, H265Encoder, H265InputFormat, 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::encoder::vp9::{detect_best_vp9_encoder, VP9Config, VP9Encoder};
use crate::video::format::{PixelFormat, Resolution}; use crate::video::format::{PixelFormat, Resolution};
use crate::video::frame::{FrameBuffer, FrameBufferPool, VideoFrame}; 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 /// Encoded video frame for distribution
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -508,7 +510,10 @@ impl SharedVideoPipeline {
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))] #[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
if needs_mjpeg_decode if needs_mjpeg_decode
&& is_rkmpp_encoder && is_rkmpp_encoder
&& matches!(config.output_codec, VideoEncoderType::H264 | VideoEncoderType::H265) && matches!(
config.output_codec,
VideoEncoderType::H264 | VideoEncoderType::H265
)
{ {
info!( info!(
"Initializing FFmpeg HW MJPEG->{} pipeline (no fallback)", "Initializing FFmpeg HW MJPEG->{} pipeline (no fallback)",
@@ -525,7 +530,11 @@ impl SharedVideoPipeline {
thread_count: 1, thread_count: 1,
}; };
let pipeline = HwMjpegH26xPipeline::new(hw_config).map_err(|e| { 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!( AppError::VideoError(format!(
"FFmpeg HW MJPEG->{} init failed: {}", "FFmpeg HW MJPEG->{} init failed: {}",
config.output_codec, detail config.output_codec, detail
@@ -899,7 +908,11 @@ impl SharedVideoPipeline {
/// Get subscriber count /// Get subscriber count
pub fn subscriber_count(&self) -> usize { 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 /// Report that a receiver has lagged behind
@@ -948,7 +961,11 @@ impl SharedVideoPipeline {
pipeline pipeline
.reconfigure(bitrate_kbps as i32, gop as i32) .reconfigure(bitrate_kbps as i32, gop as i32)
.map_err(|e| { .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!( AppError::VideoError(format!(
"FFmpeg HW reconfigure failed: {}", "FFmpeg HW reconfigure failed: {}",
detail detail
@@ -1364,8 +1381,7 @@ impl SharedVideoPipeline {
error!("Capture error: {}", e); error!("Capture error: {}", e);
} }
} else { } else {
let counter = let counter = suppressed_capture_errors.entry(key).or_insert(0);
suppressed_capture_errors.entry(key).or_insert(0);
*counter = counter.saturating_add(1); *counter = counter.saturating_add(1);
} }
} }
@@ -1380,7 +1396,7 @@ impl SharedVideoPipeline {
validate_counter = validate_counter.wrapping_add(1); validate_counter = validate_counter.wrapping_add(1);
if pixel_format.is_compressed() 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]) && !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size])
{ {
continue; continue;
@@ -1401,7 +1417,6 @@ impl SharedVideoPipeline {
*guard = Some(frame); *guard = Some(frame);
} }
let _ = frame_seq_tx.send(sequence); let _ = frame_seq_tx.send(sequence);
} }
pipeline.running_flag.store(false, Ordering::Release); 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 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)) AppError::VideoError(format!("FFmpeg HW encode failed: {}", detail))
})?; })?;
@@ -1486,9 +1505,10 @@ impl SharedVideoPipeline {
} }
let decoded_buf = if input_format.is_compressed() { let decoded_buf = if input_format.is_compressed() {
let decoder = state.mjpeg_decoder.as_mut().ok_or_else(|| { let decoder = state
AppError::VideoError("MJPEG decoder not initialized".to_string()) .mjpeg_decoder
})?; .as_mut()
.ok_or_else(|| AppError::VideoError("MJPEG decoder not initialized".to_string()))?;
let decoded = decoder.decode(raw_frame)?; let decoded = decoder.decode(raw_frame)?;
Some(decoded) Some(decoded)
} else { } else {
@@ -1518,16 +1538,18 @@ impl SharedVideoPipeline {
debug!("[Pipeline] Keyframe will be generated for this frame"); 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 // Software encoder with direct input conversion to YUV420P
let conv = state.yuv420p_converter.as_mut().unwrap(); if let Some(conv) = state.yuv420p_converter.as_mut() {
let yuv420p_data = conv let yuv420p_data = conv.convert(raw_frame).map_err(|e| {
.convert(raw_frame) AppError::VideoError(format!("YUV420P conversion failed: {}", e))
.map_err(|e| AppError::VideoError(format!("YUV420P conversion failed: {}", e)))?; })?;
encoder.encode_raw(yuv420p_data, pts_ms) encoder.encode_raw(yuv420p_data, pts_ms)
} else if state.nv12_converter.is_some() { } 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 // Hardware encoder with input conversion to NV12
let conv = state.nv12_converter.as_mut().unwrap();
let nv12_data = conv let nv12_data = conv
.convert(raw_frame) .convert(raw_frame)
.map_err(|e| AppError::VideoError(format!("NV12 conversion failed: {}", e)))?; .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. /// Returns None if video capture cannot be started or pipeline creation fails.
pub async fn subscribe_encoded_frames( pub async fn subscribe_encoded_frames(
&self, &self,
) -> Option<tokio::sync::mpsc::Receiver<std::sync::Arc< ) -> Option<
crate::video::shared_video_pipeline::EncodedVideoFrame, tokio::sync::mpsc::Receiver<
>>> { std::sync::Arc<crate::video::shared_video_pipeline::EncodedVideoFrame>,
>,
> {
// 1. Ensure video capture is initialized (for config discovery) // 1. Ensure video capture is initialized (for config discovery)
if self.streamer.state().await == StreamerState::Uninitialized { if self.streamer.state().await == StreamerState::Uninitialized {
tracing::info!("Initializing video capture for encoded frame subscription"); 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 // 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()), Ok(pipeline) => Some(pipeline.subscribe()),
Err(e) => { Err(e) => {
tracing::error!("Failed to start shared video pipeline: {}", e); tracing::error!("Failed to start shared video pipeline: {}", e);

View File

@@ -571,11 +571,9 @@ impl Streamer {
break; break;
} }
} }
} else { } else if zero_since.is_some() {
if zero_since.is_some() { info!("Clients reconnected, canceling auto-pause");
info!("Clients reconnected, canceling auto-pause"); zero_since = None;
zero_since = None;
}
} }
} }
}); });
@@ -805,7 +803,7 @@ impl Streamer {
validate_counter = validate_counter.wrapping_add(1); validate_counter = validate_counter.wrapping_add(1);
if pixel_format.is_compressed() 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]) && !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size])
{ {
continue; continue;
@@ -964,7 +962,7 @@ impl Streamer {
*streamer.state.write().await = StreamerState::Recovering; *streamer.state.write().await = StreamerState::Recovering;
// Publish reconnecting event (every 5 attempts to avoid spam) // Publish reconnecting event (every 5 attempts to avoid spam)
if attempt == 1 || attempt % 5 == 0 { if attempt == 1 || attempt.is_multiple_of(5) {
streamer streamer
.publish_event(SystemEvent::StreamReconnecting { .publish_event(SystemEvent::StreamReconnecting {
device: device_path.clone(), device: device_path.clone(),

View File

@@ -10,8 +10,8 @@ use nix::poll::{poll, PollFd, PollFlags, PollTimeout};
use tracing::{debug, warn}; use tracing::{debug, warn};
use v4l2r::bindings::{v4l2_requestbuffers, v4l2_streamparm, v4l2_streamparm__bindgen_ty_1}; use v4l2r::bindings::{v4l2_requestbuffers, v4l2_streamparm, v4l2_streamparm__bindgen_ty_1};
use v4l2r::ioctl::{ use v4l2r::ioctl::{
self, Capabilities, Capability as V4l2rCapability, MemoryConsistency, PlaneMapping, self, Capabilities, Capability as V4l2rCapability, MemoryConsistency, PlaneMapping, QBufPlane,
QBufPlane, QBuffer, QueryBuffer, V4l2Buffer, QBuffer, QueryBuffer, V4l2Buffer,
}; };
use v4l2r::memory::{MemoryType, MmapHandle}; use v4l2r::memory::{MemoryType, MmapHandle};
use v4l2r::{Format as V4l2rFormat, PixelFormat as V4l2rPixelFormat, QueueType}; 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| { let mut fmt: V4l2rFormat = ioctl::g_fmt(&fd, queue)
AppError::VideoError(format!("Failed to get device format: {}", e)) .map_err(|e| AppError::VideoError(format!("Failed to get device format: {}", e)))?;
})?;
fmt.width = resolution.width; fmt.width = resolution.width;
fmt.height = resolution.height; fmt.height = resolution.height;
fmt.pixelformat = V4l2rPixelFormat::from(&format.to_fourcc()); fmt.pixelformat = V4l2rPixelFormat::from(&format.to_fourcc());
let actual_fmt: V4l2rFormat = ioctl::s_fmt(&mut fd, (queue, &fmt)).map_err(|e| { let actual_fmt: V4l2rFormat = ioctl::s_fmt(&mut fd, (queue, &fmt))
AppError::VideoError(format!("Failed to set device format: {}", e)) .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_resolution = Resolution::new(actual_fmt.width, actual_fmt.height);
let actual_format = PixelFormat::from_v4l2r(actual_fmt.pixelformat).unwrap_or(format); let actual_format = PixelFormat::from_v4l2r(actual_fmt.pixelformat).unwrap_or(format);
let stride = actual_fmt let stride = actual_fmt
.plane_fmt .plane_fmt.first()
.get(0)
.map(|p| p.bytesperline) .map(|p| p.bytesperline)
.unwrap_or_else(|| match actual_format.bytes_per_pixel() { .unwrap_or_else(|| match actual_format.bytes_per_pixel() {
Some(bpp) => actual_resolution.width * bpp as u32, Some(bpp) => actual_resolution.width * bpp as u32,
@@ -129,10 +126,7 @@ impl V4l2rCaptureStream {
let mut plane_maps = Vec::with_capacity(query.planes.len()); let mut plane_maps = Vec::with_capacity(query.planes.len());
for plane in &query.planes { for plane in &query.planes {
let mapping = ioctl::mmap(&fd, plane.mem_offset, plane.length).map_err(|e| { let mapping = ioctl::mmap(&fd, plane.mem_offset, plane.length).map_err(|e| {
AppError::VideoError(format!( AppError::VideoError(format!("Failed to mmap buffer {}: {}", index, e))
"Failed to mmap buffer {}: {}",
index, e
))
})?; })?;
plane_maps.push(mapping); plane_maps.push(mapping);
} }
@@ -150,9 +144,8 @@ impl V4l2rCaptureStream {
}; };
stream.queue_all_buffers()?; stream.queue_all_buffers()?;
ioctl::streamon(&stream.fd, stream.queue).map_err(|e| { ioctl::streamon(&stream.fd, stream.queue)
AppError::VideoError(format!("Failed to start capture stream: {}", e)) .map_err(|e| AppError::VideoError(format!("Failed to start capture stream: {}", e)))?;
})?;
Ok(stream) Ok(stream)
} }
@@ -172,9 +165,8 @@ impl V4l2rCaptureStream {
pub fn next_into(&mut self, dst: &mut Vec<u8>) -> io::Result<CaptureMeta> { pub fn next_into(&mut self, dst: &mut Vec<u8>) -> io::Result<CaptureMeta> {
self.wait_ready()?; self.wait_ready()?;
let dqbuf: V4l2Buffer = ioctl::dqbuf(&self.fd, self.queue).map_err(|e| { let dqbuf: V4l2Buffer = ioctl::dqbuf(&self.fd, self.queue)
io::Error::new(io::ErrorKind::Other, format!("dqbuf failed: {}", e)) .map_err(|e| io::Error::other(format!("dqbuf failed: {}", e)))?;
})?;
let index = dqbuf.as_v4l2_buffer().index as usize; let index = dqbuf.as_v4l2_buffer().index as usize;
let sequence = dqbuf.as_v4l2_buffer().sequence as u64; let sequence = dqbuf.as_v4l2_buffer().sequence as u64;
@@ -211,7 +203,7 @@ impl V4l2rCaptureStream {
} }
self.queue_buffer(index as u32) 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 { Ok(CaptureMeta {
bytes_used: total, bytes_used: total,
@@ -240,7 +232,7 @@ impl V4l2rCaptureStream {
} }
fn queue_buffer(&mut self, index: u32) -> Result<()> { fn queue_buffer(&mut self, index: u32) -> Result<()> {
let handle = MmapHandle::default(); let handle = MmapHandle;
let planes = self.mappings[index as usize] let planes = self.mappings[index as usize]
.iter() .iter()
.map(|mapping| { .map(|mapping| {

View File

@@ -326,7 +326,6 @@ impl VideoSessionManager {
bitrate_preset: self.config.bitrate_preset, bitrate_preset: self.config.bitrate_preset,
fps: self.config.fps, fps: self.config.fps,
encoder_backend: self.config.encoder_backend, encoder_backend: self.config.encoder_backend,
..Default::default()
}; };
// Create new pipeline // 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 // Low-endpoint UDCs (e.g., musb) cannot handle consumer control endpoints reliably
if new_config.backend == HidBackend::Otg { if new_config.backend == HidBackend::Otg {
if let Some(udc) = if let Some(udc) = crate::otg::configfs::resolve_udc_name(new_config.otg_udc.as_deref()) {
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 { if crate::otg::configfs::is_low_endpoint_udc(&udc) && new_hid_functions.consumer {
tracing::warn!( tracing::warn!(
"UDC {} has low endpoint resources, disabling consumer control", "UDC {} has low endpoint resources, disabling consumer control",

View File

@@ -86,7 +86,7 @@ pub async fn start_extension(
// Start the extension // Start the extension
mgr.start(ext_id, &config.extensions) mgr.start(ext_id, &config.extensions)
.await .await
.map_err(|e| AppError::Internal(e))?; .map_err(AppError::Internal)?;
// Return updated status // Return updated status
Ok(Json(ExtensionInfo { Ok(Json(ExtensionInfo {
@@ -108,7 +108,7 @@ pub async fn stop_extension(
let mgr = &state.extensions; let mgr = &state.extensions;
// Stop the extension // 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 // Return updated status
Ok(Json(ExtensionInfo { Ok(Json(ExtensionInfo {
@@ -263,14 +263,16 @@ pub async fn update_gostc_config(
if was_enabled && !is_enabled { if was_enabled && !is_enabled {
state.extensions.stop(ExtensionId::Gostc).await.ok(); state.extensions.stop(ExtensionId::Gostc).await.ok();
} else if !was_enabled && is_enabled && has_key { } else if !was_enabled
if state.extensions.check_available(ExtensionId::Gostc) { && is_enabled
state && has_key
.extensions && state.extensions.check_available(ExtensionId::Gostc)
.start(ExtensionId::Gostc, &new_config.extensions) {
.await state
.ok(); .extensions
} .start(ExtensionId::Gostc, &new_config.extensions)
.await
.ok();
} }
Ok(Json(new_config.extensions.gostc.clone())) Ok(Json(new_config.extensions.gostc.clone()))
@@ -312,14 +314,16 @@ pub async fn update_easytier_config(
if was_enabled && !is_enabled { if was_enabled && !is_enabled {
state.extensions.stop(ExtensionId::Easytier).await.ok(); state.extensions.stop(ExtensionId::Easytier).await.ok();
} else if !was_enabled && is_enabled && has_name { } else if !was_enabled
if state.extensions.check_available(ExtensionId::Easytier) { && is_enabled
state && has_name
.extensions && state.extensions.check_available(ExtensionId::Easytier)
.start(ExtensionId::Easytier, &new_config.extensions) {
.await state
.ok(); .extensions
} .start(ExtensionId::Easytier, &new_config.extensions)
.await
.ok();
} }
Ok(Json(new_config.extensions.easytier.clone())) Ok(Json(new_config.extensions.easytier.clone()))

View File

@@ -205,7 +205,7 @@ fn get_cpu_model() -> String {
.count(); .count();
Some(format!("{} {}C", std::env::consts::ARCH, cores)) 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 /// 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) { if matches!(new_config.hid.backend, crate::config::HidBackend::Otg) {
let mut hid_functions = new_config.hid.effective_otg_functions(); let mut hid_functions = new_config.hid.effective_otg_functions();
if let Some(udc) = if let Some(udc) = crate::otg::configfs::resolve_udc_name(new_config.hid.otg_udc.as_deref())
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 { if crate::otg::configfs::is_low_endpoint_udc(&udc) && hid_functions.consumer {
tracing::warn!( tracing::warn!(
@@ -1842,12 +1841,12 @@ pub async fn mjpeg_stream(
break; break;
} }
// Send last frame again to keep connection alive // Send last frame again to keep connection alive
if let Some(frame) = handler_clone.current_frame() { let Some(frame) = handler_clone.current_frame() else {
if frame.is_valid_jpeg() { continue;
if tx.send(create_mjpeg_part(frame.data())).await.is_err() { };
break;
} 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); yield Ok::<bytes::Bytes, std::io::Error>(data);
// Record FPS after yield - data has been handed to Axum/hyper // Record FPS after yield - data has been handed to Axum/hyper
// This is closer to actual TCP send than recording at tx.send() // 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?; let (file_size, mut rx) = drive.read_file_stream(&file_path).await?;
// Extract filename for Content-Disposition // 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 // Create a stream from the channel receiver
let body_stream = async_stream::stream! { 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() .first_or_octet_stream()
.to_string(); .to_string();
return Some( Some(
Response::builder() Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime) .header(header::CONTENT_TYPE, mime)
.header(header::CACHE_CONTROL, "public, max-age=86400") .header(header::CACHE_CONTROL, "public, max-age=86400")
.body(Body::from(data)) .body(Body::from(data))
.unwrap(), .unwrap(),
); )
} }
Err(e) => { Err(e) => {
tracing::debug!( tracing::debug!(
@@ -143,7 +143,7 @@ fn try_serve_file(path: &str) -> Option<Response<Body>> {
file_path.display(), file_path.display(),
e e
); );
return None; None
} }
} }
} }

View File

@@ -108,18 +108,15 @@ impl TurnServer {
/// Video codec preference /// Video codec preference
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum VideoCodec { pub enum VideoCodec {
#[default]
H264, H264,
VP8, VP8,
VP9, VP9,
AV1, AV1,
} }
impl Default for VideoCodec {
fn default() -> Self {
Self::H264
}
}
impl std::fmt::Display for VideoCodec { impl std::fmt::Display for VideoCodec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View File

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

View File

@@ -330,9 +330,7 @@ impl OpusAudioTrack {
stream_id.to_string(), stream_id.to_string(),
)); ));
Self { Self { track }
track,
}
} }
/// Get the underlying WebRTC track /// Get the underlying WebRTC track
@@ -365,13 +363,10 @@ impl OpusAudioTrack {
..Default::default() ..Default::default()
}; };
self.track self.track.write_sample(&sample).await.map_err(|e| {
.write_sample(&sample) error!("Failed to write Opus sample: {}", e);
.await AppError::WebRtcError(format!("Failed to write audio sample: {}", e))
.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 data = frame.data();
let max_payload_size = 1200; // MTU - headers 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; let mut bytes_sent = 0u64;
for i in 0..packet_count { for i in 0..packet_count {

View File

@@ -292,7 +292,6 @@ impl UniversalSession {
urls: turn.urls.clone(), urls: turn.urls.clone(),
username: turn.username.clone(), username: turn.username.clone(),
credential: turn.credential.clone(), credential: turn.credential.clone(),
..Default::default()
}); });
} }
@@ -430,7 +429,9 @@ impl UniversalSession {
let candidate = IceCandidate { let candidate = IceCandidate {
candidate: candidate_str, candidate: candidate_str,
sdp_mid: candidate_json.as_ref().and_then(|j| j.sdp_mid.clone()), 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 username_fragment: candidate_json
.as_ref() .as_ref()
.and_then(|j| j.username_fragment.clone()), .and_then(|j| j.username_fragment.clone()),
@@ -615,20 +616,15 @@ impl UniversalSession {
}; };
// Verify codec matches // Verify codec matches
let frame_codec = match encoded_frame.codec { let frame_codec = encoded_frame.codec;
VideoEncoderType::H264 => VideoEncoderType::H264,
VideoEncoderType::H265 => VideoEncoderType::H265,
VideoEncoderType::VP8 => VideoEncoderType::VP8,
VideoEncoderType::VP9 => VideoEncoderType::VP9,
};
if frame_codec != expected_codec { if frame_codec != expected_codec {
continue; continue;
} }
// Debug log for H265 frames // Debug log for H265 frames
if expected_codec == VideoEncoderType::H265 { if expected_codec == VideoEncoderType::H265
if encoded_frame.is_keyframe || frames_sent % 30 == 0 { && (encoded_frame.is_keyframe || frames_sent.is_multiple_of(30)) {
debug!( debug!(
"[Session-H265] Received frame #{}: size={}, keyframe={}, seq={}", "[Session-H265] Received frame #{}: size={}, keyframe={}, seq={}",
frames_sent, frames_sent,
@@ -637,7 +633,6 @@ impl UniversalSession {
encoded_frame.sequence encoded_frame.sequence
); );
} }
}
// Ensure decoder starts from a keyframe and recover on gaps. // Ensure decoder starts from a keyframe and recover on gaps.
let mut gap_detected = false; let mut gap_detected = false;
@@ -768,7 +763,7 @@ impl UniversalSession {
// 20ms at 48kHz = 960 samples // 20ms at 48kHz = 960 samples
let samples = 960u32; let samples = 960u32;
if let Err(e) = audio_track.write_packet(&opus_frame.data, samples).await { 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); debug!("Failed to write audio packet: {}", e);
} }
} else { } else {

View File

@@ -285,7 +285,7 @@ impl UniversalVideoTrack {
} }
/// Get current statistics /// Get current statistics
///
/// Write an encoded frame to the track /// Write an encoded frame to the track
/// ///
/// Handles codec-specific processing: /// Handles codec-specific processing:
@@ -464,7 +464,6 @@ impl UniversalVideoTrack {
if let Err(e) = rtp_track.write_rtp(&packet).await { if let Err(e) = rtp_track.write_rtp(&packet).await {
trace!("H265 write_rtp failed: {}", e); trace!("H265 write_rtp failed: {}", e);
} }
} }
Ok(()) Ok(())

View File

@@ -35,8 +35,8 @@ use tokio::sync::RwLock;
use tracing::{debug, info, trace, warn}; use tracing::{debug, info, trace, warn};
use crate::audio::{AudioController, OpusFrame}; use crate::audio::{AudioController, OpusFrame};
use crate::events::EventBus;
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
use crate::events::EventBus;
use crate::hid::HidController; use crate::hid::HidController;
use crate::video::encoder::registry::EncoderBackend; use crate::video::encoder::registry::EncoderBackend;
use crate::video::encoder::registry::VideoEncoderType; use crate::video::encoder::registry::VideoEncoderType;
@@ -270,7 +270,6 @@ impl WebRtcStreamer {
bitrate_preset: config.bitrate_preset, bitrate_preset: config.bitrate_preset,
fps: config.fps, fps: config.fps,
encoder_backend: config.encoder_backend, encoder_backend: config.encoder_backend,
..Default::default()
}; };
info!("Creating shared video pipeline for {:?}", codec); info!("Creating shared video pipeline for {:?}", codec);
@@ -311,7 +310,9 @@ impl WebRtcStreamer {
} }
drop(pipeline_guard); 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; break;
} }
@@ -926,10 +927,7 @@ impl WebRtcStreamer {
let pipeline = pipeline_for_callback.clone(); let pipeline = pipeline_for_callback.clone();
let sid = sid.clone(); let sid = sid.clone();
tokio::spawn(async move { tokio::spawn(async move {
info!( info!("Requesting keyframe for session {} after reconnect", sid);
"Requesting keyframe for session {} after reconnect",
sid
);
pipeline.request_keyframe().await; pipeline.request_keyframe().await;
}); });
}); });