mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-15 07:26:44 +08:00
fix: 补齐 ATX 控制器缺失接口并完成全项目 clippy -D warnings 修复
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
@@ -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::{
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
warn!("Failed to shutdown LED sensor: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inner.led_sensor = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new ATX controller with the specified configuration
|
||||||
|
pub fn new(config: AtxControllerConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: RwLock::new(AtxInner {
|
||||||
|
config,
|
||||||
|
power_executor: None,
|
||||||
|
reset_executor: None,
|
||||||
|
led_sensor: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a disabled ATX controller
|
||||||
|
pub fn disabled() -> Self {
|
||||||
|
Self::new(AtxControllerConfig::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the ATX controller and its executors
|
||||||
|
pub async fn init(&self) -> Result<()> {
|
||||||
let mut inner = self.inner.write().await;
|
let mut inner = self.inner.write().await;
|
||||||
inner.config = new_config;
|
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -161,8 +161,7 @@ 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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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,8 +514,10 @@ 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),
|
||||||
|
tx.send(ev),
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
if send_result.is_ok() {
|
if send_result.is_ok() {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -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()))
|
||||||
)),
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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<()> {
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,15 +230,14 @@ 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
|
||||||
self.recreate_gadget().await?;
|
self.recreate_gadget().await?;
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)))?;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -571,13 +571,11 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
debug!("Background tasks already started, skipping");
|
debug!("Background tasks already started, skipping");
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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| {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,15 +263,17 @@ 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
|
||||||
|
&& has_key
|
||||||
|
&& state.extensions.check_available(ExtensionId::Gostc)
|
||||||
|
{
|
||||||
state
|
state
|
||||||
.extensions
|
.extensions
|
||||||
.start(ExtensionId::Gostc, &new_config.extensions)
|
.start(ExtensionId::Gostc, &new_config.extensions)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(new_config.extensions.gostc.clone()))
|
Ok(Json(new_config.extensions.gostc.clone()))
|
||||||
}
|
}
|
||||||
@@ -312,15 +314,17 @@ 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
|
||||||
|
&& has_name
|
||||||
|
&& state.extensions.check_available(ExtensionId::Easytier)
|
||||||
|
{
|
||||||
state
|
state
|
||||||
.extensions
|
.extensions
|
||||||
.start(ExtensionId::Easytier, &new_config.extensions)
|
.start(ExtensionId::Easytier, &new_config.extensions)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(new_config.extensions.easytier.clone()))
|
Ok(Json(new_config.extensions.easytier.clone()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,16 +1841,16 @@ 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() {
|
};
|
||||||
|
|
||||||
|
if frame.is_valid_jpeg() && tx.send(create_mjpeg_part(frame.data())).await.is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guard is automatically dropped here
|
// Guard is automatically dropped here
|
||||||
});
|
});
|
||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +363,7 @@ impl OpusAudioTrack {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
self.track
|
self.track.write_sample(&sample).await.map_err(|e| {
|
||||||
.write_sample(&sample)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("Failed to write Opus sample: {}", e);
|
error!("Failed to write Opus sample: {}", e);
|
||||||
AppError::WebRtcError(format!("Failed to write audio sample: {}", e))
|
AppError::WebRtcError(format!("Failed to write audio sample: {}", e))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user