feat(video): 事务化切换与前端统一编排,增强视频输入格式支持

- 后端:切换事务+transition_id,/stream/mode 返回 switching/transition_id 与实际 codec

- 事件:新增 mode_switching/mode_ready,config/webrtc_ready/mode_changed 关联事务

- 编码/格式:扩展 NV21/NV16/NV24/RGB/BGR 输入与转换链路,RKMPP direct input 优化

- 前端:useVideoSession 统一切换,失败回退真实切回 MJPEG,菜单格式同步修复

- 清理:useVideoStream 降级为 MJPEG-only
This commit is contained in:
mofeng-git
2026-01-11 10:41:57 +08:00
parent 9feb74b72c
commit 206594e292
110 changed files with 3955 additions and 2251 deletions

View File

@@ -56,7 +56,10 @@ fn build_common(builder: &mut Build) {
// Unsupported platforms // Unsupported platforms
if target_os != "windows" && target_os != "linux" { if target_os != "windows" && target_os != "linux" {
panic!("Unsupported OS: {}. Only Windows and Linux are supported.", target_os); panic!(
"Unsupported OS: {}. Only Windows and Linux are supported.",
target_os
);
} }
// tool // tool
@@ -103,7 +106,9 @@ mod ffmpeg {
use std::process::Command; use std::process::Command;
// Check if static linking is requested // Check if static linking is requested
let use_static = std::env::var("FFMPEG_STATIC").map(|v| v == "1").unwrap_or(false); let use_static = std::env::var("FFMPEG_STATIC")
.map(|v| v == "1")
.unwrap_or(false);
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
// Try custom library path first: // Try custom library path first:
@@ -172,10 +177,7 @@ mod ffmpeg {
for lib in &libs { for lib in &libs {
// Get cflags // Get cflags
if let Ok(output) = Command::new("pkg-config") if let Ok(output) = Command::new("pkg-config").args(["--cflags", lib]).output() {
.args(["--cflags", lib])
.output()
{
if output.status.success() { if output.status.success() {
let cflags = String::from_utf8_lossy(&output.stdout); let cflags = String::from_utf8_lossy(&output.stdout);
for flag in cflags.split_whitespace() { for flag in cflags.split_whitespace() {
@@ -193,10 +195,7 @@ mod ffmpeg {
vec!["--libs", lib] vec!["--libs", lib]
}; };
if let Ok(output) = Command::new("pkg-config") if let Ok(output) = Command::new("pkg-config").args(&pkg_config_args).output() {
.args(&pkg_config_args)
.output()
{
if output.status.success() { if output.status.success() {
let libs_str = String::from_utf8_lossy(&output.stdout); let libs_str = String::from_utf8_lossy(&output.stdout);
for flag in libs_str.split_whitespace() { for flag in libs_str.split_whitespace() {
@@ -221,7 +220,9 @@ mod ffmpeg {
panic!("pkg-config failed for {}. Install FFmpeg development libraries: sudo apt install libavcodec-dev libavutil-dev", lib); panic!("pkg-config failed for {}. Install FFmpeg development libraries: sudo apt install libavcodec-dev libavutil-dev", lib);
} }
} else { } else {
panic!("pkg-config not found. Install pkg-config and FFmpeg development libraries."); panic!(
"pkg-config not found. Install pkg-config and FFmpeg development libraries."
);
} }
} }
@@ -301,7 +302,10 @@ mod ffmpeg {
// ARM (aarch64, arm): no X11 needed, uses RKMPP/V4L2 // ARM (aarch64, arm): no X11 needed, uses RKMPP/V4L2
v v
} else { } else {
panic!("Unsupported OS: {}. Only Windows and Linux are supported.", target_os); panic!(
"Unsupported OS: {}. Only Windows and Linux are supported.",
target_os
);
}; };
for lib in dyn_libs.iter() { for lib in dyn_libs.iter() {
@@ -312,10 +316,9 @@ mod ffmpeg {
fn ffmpeg_ffi() { fn ffmpeg_ffi() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let ffmpeg_ram_dir = manifest_dir.join("cpp").join("common"); let ffmpeg_ram_dir = manifest_dir.join("cpp").join("common");
let ffi_header = ffmpeg_ram_dir let ffi_header_path = ffmpeg_ram_dir.join("ffmpeg_ffi.h");
.join("ffmpeg_ffi.h") println!("cargo:rerun-if-changed={}", ffi_header_path.display());
.to_string_lossy() let ffi_header = ffi_header_path.to_string_lossy().to_string();
.to_string();
bindgen::builder() bindgen::builder()
.header(ffi_header) .header(ffi_header)
.rustified_enum("*") .rustified_enum("*")
@@ -340,8 +343,6 @@ mod ffmpeg {
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_ram_ffi.rs")) .write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_ram_ffi.rs"))
.unwrap(); .unwrap();
builder.files( builder.files(["ffmpeg_ram_encode.cpp"].map(|f| ffmpeg_ram_dir.join(f)));
["ffmpeg_ram_encode.cpp"].map(|f| ffmpeg_ram_dir.join(f)),
);
} }
} }

View File

@@ -14,11 +14,15 @@
enum AVPixelFormat { enum AVPixelFormat {
AV_PIX_FMT_YUV420P = 0, AV_PIX_FMT_YUV420P = 0,
AV_PIX_FMT_YUYV422 = 1, AV_PIX_FMT_YUYV422 = 1,
AV_PIX_FMT_RGB24 = 2,
AV_PIX_FMT_BGR24 = 3,
AV_PIX_FMT_YUV422P = 4, // planar YUV 4:2:2 AV_PIX_FMT_YUV422P = 4, // planar YUV 4:2:2
AV_PIX_FMT_YUVJ420P = 12, // JPEG full-range YUV420P (same layout as YUV420P) AV_PIX_FMT_YUVJ420P = 12, // JPEG full-range YUV420P (same layout as YUV420P)
AV_PIX_FMT_YUVJ422P = 13, // JPEG full-range YUV422P (same layout as YUV422P) AV_PIX_FMT_YUVJ422P = 13, // JPEG full-range YUV422P (same layout as YUV422P)
AV_PIX_FMT_NV12 = 23, AV_PIX_FMT_NV12 = 23,
AV_PIX_FMT_NV21 = 24, AV_PIX_FMT_NV21 = 24,
AV_PIX_FMT_NV16 = 101,
AV_PIX_FMT_NV24 = 188,
}; };
int av_log_get_level(void); int av_log_get_level(void);

View File

@@ -388,7 +388,9 @@ private:
} }
_exit: _exit:
av_packet_unref(pkt_); av_packet_unref(pkt_);
return encoded ? 0 : -1; // If no packet is produced for this input frame, treat it as EAGAIN.
// This is not a fatal error: encoders may buffer internally (e.g., startup delay).
return encoded ? 0 : AVERROR(EAGAIN);
} }
int fill_frame(AVFrame *frame, uint8_t *data, int data_length, int fill_frame(AVFrame *frame, uint8_t *data, int data_length,

View File

@@ -3,7 +3,8 @@ use crate::{
ffmpeg::{init_av_log, AVPixelFormat}, ffmpeg::{init_av_log, AVPixelFormat},
ffmpeg_ram::{ ffmpeg_ram::{
ffmpeg_linesize_offset_length, ffmpeg_ram_encode, ffmpeg_ram_free_encoder, ffmpeg_linesize_offset_length, ffmpeg_ram_encode, ffmpeg_ram_free_encoder,
ffmpeg_ram_new_encoder, ffmpeg_ram_request_keyframe, ffmpeg_ram_set_bitrate, CodecInfo, AV_NUM_DATA_POINTERS, ffmpeg_ram_new_encoder, ffmpeg_ram_request_keyframe, ffmpeg_ram_set_bitrate, CodecInfo,
AV_NUM_DATA_POINTERS,
}, },
}; };
use log::trace; use log::trace;
@@ -123,6 +124,12 @@ impl Encoder {
self.frames as *const _ as *const c_void, self.frames as *const _ as *const c_void,
ms, ms,
); );
// ffmpeg_ram_encode returns AVERROR(EAGAIN) when the encoder accepts the frame
// but does not output a packet yet (e.g., startup delay / internal buffering).
// Treat this as a successful call with an empty output list.
if result == -11 {
return Ok(&mut *self.frames);
}
if result != 0 { if result != 0 {
return Err(result); return Err(result);
} }
@@ -358,7 +365,8 @@ impl Encoder {
if frames[0].key == 1 && elapsed < TEST_TIMEOUT_MS as _ { if frames[0].key == 1 && elapsed < TEST_TIMEOUT_MS as _ {
debug!( debug!(
"Encoder {} test passed on attempt {}", "Encoder {} test passed on attempt {}",
codec.name, attempt + 1 codec.name,
attempt + 1
); );
res.push(codec.clone()); res.push(codec.clone());
passed = true; passed = true;

View File

@@ -75,11 +75,7 @@ struct ExfatBootSector {
} }
impl ExfatBootSector { impl ExfatBootSector {
fn new( fn new(volume_length: u64, cluster_size: u32, volume_serial: u32) -> Self {
volume_length: u64,
cluster_size: u32,
volume_serial: u32,
) -> Self {
let sector_size: u32 = 512; let sector_size: u32 = 512;
let sectors_per_cluster = cluster_size / sector_size; let sectors_per_cluster = cluster_size / sector_size;
let spc_shift = sectors_per_cluster_shift(cluster_size); let spc_shift = sectors_per_cluster_shift(cluster_size);
@@ -106,7 +102,8 @@ impl ExfatBootSector {
// Cluster 3...: Upcase table (128KB, may span multiple clusters) // Cluster 3...: Upcase table (128KB, may span multiple clusters)
// Next available: Root directory // Next available: Root directory
const UPCASE_TABLE_SIZE: u64 = 128 * 1024; const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
let upcase_clusters = ((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32; let upcase_clusters =
((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
let first_cluster_of_root = 3 + upcase_clusters; let first_cluster_of_root = 3 + upcase_clusters;
Self { Self {
@@ -306,7 +303,8 @@ pub fn format_exfat<W: Write + Seek>(
// Calculate how many clusters the upcase table needs (128KB) // Calculate how many clusters the upcase table needs (128KB)
const UPCASE_TABLE_SIZE: u64 = 128 * 1024; const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
let upcase_clusters = ((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32; let upcase_clusters =
((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
let root_cluster = 3 + upcase_clusters; // Root comes after bitmap and upcase let root_cluster = 3 + upcase_clusters; // Root comes after bitmap and upcase
// FAT entries: cluster 0 and 1 are reserved // FAT entries: cluster 0 and 1 are reserved
@@ -349,7 +347,8 @@ pub fn format_exfat<W: Write + Seek>(
// Cluster 2: Allocation Bitmap // Cluster 2: Allocation Bitmap
let bitmap_size = (boot_sector.cluster_count + 7) / 8; let bitmap_size = (boot_sector.cluster_count + 7) / 8;
let _bitmap_clusters = ((bitmap_size as u64 + cluster_size as u64 - 1) / cluster_size as u64).max(1); let _bitmap_clusters =
((bitmap_size as u64 + cluster_size as u64 - 1) / cluster_size as u64).max(1);
let mut bitmap = vec![0u8; cluster_size as usize]; let mut bitmap = vec![0u8; cluster_size as usize];
// Mark clusters 2, 3..3+upcase_clusters-1, root_cluster as used // Mark clusters 2, 3..3+upcase_clusters-1, root_cluster as used

View File

@@ -53,8 +53,7 @@ impl FatCache {
if self.entries.is_empty() { if self.entries.is_empty() {
return false; return false;
} }
cluster >= self.start_cluster cluster >= self.start_cluster && cluster < self.start_cluster + self.entries.len() as u32
&& cluster < self.start_cluster + self.entries.len() as u32
} }
/// Get a FAT entry from cache (if present) /// Get a FAT entry from cache (if present)
@@ -243,7 +242,9 @@ impl ExfatFs {
/// Get the byte offset of a FAT entry /// Get the byte offset of a FAT entry
fn fat_entry_offset(&self, cluster: u32) -> u64 { fn fat_entry_offset(&self, cluster: u32) -> u64 {
self.partition_offset + self.fat_offset as u64 * self.bytes_per_sector as u64 + cluster as u64 * 4 self.partition_offset
+ self.fat_offset as u64 * self.bytes_per_sector as u64
+ cluster as u64 * 4
} }
/// Load a FAT segment into cache starting from the given cluster /// Load a FAT segment into cache starting from the given cluster
@@ -287,7 +288,10 @@ impl ExfatFs {
// Should be in cache now // Should be in cache now
self.fat_cache.get(cluster).ok_or_else(|| { self.fat_cache.get(cluster).ok_or_else(|| {
VentoyError::FilesystemError(format!("Failed to cache FAT entry for cluster {}", cluster)) VentoyError::FilesystemError(format!(
"Failed to cache FAT entry for cluster {}",
cluster
))
}) })
} }
@@ -490,9 +494,9 @@ impl ExfatFs {
fn extend_cluster_chain(&mut self, first_cluster: u32) -> Result<u32> { fn extend_cluster_chain(&mut self, first_cluster: u32) -> Result<u32> {
// Find the last cluster in the chain // Find the last cluster in the chain
let chain = self.read_cluster_chain(first_cluster)?; let chain = self.read_cluster_chain(first_cluster)?;
let last_cluster = *chain.last().ok_or_else(|| { let last_cluster = *chain
VentoyError::FilesystemError("Empty cluster chain".to_string()) .last()
})?; .ok_or_else(|| VentoyError::FilesystemError("Empty cluster chain".to_string()))?;
// Allocate one new cluster // Allocate one new cluster
let new_cluster = self.allocate_clusters(1)?; let new_cluster = self.allocate_clusters(1)?;
@@ -532,7 +536,12 @@ impl ExfatFs {
} }
/// Create file directory entries for a new file /// Create file directory entries for a new file
fn create_file_entries(name: &str, first_cluster: u32, size: u64, is_dir: bool) -> Vec<[u8; 32]> { fn create_file_entries(
name: &str,
first_cluster: u32,
size: u64,
is_dir: bool,
) -> Vec<[u8; 32]> {
let name_utf16: Vec<u16> = name.encode_utf16().collect(); let name_utf16: Vec<u16> = name.encode_utf16().collect();
let name_entries_needed = (name_utf16.len() + 14) / 15; // 15 chars per name entry let name_entries_needed = (name_utf16.len() + 14) / 15; // 15 chars per name entry
let secondary_count = 1 + name_entries_needed; // Stream + Name entries let secondary_count = 1 + name_entries_needed; // Stream + Name entries
@@ -552,11 +561,15 @@ impl ExfatFs {
.map(|d| d.as_secs() as u32) .map(|d| d.as_secs() as u32)
.unwrap_or(0); .unwrap_or(0);
// DOS timestamp format (simplified) // DOS timestamp format (simplified)
let dos_time = ((now / 2) & 0x1F) | (((now / 60) & 0x3F) << 5) | (((now / 3600) & 0x1F) << 11); let dos_time =
((now / 2) & 0x1F) | (((now / 60) & 0x3F) << 5) | (((now / 3600) & 0x1F) << 11);
let dos_date = 1 | (1 << 5) | ((45) << 9); // Jan 1, 2025 let dos_date = 1 | (1 << 5) | ((45) << 9); // Jan 1, 2025
file_entry[8..12].copy_from_slice(&(dos_date as u32 | ((dos_time as u32) << 16)).to_le_bytes()); file_entry[8..12]
file_entry[12..16].copy_from_slice(&(dos_date as u32 | ((dos_time as u32) << 16)).to_le_bytes()); .copy_from_slice(&(dos_date as u32 | ((dos_time as u32) << 16)).to_le_bytes());
file_entry[16..20].copy_from_slice(&(dos_date as u32 | ((dos_time as u32) << 16)).to_le_bytes()); file_entry[12..16]
.copy_from_slice(&(dos_date as u32 | ((dos_time as u32) << 16)).to_le_bytes());
file_entry[16..20]
.copy_from_slice(&(dos_date as u32 | ((dos_time as u32) << 16)).to_le_bytes());
entries.push(file_entry); entries.push(file_entry);
// 2. Stream Extension Entry (0xC0) // 2. Stream Extension Entry (0xC0)
@@ -588,7 +601,8 @@ impl ExfatFs {
for i in 0..15 { for i in 0..15 {
if char_index < name_utf16.len() { if char_index < name_utf16.len() {
let offset = 2 + i * 2; let offset = 2 + i * 2;
name_entry[offset..offset + 2].copy_from_slice(&name_utf16[char_index].to_le_bytes()); name_entry[offset..offset + 2]
.copy_from_slice(&name_utf16[char_index].to_le_bytes());
char_index += 1; char_index += 1;
} }
} }
@@ -603,7 +617,11 @@ impl ExfatFs {
} }
/// Find a file entry in a specific directory cluster /// Find a file entry in a specific directory cluster
fn find_entry_in_directory(&mut self, dir_cluster: u32, name: &str) -> Result<Option<FileEntryLocation>> { fn find_entry_in_directory(
&mut self,
dir_cluster: u32,
name: &str,
) -> Result<Option<FileEntryLocation>> {
let target_name_lower = name.to_lowercase(); let target_name_lower = name.to_lowercase();
// Read all clusters in the directory chain // Read all clusters in the directory chain
@@ -747,7 +765,11 @@ impl ExfatFs {
/// ///
/// If no free slot is found in existing clusters, this method will /// If no free slot is found in existing clusters, this method will
/// automatically extend the directory by allocating a new cluster. /// automatically extend the directory by allocating a new cluster.
fn find_free_slot_in_directory(&mut self, dir_cluster: u32, entries_needed: usize) -> Result<(u32, u32)> { fn find_free_slot_in_directory(
&mut self,
dir_cluster: u32,
entries_needed: usize,
) -> Result<(u32, u32)> {
let dir_clusters = self.read_cluster_chain(dir_cluster)?; let dir_clusters = self.read_cluster_chain(dir_cluster)?;
for &cluster in &dir_clusters { for &cluster in &dir_clusters {
@@ -759,10 +781,12 @@ impl ExfatFs {
while i < cluster_data.len() { while i < cluster_data.len() {
let entry_type = cluster_data[i]; let entry_type = cluster_data[i];
if entry_type == ENTRY_TYPE_END || entry_type == 0x00 if entry_type == ENTRY_TYPE_END
|| entry_type == 0x00
|| entry_type == ENTRY_TYPE_DELETED_FILE || entry_type == ENTRY_TYPE_DELETED_FILE
|| entry_type == ENTRY_TYPE_DELETED_STREAM || entry_type == ENTRY_TYPE_DELETED_STREAM
|| entry_type == ENTRY_TYPE_DELETED_NAME { || entry_type == ENTRY_TYPE_DELETED_NAME
{
if consecutive_free == 0 { if consecutive_free == 0 {
slot_start = i; slot_start = i;
} }
@@ -795,7 +819,8 @@ impl ExfatFs {
// This is critical: when we extend a directory, we need to clear any END markers // This is critical: when we extend a directory, we need to clear any END markers
// that may exist in previous clusters, otherwise list_files will stop prematurely // that may exist in previous clusters, otherwise list_files will stop prematurely
let dir_clusters_before = self.read_cluster_chain(dir_cluster)?; let dir_clusters_before = self.read_cluster_chain(dir_cluster)?;
for &cluster in &dir_clusters_before[..dir_clusters_before.len()-1] { // Exclude the newly added cluster for &cluster in &dir_clusters_before[..dir_clusters_before.len() - 1] {
// Exclude the newly added cluster
let mut cluster_data = self.read_cluster(cluster)?; let mut cluster_data = self.read_cluster(cluster)?;
// Scan for END markers and replace them with 0xFF (invalid entry, will be skipped) // Scan for END markers and replace them with 0xFF (invalid entry, will be skipped)
@@ -815,14 +840,23 @@ impl ExfatFs {
/// Find a free slot in the root directory for new entries (backward compatible) /// Find a free slot in the root directory for new entries (backward compatible)
#[allow(dead_code)] #[allow(dead_code)]
fn find_free_directory_slot(&mut self, entries_needed: usize) -> Result<u32> { fn find_free_directory_slot(&mut self, entries_needed: usize) -> Result<u32> {
let (_, offset) = self.find_free_slot_in_directory(self.first_cluster_of_root, entries_needed)?; let (_, offset) =
self.find_free_slot_in_directory(self.first_cluster_of_root, entries_needed)?;
Ok(offset) Ok(offset)
} }
/// Create an entry in a specific directory /// Create an entry in a specific directory
fn create_entry_in_directory(&mut self, dir_cluster: u32, name: &str, first_cluster: u32, size: u64, is_dir: bool) -> Result<()> { fn create_entry_in_directory(
&mut self,
dir_cluster: u32,
name: &str,
first_cluster: u32,
size: u64,
is_dir: bool,
) -> Result<()> {
let entries = Self::create_file_entries(name, first_cluster, size, is_dir); let entries = Self::create_file_entries(name, first_cluster, size, is_dir);
let (slot_cluster, slot_offset) = self.find_free_slot_in_directory(dir_cluster, entries.len())?; let (slot_cluster, slot_offset) =
self.find_free_slot_in_directory(dir_cluster, entries.len())?;
let mut cluster_data = self.read_cluster(slot_cluster)?; let mut cluster_data = self.read_cluster(slot_cluster)?;
@@ -854,7 +888,10 @@ impl ExfatFs {
} }
// Check if already exists // Check if already exists
if self.find_entry_in_directory(parent_cluster, name)?.is_some() { if self
.find_entry_in_directory(parent_cluster, name)?
.is_some()
{
return Err(VentoyError::FilesystemError(format!( return Err(VentoyError::FilesystemError(format!(
"Entry '{}' already exists", "Entry '{}' already exists",
name name
@@ -903,7 +940,11 @@ impl ExfatFs {
// ==================== Public File Operations ==================== // ==================== Public File Operations ====================
/// List files in a specific directory cluster /// List files in a specific directory cluster
fn list_files_in_directory(&mut self, dir_cluster: u32, current_path: &str) -> Result<Vec<FileInfo>> { fn list_files_in_directory(
&mut self,
dir_cluster: u32,
current_path: &str,
) -> Result<Vec<FileInfo>> {
let dir_clusters = self.read_cluster_chain(dir_cluster)?; let dir_clusters = self.read_cluster_chain(dir_cluster)?;
// Pre-allocate Vec based on estimated entries // Pre-allocate Vec based on estimated entries
@@ -1038,7 +1079,12 @@ impl ExfatFs {
} }
/// Write file data to allocated clusters and create directory entry /// Write file data to allocated clusters and create directory entry
fn write_file_data_and_entry(&mut self, dir_cluster: u32, name: &str, data: &[u8]) -> Result<()> { fn write_file_data_and_entry(
&mut self,
dir_cluster: u32,
name: &str,
data: &[u8],
) -> Result<()> {
// Calculate clusters needed // Calculate clusters needed
let clusters_needed = if data.is_empty() { let clusters_needed = if data.is_empty() {
0 0
@@ -1121,7 +1167,13 @@ impl ExfatFs {
/// Path can include directories, e.g., "iso/linux/ubuntu.iso" /// Path can include directories, e.g., "iso/linux/ubuntu.iso"
/// If create_parents is true, intermediate directories will be created. /// If create_parents is true, intermediate directories will be created.
/// If overwrite is true, existing files will be replaced. /// If overwrite is true, existing files will be replaced.
pub fn write_file_path(&mut self, path: &str, data: &[u8], create_parents: bool, overwrite: bool) -> Result<()> { pub fn write_file_path(
&mut self,
path: &str,
data: &[u8],
create_parents: bool,
overwrite: bool,
) -> Result<()> {
let resolved = self.resolve_path(path, create_parents)?; let resolved = self.resolve_path(path, create_parents)?;
// Validate filename // Validate filename
@@ -1177,9 +1229,9 @@ impl ExfatFs {
/// Read a file from the filesystem (root directory) /// Read a file from the filesystem (root directory)
pub fn read_file(&mut self, name: &str) -> Result<Vec<u8>> { pub fn read_file(&mut self, name: &str) -> Result<Vec<u8>> {
let location = self.find_file_entry(name)?.ok_or_else(|| { let location = self
VentoyError::FilesystemError(format!("File '{}' not found", name)) .find_file_entry(name)?
})?; .ok_or_else(|| VentoyError::FilesystemError(format!("File '{}' not found", name)))?;
self.read_file_from_location(&location) self.read_file_from_location(&location)
} }
@@ -1224,9 +1276,9 @@ impl ExfatFs {
/// Delete a file from the filesystem (root directory) /// Delete a file from the filesystem (root directory)
pub fn delete_file(&mut self, name: &str) -> Result<()> { pub fn delete_file(&mut self, name: &str) -> Result<()> {
let location = self.find_file_entry(name)?.ok_or_else(|| { let location = self
VentoyError::FilesystemError(format!("File '{}' not found", name)) .find_file_entry(name)?
})?; .ok_or_else(|| VentoyError::FilesystemError(format!("File '{}' not found", name)))?;
// Free cluster chain // Free cluster chain
if location.first_cluster >= 2 { if location.first_cluster >= 2 {
@@ -1244,9 +1296,9 @@ impl ExfatFs {
pub fn delete_path(&mut self, path: &str) -> Result<()> { pub fn delete_path(&mut self, path: &str) -> Result<()> {
let resolved = self.resolve_path(path, false)?; let resolved = self.resolve_path(path, false)?;
let location = resolved.location.ok_or_else(|| { let location = resolved
VentoyError::FilesystemError(format!("'{}' not found", path)) .location
})?; .ok_or_else(|| VentoyError::FilesystemError(format!("'{}' not found", path)))?;
// If it's a directory, check if it's empty // If it's a directory, check if it's empty
if location.is_directory { if location.is_directory {
@@ -1275,9 +1327,9 @@ impl ExfatFs {
pub fn delete_recursive(&mut self, path: &str) -> Result<()> { pub fn delete_recursive(&mut self, path: &str) -> Result<()> {
let resolved = self.resolve_path(path, false)?; let resolved = self.resolve_path(path, false)?;
let location = resolved.location.ok_or_else(|| { let location = resolved
VentoyError::FilesystemError(format!("'{}' not found", path)) .location
})?; .ok_or_else(|| VentoyError::FilesystemError(format!("'{}' not found", path)))?;
if location.is_directory { if location.is_directory {
// Get all contents and delete them first // Get all contents and delete them first
@@ -1344,7 +1396,12 @@ impl<'a> ExfatFileWriter<'a> {
} }
/// Create a new file writer with overwrite option /// Create a new file writer with overwrite option
pub fn create_overwrite(fs: &'a mut ExfatFs, name: &str, total_size: u64, overwrite: bool) -> Result<Self> { pub fn create_overwrite(
fs: &'a mut ExfatFs,
name: &str,
total_size: u64,
overwrite: bool,
) -> Result<Self> {
let root_cluster = fs.first_cluster_of_root; let root_cluster = fs.first_cluster_of_root;
Self::create_in_directory(fs, root_cluster, name, total_size, overwrite) Self::create_in_directory(fs, root_cluster, name, total_size, overwrite)
} }
@@ -1353,7 +1410,13 @@ impl<'a> ExfatFileWriter<'a> {
/// ///
/// If create_parents is true, intermediate directories will be created. /// If create_parents is true, intermediate directories will be created.
/// If overwrite is true, existing files will be replaced. /// If overwrite is true, existing files will be replaced.
pub fn create_at_path(fs: &'a mut ExfatFs, path: &str, total_size: u64, create_parents: bool, overwrite: bool) -> Result<Self> { pub fn create_at_path(
fs: &'a mut ExfatFs,
path: &str,
total_size: u64,
create_parents: bool,
overwrite: bool,
) -> Result<Self> {
let resolved = fs.resolve_path(path, create_parents)?; let resolved = fs.resolve_path(path, create_parents)?;
// Handle existing file // Handle existing file
@@ -1378,11 +1441,23 @@ impl<'a> ExfatFileWriter<'a> {
} }
} }
Self::create_in_directory(fs, resolved.parent_cluster, &resolved.name, total_size, false) Self::create_in_directory(
fs,
resolved.parent_cluster,
&resolved.name,
total_size,
false,
)
} }
/// Internal: Create a file writer in a specific directory /// Internal: Create a file writer in a specific directory
fn create_in_directory(fs: &'a mut ExfatFs, dir_cluster: u32, name: &str, total_size: u64, overwrite: bool) -> Result<Self> { fn create_in_directory(
fs: &'a mut ExfatFs,
dir_cluster: u32,
name: &str,
total_size: u64,
overwrite: bool,
) -> Result<Self> {
// Validate filename // Validate filename
if name.is_empty() || name.len() > 255 { if name.is_empty() || name.len() > 255 {
return Err(VentoyError::FilesystemError( return Err(VentoyError::FilesystemError(
@@ -1473,7 +1548,9 @@ impl<'a> ExfatFileWriter<'a> {
/// This must be called after all data has been written. /// This must be called after all data has been written.
pub fn finish(self) -> Result<()> { pub fn finish(self) -> Result<()> {
// Write any remaining data in buffer // Write any remaining data in buffer
if !self.cluster_buffer.is_empty() && self.current_cluster_index < self.allocated_clusters.len() { if !self.cluster_buffer.is_empty()
&& self.current_cluster_index < self.allocated_clusters.len()
{
let cluster = self.allocated_clusters[self.current_cluster_index]; let cluster = self.allocated_clusters[self.current_cluster_index];
self.fs.write_cluster(cluster, &self.cluster_buffer)?; self.fs.write_cluster(cluster, &self.cluster_buffer)?;
} }
@@ -1485,7 +1562,13 @@ impl<'a> ExfatFileWriter<'a> {
self.allocated_clusters[0] self.allocated_clusters[0]
}; };
self.fs.create_entry_in_directory(self.dir_cluster, &self.name, first_cluster, self.total_size, false)?; self.fs.create_entry_in_directory(
self.dir_cluster,
&self.name,
first_cluster,
self.total_size,
false,
)?;
self.fs.file.flush()?; self.fs.file.flush()?;
Ok(()) Ok(())
@@ -1517,9 +1600,9 @@ pub struct ExfatFileReader<'a> {
impl<'a> ExfatFileReader<'a> { impl<'a> ExfatFileReader<'a> {
/// Open a file for reading from root directory /// Open a file for reading from root directory
pub fn open(fs: &'a mut ExfatFs, name: &str) -> Result<Self> { pub fn open(fs: &'a mut ExfatFs, name: &str) -> Result<Self> {
let location = fs.find_file_entry(name)?.ok_or_else(|| { let location = fs
VentoyError::FilesystemError(format!("File '{}' not found", name)) .find_file_entry(name)?
})?; .ok_or_else(|| VentoyError::FilesystemError(format!("File '{}' not found", name)))?;
if location.is_directory { if location.is_directory {
return Err(VentoyError::FilesystemError(format!( return Err(VentoyError::FilesystemError(format!(
@@ -1635,7 +1718,8 @@ impl<'a> Read for ExfatFileReader<'a> {
// Read the cluster and copy data // Read the cluster and copy data
{ {
let cluster_data = self.read_cluster_cached(cluster_index) let cluster_data = self
.read_cluster_cached(cluster_index)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
// Copy data to buffer // Copy data to buffer
@@ -1676,11 +1760,7 @@ impl ExfatFs {
/// Read a file to a writer (streaming) /// Read a file to a writer (streaming)
/// ///
/// This is useful for reading large files without loading them into memory. /// This is useful for reading large files without loading them into memory.
pub fn read_file_to_writer<W: Write>( pub fn read_file_to_writer<W: Write>(&mut self, name: &str, writer: &mut W) -> Result<u64> {
&mut self,
name: &str,
writer: &mut W,
) -> Result<u64> {
let mut reader = ExfatFileReader::open(self, name)?; let mut reader = ExfatFileReader::open(self, name)?;
Self::do_stream_read(&mut reader, writer) Self::do_stream_read(&mut reader, writer)
} }
@@ -1701,13 +1781,13 @@ impl ExfatFs {
let mut total_bytes = 0u64; let mut total_bytes = 0u64;
loop { loop {
let bytes_read = reader.read(&mut buffer).map_err(|e| { let bytes_read = reader.read(&mut buffer).map_err(|e| VentoyError::Io(e))?;
VentoyError::Io(e)
})?;
if bytes_read == 0 { if bytes_read == 0 {
break; break;
} }
writer.write_all(&buffer[..bytes_read]).map_err(VentoyError::Io)?; writer
.write_all(&buffer[..bytes_read])
.map_err(VentoyError::Io)?;
total_bytes += bytes_read as u64; total_bytes += bytes_read as u64;
} }
@@ -1755,7 +1835,8 @@ impl ExfatFs {
create_parents: bool, create_parents: bool,
overwrite: bool, overwrite: bool,
) -> Result<()> { ) -> Result<()> {
let mut writer = ExfatFileWriter::create_at_path(self, path, size, create_parents, overwrite)?; let mut writer =
ExfatFileWriter::create_at_path(self, path, size, create_parents, overwrite)?;
Self::do_stream_write(&mut writer, reader)?; Self::do_stream_write(&mut writer, reader)?;
writer.finish() writer.finish()
} }
@@ -1804,7 +1885,12 @@ mod tests {
file.set_len(size).unwrap(); file.set_len(size).unwrap();
// Format data partition (this will use 4KB clusters for 64MB volume) // Format data partition (this will use 4KB clusters for 64MB volume)
crate::exfat::format::format_exfat(&mut file, layout.data_offset(), layout.data_size(), "TEST") crate::exfat::format::format_exfat(
&mut file,
layout.data_offset(),
layout.data_size(),
"TEST",
)
.unwrap(); .unwrap();
drop(file); drop(file);
@@ -1826,21 +1912,12 @@ mod tests {
let data = format!("content {}", i); let data = format!("content {}", i);
let mut cursor = Cursor::new(data.as_bytes()); let mut cursor = Cursor::new(data.as_bytes());
fs.write_file_from_reader( fs.write_file_from_reader(&filename, &mut cursor, data.len() as u64)?;
&filename,
&mut cursor,
data.len() as u64,
)?;
} }
// Verify all files were created // Verify all files were created
let files = fs.list_files().unwrap(); let files = fs.list_files().unwrap();
assert_eq!( assert_eq!(files.len(), 50, "Expected 50 files, found {}", files.len());
files.len(),
50,
"Expected 50 files, found {}",
files.len()
);
// Verify we can read all files back // Verify we can read all files back
for i in 0..50 { for i in 0..50 {
@@ -1882,7 +1959,12 @@ mod tests {
file.set_len(size).unwrap(); file.set_len(size).unwrap();
// Format data partition // Format data partition
crate::exfat::format::format_exfat(&mut file, layout.data_offset(), layout.data_size(), "TEST") crate::exfat::format::format_exfat(
&mut file,
layout.data_offset(),
layout.data_size(),
"TEST",
)
.unwrap(); .unwrap();
drop(file); drop(file);
@@ -1903,7 +1985,8 @@ mod tests {
assert_eq!(reader.position(), 0); assert_eq!(reader.position(), 0);
let mut read_data = Vec::new(); let mut read_data = Vec::new();
let bytes_read = reader.read_to_end(&mut read_data) let bytes_read = reader
.read_to_end(&mut read_data)
.map_err(|e| VentoyError::Io(e))?; .map_err(|e| VentoyError::Io(e))?;
assert_eq!(bytes_read, test_data.len()); assert_eq!(bytes_read, test_data.len());
@@ -1932,22 +2015,32 @@ mod tests {
let mut reader = ExfatFileReader::open(&mut fs, "large_file.bin")?; let mut reader = ExfatFileReader::open(&mut fs, "large_file.bin")?;
// Seek to middle // Seek to middle
reader.seek(SeekFrom::Start(10000)).map_err(|e| VentoyError::Io(e))?; reader
.seek(SeekFrom::Start(10000))
.map_err(|e| VentoyError::Io(e))?;
assert_eq!(reader.position(), 10000); assert_eq!(reader.position(), 10000);
let mut buffer = [0u8; 10]; let mut buffer = [0u8; 10];
reader.read_exact(&mut buffer).map_err(|e| VentoyError::Io(e))?; reader
.read_exact(&mut buffer)
.map_err(|e| VentoyError::Io(e))?;
assert_eq!(&buffer, &test_data[10000..10010]); assert_eq!(&buffer, &test_data[10000..10010]);
// Seek from current position // Seek from current position
reader.seek(SeekFrom::Current(-5)).map_err(|e| VentoyError::Io(e))?; reader
.seek(SeekFrom::Current(-5))
.map_err(|e| VentoyError::Io(e))?;
assert_eq!(reader.position(), 10005); assert_eq!(reader.position(), 10005);
// Seek from end // Seek from end
reader.seek(SeekFrom::End(-100)).map_err(|e| VentoyError::Io(e))?; reader
.seek(SeekFrom::End(-100))
.map_err(|e| VentoyError::Io(e))?;
assert_eq!(reader.position(), test_data.len() as u64 - 100); assert_eq!(reader.position(), test_data.len() as u64 - 100);
reader.read_exact(&mut buffer).map_err(|e| VentoyError::Io(e))?; reader
.read_exact(&mut buffer)
.map_err(|e| VentoyError::Io(e))?;
let expected_start = test_data.len() - 100; let expected_start = test_data.len() - 100;
assert_eq!(&buffer, &test_data[expected_start..expected_start + 10]); assert_eq!(&buffer, &test_data[expected_start..expected_start + 10]);
} }
@@ -1981,7 +2074,12 @@ mod tests {
file.set_len(size).unwrap(); file.set_len(size).unwrap();
// Format data partition // Format data partition
crate::exfat::format::format_exfat(&mut file, layout.data_offset(), layout.data_size(), "TEST") crate::exfat::format::format_exfat(
&mut file,
layout.data_offset(),
layout.data_size(),
"TEST",
)
.unwrap(); .unwrap();
drop(file); drop(file);

View File

@@ -22,7 +22,11 @@ impl VentoyImage {
let size = parse_size(size_str)?; let size = parse_size(size_str)?;
let layout = PartitionLayout::calculate(size)?; let layout = PartitionLayout::calculate(size)?;
println!("[INFO] Creating {}MB image: {}", size / (1024 * 1024), path.display()); println!(
"[INFO] Creating {}MB image: {}",
size / (1024 * 1024),
path.display()
);
// Create sparse file // Create sparse file
let mut file = File::create(path)?; let mut file = File::create(path)?;
@@ -247,7 +251,11 @@ impl VentoyImage {
/// ///
/// This is the preferred method for large files as it doesn't load /// This is the preferred method for large files as it doesn't load
/// the entire file into memory. /// the entire file into memory.
pub fn read_file_to_writer<W: std::io::Write>(&self, path: &str, writer: &mut W) -> Result<u64> { pub fn read_file_to_writer<W: std::io::Write>(
&self,
path: &str,
writer: &mut W,
) -> Result<u64> {
let mut fs = ExfatFs::open(&self.path, &self.layout)?; let mut fs = ExfatFs::open(&self.path, &self.layout)?;
fs.read_file_path_to_writer(path, writer) fs.read_file_path_to_writer(path, writer)
} }

View File

@@ -45,4 +45,4 @@ pub use error::{Result, VentoyError};
pub use exfat::FileInfo; pub use exfat::FileInfo;
pub use image::VentoyImage; pub use image::VentoyImage;
pub use partition::{parse_size, PartitionLayout}; pub use partition::{parse_size, PartitionLayout};
pub use resources::{init_resources, get_resource_dir, is_initialized, required_files}; pub use resources::{get_resource_dir, init_resources, is_initialized, required_files};

View File

@@ -4,7 +4,7 @@ use clap::{Parser, Subcommand};
use std::path::PathBuf; use std::path::PathBuf;
use std::process::ExitCode; use std::process::ExitCode;
use ventoy_img::{VentoyImage, Result, VentoyError}; use ventoy_img::{Result, VentoyError, VentoyImage};
#[derive(Parser)] #[derive(Parser)]
#[command(name = "ventoy-img")] #[command(name = "ventoy-img")]
@@ -103,11 +103,33 @@ fn main() -> ExitCode {
let cli = Cli::parse(); let cli = Cli::parse();
let result = match cli.command { let result = match cli.command {
Commands::Create { size, output, label } => cmd_create(&output, &size, &label), Commands::Create {
Commands::Add { image, file, dest, force, parents } => cmd_add(&image, &file, dest.as_deref(), force, parents), size,
Commands::List { image, path, recursive } => cmd_list(&image, path.as_deref(), recursive), output,
Commands::Remove { image, path, recursive } => cmd_remove(&image, &path, recursive), label,
Commands::Mkdir { image, path, parents } => cmd_mkdir(&image, &path, parents), } => cmd_create(&output, &size, &label),
Commands::Add {
image,
file,
dest,
force,
parents,
} => cmd_add(&image, &file, dest.as_deref(), force, parents),
Commands::List {
image,
path,
recursive,
} => cmd_list(&image, path.as_deref(), recursive),
Commands::Remove {
image,
path,
recursive,
} => cmd_remove(&image, &path, recursive),
Commands::Mkdir {
image,
path,
parents,
} => cmd_mkdir(&image, &path, parents),
Commands::Info { image } => cmd_info(&image), Commands::Info { image } => cmd_info(&image),
}; };
@@ -138,7 +160,13 @@ fn cmd_create(output: &PathBuf, size: &str, label: &str) -> Result<()> {
Ok(()) Ok(())
} }
fn cmd_add(image: &PathBuf, file: &PathBuf, dest: Option<&str>, force: bool, parents: bool) -> Result<()> { fn cmd_add(
image: &PathBuf,
file: &PathBuf,
dest: Option<&str>,
force: bool,
parents: bool,
) -> Result<()> {
if !file.exists() { if !file.exists() {
return Err(VentoyError::FileNotFound(file.display().to_string())); return Err(VentoyError::FileNotFound(file.display().to_string()));
} }
@@ -234,18 +262,23 @@ fn cmd_info(image: &PathBuf) -> Result<()> {
println!(); println!();
println!("Partition Layout:"); println!("Partition Layout:");
println!(" Data partition:"); println!(" Data partition:");
println!(" Start: sector {} (offset {})", println!(
" Start: sector {} (offset {})",
layout.data_start_sector, layout.data_start_sector,
format_size(layout.data_offset())); format_size(layout.data_offset())
println!(" Size: {} sectors ({})", );
println!(
" Size: {} sectors ({})",
layout.data_size_sectors, layout.data_size_sectors,
format_size(layout.data_size())); format_size(layout.data_size())
);
println!(" EFI partition:"); println!(" EFI partition:");
println!(" Start: sector {} (offset {})", println!(
" Start: sector {} (offset {})",
layout.efi_start_sector, layout.efi_start_sector,
format_size(layout.efi_offset())); format_size(layout.efi_offset())
println!(" Size: {} sectors (32 MB)", );
layout.efi_size_sectors); println!(" Size: {} sectors (32 MB)", layout.efi_size_sectors);
Ok(()) Ok(())
} }

View File

@@ -225,7 +225,8 @@ fn link_system() -> bool {
} }
// Then standard paths // Then standard paths
lib_paths.extend([ lib_paths.extend(
[
"/usr/local/lib", // Custom builds "/usr/local/lib", // Custom builds
"/usr/local/lib64", "/usr/local/lib64",
"/usr/lib", "/usr/lib",
@@ -233,7 +234,10 @@ fn link_system() -> bool {
"/usr/lib/x86_64-linux-gnu", // Debian/Ubuntu x86_64 "/usr/lib/x86_64-linux-gnu", // Debian/Ubuntu x86_64
"/usr/lib/aarch64-linux-gnu", // Debian/Ubuntu ARM64 "/usr/lib/aarch64-linux-gnu", // Debian/Ubuntu ARM64
"/usr/lib/arm-linux-gnueabihf", // Debian/Ubuntu ARMv7 "/usr/lib/arm-linux-gnueabihf", // Debian/Ubuntu ARMv7
].iter().map(|s| s.to_string())); ]
.iter()
.map(|s| s.to_string()),
);
for path in &lib_paths { for path in &lib_paths {
let lib_path = Path::new(path); let lib_path = Path::new(path);
@@ -245,7 +249,10 @@ fn link_system() -> bool {
println!("cargo:rustc-link-search=native={}", path); println!("cargo:rustc-link-search=native={}", path);
println!("cargo:rustc-link-lib=static=yuv"); println!("cargo:rustc-link-lib=static=yuv");
println!("cargo:rustc-link-lib=stdc++"); println!("cargo:rustc-link-lib=stdc++");
println!("cargo:info=Using system libyuv from {} (static linking)", path); println!(
"cargo:info=Using system libyuv from {} (static linking)",
path
);
return true; return true;
} }
@@ -257,7 +264,10 @@ fn link_system() -> bool {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
println!("cargo:rustc-link-lib=stdc++"); println!("cargo:rustc-link-lib=stdc++");
println!("cargo:info=Using system libyuv from {} (dynamic linking)", path); println!(
"cargo:info=Using system libyuv from {} (dynamic linking)",
path
);
return true; return true;
} }
} }

View File

@@ -404,6 +404,37 @@ pub fn nv12_to_i420(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Resu
)) ))
} }
/// Convert NV21 to I420 (YUV420P)
pub fn nv21_to_i420(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Result<()> {
if width % 2 != 0 || height % 2 != 0 {
return Err(YuvError::InvalidDimensions);
}
let w = width as usize;
let h = height as usize;
let y_size = w * h;
let uv_size = (w / 2) * (h / 2);
if src.len() < nv12_size(w, h) || dst.len() < i420_size(w, h) {
return Err(YuvError::BufferTooSmall);
}
call_yuv!(NV21ToI420(
src.as_ptr(),
width,
src[y_size..].as_ptr(),
width,
dst.as_mut_ptr(),
width,
dst[y_size..].as_mut_ptr(),
width / 2,
dst[y_size + uv_size..].as_mut_ptr(),
width / 2,
width,
height,
))
}
// ============================================================================ // ============================================================================
// ARGB/BGRA conversions (32-bit) // ARGB/BGRA conversions (32-bit)
// Note: libyuv ARGB = BGRA in memory on little-endian systems // Note: libyuv ARGB = BGRA in memory on little-endian systems

View File

@@ -55,7 +55,10 @@ impl LedSensor {
.map_err(|e| AppError::Internal(format!("LED GPIO chip failed: {}", e)))?; .map_err(|e| AppError::Internal(format!("LED GPIO chip failed: {}", e)))?;
let line = chip.get_line(self.config.gpio_pin).map_err(|e| { let line = chip.get_line(self.config.gpio_pin).map_err(|e| {
AppError::Internal(format!("LED GPIO line {} failed: {}", self.config.gpio_pin, e)) AppError::Internal(format!(
"LED GPIO line {} failed: {}",
self.config.gpio_pin, e
))
})?; })?;
let handle = line let handle = line

View File

@@ -52,8 +52,8 @@ mod wol;
pub use controller::{AtxController, AtxControllerConfig}; pub use controller::{AtxController, AtxControllerConfig};
pub use executor::timing; pub use executor::timing;
pub use types::{ pub use types::{
ActiveLevel, AtxAction, AtxDevices, AtxDriverType, AtxKeyConfig, AtxLedConfig, ActiveLevel, AtxAction, AtxDevices, AtxDriverType, AtxKeyConfig, AtxLedConfig, AtxPowerRequest,
AtxPowerRequest, AtxState, PowerStatus, AtxState, PowerStatus,
}; };
pub use wol::send_wol; pub use wol::send_wol;

View File

@@ -22,7 +22,10 @@ fn parse_mac_address(mac: &str) -> Result<[u8; 6]> {
} else if mac.contains('-') { } else if mac.contains('-') {
mac.split('-').collect() mac.split('-').collect()
} else { } else {
return Err(AppError::Config(format!("Invalid MAC address format: {}", mac))); return Err(AppError::Config(format!(
"Invalid MAC address format: {}",
mac
)));
}; };
if parts.len() != 6 { if parts.len() != 6 {
@@ -34,9 +37,8 @@ fn parse_mac_address(mac: &str) -> Result<[u8; 6]> {
let mut bytes = [0u8; 6]; let mut bytes = [0u8; 6];
for (i, part) in parts.iter().enumerate() { for (i, part) in parts.iter().enumerate() {
bytes[i] = u8::from_str_radix(part, 16).map_err(|_| { bytes[i] = u8::from_str_radix(part, 16)
AppError::Config(format!("Invalid MAC address byte: {}", part)) .map_err(|_| AppError::Config(format!("Invalid MAC address byte: {}", part)))?;
})?;
} }
Ok(bytes) Ok(bytes)

View File

@@ -201,7 +201,15 @@ 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(config, state, stats, frame_tx, stop_flag, sequence, log_throttler); capture_loop(
config,
state,
stats,
frame_tx,
stop_flag,
sequence,
log_throttler,
);
}); });
*self.capture_handle.lock().await = Some(handle); *self.capture_handle.lock().await = Some(handle);
@@ -274,40 +282,34 @@ fn run_capture(
// Configure hardware parameters // Configure hardware parameters
{ {
let hwp = HwParams::any(&pcm).map_err(|e| { let hwp = HwParams::any(&pcm)
AppError::AudioError(format!("Failed to get HwParams: {}", e)) .map_err(|e| AppError::AudioError(format!("Failed to get HwParams: {}", e)))?;
})?;
hwp.set_channels(config.channels).map_err(|e| { hwp.set_channels(config.channels)
AppError::AudioError(format!("Failed to set channels: {}", e)) .map_err(|e| AppError::AudioError(format!("Failed to set channels: {}", e)))?;
})?;
hwp.set_rate(config.sample_rate, ValueOr::Nearest).map_err(|e| { hwp.set_rate(config.sample_rate, ValueOr::Nearest)
AppError::AudioError(format!("Failed to set sample rate: {}", e)) .map_err(|e| AppError::AudioError(format!("Failed to set sample rate: {}", e)))?;
})?;
hwp.set_format(Format::s16()).map_err(|e| { hwp.set_format(Format::s16())
AppError::AudioError(format!("Failed to set format: {}", e)) .map_err(|e| AppError::AudioError(format!("Failed to set format: {}", e)))?;
})?;
hwp.set_access(Access::RWInterleaved).map_err(|e| { hwp.set_access(Access::RWInterleaved)
AppError::AudioError(format!("Failed to set access: {}", e)) .map_err(|e| AppError::AudioError(format!("Failed to set access: {}", e)))?;
})?;
hwp.set_buffer_size_near(config.buffer_frames as Frames).map_err(|e| { hwp.set_buffer_size_near(config.buffer_frames as Frames)
AppError::AudioError(format!("Failed to set buffer size: {}", e)) .map_err(|e| AppError::AudioError(format!("Failed to set buffer size: {}", e)))?;
})?;
hwp.set_period_size_near(config.period_frames as Frames, ValueOr::Nearest) hwp.set_period_size_near(config.period_frames as Frames, ValueOr::Nearest)
.map_err(|e| AppError::AudioError(format!("Failed to set period size: {}", e)))?; .map_err(|e| AppError::AudioError(format!("Failed to set period size: {}", e)))?;
pcm.hw_params(&hwp).map_err(|e| { pcm.hw_params(&hwp)
AppError::AudioError(format!("Failed to apply hw params: {}", e)) .map_err(|e| AppError::AudioError(format!("Failed to apply hw params: {}", e)))?;
})?;
} }
// Get actual configuration // Get actual configuration
let actual_rate = pcm.hw_params_current() let actual_rate = pcm
.hw_params_current()
.map(|h| h.get_rate().unwrap_or(config.sample_rate)) .map(|h| h.get_rate().unwrap_or(config.sample_rate))
.unwrap_or(config.sample_rate); .unwrap_or(config.sample_rate);
@@ -317,9 +319,8 @@ fn run_capture(
); );
// Prepare for capture // Prepare for capture
pcm.prepare().map_err(|e| { pcm.prepare()
AppError::AudioError(format!("Failed to prepare PCM: {}", e)) .map_err(|e| AppError::AudioError(format!("Failed to prepare PCM: {}", e)))?;
})?;
let _ = state.send(CaptureState::Running); let _ = state.send(CaptureState::Running);
@@ -340,7 +341,11 @@ fn run_capture(
continue; continue;
} }
State::Suspended => { State::Suspended => {
warn_throttled!(log_throttler, "suspended", "Audio device suspended, recovering"); warn_throttled!(
log_throttler,
"suspended",
"Audio device suspended, recovering"
);
let _ = pcm.resume(); let _ = pcm.resume();
continue; continue;
} }
@@ -363,11 +368,8 @@ fn run_capture(
// Directly use the buffer slice (already in correct byte format) // Directly use the buffer slice (already in correct byte format)
let seq = sequence.fetch_add(1, Ordering::Relaxed); let seq = sequence.fetch_add(1, Ordering::Relaxed);
let frame = AudioFrame::new( let frame =
Bytes::copy_from_slice(&buffer[..byte_count]), AudioFrame::new(Bytes::copy_from_slice(&buffer[..byte_count]), config, seq);
config,
seq,
);
// Send to subscribers // Send to subscribers
if frame_tx.receiver_count() > 0 { if frame_tx.receiver_count() > 0 {

View File

@@ -193,7 +193,9 @@ impl AudioController {
pub async fn select_device(&self, device: &str) -> Result<()> { pub async fn select_device(&self, device: &str) -> Result<()> {
// Validate device exists // Validate device exists
let devices = self.list_devices().await?; let devices = self.list_devices().await?;
let found = devices.iter().any(|d| d.name == device || d.description.contains(device)); let found = devices
.iter()
.any(|d| d.name == device || d.description.contains(device));
if !found && device != "default" { if !found && device != "default" {
return Err(AppError::AudioError(format!( return Err(AppError::AudioError(format!(
@@ -244,7 +246,11 @@ impl AudioController {
}) })
.await; .await;
info!("Audio quality set to: {:?} ({}bps)", quality, quality.bitrate()); info!(
"Audio quality set to: {:?} ({}bps)",
quality,
quality.bitrate()
);
Ok(()) Ok(())
} }
@@ -346,11 +352,14 @@ impl AudioController {
let streaming = self.is_streaming().await; let streaming = self.is_streaming().await;
let error = self.last_error.read().await.clone(); let error = self.last_error.read().await.clone();
let (subscriber_count, frames_encoded, bytes_output) = if let Some(ref streamer) = let (subscriber_count, frames_encoded, bytes_output) =
*self.streamer.read().await if let Some(ref streamer) = *self.streamer.read().await {
{
let stats = streamer.stats().await; let stats = streamer.stats().await;
(stats.subscriber_count, stats.frames_encoded, stats.bytes_output) (
stats.subscriber_count,
stats.frames_encoded,
stats.bytes_output,
)
} else { } else {
(0, 0, 0) (0, 0, 0)
}; };
@@ -383,7 +392,11 @@ impl AudioController {
/// Subscribe to Opus frames (async version) /// Subscribe to Opus frames (async version)
pub async fn subscribe_opus_async(&self) -> Option<broadcast::Receiver<OpusFrame>> { pub async fn subscribe_opus_async(&self) -> Option<broadcast::Receiver<OpusFrame>> {
self.streamer.read().await.as_ref().map(|s| s.subscribe_opus()) self.streamer
.read()
.await
.as_ref()
.map(|s| s.subscribe_opus())
} }
/// Enable or disable audio /// Enable or disable audio

View File

@@ -55,7 +55,12 @@ fn get_usb_bus_info(card_index: i32) -> Option<String> {
// Match patterns like "1-1", "1-2", "1-1.2", "2-1.3.1" // Match patterns like "1-1", "1-2", "1-1.2", "2-1.3.1"
if component.contains('-') && !component.contains(':') { if component.contains('-') && !component.contains(':') {
// Verify it looks like a USB port (starts with digit) // Verify it looks like a USB port (starts with digit)
if component.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) { if component
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
return Some(component.to_string()); return Some(component.to_string());
} }
} }
@@ -223,15 +228,14 @@ pub fn find_best_audio_device() -> Result<AudioDeviceInfo> {
let devices = enumerate_audio_devices()?; let devices = enumerate_audio_devices()?;
if devices.is_empty() { if devices.is_empty() {
return Err(AppError::AudioError("No audio capture devices found".to_string())); return Err(AppError::AudioError(
"No audio capture devices found".to_string(),
));
} }
// First, look for HDMI/capture card devices that support 48kHz stereo // First, look for HDMI/capture card devices that support 48kHz stereo
for device in &devices { for device in &devices {
if device.is_hdmi if device.is_hdmi && device.sample_rates.contains(&48000) && device.channels.contains(&2) {
&& device.sample_rates.contains(&48000)
&& device.channels.contains(&2)
{
info!("Selected HDMI audio device: {}", device.description); info!("Selected HDMI audio device: {}", device.description);
return Ok(device.clone()); return Ok(device.clone());
} }

View File

@@ -137,9 +137,8 @@ impl OpusEncoder {
let channels = config.to_audiopus_channels(); let channels = config.to_audiopus_channels();
let application = config.to_audiopus_application(); let application = config.to_audiopus_application();
let mut encoder = Encoder::new(sample_rate, channels, application).map_err(|e| { let mut encoder = Encoder::new(sample_rate, channels, application)
AppError::AudioError(format!("Failed to create Opus encoder: {:?}", e)) .map_err(|e| AppError::AudioError(format!("Failed to create Opus encoder: {:?}", e)))?;
})?;
// Configure encoder // Configure encoder
encoder encoder

View File

@@ -22,5 +22,7 @@ pub use controller::{AudioController, AudioControllerConfig, AudioQuality, Audio
pub use device::{enumerate_audio_devices, enumerate_audio_devices_with_current, AudioDeviceInfo}; pub use device::{enumerate_audio_devices, enumerate_audio_devices_with_current, AudioDeviceInfo};
pub use encoder::{OpusConfig, OpusEncoder, OpusFrame}; pub use encoder::{OpusConfig, OpusEncoder, OpusFrame};
pub use monitor::{AudioHealthMonitor, AudioHealthStatus, AudioMonitorConfig}; pub use monitor::{AudioHealthMonitor, AudioHealthStatus, AudioMonitorConfig};
pub use shared_pipeline::{SharedAudioPipeline, SharedAudioPipelineConfig, SharedAudioPipelineStats}; pub use shared_pipeline::{
SharedAudioPipeline, SharedAudioPipelineConfig, SharedAudioPipelineStats,
};
pub use streamer::{AudioStreamState, AudioStreamer, AudioStreamerConfig}; pub use streamer::{AudioStreamState, AudioStreamer, AudioStreamerConfig};

View File

@@ -329,9 +329,7 @@ mod tests {
let monitor = AudioHealthMonitor::with_defaults(); let monitor = AudioHealthMonitor::with_defaults();
for i in 1..=5 { for i in 1..=5 {
monitor monitor.report_error(None, "Error", "io_error").await;
.report_error(None, "Error", "io_error")
.await;
assert_eq!(monitor.retry_count(), i); assert_eq!(monitor.retry_count(), i);
} }
} }
@@ -340,9 +338,7 @@ mod tests {
async fn test_reset() { async fn test_reset() {
let monitor = AudioHealthMonitor::with_defaults(); let monitor = AudioHealthMonitor::with_defaults();
monitor monitor.report_error(None, "Error", "io_error").await;
.report_error(None, "Error", "io_error")
.await;
assert!(monitor.is_error().await); assert!(monitor.is_error().await);
monitor.reset().await; monitor.reset().await;

View File

@@ -320,11 +320,8 @@ impl SharedAudioPipeline {
} }
// Receive audio frame with timeout // Receive audio frame with timeout
let recv_result = tokio::time::timeout( let recv_result =
std::time::Duration::from_secs(2), tokio::time::timeout(std::time::Duration::from_secs(2), audio_rx.recv()).await;
audio_rx.recv(),
)
.await;
match recv_result { match recv_result {
Ok(Ok(audio_frame)) => { Ok(Ok(audio_frame)) => {

View File

@@ -297,11 +297,8 @@ impl AudioStreamer {
} }
// Receive PCM frame with timeout // Receive PCM frame with timeout
let recv_result = tokio::time::timeout( let recv_result =
std::time::Duration::from_secs(2), tokio::time::timeout(std::time::Duration::from_secs(2), pcm_rx.recv()).await;
pcm_rx.recv(),
)
.await;
match recv_result { match recv_result {
Ok(Ok(audio_frame)) => { Ok(Ok(audio_frame)) => {

View File

@@ -46,7 +46,10 @@ pub async fn auth_middleware(
if !state.config.is_initialized() { if !state.config.is_initialized() {
// Allow access to setup endpoints when not initialized // Allow access to setup endpoints when not initialized
let path = request.uri().path(); let path = request.uri().path();
if path.starts_with("/api/setup") || path == "/api/info" || path.starts_with("/") && !path.starts_with("/api/") { if path.starts_with("/api/setup")
|| path == "/api/info"
|| path.starts_with("/") && !path.starts_with("/api/")
{
return Ok(next.run(request).await); return Ok(next.run(request).await);
} }
} }

View File

@@ -1,9 +1,9 @@
pub mod middleware;
mod password; mod password;
mod session; mod session;
mod user; mod user;
pub mod middleware;
pub use middleware::{auth_middleware, require_admin, AuthLayer, SESSION_COOKIE};
pub use password::{hash_password, verify_password}; pub use password::{hash_password, verify_password};
pub use session::{Session, SessionStore}; pub use session::{Session, SessionStore};
pub use user::{User, UserStore}; pub use user::{User, UserStore};
pub use middleware::{AuthLayer, SESSION_COOKIE, auth_middleware, require_admin};

View File

@@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use uuid::Uuid; use uuid::Uuid;
use crate::error::{AppError, Result};
use super::password::{hash_password, verify_password}; use super::password::{hash_password, verify_password};
use crate::error::{AppError, Result};
/// User row type from database /// User row type from database
type UserRow = (String, String, String, i32, String, String); type UserRow = (String, String, String, i32, String, String);
@@ -134,9 +134,8 @@ impl UserStore {
let password_hash = hash_password(new_password)?; let password_hash = hash_password(new_password)?;
let now = Utc::now(); let now = Utc::now();
let result = sqlx::query( let result =
"UPDATE users SET password_hash = ?1, updated_at = ?2 WHERE id = ?3", sqlx::query("UPDATE users SET password_hash = ?1, updated_at = ?2 WHERE id = ?3")
)
.bind(&password_hash) .bind(&password_hash)
.bind(now.to_rfc3339()) .bind(now.to_rfc3339())
.bind(user_id) .bind(user_id)

View File

@@ -1,6 +1,6 @@
use crate::video::encoder::BitratePreset;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typeshare::typeshare; use typeshare::typeshare;
use crate::video::encoder::BitratePreset;
// Re-export ExtensionsConfig from extensions module // Re-export ExtensionsConfig from extensions module
pub use crate::extensions::ExtensionsConfig; pub use crate::extensions::ExtensionsConfig;
@@ -425,8 +425,15 @@ impl StreamConfig {
/// Check if using public ICE servers (user left fields empty) /// Check if using public ICE servers (user left fields empty)
pub fn is_using_public_ice_servers(&self) -> bool { pub fn is_using_public_ice_servers(&self) -> bool {
use crate::webrtc::config::public_ice; use crate::webrtc::config::public_ice;
self.stun_server.as_ref().map(|s| s.is_empty()).unwrap_or(true) self.stun_server
&& self.turn_server.as_ref().map(|s| s.is_empty()).unwrap_or(true) .as_ref()
.map(|s| s.is_empty())
.unwrap_or(true)
&& self
.turn_server
.as_ref()
.map(|s| s.is_empty())
.unwrap_or(true)
&& public_ice::is_configured() && public_ice::is_configured()
} }
} }

View File

@@ -126,9 +126,8 @@ impl ConfigStore {
/// Load configuration from database /// Load configuration from database
async fn load_config(pool: &Pool<Sqlite>) -> Result<AppConfig> { async fn load_config(pool: &Pool<Sqlite>) -> Result<AppConfig> {
let row: Option<(String,)> = sqlx::query_as( let row: Option<(String,)> =
"SELECT value FROM config WHERE key = 'app_config'" sqlx::query_as("SELECT value FROM config WHERE key = 'app_config'")
)
.fetch_optional(pool) .fetch_optional(pool)
.await?; .await?;
@@ -245,10 +244,13 @@ mod tests {
assert!(!config.initialized); assert!(!config.initialized);
// Update config // Update config
store.update(|c| { store
.update(|c| {
c.initialized = true; c.initialized = true;
c.web.http_port = 9000; c.web.http_port = 9000;
}).await.unwrap(); })
.await
.unwrap();
// Verify update // Verify update
let config = store.get(); let config = store.get();

View File

@@ -6,7 +6,8 @@
pub mod types; pub mod types;
pub use types::{ pub use types::{
AtxDeviceInfo, AudioDeviceInfo, ClientStats, HidDeviceInfo, MsdDeviceInfo, SystemEvent, VideoDeviceInfo, AtxDeviceInfo, AudioDeviceInfo, ClientStats, HidDeviceInfo, MsdDeviceInfo, SystemEvent,
VideoDeviceInfo,
}; };
use tokio::sync::broadcast; use tokio::sync::broadcast;

View File

@@ -128,6 +128,20 @@ pub enum SystemEvent {
// ============================================================================ // ============================================================================
// Video Stream Events // Video Stream Events
// ============================================================================ // ============================================================================
/// Stream mode switching started (transactional, correlates all following events)
///
/// Sent immediately after a mode switch request is accepted.
/// Clients can use `transition_id` to correlate subsequent `stream.*` events.
#[serde(rename = "stream.mode_switching")]
StreamModeSwitching {
/// Unique transition ID for this mode switch transaction
transition_id: String,
/// Target mode: "mjpeg", "h264", "h265", "vp8", "vp9"
to_mode: String,
/// Previous mode: "mjpeg", "h264", "h265", "vp8", "vp9"
from_mode: String,
},
/// Stream state changed (e.g., started, stopped, error) /// Stream state changed (e.g., started, stopped, error)
#[serde(rename = "stream.state_changed")] #[serde(rename = "stream.state_changed")]
StreamStateChanged { StreamStateChanged {
@@ -143,6 +157,9 @@ pub enum SystemEvent {
/// the stream will be interrupted temporarily. /// the stream will be interrupted temporarily.
#[serde(rename = "stream.config_changing")] #[serde(rename = "stream.config_changing")]
StreamConfigChanging { StreamConfigChanging {
/// Optional transition ID if this config change is part of a mode switch transaction
#[serde(skip_serializing_if = "Option::is_none")]
transition_id: Option<String>,
/// Reason for change: "device_switch", "resolution_change", "format_change" /// Reason for change: "device_switch", "resolution_change", "format_change"
reason: String, reason: String,
}, },
@@ -152,6 +169,9 @@ pub enum SystemEvent {
/// Sent after new configuration is active. Clients can reconnect now. /// Sent after new configuration is active. Clients can reconnect now.
#[serde(rename = "stream.config_applied")] #[serde(rename = "stream.config_applied")]
StreamConfigApplied { StreamConfigApplied {
/// Optional transition ID if this config change is part of a mode switch transaction
#[serde(skip_serializing_if = "Option::is_none")]
transition_id: Option<String>,
/// Device path /// Device path
device: String, device: String,
/// Resolution (width, height) /// Resolution (width, height)
@@ -193,6 +213,9 @@ pub enum SystemEvent {
/// Clients should wait for this event before attempting to create WebRTC sessions. /// Clients should wait for this event before attempting to create WebRTC sessions.
#[serde(rename = "stream.webrtc_ready")] #[serde(rename = "stream.webrtc_ready")]
WebRTCReady { WebRTCReady {
/// Optional transition ID if this readiness is part of a mode switch transaction
#[serde(skip_serializing_if = "Option::is_none")]
transition_id: Option<String>,
/// Current video codec /// Current video codec
codec: String, codec: String,
/// Whether hardware encoding is being used /// Whether hardware encoding is being used
@@ -215,12 +238,26 @@ pub enum SystemEvent {
/// from the current stream and reconnect using the new mode. /// from the current stream and reconnect using the new mode.
#[serde(rename = "stream.mode_changed")] #[serde(rename = "stream.mode_changed")]
StreamModeChanged { StreamModeChanged {
/// Optional transition ID if this change is part of a mode switch transaction
#[serde(skip_serializing_if = "Option::is_none")]
transition_id: Option<String>,
/// New mode: "mjpeg", "h264", "h265", "vp8", or "vp9" /// New mode: "mjpeg", "h264", "h265", "vp8", or "vp9"
mode: String, mode: String,
/// Previous mode: "mjpeg", "h264", "h265", "vp8", or "vp9" /// Previous mode: "mjpeg", "h264", "h265", "vp8", or "vp9"
previous_mode: String, previous_mode: String,
}, },
/// Stream mode switching completed (transactional end marker)
///
/// Sent when the backend considers the new mode ready for clients to connect.
#[serde(rename = "stream.mode_ready")]
StreamModeReady {
/// Unique transition ID for this mode switch transaction
transition_id: String,
/// Active mode after switch: "mjpeg", "h264", "h265", "vp8", "vp9"
mode: String,
},
// ============================================================================ // ============================================================================
// HID Events // HID Events
// ============================================================================ // ============================================================================
@@ -491,6 +528,7 @@ impl SystemEvent {
/// Get the event name (for filtering/routing) /// Get the event name (for filtering/routing)
pub fn event_name(&self) -> &'static str { pub fn event_name(&self) -> &'static str {
match self { match self {
Self::StreamModeSwitching { .. } => "stream.mode_switching",
Self::StreamStateChanged { .. } => "stream.state_changed", Self::StreamStateChanged { .. } => "stream.state_changed",
Self::StreamConfigChanging { .. } => "stream.config_changing", Self::StreamConfigChanging { .. } => "stream.config_changing",
Self::StreamConfigApplied { .. } => "stream.config_applied", Self::StreamConfigApplied { .. } => "stream.config_applied",
@@ -500,6 +538,7 @@ impl SystemEvent {
Self::WebRTCReady { .. } => "stream.webrtc_ready", Self::WebRTCReady { .. } => "stream.webrtc_ready",
Self::StreamStatsUpdate { .. } => "stream.stats_update", Self::StreamStatsUpdate { .. } => "stream.stats_update",
Self::StreamModeChanged { .. } => "stream.mode_changed", Self::StreamModeChanged { .. } => "stream.mode_changed",
Self::StreamModeReady { .. } => "stream.mode_ready",
Self::HidStateChanged { .. } => "hid.state_changed", Self::HidStateChanged { .. } => "hid.state_changed",
Self::HidBackendSwitching { .. } => "hid.backend_switching", Self::HidBackendSwitching { .. } => "hid.backend_switching",
Self::HidDeviceLost { .. } => "hid.device_lost", Self::HidDeviceLost { .. } => "hid.device_lost",
@@ -589,6 +628,7 @@ mod tests {
#[test] #[test]
fn test_serialization() { fn test_serialization() {
let event = SystemEvent::StreamConfigApplied { let event = SystemEvent::StreamConfigApplied {
transition_id: None,
device: "/dev/video0".to_string(), device: "/dev/video0".to_string(),
resolution: (1920, 1080), resolution: (1920, 1080),
format: "mjpeg".to_string(), format: "mjpeg".to_string(),
@@ -600,6 +640,9 @@ mod tests {
assert!(json.contains("/dev/video0")); assert!(json.contains("/dev/video0"));
let deserialized: SystemEvent = serde_json::from_str(&json).unwrap(); let deserialized: SystemEvent = serde_json::from_str(&json).unwrap();
assert!(matches!(deserialized, SystemEvent::StreamConfigApplied { .. })); assert!(matches!(
deserialized,
SystemEvent::StreamConfigApplied { .. }
));
} }
} }

View File

@@ -210,7 +210,11 @@ impl ExtensionManager {
} }
/// Build command arguments for an extension /// Build command arguments for an extension
async fn build_args(&self, id: ExtensionId, config: &ExtensionsConfig) -> Result<Vec<String>, String> { async fn build_args(
&self,
id: ExtensionId,
config: &ExtensionsConfig,
) -> Result<Vec<String>, String> {
match id { match id {
ExtensionId::Ttyd => { ExtensionId::Ttyd => {
let c = &config.ttyd; let c = &config.ttyd;
@@ -219,8 +223,10 @@ impl ExtensionManager {
Self::prepare_ttyd_socket().await?; Self::prepare_ttyd_socket().await?;
let mut args = vec![ let mut args = vec![
"-i".to_string(), TTYD_SOCKET_PATH.to_string(), // Unix socket "-i".to_string(),
"-b".to_string(), "/api/terminal".to_string(), // Base path for reverse proxy TTYD_SOCKET_PATH.to_string(), // Unix socket
"-b".to_string(),
"/api/terminal".to_string(), // Base path for reverse proxy
"-W".to_string(), // Writable (allow input) "-W".to_string(), // Writable (allow input)
]; ];
@@ -313,7 +319,10 @@ impl ExtensionManager {
} }
// Remove old socket file if exists // Remove old socket file if exists
if tokio::fs::try_exists(TTYD_SOCKET_PATH).await.unwrap_or(false) { if tokio::fs::try_exists(TTYD_SOCKET_PATH)
.await
.unwrap_or(false)
{
tokio::fs::remove_file(TTYD_SOCKET_PATH) tokio::fs::remove_file(TTYD_SOCKET_PATH)
.await .await
.map_err(|e| format!("Failed to remove old socket: {}", e))?; .map_err(|e| format!("Failed to remove old socket: {}", e))?;
@@ -374,8 +383,8 @@ impl ExtensionManager {
/// Start all enabled extensions in parallel /// Start all enabled extensions in parallel
pub async fn start_enabled(&self, config: &ExtensionsConfig) { pub async fn start_enabled(&self, config: &ExtensionsConfig) {
use std::pin::Pin;
use futures::Future; use futures::Future;
use std::pin::Pin;
let mut start_futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + '_>>> = Vec::new(); let mut start_futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + '_>>> = Vec::new();
@@ -416,10 +425,7 @@ impl ExtensionManager {
/// Stop all running extensions in parallel /// Stop all running extensions in parallel
pub async fn stop_all(&self) { pub async fn stop_all(&self) {
let stop_futures: Vec<_> = ExtensionId::all() let stop_futures: Vec<_> = ExtensionId::all().iter().map(|id| self.stop(*id)).collect();
.iter()
.map(|id| self.stop(*id))
.collect();
futures::future::join_all(stop_futures).await; futures::future::join_all(stop_futures).await;
} }
} }

View File

@@ -21,7 +21,7 @@ use async_trait::async_trait;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU8, AtomicU32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU32, AtomicU8, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tracing::{debug, info, trace, warn}; use tracing::{debug, info, trace, warn};
@@ -358,8 +358,7 @@ impl Response {
/// Check if the response indicates success /// Check if the response indicates success
pub fn is_success(&self) -> bool { pub fn is_success(&self) -> bool {
!self.is_error !self.is_error && (self.data.is_empty() || self.data[0] == Ch9329Error::Success as u8)
&& (self.data.is_empty() || self.data[0] == Ch9329Error::Success as u8)
} }
} }
@@ -489,7 +488,10 @@ impl Ch9329Backend {
.map_err(|e| Self::serial_error_to_hid_error(e, "Failed to open serial port"))?; .map_err(|e| Self::serial_error_to_hid_error(e, "Failed to open serial port"))?;
*self.port.lock() = Some(port); *self.port.lock() = Some(port);
info!("CH9329 serial port reopened: {} @ {} baud", self.port_path, self.baud_rate); info!(
"CH9329 serial port reopened: {} @ {} baud",
self.port_path, self.baud_rate
);
// Verify connection with GET_INFO command // Verify connection with GET_INFO command
self.query_chip_info().map_err(|e| { self.query_chip_info().map_err(|e| {
@@ -518,7 +520,10 @@ impl Ch9329Backend {
/// Returns the packet buffer and the actual length /// Returns the packet buffer and the actual length
#[inline] #[inline]
fn build_packet_buf(&self, cmd: u8, data: &[u8]) -> ([u8; MAX_PACKET_SIZE], usize) { fn build_packet_buf(&self, cmd: u8, data: &[u8]) -> ([u8; MAX_PACKET_SIZE], usize) {
debug_assert!(data.len() <= MAX_DATA_LEN, "Data too long for CH9329 packet"); debug_assert!(
data.len() <= MAX_DATA_LEN,
"Data too long for CH9329 packet"
);
let len = data.len() as u8; let len = data.len() as u8;
let packet_len = 6 + data.len(); let packet_len = 6 + data.len();
@@ -554,16 +559,19 @@ impl Ch9329Backend {
let mut port_guard = self.port.lock(); let mut port_guard = self.port.lock();
if let Some(ref mut port) = *port_guard { if let Some(ref mut port) = *port_guard {
port.write_all(&packet[..packet_len]).map_err(|e| { port.write_all(&packet[..packet_len])
AppError::HidError { .map_err(|e| AppError::HidError {
backend: "ch9329".to_string(), backend: "ch9329".to_string(),
reason: format!("Failed to write to CH9329: {}", e), reason: format!("Failed to write to CH9329: {}", e),
error_code: "write_failed".to_string(), error_code: "write_failed".to_string(),
}
})?; })?;
// Only log mouse button events at debug level to avoid flooding // Only log mouse button events at debug level to avoid flooding
if cmd == cmd::SEND_MS_ABS_DATA && data.len() >= 2 && data[1] != 0 { if cmd == cmd::SEND_MS_ABS_DATA && data.len() >= 2 && data[1] != 0 {
debug!("CH9329 TX [cmd=0x{:02X}]: {:02X?}", cmd, &packet[..packet_len]); debug!(
"CH9329 TX [cmd=0x{:02X}]: {:02X?}",
cmd,
&packet[..packet_len]
);
} }
Ok(()) Ok(())
} else { } else {
@@ -655,7 +663,11 @@ impl Ch9329Backend {
info!( info!(
"CH9329: Recovery successful, chip version: {}, USB: {}", "CH9329: Recovery successful, chip version: {}, USB: {}",
info.version, info.version,
if info.usb_connected { "connected" } else { "disconnected" } if info.usb_connected {
"connected"
} else {
"disconnected"
}
); );
// Reset error count on successful recovery // Reset error count on successful recovery
self.error_count.store(0, Ordering::Relaxed); self.error_count.store(0, Ordering::Relaxed);
@@ -695,9 +707,8 @@ impl Ch9329Backend {
let mut port_guard = self.port.lock(); let mut port_guard = self.port.lock();
if let Some(ref mut port) = *port_guard { if let Some(ref mut port) = *port_guard {
// Send packet // Send packet
port.write_all(&packet).map_err(|e| { port.write_all(&packet)
AppError::Internal(format!("Failed to write to CH9329: {}", e)) .map_err(|e| AppError::Internal(format!("Failed to write to CH9329: {}", e)))?;
})?;
trace!("CH9329 TX: {:02X?}", packet); trace!("CH9329 TX: {:02X?}", packet);
// Wait for response - use shorter delay for faster response // Wait for response - use shorter delay for faster response
@@ -725,7 +736,10 @@ impl Ch9329Backend {
debug!("CH9329 response timeout (may be normal)"); debug!("CH9329 response timeout (may be normal)");
Err(AppError::Internal("CH9329 response timeout".to_string())) Err(AppError::Internal("CH9329 response timeout".to_string()))
} }
Err(e) => Err(AppError::Internal(format!("Failed to read from CH9329: {}", e))), Err(e) => Err(AppError::Internal(format!(
"Failed to read from CH9329: {}",
e
))),
} }
} else { } else {
Err(AppError::Internal("CH9329 port not opened".to_string())) Err(AppError::Internal("CH9329 port not opened".to_string()))
@@ -799,7 +813,9 @@ impl Ch9329Backend {
if response.is_success() { if response.is_success() {
Ok(()) Ok(())
} else { } else {
Err(AppError::Internal("Failed to restore factory defaults".to_string())) Err(AppError::Internal(
"Failed to restore factory defaults".to_string(),
))
} }
} }
@@ -820,7 +836,9 @@ impl Ch9329Backend {
/// For other multimedia keys: data = [0x02, byte2, byte3, byte4] /// For other multimedia keys: data = [0x02, byte2, byte3, byte4]
pub fn send_media_key(&self, data: &[u8]) -> Result<()> { pub fn send_media_key(&self, data: &[u8]) -> Result<()> {
if data.len() < 2 || data.len() > 4 { if data.len() < 2 || data.len() > 4 {
return Err(AppError::Internal("Invalid media key data length".to_string())); return Err(AppError::Internal(
"Invalid media key data length".to_string(),
));
} }
self.send_packet(cmd::SEND_KB_MEDIA_DATA, data) self.send_packet(cmd::SEND_KB_MEDIA_DATA, data)
} }
@@ -871,10 +889,7 @@ impl Ch9329Backend {
// Use send_packet which has retry logic built-in // Use send_packet which has retry logic built-in
self.send_packet(cmd::SEND_MS_ABS_DATA, &data)?; self.send_packet(cmd::SEND_MS_ABS_DATA, &data)?;
trace!( trace!("CH9329 mouse: buttons=0x{:02X} pos=({},{})", buttons, x, y);
"CH9329 mouse: buttons=0x{:02X} pos=({},{})",
buttons, x, y
);
Ok(()) Ok(())
} }
@@ -930,7 +945,11 @@ impl HidBackend for Ch9329Backend {
info!( info!(
"CH9329 chip detected: {}, USB: {}, LEDs: NumLock={}, CapsLock={}, ScrollLock={}", "CH9329 chip detected: {}, USB: {}, LEDs: NumLock={}, CapsLock={}, ScrollLock={}",
info.version, info.version,
if info.usb_connected { "connected" } else { "disconnected" }, if info.usb_connected {
"connected"
} else {
"disconnected"
},
info.num_lock, info.num_lock,
info.caps_lock, info.caps_lock,
info.scroll_lock info.scroll_lock
@@ -1128,10 +1147,7 @@ pub fn detect_ch9329() -> Option<String> {
&& response[0] == PACKET_HEADER[0] && response[0] == PACKET_HEADER[0]
&& response[1] == PACKET_HEADER[1] && response[1] == PACKET_HEADER[1]
{ {
info!( info!("CH9329 detected on {} @ {} baud", port_path, baud_rate);
"CH9329 detected on {} @ {} baud",
port_path, baud_rate
);
return Some(port_path.to_string()); return Some(port_path.to_string());
} }
} }
@@ -1176,10 +1192,7 @@ pub fn detect_ch9329_with_baud() -> Option<(String, u32)> {
&& response[0] == PACKET_HEADER[0] && response[0] == PACKET_HEADER[0]
&& response[1] == PACKET_HEADER[1] && response[1] == PACKET_HEADER[1]
{ {
info!( info!("CH9329 detected on {} @ {} baud", port_path, baud_rate);
"CH9329 detected on {} @ {} baud",
port_path, baud_rate
);
return Some((port_path.to_string(), baud_rate)); return Some((port_path.to_string(), baud_rate));
} }
} }
@@ -1248,7 +1261,9 @@ mod tests {
assert_eq!(checksum, 0x03); assert_eq!(checksum, 0x03);
// Known packet: Keyboard 'A' press // Known packet: Keyboard 'A' press
let packet = [0x57u8, 0xAB, 0x00, 0x02, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; let packet = [
0x57u8, 0xAB, 0x00, 0x02, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let checksum = Ch9329Backend::calculate_checksum(&packet); let checksum = Ch9329Backend::calculate_checksum(&packet);
assert_eq!(checksum, 0x10); assert_eq!(checksum, 0x10);
} }

View File

@@ -210,27 +210,23 @@ pub fn encode_mouse_event(event: &MouseEvent) -> Vec<u8> {
let y_bytes = (event.y as i16).to_le_bytes(); let y_bytes = (event.y as i16).to_le_bytes();
let extra = match event.event_type { let extra = match event.event_type {
MouseEventType::Down | MouseEventType::Up => { MouseEventType::Down | MouseEventType::Up => event
event.button.as_ref().map(|b| match b { .button
.as_ref()
.map(|b| match b {
MouseButton::Left => 0u8, MouseButton::Left => 0u8,
MouseButton::Middle => 1u8, MouseButton::Middle => 1u8,
MouseButton::Right => 2u8, MouseButton::Right => 2u8,
MouseButton::Back => 3u8, MouseButton::Back => 3u8,
MouseButton::Forward => 4u8, MouseButton::Forward => 4u8,
}).unwrap_or(0) })
} .unwrap_or(0),
MouseEventType::Scroll => event.scroll as u8, MouseEventType::Scroll => event.scroll as u8,
_ => 0, _ => 0,
}; };
vec![ vec![
MSG_MOUSE, MSG_MOUSE, event_type, x_bytes[0], x_bytes[1], y_bytes[0], y_bytes[1], extra,
event_type,
x_bytes[0],
x_bytes[1],
y_bytes[0],
y_bytes[1],
extra,
] ]
} }

View File

@@ -102,8 +102,14 @@ impl HidController {
info!("Creating OTG HID backend from device paths"); info!("Creating OTG HID backend from device paths");
Box::new(otg::OtgBackend::from_handles(handles)?) Box::new(otg::OtgBackend::from_handles(handles)?)
} }
HidBackendType::Ch9329 { ref port, baud_rate } => { HidBackendType::Ch9329 {
info!("Initializing CH9329 HID backend on {} @ {} baud", port, baud_rate); ref port,
baud_rate,
} => {
info!(
"Initializing CH9329 HID backend on {} @ {} baud",
port, baud_rate
);
Box::new(ch9329::Ch9329Backend::with_baud_rate(port, baud_rate)?) Box::new(ch9329::Ch9329Backend::with_baud_rate(port, baud_rate)?)
} }
HidBackendType::None => { HidBackendType::None => {
@@ -157,16 +163,25 @@ impl HidController {
// Report error to monitor, but skip temporary EAGAIN retries // Report error to monitor, but skip temporary EAGAIN retries
// - "eagain_retry": within threshold, just temporary busy // - "eagain_retry": within threshold, just temporary busy
// - "eagain": exceeded threshold, report as error // - "eagain": exceeded threshold, report as error
if let AppError::HidError { ref backend, ref reason, ref error_code } = e { if let AppError::HidError {
ref backend,
ref reason,
ref error_code,
} = e
{
if error_code != "eagain_retry" { if error_code != "eagain_retry" {
self.monitor.report_error(backend, None, reason, error_code).await; self.monitor
.report_error(backend, None, reason, error_code)
.await;
} }
} }
Err(e) Err(e)
} }
} }
} }
None => Err(AppError::BadRequest("HID backend not available".to_string())), None => Err(AppError::BadRequest(
"HID backend not available".to_string(),
)),
} }
} }
@@ -188,16 +203,25 @@ impl HidController {
// Report error to monitor, but skip temporary EAGAIN retries // Report error to monitor, but skip temporary EAGAIN retries
// - "eagain_retry": within threshold, just temporary busy // - "eagain_retry": within threshold, just temporary busy
// - "eagain": exceeded threshold, report as error // - "eagain": exceeded threshold, report as error
if let AppError::HidError { ref backend, ref reason, ref error_code } = e { if let AppError::HidError {
ref backend,
ref reason,
ref error_code,
} = e
{
if error_code != "eagain_retry" { if error_code != "eagain_retry" {
self.monitor.report_error(backend, None, reason, error_code).await; self.monitor
.report_error(backend, None, reason, error_code)
.await;
} }
} }
Err(e) Err(e)
} }
} }
} }
None => Err(AppError::BadRequest("HID backend not available".to_string())), None => Err(AppError::BadRequest(
"HID backend not available".to_string(),
)),
} }
} }
@@ -205,8 +229,7 @@ impl HidController {
pub async fn send_consumer(&self, event: ConsumerEvent) -> Result<()> { pub async fn send_consumer(&self, event: ConsumerEvent) -> Result<()> {
let backend = self.backend.read().await; let backend = self.backend.read().await;
match backend.as_ref() { match backend.as_ref() {
Some(b) => { Some(b) => match b.send_consumer(event).await {
match b.send_consumer(event).await {
Ok(_) => { Ok(_) => {
if self.monitor.is_error().await { if self.monitor.is_error().await {
let backend_type = self.backend_type.read().await; let backend_type = self.backend_type.read().await;
@@ -215,16 +238,24 @@ impl HidController {
Ok(()) Ok(())
} }
Err(e) => { Err(e) => {
if let AppError::HidError { ref backend, ref reason, ref error_code } = e { if let AppError::HidError {
ref backend,
ref reason,
ref error_code,
} = e
{
if error_code != "eagain_retry" { if error_code != "eagain_retry" {
self.monitor.report_error(backend, None, reason, error_code).await; self.monitor
.report_error(backend, None, reason, error_code)
.await;
} }
} }
Err(e) Err(e)
} }
} },
} None => Err(AppError::BadRequest(
None => Err(AppError::BadRequest("HID backend not available".to_string())), "HID backend not available".to_string(),
)),
} }
} }
@@ -269,9 +300,9 @@ impl HidController {
// Include error information from monitor // Include error information from monitor
let (error, error_code) = match self.monitor.status().await { let (error, error_code) = match self.monitor.status().await {
HidHealthStatus::Error { reason, error_code, .. } => { HidHealthStatus::Error {
(Some(reason), Some(error_code)) reason, error_code, ..
} } => (Some(reason), Some(error_code)),
_ => (None, None), _ => (None, None),
}; };
@@ -320,7 +351,7 @@ impl HidController {
None => { None => {
warn!("OTG backend requires OtgService, but it's not available"); warn!("OTG backend requires OtgService, but it's not available");
return Err(AppError::Config( return Err(AppError::Config(
"OTG backend not available (OtgService missing)".to_string() "OTG backend not available (OtgService missing)".to_string(),
)); ));
} }
}; };
@@ -341,7 +372,10 @@ impl HidController {
warn!("Failed to initialize OTG backend: {}", e); warn!("Failed to initialize OTG backend: {}", e);
// Cleanup: disable HID in OtgService // Cleanup: disable HID in OtgService
if let Err(e2) = otg_service.disable_hid().await { if let Err(e2) = otg_service.disable_hid().await {
warn!("Failed to cleanup HID after init failure: {}", e2); warn!(
"Failed to cleanup HID after init failure: {}",
e2
);
} }
None None
} }
@@ -363,8 +397,14 @@ impl HidController {
} }
} }
} }
HidBackendType::Ch9329 { ref port, baud_rate } => { HidBackendType::Ch9329 {
info!("Initializing CH9329 HID backend on {} @ {} baud", port, baud_rate); ref port,
baud_rate,
} => {
info!(
"Initializing CH9329 HID backend on {} @ {} baud",
port, baud_rate
);
match ch9329::Ch9329Backend::with_baud_rate(port, baud_rate) { match ch9329::Ch9329Backend::with_baud_rate(port, baud_rate) {
Ok(b) => { Ok(b) => {
let boxed = Box::new(b); let boxed = Box::new(b);

View File

@@ -144,7 +144,8 @@ impl HidHealthMonitor {
// Check if we're in cooldown period after recent recovery // Check if we're in cooldown period after recent recovery
let current_ms = self.start_instant.elapsed().as_millis() as u64; let current_ms = self.start_instant.elapsed().as_millis() as u64;
let last_recovery = self.last_recovery_ms.load(Ordering::Relaxed); let last_recovery = self.last_recovery_ms.load(Ordering::Relaxed);
let in_cooldown = last_recovery > 0 && current_ms < last_recovery + self.config.recovery_cooldown_ms; let in_cooldown =
last_recovery > 0 && current_ms < last_recovery + self.config.recovery_cooldown_ms;
// Check if error code changed // Check if error code changed
let error_changed = { let error_changed = {
@@ -229,10 +230,7 @@ impl HidHealthMonitor {
// Only log and publish events if there were multiple retries // Only log and publish events if there were multiple retries
// (avoid log spam for transient single-retry recoveries) // (avoid log spam for transient single-retry recoveries)
if retry_count > 1 { if retry_count > 1 {
debug!( debug!("HID {} recovered after {} retries", backend, retry_count);
"HID {} recovered after {} retries",
backend, retry_count
);
// Publish recovery event // Publish recovery event
if let Some(ref events) = *self.events.read().await { if let Some(ref events) = *self.events.read().await {
@@ -372,9 +370,7 @@ mod tests {
let monitor = HidHealthMonitor::with_defaults(); let monitor = HidHealthMonitor::with_defaults();
for i in 1..=5 { for i in 1..=5 {
monitor monitor.report_error("otg", None, "Error", "io_error").await;
.report_error("otg", None, "Error", "io_error")
.await;
assert_eq!(monitor.retry_count(), i); assert_eq!(monitor.retry_count(), i);
} }
} }
@@ -387,9 +383,7 @@ mod tests {
}); });
for _ in 0..100 { for _ in 0..100 {
monitor monitor.report_error("otg", None, "Error", "io_error").await;
.report_error("otg", None, "Error", "io_error")
.await;
assert!(monitor.should_retry()); assert!(monitor.should_retry());
} }
} }
@@ -417,9 +411,7 @@ mod tests {
async fn test_reset() { async fn test_reset() {
let monitor = HidHealthMonitor::with_defaults(); let monitor = HidHealthMonitor::with_defaults();
monitor monitor.report_error("otg", None, "Error", "io_error").await;
.report_error("otg", None, "Error", "io_error")
.await;
assert!(monitor.is_error().await); assert!(monitor.is_error().await);
monitor.reset().await; monitor.reset().await;

View File

@@ -30,9 +30,11 @@ use tracing::{debug, info, trace, warn};
use super::backend::HidBackend; use super::backend::HidBackend;
use super::keymap; use super::keymap;
use super::types::{ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType}; use super::types::{
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardReport, MouseEvent, MouseEventType,
};
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
use crate::otg::{HidDevicePaths, wait_for_hid_devices}; use crate::otg::{wait_for_hid_devices, HidDevicePaths};
/// Device type for ensure_device operations /// Device type for ensure_device operations
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -73,11 +75,21 @@ impl LedState {
/// Convert to raw byte /// Convert to raw byte
pub fn to_byte(&self) -> u8 { pub fn to_byte(&self) -> u8 {
let mut b = 0u8; let mut b = 0u8;
if self.num_lock { b |= 0x01; } if self.num_lock {
if self.caps_lock { b |= 0x02; } b |= 0x01;
if self.scroll_lock { b |= 0x04; } }
if self.compose { b |= 0x08; } if self.caps_lock {
if self.kana { b |= 0x10; } b |= 0x02;
}
if self.scroll_lock {
b |= 0x04;
}
if self.compose {
b |= 0x08;
}
if self.kana {
b |= 0x10;
}
b b
} }
} }
@@ -145,7 +157,9 @@ impl OtgBackend {
keyboard_path: paths.keyboard, keyboard_path: paths.keyboard,
mouse_rel_path: paths.mouse_relative, mouse_rel_path: paths.mouse_relative,
mouse_abs_path: paths.mouse_absolute, mouse_abs_path: paths.mouse_absolute,
consumer_path: paths.consumer.unwrap_or_else(|| PathBuf::from("/dev/hidg3")), consumer_path: paths
.consumer
.unwrap_or_else(|| PathBuf::from("/dev/hidg3")),
keyboard_dev: Mutex::new(None), keyboard_dev: Mutex::new(None),
mouse_rel_dev: Mutex::new(None), mouse_rel_dev: Mutex::new(None),
mouse_abs_dev: Mutex::new(None), mouse_abs_dev: Mutex::new(None),
@@ -198,7 +212,8 @@ impl OtgBackend {
Ok(1) => { Ok(1) => {
// Device ready, check for errors // Device ready, check for errors
if let Some(revents) = pollfd[0].revents() { if let Some(revents) = pollfd[0].revents() {
if revents.contains(PollFlags::POLLERR) || revents.contains(PollFlags::POLLHUP) { if revents.contains(PollFlags::POLLERR) || revents.contains(PollFlags::POLLHUP)
{
return Err(std::io::Error::new( return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe, std::io::ErrorKind::BrokenPipe,
"Device error or hangup", "Device error or hangup",
@@ -297,7 +312,10 @@ impl OtgBackend {
// Close the device if open (device was removed) // Close the device if open (device was removed)
let mut dev = dev_mutex.lock(); let mut dev = dev_mutex.lock();
if dev.is_some() { if dev.is_some() {
debug!("Device path {} no longer exists, closing handle", path.display()); debug!(
"Device path {} no longer exists, closing handle",
path.display()
);
*dev = None; *dev = None;
} }
self.online.store(false, Ordering::Relaxed); self.online.store(false, Ordering::Relaxed);
@@ -335,7 +353,11 @@ impl OtgBackend {
.custom_flags(libc::O_NONBLOCK) .custom_flags(libc::O_NONBLOCK)
.open(path) .open(path)
.map_err(|e| { .map_err(|e| {
AppError::Internal(format!("Failed to open HID device {}: {}", path.display(), e)) AppError::Internal(format!(
"Failed to open HID device {}: {}",
path.display(),
e
))
}) })
} }
@@ -361,9 +383,7 @@ 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.exists() self.keyboard_path.exists() && self.mouse_rel_path.exists() && self.mouse_abs_path.exists()
&& self.mouse_rel_path.exists()
&& self.mouse_abs_path.exists()
} }
/// Get list of missing device paths /// Get list of missing device paths
@@ -415,7 +435,10 @@ impl OtgBackend {
self.eagain_count.store(0, Ordering::Relaxed); self.eagain_count.store(0, Ordering::Relaxed);
debug!("Keyboard ESHUTDOWN, closing for recovery"); debug!("Keyboard ESHUTDOWN, closing for recovery");
*dev = None; *dev = None;
Err(Self::io_error_to_hid_error(e, "Failed to write keyboard report")) Err(Self::io_error_to_hid_error(
e,
"Failed to write keyboard report",
))
} }
Some(11) => { Some(11) => {
// EAGAIN after poll - should be rare, silently drop // EAGAIN after poll - should be rare, silently drop
@@ -426,7 +449,10 @@ impl OtgBackend {
self.online.store(false, Ordering::Relaxed); self.online.store(false, Ordering::Relaxed);
self.eagain_count.store(0, Ordering::Relaxed); self.eagain_count.store(0, Ordering::Relaxed);
warn!("Keyboard write error: {}", e); warn!("Keyboard write error: {}", e);
Err(Self::io_error_to_hid_error(e, "Failed to write keyboard report")) Err(Self::io_error_to_hid_error(
e,
"Failed to write keyboard report",
))
} }
} }
} }
@@ -472,7 +498,10 @@ impl OtgBackend {
self.eagain_count.store(0, Ordering::Relaxed); self.eagain_count.store(0, Ordering::Relaxed);
debug!("Relative mouse ESHUTDOWN, closing for recovery"); debug!("Relative mouse ESHUTDOWN, closing for recovery");
*dev = None; *dev = None;
Err(Self::io_error_to_hid_error(e, "Failed to write mouse report")) Err(Self::io_error_to_hid_error(
e,
"Failed to write mouse report",
))
} }
Some(11) => { Some(11) => {
// EAGAIN after poll - should be rare, silently drop // EAGAIN after poll - should be rare, silently drop
@@ -482,7 +511,10 @@ impl OtgBackend {
self.online.store(false, Ordering::Relaxed); self.online.store(false, Ordering::Relaxed);
self.eagain_count.store(0, Ordering::Relaxed); self.eagain_count.store(0, Ordering::Relaxed);
warn!("Relative mouse write error: {}", e); warn!("Relative mouse write error: {}", e);
Err(Self::io_error_to_hid_error(e, "Failed to write mouse report")) Err(Self::io_error_to_hid_error(
e,
"Failed to write mouse report",
))
} }
} }
} }
@@ -534,7 +566,10 @@ impl OtgBackend {
self.eagain_count.store(0, Ordering::Relaxed); self.eagain_count.store(0, Ordering::Relaxed);
debug!("Absolute mouse ESHUTDOWN, closing for recovery"); debug!("Absolute mouse ESHUTDOWN, closing for recovery");
*dev = None; *dev = None;
Err(Self::io_error_to_hid_error(e, "Failed to write mouse report")) Err(Self::io_error_to_hid_error(
e,
"Failed to write mouse report",
))
} }
Some(11) => { Some(11) => {
// EAGAIN after poll - should be rare, silently drop // EAGAIN after poll - should be rare, silently drop
@@ -544,7 +579,10 @@ impl OtgBackend {
self.online.store(false, Ordering::Relaxed); self.online.store(false, Ordering::Relaxed);
self.eagain_count.store(0, Ordering::Relaxed); self.eagain_count.store(0, Ordering::Relaxed);
warn!("Absolute mouse write error: {}", e); warn!("Absolute mouse write error: {}", e);
Err(Self::io_error_to_hid_error(e, "Failed to write mouse report")) Err(Self::io_error_to_hid_error(
e,
"Failed to write mouse report",
))
} }
} }
} }
@@ -590,7 +628,10 @@ impl OtgBackend {
self.online.store(false, Ordering::Relaxed); self.online.store(false, Ordering::Relaxed);
debug!("Consumer control ESHUTDOWN, closing for recovery"); debug!("Consumer control ESHUTDOWN, closing for recovery");
*dev = None; *dev = None;
Err(Self::io_error_to_hid_error(e, "Failed to write consumer report")) Err(Self::io_error_to_hid_error(
e,
"Failed to write consumer report",
))
} }
Some(11) => { Some(11) => {
// EAGAIN after poll - silently drop // EAGAIN after poll - silently drop
@@ -599,7 +640,10 @@ impl OtgBackend {
_ => { _ => {
self.online.store(false, Ordering::Relaxed); self.online.store(false, Ordering::Relaxed);
warn!("Consumer control write error: {}", e); warn!("Consumer control write error: {}", e);
Err(Self::io_error_to_hid_error(e, "Failed to write consumer report")) Err(Self::io_error_to_hid_error(
e,
"Failed to write consumer report",
))
} }
} }
} }
@@ -632,7 +676,10 @@ impl OtgBackend {
} }
Ok(_) => Ok(None), // No data available Ok(_) => Ok(None), // No data available
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None), Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
Err(e) => Err(AppError::Internal(format!("Failed to read LED state: {}", e))), Err(e) => Err(AppError::Internal(format!(
"Failed to read LED state: {}",
e
))),
} }
} else { } else {
Ok(None) Ok(None)
@@ -677,34 +724,55 @@ impl HidBackend for OtgBackend {
*self.keyboard_dev.lock() = Some(file); *self.keyboard_dev.lock() = Some(file);
info!("Keyboard device opened: {}", self.keyboard_path.display()); info!("Keyboard device opened: {}", self.keyboard_path.display());
} else { } else {
warn!("Keyboard device not found: {}", self.keyboard_path.display()); warn!(
"Keyboard device not found: {}",
self.keyboard_path.display()
);
} }
// Open relative mouse device // Open relative mouse device
if self.mouse_rel_path.exists() { if self.mouse_rel_path.exists() {
let file = Self::open_device(&self.mouse_rel_path)?; let file = Self::open_device(&self.mouse_rel_path)?;
*self.mouse_rel_dev.lock() = Some(file); *self.mouse_rel_dev.lock() = Some(file);
info!("Relative mouse device opened: {}", self.mouse_rel_path.display()); info!(
"Relative mouse device opened: {}",
self.mouse_rel_path.display()
);
} else { } else {
warn!("Relative mouse device not found: {}", self.mouse_rel_path.display()); warn!(
"Relative mouse device not found: {}",
self.mouse_rel_path.display()
);
} }
// Open absolute mouse device // Open absolute mouse device
if self.mouse_abs_path.exists() { if self.mouse_abs_path.exists() {
let file = Self::open_device(&self.mouse_abs_path)?; let file = Self::open_device(&self.mouse_abs_path)?;
*self.mouse_abs_dev.lock() = Some(file); *self.mouse_abs_dev.lock() = Some(file);
info!("Absolute mouse device opened: {}", self.mouse_abs_path.display()); info!(
"Absolute mouse device opened: {}",
self.mouse_abs_path.display()
);
} else { } else {
warn!("Absolute mouse device not found: {}", self.mouse_abs_path.display()); warn!(
"Absolute mouse device not found: {}",
self.mouse_abs_path.display()
);
} }
// Open consumer control device (optional, may not exist on older setups) // Open consumer control device (optional, may not exist on older setups)
if self.consumer_path.exists() { if self.consumer_path.exists() {
let file = Self::open_device(&self.consumer_path)?; let file = Self::open_device(&self.consumer_path)?;
*self.consumer_dev.lock() = Some(file); *self.consumer_dev.lock() = Some(file);
info!("Consumer control device opened: {}", self.consumer_path.display()); info!(
"Consumer control device opened: {}",
self.consumer_path.display()
);
} else { } else {
debug!("Consumer control device not found: {}", self.consumer_path.display()); debug!(
"Consumer control device not found: {}",
self.consumer_path.display()
);
} }
// Mark as online if all devices opened successfully // Mark as online if all devices opened successfully

View File

@@ -341,12 +341,7 @@ pub struct MouseReport {
impl MouseReport { impl MouseReport {
/// Convert to bytes for USB HID (relative mouse) /// Convert to bytes for USB HID (relative mouse)
pub fn to_bytes_relative(&self) -> [u8; 4] { pub fn to_bytes_relative(&self) -> [u8; 4] {
[ [self.buttons, self.x as u8, self.y as u8, self.wheel as u8]
self.buttons,
self.x as u8,
self.y as u8,
self.wheel as u8,
]
} }
/// Convert to bytes for USB HID (absolute mouse) /// Convert to bytes for USB HID (absolute mouse)

View File

@@ -50,7 +50,11 @@ async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>) {
vec![RESP_ERR_HID_UNAVAILABLE] vec![RESP_ERR_HID_UNAVAILABLE]
}; };
if sender.send(Message::Binary(initial_response.into())).await.is_err() { if sender
.send(Message::Binary(initial_response.into()))
.await
.is_err()
{
error!("Failed to send initial HID status"); error!("Failed to send initial HID status");
return; return;
} }
@@ -66,7 +70,9 @@ async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>) {
warn!("HID controller not available, ignoring message"); warn!("HID controller not available, ignoring message");
} }
// Send error response (optional, for client awareness) // Send error response (optional, for client awareness)
let _ = sender.send(Message::Binary(vec![RESP_ERR_HID_UNAVAILABLE].into())).await; let _ = sender
.send(Message::Binary(vec![RESP_ERR_HID_UNAVAILABLE].into()))
.await;
continue; continue;
} }
@@ -81,9 +87,14 @@ async fn handle_hid_socket(socket: WebSocket, state: Arc<AppState>) {
Ok(Message::Text(text)) => { Ok(Message::Text(text)) => {
// Text messages are no longer supported // Text messages are no longer supported
if log_throttler.should_log("text_message_rejected") { if log_throttler.should_log("text_message_rejected") {
debug!("Received text message (not supported): {} bytes", text.len()); debug!(
"Received text message (not supported): {} bytes",
text.len()
);
} }
let _ = sender.send(Message::Binary(vec![RESP_ERR_INVALID_MESSAGE].into())).await; let _ = sender
.send(Message::Binary(vec![RESP_ERR_INVALID_MESSAGE].into()))
.await;
} }
Ok(Message::Ping(data)) => { Ok(Message::Ping(data)) => {
let _ = sender.send(Message::Pong(data)).await; let _ = sender.send(Message::Pong(data)).await;
@@ -142,7 +153,7 @@ async fn handle_binary_message(data: &[u8], state: &AppState) -> Result<(), Stri
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::hid::datachannel::{MSG_KEYBOARD, MSG_MOUSE, KB_EVENT_DOWN, MS_EVENT_MOVE}; use crate::hid::datachannel::{KB_EVENT_DOWN, MSG_KEYBOARD, MSG_MOUSE, MS_EVENT_MOVE};
#[test] #[test]
fn test_response_codes() { fn test_response_codes() {

View File

@@ -4,9 +4,9 @@ use std::sync::Arc;
use axum_server::tls_rustls::RustlsConfig; use axum_server::tls_rustls::RustlsConfig;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use rustls::crypto::{ring, CryptoProvider};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use rustls::crypto::{ring, CryptoProvider};
use one_kvm::atx::AtxController; use one_kvm::atx::AtxController;
use one_kvm::audio::{AudioController, AudioControllerConfig, AudioQuality}; use one_kvm::audio::{AudioController, AudioControllerConfig, AudioQuality};
@@ -26,7 +26,15 @@ use one_kvm::webrtc::{WebRtcStreamer, WebRtcStreamerConfig};
/// Log level for the application /// Log level for the application
#[derive(Debug, Clone, Copy, Default, ValueEnum)] #[derive(Debug, Clone, Copy, Default, ValueEnum)]
enum LogLevel {Error, Warn, #[default] Info, Verbose, Debug, Trace,} enum LogLevel {
Error,
Warn,
#[default]
Info,
Verbose,
Debug,
Trace,
}
/// One-KVM command line arguments /// One-KVM command line arguments
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -82,10 +90,7 @@ async fn main() -> anyhow::Result<()> {
CryptoProvider::install_default(ring::default_provider()) CryptoProvider::install_default(ring::default_provider())
.expect("Failed to install rustls crypto provider"); .expect("Failed to install rustls crypto provider");
tracing::info!( tracing::info!("Starting One-KVM v{}", env!("CARGO_PKG_VERSION"));
"Starting One-KVM v{}",
env!("CARGO_PKG_VERSION")
);
// Determine data directory (CLI arg takes precedence) // Determine data directory (CLI arg takes precedence)
let data_dir = args.data_dir.unwrap_or_else(get_data_dir); let data_dir = args.data_dir.unwrap_or_else(get_data_dir);
@@ -153,21 +158,37 @@ async fn main() -> anyhow::Result<()> {
// Parse video configuration once (avoid duplication) // Parse video configuration once (avoid duplication)
let (video_format, video_resolution) = parse_video_config(&config); let (video_format, video_resolution) = parse_video_config(&config);
tracing::debug!("Parsed video config: {} @ {}x{}", video_format, video_resolution.width, video_resolution.height); tracing::debug!(
"Parsed video config: {} @ {}x{}",
video_format,
video_resolution.width,
video_resolution.height
);
// Create video streamer and initialize with config if device is set // Create video streamer and initialize with config if device is set
let streamer = Streamer::new(); let streamer = Streamer::new();
streamer.set_event_bus(events.clone()).await; streamer.set_event_bus(events.clone()).await;
if let Some(ref device_path) = config.video.device { if let Some(ref device_path) = config.video.device {
if let Err(e) = streamer if let Err(e) = streamer
.apply_video_config(device_path, video_format, video_resolution, config.video.fps) .apply_video_config(
device_path,
video_format,
video_resolution,
config.video.fps,
)
.await .await
{ {
tracing::warn!("Failed to initialize video with config: {}, will auto-detect", e); tracing::warn!(
"Failed to initialize video with config: {}, will auto-detect",
e
);
} else { } else {
tracing::info!( tracing::info!(
"Video configured: {} @ {}x{} {}", "Video configured: {} @ {}x{} {}",
device_path, video_resolution.width, video_resolution.height, video_format device_path,
video_resolution.width,
video_resolution.height,
video_format
); );
} }
} }
@@ -185,8 +206,18 @@ async fn main() -> anyhow::Result<()> {
let mut turn_servers = vec![]; let mut turn_servers = vec![];
// Check if user configured custom servers // Check if user configured custom servers
let has_custom_stun = config.stream.stun_server.as_ref().map(|s| !s.is_empty()).unwrap_or(false); let has_custom_stun = config
let has_custom_turn = config.stream.turn_server.as_ref().map(|s| !s.is_empty()).unwrap_or(false); .stream
.stun_server
.as_ref()
.map(|s| !s.is_empty())
.unwrap_or(false);
let has_custom_turn = config
.stream
.turn_server
.as_ref()
.map(|s| !s.is_empty())
.unwrap_or(false);
// If no custom servers, use public ICE servers (like RustDesk) // If no custom servers, use public ICE servers (like RustDesk)
if !has_custom_stun && !has_custom_turn { if !has_custom_stun && !has_custom_turn {
@@ -201,7 +232,9 @@ async fn main() -> anyhow::Result<()> {
turn_servers.push(turn); turn_servers.push(turn);
} }
} else { } else {
tracing::info!("No public ICE servers configured, using host candidates only"); tracing::info!(
"No public ICE servers configured, using host candidates only"
);
} }
} else { } else {
// Use custom servers // Use custom servers
@@ -214,13 +247,18 @@ async fn main() -> anyhow::Result<()> {
if let Some(ref turn) = config.stream.turn_server { if let Some(ref turn) = config.stream.turn_server {
if !turn.is_empty() { if !turn.is_empty() {
let username = config.stream.turn_username.clone().unwrap_or_default(); let username = config.stream.turn_username.clone().unwrap_or_default();
let credential = config.stream.turn_password.clone().unwrap_or_default(); let credential =
config.stream.turn_password.clone().unwrap_or_default();
turn_servers.push(one_kvm::webrtc::config::TurnServer::new( turn_servers.push(one_kvm::webrtc::config::TurnServer::new(
turn.clone(), turn.clone(),
username.clone(), username.clone(),
credential, credential,
)); ));
tracing::info!("Using custom TURN server: {} (user: {})", turn, username); tracing::info!(
"Using custom TURN server: {} (user: {})",
turn,
username
);
} }
} }
} }
@@ -237,7 +275,6 @@ async fn main() -> anyhow::Result<()> {
}; };
tracing::info!("WebRTC streamer created (supports H264, extensible to VP8/VP9/H265)"); tracing::info!("WebRTC streamer created (supports H264, extensible to VP8/VP9/H265)");
// Create OTG Service (single instance for centralized USB gadget management) // Create OTG Service (single instance for centralized USB gadget management)
let otg_service = Arc::new(OtgService::new()); let otg_service = Arc::new(OtgService::new());
tracing::info!("OTG Service created"); tracing::info!("OTG Service created");
@@ -285,14 +322,26 @@ async fn main() -> anyhow::Result<()> {
if ventoy_resource_dir.exists() { if ventoy_resource_dir.exists() {
if let Err(e) = ventoy_img::init_resources(&ventoy_resource_dir) { if let Err(e) = ventoy_img::init_resources(&ventoy_resource_dir) {
tracing::warn!("Failed to initialize Ventoy resources: {}", e); tracing::warn!("Failed to initialize Ventoy resources: {}", e);
tracing::info!("Ventoy resource files should be placed in: {}", ventoy_resource_dir.display()); tracing::info!(
"Ventoy resource files should be placed in: {}",
ventoy_resource_dir.display()
);
tracing::info!("Required files: {:?}", ventoy_img::required_files()); tracing::info!("Required files: {:?}", ventoy_img::required_files());
} else { } else {
tracing::info!("Ventoy resources initialized from {}", ventoy_resource_dir.display()); tracing::info!(
"Ventoy resources initialized from {}",
ventoy_resource_dir.display()
);
} }
} else { } else {
tracing::warn!("Ventoy resource directory not found: {}", ventoy_resource_dir.display()); tracing::warn!(
tracing::info!("Create the directory and place the following files: {:?}", ventoy_img::required_files()); "Ventoy resource directory not found: {}",
ventoy_resource_dir.display()
);
tracing::info!(
"Create the directory and place the following files: {:?}",
ventoy_img::required_files()
);
} }
let controller = MsdController::new( let controller = MsdController::new(
@@ -382,27 +431,42 @@ async fn main() -> anyhow::Result<()> {
let (actual_format, actual_resolution, actual_fps) = streamer.current_video_config().await; let (actual_format, actual_resolution, actual_fps) = streamer.current_video_config().await;
tracing::info!( tracing::info!(
"Initial video config from capturer: {}x{} {:?} @ {}fps", "Initial video config from capturer: {}x{} {:?} @ {}fps",
actual_resolution.width, actual_resolution.height, actual_format, actual_fps actual_resolution.width,
actual_resolution.height,
actual_format,
actual_fps
); );
webrtc_streamer.update_video_config(actual_resolution, actual_format, actual_fps).await; webrtc_streamer
.update_video_config(actual_resolution, actual_format, actual_fps)
.await;
webrtc_streamer.set_video_source(frame_tx).await; webrtc_streamer.set_video_source(frame_tx).await;
tracing::info!("WebRTC streamer connected to video frame source"); tracing::info!("WebRTC streamer connected to video frame source");
} else { } else {
tracing::warn!("Video capturer not ready, WebRTC will connect to frame source when available"); tracing::warn!(
"Video capturer not ready, WebRTC will connect to frame source when available"
);
} }
// Create video stream manager (unified MJPEG/WebRTC management) // Create video stream manager (unified MJPEG/WebRTC management)
// Use with_webrtc_streamer to ensure we use the same WebRtcStreamer instance // Use with_webrtc_streamer to ensure we use the same WebRtcStreamer instance
let stream_manager = VideoStreamManager::with_webrtc_streamer(streamer.clone(), webrtc_streamer.clone()); let stream_manager =
VideoStreamManager::with_webrtc_streamer(streamer.clone(), webrtc_streamer.clone());
stream_manager.set_event_bus(events.clone()).await; stream_manager.set_event_bus(events.clone()).await;
stream_manager.set_config_store(config_store.clone()).await; stream_manager.set_config_store(config_store.clone()).await;
// Initialize stream manager with configured mode // Initialize stream manager with configured mode
let initial_mode = config.stream.mode.clone(); let initial_mode = config.stream.mode.clone();
if let Err(e) = stream_manager.init_with_mode(initial_mode.clone()).await { if let Err(e) = stream_manager.init_with_mode(initial_mode.clone()).await {
tracing::warn!("Failed to initialize stream manager with mode {:?}: {}", initial_mode, e); tracing::warn!(
"Failed to initialize stream manager with mode {:?}: {}",
initial_mode,
e
);
} else { } else {
tracing::info!("Video stream manager initialized with mode: {:?}", initial_mode); tracing::info!(
"Video stream manager initialized with mode: {:?}",
initial_mode
);
} }
// Create RustDesk service (optional, based on config) // Create RustDesk service (optional, based on config)
@@ -421,7 +485,9 @@ async fn main() -> anyhow::Result<()> {
Some(Arc::new(service)) Some(Arc::new(service))
} else { } else {
if config.rustdesk.enabled { if config.rustdesk.enabled {
tracing::warn!("RustDesk enabled but configuration is incomplete (missing server or credentials)"); tracing::warn!(
"RustDesk enabled but configuration is incomplete (missing server or credentials)"
);
} else { } else {
tracing::info!("RustDesk disabled in configuration"); tracing::info!("RustDesk disabled in configuration");
} }
@@ -458,7 +524,8 @@ async fn main() -> anyhow::Result<()> {
cfg.rustdesk.public_key = updated_config.public_key.clone(); cfg.rustdesk.public_key = updated_config.public_key.clone();
cfg.rustdesk.private_key = updated_config.private_key.clone(); cfg.rustdesk.private_key = updated_config.private_key.clone();
cfg.rustdesk.signing_public_key = updated_config.signing_public_key.clone(); cfg.rustdesk.signing_public_key = updated_config.signing_public_key.clone();
cfg.rustdesk.signing_private_key = updated_config.signing_private_key.clone(); cfg.rustdesk.signing_private_key =
updated_config.signing_private_key.clone();
cfg.rustdesk.uuid = updated_config.uuid.clone(); cfg.rustdesk.uuid = updated_config.uuid.clone();
}) })
.await .await
@@ -542,8 +609,7 @@ async fn main() -> anyhow::Result<()> {
tracing::info!("Starting HTTPS server on {}", bind_addr); tracing::info!("Starting HTTPS server on {}", bind_addr);
let server = axum_server::bind_rustls(bind_addr, tls_config) let server = axum_server::bind_rustls(bind_addr, tls_config).serve(app.into_make_service());
.serve(app.into_make_service());
tokio::select! { tokio::select! {
_ = shutdown_signal => { _ = shutdown_signal => {
@@ -600,8 +666,8 @@ fn init_logging(level: LogLevel, verbose_count: u8) {
}; };
// Environment variable takes highest priority // Environment variable takes highest priority
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() let env_filter =
.unwrap_or_else(|_| filter.into()); tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into());
tracing_subscriber::registry() tracing_subscriber::registry()
.with(env_filter) .with(env_filter)
@@ -662,7 +728,8 @@ fn spawn_device_info_broadcaster(state: Arc<AppState>, events: Arc<EventBus>) {
loop { loop {
// Use timeout to handle pending broadcasts // Use timeout to handle pending broadcasts
let recv_result = if pending_broadcast { let recv_result = if pending_broadcast {
let remaining = DEBOUNCE_MS.saturating_sub(last_broadcast.elapsed().as_millis() as u64); let remaining =
DEBOUNCE_MS.saturating_sub(last_broadcast.elapsed().as_millis() as u64);
tokio::time::timeout(Duration::from_millis(remaining), rx.recv()).await tokio::time::timeout(Duration::from_millis(remaining), rx.recv()).await
} else { } else {
Ok(rx.recv().await) Ok(rx.recv().await)
@@ -674,6 +741,7 @@ fn spawn_device_info_broadcaster(state: Arc<AppState>, events: Arc<EventBus>) {
event, event,
SystemEvent::StreamStateChanged { .. } SystemEvent::StreamStateChanged { .. }
| SystemEvent::StreamConfigApplied { .. } | SystemEvent::StreamConfigApplied { .. }
| SystemEvent::StreamModeReady { .. }
| SystemEvent::HidStateChanged { .. } | SystemEvent::HidStateChanged { .. }
| SystemEvent::MsdStateChanged { .. } | SystemEvent::MsdStateChanged { .. }
| SystemEvent::AtxStateChanged { .. } | SystemEvent::AtxStateChanged { .. }
@@ -706,7 +774,10 @@ fn spawn_device_info_broadcaster(state: Arc<AppState>, events: Arc<EventBus>) {
} }
}); });
tracing::info!("DeviceInfo broadcaster task started (debounce: {}ms)", DEBOUNCE_MS); tracing::info!(
"DeviceInfo broadcaster task started (debounce: {}ms)",
DEBOUNCE_MS
);
} }
/// Clean up subsystems on shutdown /// Clean up subsystems on shutdown

View File

@@ -99,7 +99,10 @@ impl MsdController {
initialized: true, initialized: true,
path: self.drive_path.clone(), path: self.drive_path.clone(),
}); });
debug!("Found existing virtual drive: {}", self.drive_path.display()); debug!(
"Found existing virtual drive: {}",
self.drive_path.display()
);
} }
} }
@@ -146,7 +149,12 @@ impl MsdController {
/// * `image` - Image info to mount /// * `image` - Image info to mount
/// * `cdrom` - Mount as CD-ROM (read-only, removable) /// * `cdrom` - Mount as CD-ROM (read-only, removable)
/// * `read_only` - Mount as read-only /// * `read_only` - Mount as read-only
pub async fn connect_image(&self, image: &ImageInfo, cdrom: bool, read_only: bool) -> Result<()> { pub async fn connect_image(
&self,
image: &ImageInfo,
cdrom: bool,
read_only: bool,
) -> Result<()> {
// Acquire operation lock to prevent concurrent operations // Acquire operation lock to prevent concurrent operations
let _op_guard = self.operation_lock.write().await; let _op_guard = self.operation_lock.write().await;
@@ -154,7 +162,9 @@ impl MsdController {
if !state.available { if !state.available {
let err = AppError::Internal("MSD not available".to_string()); let err = AppError::Internal("MSD not available".to_string());
self.monitor.report_error("MSD not available", "not_available").await; self.monitor
.report_error("MSD not available", "not_available")
.await;
return Err(err); return Err(err);
} }
@@ -167,7 +177,9 @@ impl MsdController {
// Verify image exists // Verify image exists
if !image.path.exists() { if !image.path.exists() {
let error_msg = format!("Image file not found: {}", image.path.display()); let error_msg = format!("Image file not found: {}", image.path.display());
self.monitor.report_error(&error_msg, "image_not_found").await; self.monitor
.report_error(&error_msg, "image_not_found")
.await;
return Err(AppError::Internal(error_msg)); return Err(AppError::Internal(error_msg));
} }
@@ -182,12 +194,16 @@ impl MsdController {
if let Some(ref msd) = *self.msd_function.read().await { if let Some(ref msd) = *self.msd_function.read().await {
if let Err(e) = msd.configure_lun_async(&gadget_path, 0, &config).await { if let Err(e) = msd.configure_lun_async(&gadget_path, 0, &config).await {
let error_msg = format!("Failed to configure LUN: {}", e); let error_msg = format!("Failed to configure LUN: {}", e);
self.monitor.report_error(&error_msg, "configfs_error").await; self.monitor
.report_error(&error_msg, "configfs_error")
.await;
return Err(e); return Err(e);
} }
} else { } else {
let err = AppError::Internal("MSD function not initialized".to_string()); let err = AppError::Internal("MSD function not initialized".to_string());
self.monitor.report_error("MSD function not initialized", "not_initialized").await; self.monitor
.report_error("MSD function not initialized", "not_initialized")
.await;
return Err(err); return Err(err);
} }
@@ -236,7 +252,9 @@ impl MsdController {
if !state.available { if !state.available {
let err = AppError::Internal("MSD not available".to_string()); let err = AppError::Internal("MSD not available".to_string());
self.monitor.report_error("MSD not available", "not_available").await; self.monitor
.report_error("MSD not available", "not_available")
.await;
return Err(err); return Err(err);
} }
@@ -248,10 +266,11 @@ impl MsdController {
// Check drive exists // Check drive exists
if !self.drive_path.exists() { if !self.drive_path.exists() {
let err = AppError::Internal( let err =
"Virtual drive not initialized. Call init first.".to_string(), AppError::Internal("Virtual drive not initialized. Call init first.".to_string());
); self.monitor
self.monitor.report_error("Virtual drive not initialized", "drive_not_found").await; .report_error("Virtual drive not initialized", "drive_not_found")
.await;
return Err(err); return Err(err);
} }
@@ -262,12 +281,16 @@ impl MsdController {
if let Some(ref msd) = *self.msd_function.read().await { if let Some(ref msd) = *self.msd_function.read().await {
if let Err(e) = msd.configure_lun_async(&gadget_path, 0, &config).await { if let Err(e) = msd.configure_lun_async(&gadget_path, 0, &config).await {
let error_msg = format!("Failed to configure LUN: {}", e); let error_msg = format!("Failed to configure LUN: {}", e);
self.monitor.report_error(&error_msg, "configfs_error").await; self.monitor
.report_error(&error_msg, "configfs_error")
.await;
return Err(e); return Err(e);
} }
} else { } else {
let err = AppError::Internal("MSD function not initialized".to_string()); let err = AppError::Internal("MSD function not initialized".to_string());
self.monitor.report_error("MSD function not initialized", "not_initialized").await; self.monitor
.report_error("MSD function not initialized", "not_initialized")
.await;
return Err(err); return Err(err);
} }
@@ -381,12 +404,9 @@ impl MsdController {
} }
// Extract filename for initial response // Extract filename for initial response
let display_filename = filename.clone().unwrap_or_else(|| { let display_filename = filename
url.rsplit('/') .clone()
.next() .unwrap_or_else(|| url.rsplit('/').next().unwrap_or("download").to_string());
.unwrap_or("download")
.to_string()
});
// Create initial progress // Create initial progress
let initial_progress = DownloadProgress { let initial_progress = DownloadProgress {

View File

@@ -42,9 +42,8 @@ impl ImageManager {
/// Ensure images directory exists /// Ensure images directory exists
pub fn ensure_dir(&self) -> Result<()> { pub fn ensure_dir(&self) -> Result<()> {
fs::create_dir_all(&self.images_path).map_err(|e| { fs::create_dir_all(&self.images_path)
AppError::Internal(format!("Failed to create images directory: {}", e)) .map_err(|e| AppError::Internal(format!("Failed to create images directory: {}", e)))?;
})?;
Ok(()) Ok(())
} }
@@ -54,9 +53,9 @@ impl ImageManager {
let mut images = Vec::new(); let mut images = Vec::new();
for entry in fs::read_dir(&self.images_path).map_err(|e| { for entry in fs::read_dir(&self.images_path)
AppError::Internal(format!("Failed to read images directory: {}", e)) .map_err(|e| AppError::Internal(format!("Failed to read images directory: {}", e)))?
})? { {
let entry = entry.map_err(|e| { let entry = entry.map_err(|e| {
AppError::Internal(format!("Failed to read directory entry: {}", e)) AppError::Internal(format!("Failed to read directory entry: {}", e))
})?; })?;
@@ -146,9 +145,8 @@ impl ImageManager {
))); )));
} }
let mut file = File::create(&path).map_err(|e| { let mut file = File::create(&path)
AppError::Internal(format!("Failed to create image file: {}", e)) .map_err(|e| AppError::Internal(format!("Failed to create image file: {}", e)))?;
})?;
file.write_all(data).map_err(|e| { file.write_all(data).map_err(|e| {
// Try to clean up on error // Try to clean up on error
@@ -193,9 +191,8 @@ impl ImageManager {
} }
// Create file and copy data // Create file and copy data
let mut file = File::create(&path).map_err(|e| { let mut file = File::create(&path)
AppError::Internal(format!("Failed to create image file: {}", e)) .map_err(|e| AppError::Internal(format!("Failed to create image file: {}", e)))?;
})?;
let bytes_written = io::copy(reader, &mut file).map_err(|e| { let bytes_written = io::copy(reader, &mut file).map_err(|e| {
let _ = fs::remove_file(&path); let _ = fs::remove_file(&path);
@@ -244,9 +241,11 @@ impl ImageManager {
let mut bytes_written: u64 = 0; let mut bytes_written: u64 = 0;
// Stream chunks directly to disk // Stream chunks directly to disk
while let Some(chunk) = field.chunk().await.map_err(|e| { while let Some(chunk) = field
AppError::Internal(format!("Failed to read upload chunk: {}", e)) .chunk()
})? { .await
.map_err(|e| AppError::Internal(format!("Failed to read upload chunk: {}", e)))?
{
// Check size limit // Check size limit
bytes_written += chunk.len() as u64; bytes_written += chunk.len() as u64;
if bytes_written > MAX_IMAGE_SIZE { if bytes_written > MAX_IMAGE_SIZE {
@@ -260,15 +259,15 @@ impl ImageManager {
} }
// Write chunk to file // Write chunk to file
file.write_all(&chunk).await.map_err(|e| { file.write_all(&chunk)
AppError::Internal(format!("Failed to write chunk: {}", e)) .await
})?; .map_err(|e| AppError::Internal(format!("Failed to write chunk: {}", e)))?;
} }
// Flush and close file // Flush and close file
file.flush().await.map_err(|e| { file.flush()
AppError::Internal(format!("Failed to flush file: {}", e)) .await
})?; .map_err(|e| AppError::Internal(format!("Failed to flush file: {}", e)))?;
drop(file); drop(file);
// Move temp file to final location // Move temp file to final location
@@ -279,7 +278,10 @@ impl ImageManager {
AppError::Internal(format!("Failed to rename temp file: {}", e)) AppError::Internal(format!("Failed to rename temp file: {}", e))
})?; })?;
info!("Created image (streaming): {} ({} bytes)", name, bytes_written); info!(
"Created image (streaming): {} ({} bytes)",
name, bytes_written
);
self.get_by_name(&name) self.get_by_name(&name)
} }
@@ -288,9 +290,8 @@ impl ImageManager {
pub fn delete(&self, id: &str) -> Result<()> { pub fn delete(&self, id: &str) -> Result<()> {
let image = self.get(id)?; let image = self.get(id)?;
fs::remove_file(&image.path).map_err(|e| { fs::remove_file(&image.path)
AppError::Internal(format!("Failed to delete image: {}", e)) .map_err(|e| AppError::Internal(format!("Failed to delete image: {}", e)))?;
})?;
info!("Deleted image: {}", image.name); info!("Deleted image: {}", image.name);
Ok(()) Ok(())
@@ -304,9 +305,8 @@ impl ImageManager {
return Err(AppError::NotFound(format!("Image not found: {}", name))); return Err(AppError::NotFound(format!("Image not found: {}", name)));
} }
fs::remove_file(&path).map_err(|e| { fs::remove_file(&path)
AppError::Internal(format!("Failed to delete image: {}", e)) .map_err(|e| AppError::Internal(format!("Failed to delete image: {}", e)))?;
})?;
info!("Deleted image: {}", name); info!("Deleted image: {}", name);
Ok(()) Ok(())
@@ -414,7 +414,9 @@ impl ImageManager {
}; };
if final_filename.is_empty() { if final_filename.is_empty() {
return Err(AppError::BadRequest("Could not determine filename".to_string())); return Err(AppError::BadRequest(
"Could not determine filename".to_string(),
));
} }
// Check if file already exists // Check if file already exists
@@ -468,12 +470,10 @@ impl ImageManager {
progress_callback(0, content_length); progress_callback(0, content_length);
while let Some(chunk_result) = stream.next().await { while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result let chunk =
.map_err(|e| AppError::Internal(format!("Download error: {}", e)))?; chunk_result.map_err(|e| AppError::Internal(format!("Download error: {}", e)))?;
file.write_all(&chunk) file.write_all(&chunk).await.map_err(|e| {
.await
.map_err(|e| {
// Cleanup on error // Cleanup on error
let _ = std::fs::remove_file(&temp_path); let _ = std::fs::remove_file(&temp_path);
AppError::Internal(format!("Failed to write data: {}", e)) AppError::Internal(format!("Failed to write data: {}", e))

View File

@@ -15,19 +15,19 @@
//! ``` //! ```
pub mod controller; pub mod controller;
pub mod ventoy_drive;
pub mod image; pub mod image;
pub mod monitor; pub mod monitor;
pub mod types; pub mod types;
pub mod ventoy_drive;
pub use controller::MsdController; pub use controller::MsdController;
pub use ventoy_drive::VentoyDrive;
pub use image::ImageManager; pub use image::ImageManager;
pub use monitor::{MsdHealthMonitor, MsdHealthStatus, MsdMonitorConfig}; pub use monitor::{MsdHealthMonitor, MsdHealthStatus, MsdMonitorConfig};
pub use types::{ pub use types::{
DownloadProgress, DownloadStatus, DriveFile, DriveInfo, DriveInitRequest, ImageDownloadRequest, DownloadProgress, DownloadStatus, DriveFile, DriveInfo, DriveInitRequest, ImageDownloadRequest,
ImageInfo, MsdConnectRequest, MsdMode, MsdState, ImageInfo, MsdConnectRequest, MsdMode, MsdState,
}; };
pub use ventoy_drive::VentoyDrive;
// Re-export from otg module for backward compatibility // Re-export from otg module for backward compatibility
pub use crate::otg::{MsdFunction, MsdLunConfig}; pub use crate::otg::{MsdFunction, MsdLunConfig};

View File

@@ -120,7 +120,10 @@ impl MsdHealthMonitor {
// Log with throttling (always log if error type changed) // Log with throttling (always log if error type changed)
let throttle_key = format!("msd_{}", error_code); let throttle_key = format!("msd_{}", error_code);
if error_changed || self.throttler.should_log(&throttle_key) { if error_changed || self.throttler.should_log(&throttle_key) {
warn!("MSD error: {} (code: {}, count: {})", reason, error_code, count); warn!(
"MSD error: {} (code: {}, count: {})",
reason, error_code, count
);
} }
// Update last error code // Update last error code

View File

@@ -71,13 +71,11 @@ impl VentoyDrive {
// Run Ventoy creation in blocking task // Run Ventoy creation in blocking task
let info = tokio::task::spawn_blocking(move || { let info = tokio::task::spawn_blocking(move || {
VentoyImage::create(&path, &size_str, DEFAULT_LABEL) VentoyImage::create(&path, &size_str, DEFAULT_LABEL).map_err(ventoy_to_app_error)?;
.map_err(ventoy_to_app_error)?;
// Get file metadata for DriveInfo // Get file metadata for DriveInfo
let metadata = std::fs::metadata(&path).map_err(|e| { let metadata = std::fs::metadata(&path)
AppError::Internal(format!("Failed to read drive metadata: {}", e)) .map_err(|e| AppError::Internal(format!("Failed to read drive metadata: {}", e)))?;
})?;
Ok::<DriveInfo, AppError>(DriveInfo { Ok::<DriveInfo, AppError>(DriveInfo {
size: metadata.len(), size: metadata.len(),
@@ -104,16 +102,13 @@ impl VentoyDrive {
let _lock = self.lock.read().await; // Read lock for info query let _lock = self.lock.read().await; // Read lock for info query
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let metadata = std::fs::metadata(&path).map_err(|e| { let metadata = std::fs::metadata(&path)
AppError::Internal(format!("Failed to read drive metadata: {}", e)) .map_err(|e| AppError::Internal(format!("Failed to read drive metadata: {}", e)))?;
})?;
// Open image to get file list and calculate used space // Open image to get file list and calculate used space
let image = VentoyImage::open(&path).map_err(ventoy_to_app_error)?; let image = VentoyImage::open(&path).map_err(ventoy_to_app_error)?;
let files = image let files = image.list_files_recursive().map_err(ventoy_to_app_error)?;
.list_files_recursive()
.map_err(ventoy_to_app_error)?;
let used: u64 = files let used: u64 = files
.iter() .iter()
@@ -190,9 +185,11 @@ impl VentoyDrive {
let mut bytes_written: u64 = 0; let mut bytes_written: u64 = 0;
while let Some(chunk) = field.chunk().await.map_err(|e| { while let Some(chunk) = field
AppError::Internal(format!("Failed to read upload chunk: {}", e)) .chunk()
})? { .await
.map_err(|e| AppError::Internal(format!("Failed to read upload chunk: {}", e)))?
{
bytes_written += chunk.len() as u64; bytes_written += chunk.len() as u64;
tokio::io::AsyncWriteExt::write_all(&mut temp_file, &chunk) tokio::io::AsyncWriteExt::write_all(&mut temp_file, &chunk)
.await .await
@@ -248,9 +245,7 @@ impl VentoyDrive {
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let image = VentoyImage::open(&path).map_err(ventoy_to_app_error)?; let image = VentoyImage::open(&path).map_err(ventoy_to_app_error)?;
image image.read_file(&file_path).map_err(ventoy_to_app_error)
.read_file(&file_path)
.map_err(ventoy_to_app_error)
}) })
.await .await
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))? .map_err(|e| AppError::Internal(format!("Task join error: {}", e)))?
@@ -321,7 +316,8 @@ impl VentoyDrive {
let lock = self.lock.clone(); let lock = self.lock.clone();
// Create a channel for streaming data // Create a channel for streaming data
let (tx, rx) = tokio::sync::mpsc::channel::<std::result::Result<bytes::Bytes, std::io::Error>>(8); let (tx, rx) =
tokio::sync::mpsc::channel::<std::result::Result<bytes::Bytes, std::io::Error>>(8);
// Spawn blocking task to read and send chunks // Spawn blocking task to read and send chunks
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
@@ -404,20 +400,14 @@ fn ventoy_to_app_error(err: VentoyError) -> AppError {
match err { match err {
VentoyError::Io(e) => AppError::Io(e), VentoyError::Io(e) => AppError::Io(e),
VentoyError::InvalidSize(s) => AppError::BadRequest(format!("Invalid size: {}", s)), VentoyError::InvalidSize(s) => AppError::BadRequest(format!("Invalid size: {}", s)),
VentoyError::SizeParseError(s) => { VentoyError::SizeParseError(s) => AppError::BadRequest(format!("Size parse error: {}", s)),
AppError::BadRequest(format!("Size parse error: {}", s)) VentoyError::FilesystemError(s) => AppError::Internal(format!("Filesystem error: {}", s)),
}
VentoyError::FilesystemError(s) => {
AppError::Internal(format!("Filesystem error: {}", s))
}
VentoyError::ImageError(s) => AppError::Internal(format!("Image error: {}", s)), VentoyError::ImageError(s) => AppError::Internal(format!("Image error: {}", s)),
VentoyError::FileNotFound(s) => AppError::NotFound(format!("File not found: {}", s)), VentoyError::FileNotFound(s) => AppError::NotFound(format!("File not found: {}", s)),
VentoyError::ResourceNotFound(s) => { VentoyError::ResourceNotFound(s) => {
AppError::Internal(format!("Resource not found: {}", s)) AppError::Internal(format!("Resource not found: {}", s))
} }
VentoyError::PartitionError(s) => { VentoyError::PartitionError(s) => AppError::Internal(format!("Partition error: {}", s)),
AppError::Internal(format!("Partition error: {}", s))
}
} }
} }
@@ -481,7 +471,8 @@ impl std::io::Write for ChannelWriter {
let space = STREAM_CHUNK_SIZE - self.buffer.len(); let space = STREAM_CHUNK_SIZE - self.buffer.len();
let to_copy = std::cmp::min(space, buf.len() - written); let to_copy = std::cmp::min(space, buf.len() - written);
self.buffer.extend_from_slice(&buf[written..written + to_copy]); self.buffer
.extend_from_slice(&buf[written..written + to_copy]);
written += to_copy; written += to_copy;
if self.buffer.len() >= STREAM_CHUNK_SIZE { if self.buffer.len() >= STREAM_CHUNK_SIZE {
@@ -512,10 +503,7 @@ mod tests {
use tempfile::TempDir; use tempfile::TempDir;
/// Path to ventoy resources directory /// Path to ventoy resources directory
static RESOURCE_DIR: &str = concat!( static RESOURCE_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../ventoy-img-rs/resources");
env!("CARGO_MANIFEST_DIR"),
"/../ventoy-img-rs/resources"
);
/// Initialize ventoy resources once /// Initialize ventoy resources once
fn init_ventoy_resources() -> bool { fn init_ventoy_resources() -> bool {
@@ -561,7 +549,10 @@ mod tests {
if !output.status.success() { if !output.status.success() {
return Err(std::io::Error::new( return Err(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
format!("xz decompress failed: {}", String::from_utf8_lossy(&output.stderr)), format!(
"xz decompress failed: {}",
String::from_utf8_lossy(&output.stderr)
),
)); ));
} }

View File

@@ -109,15 +109,25 @@ pub fn read_file(path: &Path) -> Result<String> {
/// Create directory if not exists /// Create directory if not exists
pub fn create_dir(path: &Path) -> Result<()> { pub fn create_dir(path: &Path) -> Result<()> {
fs::create_dir_all(path) fs::create_dir_all(path).map_err(|e| {
.map_err(|e| AppError::Internal(format!("Failed to create directory {}: {}", path.display(), e))) AppError::Internal(format!(
"Failed to create directory {}: {}",
path.display(),
e
))
})
} }
/// Remove directory /// Remove directory
pub fn remove_dir(path: &Path) -> Result<()> { pub fn remove_dir(path: &Path) -> Result<()> {
if path.exists() { if path.exists() {
fs::remove_dir(path) fs::remove_dir(path).map_err(|e| {
.map_err(|e| AppError::Internal(format!("Failed to remove directory {}: {}", path.display(), e)))?; AppError::Internal(format!(
"Failed to remove directory {}: {}",
path.display(),
e
))
})?;
} }
Ok(()) Ok(())
} }
@@ -125,14 +135,21 @@ pub fn remove_dir(path: &Path) -> Result<()> {
/// Remove file /// Remove file
pub fn remove_file(path: &Path) -> Result<()> { pub fn remove_file(path: &Path) -> Result<()> {
if path.exists() { if path.exists() {
fs::remove_file(path) fs::remove_file(path).map_err(|e| {
.map_err(|e| AppError::Internal(format!("Failed to remove file {}: {}", path.display(), e)))?; AppError::Internal(format!("Failed to remove file {}: {}", path.display(), e))
})?;
} }
Ok(()) Ok(())
} }
/// Create symlink /// Create symlink
pub fn create_symlink(src: &Path, dest: &Path) -> Result<()> { pub fn create_symlink(src: &Path, dest: &Path) -> Result<()> {
std::os::unix::fs::symlink(src, dest) std::os::unix::fs::symlink(src, dest).map_err(|e| {
.map_err(|e| AppError::Internal(format!("Failed to create symlink {} -> {}: {}", dest.display(), src.display(), e))) AppError::Internal(format!(
"Failed to create symlink {} -> {}: {}",
dest.display(),
src.display(),
e
))
})
} }

View File

@@ -3,7 +3,9 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tracing::debug; use tracing::debug;
use super::configfs::{create_dir, create_symlink, remove_dir, remove_file, write_bytes, write_file}; use super::configfs::{
create_dir, create_symlink, remove_dir, remove_file, write_bytes, write_file,
};
use super::function::{FunctionMeta, GadgetFunction}; use super::function::{FunctionMeta, GadgetFunction};
use super::report_desc::{CONSUMER_CONTROL, KEYBOARD, MOUSE_ABSOLUTE, MOUSE_RELATIVE}; use super::report_desc::{CONSUMER_CONTROL, KEYBOARD, MOUSE_ABSOLUTE, MOUSE_RELATIVE};
use crate::error::Result; use crate::error::Result;
@@ -169,14 +171,27 @@ impl GadgetFunction for HidFunction {
create_dir(&func_path)?; create_dir(&func_path)?;
// Set HID parameters // Set HID parameters
write_file(&func_path.join("protocol"), &self.func_type.protocol().to_string())?; write_file(
write_file(&func_path.join("subclass"), &self.func_type.subclass().to_string())?; &func_path.join("protocol"),
write_file(&func_path.join("report_length"), &self.func_type.report_length().to_string())?; &self.func_type.protocol().to_string(),
)?;
write_file(
&func_path.join("subclass"),
&self.func_type.subclass().to_string(),
)?;
write_file(
&func_path.join("report_length"),
&self.func_type.report_length().to_string(),
)?;
// Write report descriptor // Write report descriptor
write_bytes(&func_path.join("report_desc"), self.func_type.report_desc())?; write_bytes(&func_path.join("report_desc"), self.func_type.report_desc())?;
debug!("Created HID function: {} at {}", self.name(), func_path.display()); debug!(
"Created HID function: {} at {}",
self.name(),
func_path.display()
);
Ok(()) Ok(())
} }

View File

@@ -7,7 +7,8 @@ use tracing::{debug, error, info, warn};
use super::configfs::{ use super::configfs::{
create_dir, find_udc, is_configfs_available, remove_dir, write_file, CONFIGFS_PATH, create_dir, find_udc, is_configfs_available, remove_dir, write_file, CONFIGFS_PATH,
DEFAULT_GADGET_NAME, DEFAULT_USB_BCD_DEVICE, USB_BCD_USB, DEFAULT_USB_PRODUCT_ID, DEFAULT_USB_VENDOR_ID, DEFAULT_GADGET_NAME, DEFAULT_USB_BCD_DEVICE, DEFAULT_USB_PRODUCT_ID, DEFAULT_USB_VENDOR_ID,
USB_BCD_USB,
}; };
use super::endpoint::{EndpointAllocator, DEFAULT_MAX_ENDPOINTS}; use super::endpoint::{EndpointAllocator, DEFAULT_MAX_ENDPOINTS};
use super::function::{FunctionMeta, GadgetFunction}; use super::function::{FunctionMeta, GadgetFunction};
@@ -77,7 +78,11 @@ impl OtgGadgetManager {
} }
/// Create a new gadget manager with custom descriptor /// Create a new gadget manager with custom descriptor
pub fn with_descriptor(gadget_name: &str, max_endpoints: u8, descriptor: GadgetDescriptor) -> Self { pub fn with_descriptor(
gadget_name: &str,
max_endpoints: u8,
descriptor: GadgetDescriptor,
) -> Self {
let gadget_path = PathBuf::from(CONFIGFS_PATH).join(gadget_name); let gadget_path = PathBuf::from(CONFIGFS_PATH).join(gadget_name);
let config_path = gadget_path.join("configs/c.1"); let config_path = gadget_path.join("configs/c.1");
@@ -303,10 +308,22 @@ impl OtgGadgetManager {
/// Set USB device descriptors /// Set USB device descriptors
fn set_device_descriptors(&self) -> Result<()> { fn set_device_descriptors(&self) -> Result<()> {
write_file(&self.gadget_path.join("idVendor"), &format!("0x{:04x}", self.descriptor.vendor_id))?; write_file(
write_file(&self.gadget_path.join("idProduct"), &format!("0x{:04x}", self.descriptor.product_id))?; &self.gadget_path.join("idVendor"),
write_file(&self.gadget_path.join("bcdDevice"), &format!("0x{:04x}", self.descriptor.device_version))?; &format!("0x{:04x}", self.descriptor.vendor_id),
write_file(&self.gadget_path.join("bcdUSB"), &format!("0x{:04x}", USB_BCD_USB))?; )?;
write_file(
&self.gadget_path.join("idProduct"),
&format!("0x{:04x}", self.descriptor.product_id),
)?;
write_file(
&self.gadget_path.join("bcdDevice"),
&format!("0x{:04x}", self.descriptor.device_version),
)?;
write_file(
&self.gadget_path.join("bcdUSB"),
&format!("0x{:04x}", USB_BCD_USB),
)?;
write_file(&self.gadget_path.join("bDeviceClass"), "0x00")?; // Composite device write_file(&self.gadget_path.join("bDeviceClass"), "0x00")?; // Composite device
write_file(&self.gadget_path.join("bDeviceSubClass"), "0x00")?; write_file(&self.gadget_path.join("bDeviceSubClass"), "0x00")?;
write_file(&self.gadget_path.join("bDeviceProtocol"), "0x00")?; write_file(&self.gadget_path.join("bDeviceProtocol"), "0x00")?;
@@ -319,8 +336,14 @@ impl OtgGadgetManager {
let strings_path = self.gadget_path.join("strings/0x409"); let strings_path = self.gadget_path.join("strings/0x409");
create_dir(&strings_path)?; create_dir(&strings_path)?;
write_file(&strings_path.join("serialnumber"), &self.descriptor.serial_number)?; write_file(
write_file(&strings_path.join("manufacturer"), &self.descriptor.manufacturer)?; &strings_path.join("serialnumber"),
&self.descriptor.serial_number,
)?;
write_file(
&strings_path.join("manufacturer"),
&self.descriptor.manufacturer,
)?;
write_file(&strings_path.join("product"), &self.descriptor.product)?; write_file(&strings_path.join("product"), &self.descriptor.product)?;
debug!("Created USB strings"); debug!("Created USB strings");
Ok(()) Ok(())
@@ -349,7 +372,10 @@ impl OtgGadgetManager {
/// Get endpoint usage info /// Get endpoint usage info
pub fn endpoint_info(&self) -> (u8, u8) { pub fn endpoint_info(&self) -> (u8, u8) {
(self.endpoint_allocator.used(), self.endpoint_allocator.max()) (
self.endpoint_allocator.used(),
self.endpoint_allocator.max(),
)
} }
/// Get gadget path /// Get gadget path

View File

@@ -161,7 +161,10 @@ impl MsdFunction {
// Write only changed attributes // Write only changed attributes
let cdrom_changed = current_cdrom != new_cdrom; let cdrom_changed = current_cdrom != new_cdrom;
if cdrom_changed { if cdrom_changed {
debug!("Updating LUN {} cdrom: {} -> {}", lun, current_cdrom, new_cdrom); debug!(
"Updating LUN {} cdrom: {} -> {}",
lun, current_cdrom, new_cdrom
);
write_file(&lun_path.join("cdrom"), new_cdrom)?; write_file(&lun_path.join("cdrom"), new_cdrom)?;
} }
if current_ro != new_ro { if current_ro != new_ro {
@@ -169,11 +172,17 @@ impl MsdFunction {
write_file(&lun_path.join("ro"), new_ro)?; write_file(&lun_path.join("ro"), new_ro)?;
} }
if current_removable != new_removable { if current_removable != new_removable {
debug!("Updating LUN {} removable: {} -> {}", lun, current_removable, new_removable); debug!(
"Updating LUN {} removable: {} -> {}",
lun, current_removable, new_removable
);
write_file(&lun_path.join("removable"), new_removable)?; write_file(&lun_path.join("removable"), new_removable)?;
} }
if current_nofua != new_nofua { if current_nofua != new_nofua {
debug!("Updating LUN {} nofua: {} -> {}", lun, current_nofua, new_nofua); debug!(
"Updating LUN {} nofua: {} -> {}",
lun, current_nofua, new_nofua
);
write_file(&lun_path.join("nofua"), new_nofua)?; write_file(&lun_path.join("nofua"), new_nofua)?;
} }
@@ -258,11 +267,17 @@ impl MsdFunction {
// forced_eject forcibly detaches the backing file regardless of host state // forced_eject forcibly detaches the backing file regardless of host state
let forced_eject_path = lun_path.join("forced_eject"); let forced_eject_path = lun_path.join("forced_eject");
if forced_eject_path.exists() { if forced_eject_path.exists() {
debug!("Using forced_eject to disconnect LUN {} at {:?}", lun, forced_eject_path); debug!(
"Using forced_eject to disconnect LUN {} at {:?}",
lun, forced_eject_path
);
match write_file(&forced_eject_path, "1") { match write_file(&forced_eject_path, "1") {
Ok(_) => debug!("forced_eject write succeeded"), Ok(_) => debug!("forced_eject write succeeded"),
Err(e) => { Err(e) => {
warn!("forced_eject write failed: {}, falling back to clearing file", e); warn!(
"forced_eject write failed: {}, falling back to clearing file",
e
);
write_file(&lun_path.join("file"), "")?; write_file(&lun_path.join("file"), "")?;
} }
} }

View File

@@ -27,8 +27,8 @@ use tracing::{debug, info, warn};
use super::manager::{wait_for_hid_devices, GadgetDescriptor, OtgGadgetManager}; use super::manager::{wait_for_hid_devices, GadgetDescriptor, OtgGadgetManager};
use super::msd::MsdFunction; use super::msd::MsdFunction;
use crate::error::{AppError, Result};
use crate::config::OtgDescriptorConfig; use crate::config::OtgDescriptorConfig;
use crate::error::{AppError, Result};
/// Bitflags for requested functions (lock-free) /// Bitflags for requested functions (lock-free)
const FLAG_HID: u8 = 0b01; const FLAG_HID: u8 = 0b01;
@@ -254,8 +254,9 @@ impl OtgService {
// Get MSD function // Get MSD function
let msd = self.msd_function.read().await; let msd = self.msd_function.read().await;
msd.clone() msd.clone().ok_or_else(|| {
.ok_or_else(|| AppError::Internal("MSD function not set after gadget setup".to_string())) AppError::Internal("MSD function not set after gadget setup".to_string())
})
} }
/// Disable MSD function /// Disable MSD function
@@ -465,7 +466,10 @@ impl OtgService {
device_version: super::configfs::DEFAULT_USB_BCD_DEVICE, device_version: super::configfs::DEFAULT_USB_BCD_DEVICE,
manufacturer: config.manufacturer.clone(), manufacturer: config.manufacturer.clone(),
product: config.product.clone(), product: config.product.clone(),
serial_number: config.serial_number.clone().unwrap_or_else(|| "0123456789".to_string()), serial_number: config
.serial_number
.clone()
.unwrap_or_else(|| "0123456789".to_string()),
}; };
// Update stored descriptor // Update stored descriptor

View File

@@ -34,7 +34,10 @@ pub fn encode_frame(data: &[u8]) -> io::Result<Vec<u8>> {
let h = ((len << 2) as u32) | 0x3; let h = ((len << 2) as u32) | 0x3;
buf.extend_from_slice(&h.to_le_bytes()); buf.extend_from_slice(&h.to_le_bytes());
} else { } else {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Message too large")); return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Message too large",
));
} }
buf.extend_from_slice(data); buf.extend_from_slice(data);
@@ -79,7 +82,10 @@ pub async fn read_frame<R: AsyncRead + Unpin>(reader: &mut R) -> io::Result<Byte
let (_, msg_len) = decode_header(first_byte[0], &header_rest); let (_, msg_len) = decode_header(first_byte[0], &header_rest);
if msg_len > MAX_PACKET_LENGTH { if msg_len > MAX_PACKET_LENGTH {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Message too large")); return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Message too large",
));
} }
// Read message body // Read message body
@@ -133,7 +139,10 @@ pub fn encode_frame_into(data: &[u8], buf: &mut BytesMut) -> io::Result<()> {
} else if len <= MAX_PACKET_LENGTH { } else if len <= MAX_PACKET_LENGTH {
buf.put_u32_le(((len << 2) as u32) | 0x3); buf.put_u32_le(((len << 2) as u32) | 0x3);
} else { } else {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Message too large")); return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Message too large",
));
} }
buf.extend_from_slice(data); buf.extend_from_slice(data);
@@ -216,7 +225,10 @@ impl BytesCodec {
n >>= 2; n >>= 2;
if n > self.max_packet_length { if n > self.max_packet_length {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Message too large")); return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Message too large",
));
} }
src.advance(head_len); src.advance(head_len);
@@ -245,7 +257,10 @@ impl BytesCodec {
} else if len <= MAX_PACKET_LENGTH { } else if len <= MAX_PACKET_LENGTH {
buf.put_u32_le(((len << 2) as u32) | 0x3); buf.put_u32_le(((len << 2) as u32) | 0x3);
} else { } else {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Message too large")); return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Message too large",
));
} }
buf.extend(data); buf.extend(data);

View File

@@ -116,9 +116,9 @@ impl RustDeskConfig {
/// Get the UUID bytes (returns None if not set) /// Get the UUID bytes (returns None if not set)
pub fn get_uuid_bytes(&self) -> Option<[u8; 16]> { pub fn get_uuid_bytes(&self) -> Option<[u8; 16]> {
self.uuid.as_ref().and_then(|s| { self.uuid
uuid::Uuid::parse_str(s).ok().map(|u| *u.as_bytes()) .as_ref()
}) .and_then(|s| uuid::Uuid::parse_str(s).ok().map(|u| *u.as_bytes()))
} }
/// Get the rendezvous server address with default port /// Get the rendezvous server address with default port
@@ -135,13 +135,16 @@ impl RustDeskConfig {
/// Get the relay server address with default port /// Get the relay server address with default port
pub fn relay_addr(&self) -> Option<String> { pub fn relay_addr(&self) -> Option<String> {
self.relay_server.as_ref().map(|s| { self.relay_server
.as_ref()
.map(|s| {
if s.contains(':') { if s.contains(':') {
s.clone() s.clone()
} else { } else {
format!("{}:21117", s) format!("{}:21117", s)
} }
}).or_else(|| { })
.or_else(|| {
// Default: same host as rendezvous server // Default: same host as rendezvous server
let server = &self.rendezvous_server; let server = &self.rendezvous_server;
if !server.is_empty() { if !server.is_empty() {
@@ -222,7 +225,10 @@ mod tests {
// Explicit relay server // Explicit relay server
config.relay_server = Some("relay.example.com".to_string()); config.relay_server = Some("relay.example.com".to_string());
assert_eq!(config.relay_addr(), Some("relay.example.com:21117".to_string())); assert_eq!(
config.relay_addr(),
Some("relay.example.com:21117".to_string())
);
// No rendezvous server, relay is None // No rendezvous server, relay is None
config.rendezvous_server = String::new(); config.rendezvous_server = String::new();

View File

@@ -13,16 +13,16 @@ use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use sodiumoxide::crypto::box_;
use parking_lot::RwLock; use parking_lot::RwLock;
use protobuf::Message as ProtobufMessage; use protobuf::Message as ProtobufMessage;
use tokio::net::TcpStream; use sodiumoxide::crypto::box_;
use tokio::net::tcp::OwnedWriteHalf; use tokio::net::tcp::OwnedWriteHalf;
use tokio::net::TcpStream;
use tokio::sync::{broadcast, mpsc, Mutex}; use tokio::sync::{broadcast, mpsc, Mutex};
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use crate::audio::AudioController; use crate::audio::AudioController;
use crate::hid::{HidController, KeyboardEvent, KeyEventType, KeyboardModifiers}; use crate::hid::{HidController, KeyEventType, KeyboardEvent, KeyboardModifiers};
use crate::video::encoder::registry::{EncoderRegistry, VideoEncoderType}; use crate::video::encoder::registry::{EncoderRegistry, VideoEncoderType};
use crate::video::encoder::BitratePreset; use crate::video::encoder::BitratePreset;
use crate::video::stream_manager::VideoStreamManager; use crate::video::stream_manager::VideoStreamManager;
@@ -33,10 +33,9 @@ use super::crypto::{self, KeyPair, SigningKeyPair};
use super::frame_adapters::{AudioFrameAdapter, VideoCodec, VideoFrameAdapter}; use super::frame_adapters::{AudioFrameAdapter, VideoCodec, VideoFrameAdapter};
use super::hid_adapter::{convert_key_event, convert_mouse_event, mouse_type}; use super::hid_adapter::{convert_key_event, convert_mouse_event, mouse_type};
use super::protocol::{ use super::protocol::{
message, misc, login_response, decode_message, login_response, message, misc, Clipboard, ControlKey, DisplayInfo, Hash,
KeyEvent, MouseEvent, Clipboard, Misc, LoginRequest, LoginResponse, PeerInfo, HbbMessage, IdPk, KeyEvent, LoginRequest, LoginResponse, Misc, MouseEvent, OptionMessage,
IdPk, SignedId, Hash, TestDelay, ControlKey, PeerInfo, PublicKey, SignedId, SupportedEncoding, TestDelay,
decode_message, HbbMessage, DisplayInfo, SupportedEncoding, OptionMessage, PublicKey,
}; };
use sodiumoxide::crypto::secretbox; use sodiumoxide::crypto::secretbox;
@@ -268,7 +267,11 @@ impl Connection {
} }
/// Handle an incoming TCP connection /// Handle an incoming TCP connection
pub async fn handle_tcp(&mut self, stream: TcpStream, peer_addr: SocketAddr) -> anyhow::Result<()> { pub async fn handle_tcp(
&mut self,
stream: TcpStream,
peer_addr: SocketAddr,
) -> anyhow::Result<()> {
info!("New connection from {}", peer_addr); info!("New connection from {}", peer_addr);
*self.state.write() = ConnectionState::Handshaking; *self.state.write() = ConnectionState::Handshaking;
@@ -279,7 +282,9 @@ impl Connection {
// Send our SignedId first (this is what RustDesk protocol expects) // Send our SignedId first (this is what RustDesk protocol expects)
// The SignedId contains our device ID and temporary public key // The SignedId contains our device ID and temporary public key
let signed_id_msg = self.create_signed_id_message(&self.device_id.clone()); let signed_id_msg = self.create_signed_id_message(&self.device_id.clone());
let signed_id_bytes = signed_id_msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode SignedId: {}", e))?; let signed_id_bytes = signed_id_msg
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode SignedId: {}", e))?;
debug!("Sending SignedId with device_id={}", self.device_id); debug!("Sending SignedId with device_id={}", self.device_id);
self.send_framed_arc(&writer, &signed_id_bytes).await?; self.send_framed_arc(&writer, &signed_id_bytes).await?;
@@ -402,7 +407,11 @@ impl Connection {
} }
/// Send framed message using Arc<Mutex<OwnedWriteHalf>> with RustDesk's variable-length encoding /// Send framed message using Arc<Mutex<OwnedWriteHalf>> with RustDesk's variable-length encoding
async fn send_framed_arc(&self, writer: &Arc<Mutex<OwnedWriteHalf>>, data: &[u8]) -> anyhow::Result<()> { async fn send_framed_arc(
&self,
writer: &Arc<Mutex<OwnedWriteHalf>>,
data: &[u8],
) -> anyhow::Result<()> {
let mut w = writer.lock().await; let mut w = writer.lock().await;
write_frame(&mut *w, data).await?; write_frame(&mut *w, data).await?;
Ok(()) Ok(())
@@ -480,7 +489,9 @@ impl Connection {
pk.symmetric_value.len() pk.symmetric_value.len()
); );
if pk.asymmetric_value.is_empty() && pk.symmetric_value.is_empty() { if pk.asymmetric_value.is_empty() && pk.symmetric_value.is_empty() {
warn!("Received EMPTY PublicKey - client may have failed signature verification!"); warn!(
"Received EMPTY PublicKey - client may have failed signature verification!"
);
} }
self.handle_peer_public_key(pk, writer).await?; self.handle_peer_public_key(pk, writer).await?;
} }
@@ -535,7 +546,7 @@ impl Connection {
info!("Received SignedId from peer, id_len={}", si.id.len()); info!("Received SignedId from peer, id_len={}", si.id.len());
self.handle_signed_id(si, writer).await?; self.handle_signed_id(si, writer).await?;
return Ok(()); return Ok(());
}, }
message::Union::Hash(_) => "Hash", message::Union::Hash(_) => "Hash",
message::Union::VideoFrame(_) => "VideoFrame", message::Union::VideoFrame(_) => "VideoFrame",
message::Union::CursorData(_) => "CursorData", message::Union::CursorData(_) => "CursorData",
@@ -564,16 +575,26 @@ impl Connection {
lr: &LoginRequest, lr: &LoginRequest,
writer: &Arc<Mutex<OwnedWriteHalf>>, writer: &Arc<Mutex<OwnedWriteHalf>>,
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
info!("Login request from {} ({}), password_len={}", lr.my_id, lr.my_name, lr.password.len()); info!(
"Login request from {} ({}), password_len={}",
lr.my_id,
lr.my_name,
lr.password.len()
);
// Check if our server requires a password // Check if our server requires a password
if !self.password.is_empty() { if !self.password.is_empty() {
// Server requires password // Server requires password
if lr.password.is_empty() { if lr.password.is_empty() {
// Client sent empty password - tell them to enter password // Client sent empty password - tell them to enter password
info!("Empty password from {}, requesting password input", lr.my_id); info!(
"Empty password from {}, requesting password input",
lr.my_id
);
let error_response = self.create_login_error_response("Empty Password"); let error_response = self.create_login_error_response("Empty Password");
let response_bytes = error_response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let response_bytes = error_response
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_encrypted_arc(writer, &response_bytes).await?; self.send_encrypted_arc(writer, &response_bytes).await?;
// Don't close connection - wait for retry with password // Don't close connection - wait for retry with password
return Ok(false); return Ok(false);
@@ -583,7 +604,9 @@ impl Connection {
if !self.verify_password(&lr.password) { if !self.verify_password(&lr.password) {
warn!("Wrong password from {}", lr.my_id); warn!("Wrong password from {}", lr.my_id);
let error_response = self.create_login_error_response("Wrong Password"); let error_response = self.create_login_error_response("Wrong Password");
let response_bytes = error_response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let response_bytes = error_response
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_encrypted_arc(writer, &response_bytes).await?; self.send_encrypted_arc(writer, &response_bytes).await?;
// Don't close connection - wait for retry with correct password // Don't close connection - wait for retry with correct password
return Ok(false); return Ok(false);
@@ -601,7 +624,9 @@ impl Connection {
info!("Negotiated video codec: {:?}", negotiated); info!("Negotiated video codec: {:?}", negotiated);
let response = self.create_login_response(true); let response = self.create_login_response(true);
let response_bytes = response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let response_bytes = response
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_encrypted_arc(writer, &response_bytes).await?; self.send_encrypted_arc(writer, &response_bytes).await?;
Ok(true) Ok(true)
} }
@@ -679,7 +704,10 @@ impl Connection {
}; };
if let Some(preset) = preset { if let Some(preset) = preset {
info!("Client requested quality preset: {:?} (image_quality={})", preset, image_quality); info!(
"Client requested quality preset: {:?} (image_quality={})",
preset, image_quality
);
if let Some(ref video_manager) = self.video_manager { if let Some(ref video_manager) = self.video_manager {
if let Err(e) = video_manager.set_bitrate_preset(preset).await { if let Err(e) = video_manager.set_bitrate_preset(preset).await {
warn!("Failed to set bitrate preset: {}", e); warn!("Failed to set bitrate preset: {}", e);
@@ -729,7 +757,10 @@ impl Connection {
// Log custom_image_quality (accept but don't process) // Log custom_image_quality (accept but don't process)
if opt.custom_image_quality > 0 { if opt.custom_image_quality > 0 {
debug!("Client sent custom_image_quality: {} (ignored)", opt.custom_image_quality); debug!(
"Client sent custom_image_quality: {} (ignored)",
opt.custom_image_quality
);
} }
if opt.custom_fps > 0 { if opt.custom_fps > 0 {
debug!("Client requested FPS: {}", opt.custom_fps); debug!("Client requested FPS: {}", opt.custom_fps);
@@ -779,7 +810,10 @@ impl Connection {
let negotiated_codec = self.negotiated_codec.unwrap_or(VideoEncoderType::H264); let negotiated_codec = self.negotiated_codec.unwrap_or(VideoEncoderType::H264);
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
info!("Starting video streaming for connection {} with codec {:?}", conn_id, negotiated_codec); info!(
"Starting video streaming for connection {} with codec {:?}",
conn_id, negotiated_codec
);
if let Err(e) = run_video_streaming( if let Err(e) = run_video_streaming(
conn_id, conn_id,
@@ -788,7 +822,9 @@ impl Connection {
state, state,
shutdown_tx, shutdown_tx,
negotiated_codec, negotiated_codec,
).await { )
.await
{
error!("Video streaming error for connection {}: {}", conn_id, e); error!("Video streaming error for connection {}: {}", conn_id, e);
} }
@@ -815,13 +851,9 @@ impl Connection {
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
info!("Starting audio streaming for connection {}", conn_id); info!("Starting audio streaming for connection {}", conn_id);
if let Err(e) = run_audio_streaming( if let Err(e) =
conn_id, run_audio_streaming(conn_id, audio_controller, audio_tx, state, shutdown_tx).await
audio_controller, {
audio_tx,
state,
shutdown_tx,
).await {
error!("Audio streaming error for connection {}: {}", conn_id, e); error!("Audio streaming error for connection {}: {}", conn_id, e);
} }
@@ -894,7 +926,10 @@ impl Connection {
self.encryption_enabled = true; self.encryption_enabled = true;
} }
Err(e) => { Err(e) => {
warn!("Failed to decrypt session key: {:?}, falling back to unencrypted", e); warn!(
"Failed to decrypt session key: {:?}, falling back to unencrypted",
e
);
// Continue without encryption - some clients may not support it // Continue without encryption - some clients may not support it
self.encryption_enabled = false; self.encryption_enabled = false;
} }
@@ -917,8 +952,13 @@ impl Connection {
// This tells the client what salt to use for password hashing // This tells the client what salt to use for password hashing
// Must be encrypted if session key was negotiated // Must be encrypted if session key was negotiated
let hash_msg = self.create_hash_message(); let hash_msg = self.create_hash_message();
let hash_bytes = hash_msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let hash_bytes = hash_msg
debug!("Sending Hash message for password authentication (encrypted={})", self.encryption_enabled); .write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
debug!(
"Sending Hash message for password authentication (encrypted={})",
self.encryption_enabled
);
self.send_encrypted_arc(writer, &hash_bytes).await?; self.send_encrypted_arc(writer, &hash_bytes).await?;
Ok(()) Ok(())
@@ -971,7 +1011,9 @@ impl Connection {
// If we haven't sent our SignedId yet, send it now // If we haven't sent our SignedId yet, send it now
// (This handles the case where client sends SignedId before we do) // (This handles the case where client sends SignedId before we do)
let signed_id_msg = self.create_signed_id_message(&self.device_id.clone()); let signed_id_msg = self.create_signed_id_message(&self.device_id.clone());
let signed_id_bytes = signed_id_msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let signed_id_bytes = signed_id_msg
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_framed_arc(writer, &signed_id_bytes).await?; self.send_framed_arc(writer, &signed_id_bytes).await?;
Ok(()) Ok(())
@@ -1073,7 +1115,8 @@ impl Connection {
msg msg
} else { } else {
let mut login_response = LoginResponse::new(); let mut login_response = LoginResponse::new();
login_response.union = Some(login_response::Union::Error("Invalid password".to_string())); login_response.union =
Some(login_response::Union::Error("Invalid password".to_string()));
login_response.enable_trusted_devices = false; login_response.enable_trusted_devices = false;
let mut msg = HbbMessage::new(); let mut msg = HbbMessage::new();
@@ -1133,7 +1176,9 @@ impl Connection {
let mut response = HbbMessage::new(); let mut response = HbbMessage::new();
response.union = Some(message::Union::TestDelay(test_delay)); response.union = Some(message::Union::TestDelay(test_delay));
let data = response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let data = response
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_encrypted_arc(writer, &data).await?; self.send_encrypted_arc(writer, &data).await?;
debug!( debug!(
@@ -1161,10 +1206,7 @@ impl Connection {
/// The client will echo this back, allowing us to calculate RTT. /// The client will echo this back, allowing us to calculate RTT.
/// The measured delay is then included in future TestDelay messages /// The measured delay is then included in future TestDelay messages
/// for the client to display. /// for the client to display.
async fn send_test_delay( async fn send_test_delay(&mut self, writer: &Arc<Mutex<OwnedWriteHalf>>) -> anyhow::Result<()> {
&mut self,
writer: &Arc<Mutex<OwnedWriteHalf>>,
) -> anyhow::Result<()> {
// Get current time in milliseconds since epoch // Get current time in milliseconds since epoch
let time_ms = SystemTime::now() let time_ms = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@@ -1180,13 +1222,18 @@ impl Connection {
let mut msg = HbbMessage::new(); let mut msg = HbbMessage::new();
msg.union = Some(message::Union::TestDelay(test_delay)); msg.union = Some(message::Union::TestDelay(test_delay));
let data = msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let data = msg
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
self.send_encrypted_arc(writer, &data).await?; self.send_encrypted_arc(writer, &data).await?;
// Record when we sent this, so we can calculate RTT when client echoes back // Record when we sent this, so we can calculate RTT when client echoes back
self.last_test_delay_sent = Some(Instant::now()); self.last_test_delay_sent = Some(Instant::now());
debug!("TestDelay sent: time={}, last_delay={}ms", time_ms, self.last_delay); debug!(
"TestDelay sent: time={}, last_delay={}ms",
time_ms, self.last_delay
);
Ok(()) Ok(())
} }
@@ -1208,7 +1255,10 @@ impl Connection {
self.last_caps_lock = caps_lock_in_modifiers; self.last_caps_lock = caps_lock_in_modifiers;
// Send CapsLock key press (down + up) to toggle state on target // Send CapsLock key press (down + up) to toggle state on target
if let Some(ref hid) = self.hid { if let Some(ref hid) = self.hid {
debug!("CapsLock state changed to {}, sending CapsLock key", caps_lock_in_modifiers); debug!(
"CapsLock state changed to {}, sending CapsLock key",
caps_lock_in_modifiers
);
let caps_down = KeyboardEvent { let caps_down = KeyboardEvent {
event_type: KeyEventType::Down, event_type: KeyEventType::Down,
key: 0x39, // USB HID CapsLock key: 0x39, // USB HID CapsLock
@@ -1234,7 +1284,9 @@ impl Connection {
if let Some(kb_event) = convert_key_event(ke) { if let Some(kb_event) = convert_key_event(ke) {
debug!( debug!(
"Converted to HID: key=0x{:02X}, event_type={:?}, modifiers={:02X}", "Converted to HID: key=0x{:02X}, event_type={:?}, modifiers={:02X}",
kb_event.key, kb_event.event_type, kb_event.modifiers.to_hid_byte() kb_event.key,
kb_event.event_type,
kb_event.modifiers.to_hid_byte()
); );
// 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 {
@@ -1393,7 +1445,11 @@ impl ConnectionManager {
} }
/// Accept a new connection /// Accept a new connection
pub async fn accept_connection(&self, stream: TcpStream, peer_addr: SocketAddr) -> anyhow::Result<u32> { pub async fn accept_connection(
&self,
stream: TcpStream,
peer_addr: SocketAddr,
) -> anyhow::Result<u32> {
let id = { let id = {
let mut next = self.next_id.write(); let mut next = self.next_id.write();
let id = *next; let id = *next;
@@ -1406,14 +1462,14 @@ impl ConnectionManager {
let hid = self.hid.read().clone(); let hid = self.hid.read().clone();
let audio = self.audio.read().clone(); let audio = self.audio.read().clone();
let video_manager = self.video_manager.read().clone(); let video_manager = self.video_manager.read().clone();
let (mut conn, _rx) = Connection::new(id, &config, signing_keypair, hid, audio, video_manager); let (mut conn, _rx) =
Connection::new(id, &config, signing_keypair, hid, audio, video_manager);
// Track connection state for external access // Track connection state for external access
let state = conn.state.clone(); let state = conn.state.clone();
self.connections.write().push(Arc::new(RwLock::new(ConnectionInfo { self.connections
id, .write()
state, .push(Arc::new(RwLock::new(ConnectionInfo { id, state })));
})));
// Spawn connection handler - Connection is moved, not locked // Spawn connection handler - Connection is moved, not locked
tokio::spawn(async move { tokio::spawn(async move {
@@ -1466,7 +1522,10 @@ async fn run_video_streaming(
}; };
// Set the video codec on the shared pipeline before subscribing // Set the video codec on the shared pipeline before subscribing
info!("Setting video codec to {:?} for connection {}", negotiated_codec, conn_id); info!(
"Setting video codec to {:?} for connection {}",
negotiated_codec, conn_id
);
if let Err(e) = video_manager.set_video_codec(webrtc_codec).await { if let Err(e) = video_manager.set_video_codec(webrtc_codec).await {
error!("Failed to set video codec: {}", e); error!("Failed to set video codec: {}", e);
// Continue anyway, will use whatever codec the pipeline already has // Continue anyway, will use whatever codec the pipeline already has
@@ -1485,7 +1544,10 @@ async fn run_video_streaming(
let mut encoded_count: u64 = 0; let mut encoded_count: u64 = 0;
let mut last_log_time = Instant::now(); let mut last_log_time = Instant::now();
info!("Started shared video streaming for connection {} (codec: {:?})", conn_id, codec); info!(
"Started shared video streaming for connection {} (codec: {:?})",
conn_id, codec
);
// Outer loop: handles pipeline restarts by re-subscribing // Outer loop: handles pipeline restarts by re-subscribing
'subscribe_loop: loop { 'subscribe_loop: loop {
@@ -1500,7 +1562,10 @@ async fn run_video_streaming(
Some(rx) => rx, Some(rx) => rx,
None => { None => {
// Pipeline not ready yet, wait and retry // Pipeline not ready yet, wait and retry
debug!("No encoded frame source available for connection {}, retrying...", conn_id); debug!(
"No encoded frame source available for connection {}, retrying...",
conn_id
);
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(100)).await;
continue 'subscribe_loop; continue 'subscribe_loop;
} }
@@ -1619,13 +1684,19 @@ async fn run_audio_streaming(
Some(rx) => rx, Some(rx) => rx,
None => { None => {
// Audio not available, wait and retry // Audio not available, wait and retry
debug!("No audio source available for connection {}, retrying...", conn_id); debug!(
"No audio source available for connection {}, retrying...",
conn_id
);
tokio::time::sleep(Duration::from_millis(500)).await; tokio::time::sleep(Duration::from_millis(500)).await;
continue 'subscribe_loop; continue 'subscribe_loop;
} }
}; };
info!("RustDesk connection {} subscribed to audio pipeline", conn_id); info!(
"RustDesk connection {} subscribed to audio pipeline",
conn_id
);
// Send audio format message once before sending frames // Send audio format message once before sending frames
if !audio_adapter.format_sent() { if !audio_adapter.format_sent() {

View File

@@ -86,8 +86,12 @@ impl KeyPair {
/// Create from base64-encoded keys /// Create from base64-encoded keys
pub fn from_base64(public_key: &str, secret_key: &str) -> Result<Self, CryptoError> { pub fn from_base64(public_key: &str, secret_key: &str) -> Result<Self, CryptoError> {
let pk_bytes = BASE64.decode(public_key).map_err(|_| CryptoError::InvalidKeyLength)?; let pk_bytes = BASE64
let sk_bytes = BASE64.decode(secret_key).map_err(|_| CryptoError::InvalidKeyLength)?; .decode(public_key)
.map_err(|_| CryptoError::InvalidKeyLength)?;
let sk_bytes = BASE64
.decode(secret_key)
.map_err(|_| CryptoError::InvalidKeyLength)?;
Self::from_keys(&pk_bytes, &sk_bytes) Self::from_keys(&pk_bytes, &sk_bytes)
} }
} }
@@ -140,7 +144,10 @@ pub fn decrypt_with_key(
/// Compute a shared symmetric key from public/private keypair /// Compute a shared symmetric key from public/private keypair
/// This is the precomputed key for the NaCl box /// This is the precomputed key for the NaCl box
pub fn precompute_key(their_public_key: &PublicKey, our_secret_key: &SecretKey) -> box_::PrecomputedKey { pub fn precompute_key(
their_public_key: &PublicKey,
our_secret_key: &SecretKey,
) -> box_::PrecomputedKey {
box_::precompute(their_public_key, our_secret_key) box_::precompute(their_public_key, our_secret_key)
} }
@@ -207,8 +214,8 @@ pub fn decrypt_symmetric_key(
return Err(CryptoError::InvalidKeyLength); return Err(CryptoError::InvalidKeyLength);
} }
let their_pk = PublicKey::from_slice(their_temp_public_key) let their_pk =
.ok_or(CryptoError::InvalidKeyLength)?; PublicKey::from_slice(their_temp_public_key).ok_or(CryptoError::InvalidKeyLength)?;
// Use zero nonce as per RustDesk protocol // Use zero nonce as per RustDesk protocol
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]); let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
@@ -294,8 +301,12 @@ impl SigningKeyPair {
/// Create from base64-encoded keys /// Create from base64-encoded keys
pub fn from_base64(public_key: &str, secret_key: &str) -> Result<Self, CryptoError> { pub fn from_base64(public_key: &str, secret_key: &str) -> Result<Self, CryptoError> {
let pk_bytes = BASE64.decode(public_key).map_err(|_| CryptoError::InvalidKeyLength)?; let pk_bytes = BASE64
let sk_bytes = BASE64.decode(secret_key).map_err(|_| CryptoError::InvalidKeyLength)?; .decode(public_key)
.map_err(|_| CryptoError::InvalidKeyLength)?;
let sk_bytes = BASE64
.decode(secret_key)
.map_err(|_| CryptoError::InvalidKeyLength)?;
Self::from_keys(&pk_bytes, &sk_bytes) Self::from_keys(&pk_bytes, &sk_bytes)
} }
@@ -321,8 +332,7 @@ impl SigningKeyPair {
/// which is required by RustDesk's protocol where clients encrypt the /// which is required by RustDesk's protocol where clients encrypt the
/// symmetric key using the public key from IdPk. /// symmetric key using the public key from IdPk.
pub fn to_curve25519_pk(&self) -> Result<PublicKey, CryptoError> { pub fn to_curve25519_pk(&self) -> Result<PublicKey, CryptoError> {
ed25519::to_curve25519_pk(&self.public_key) ed25519::to_curve25519_pk(&self.public_key).map_err(|_| CryptoError::KeyConversionFailed)
.map_err(|_| CryptoError::KeyConversionFailed)
} }
/// Convert Ed25519 secret key to Curve25519 secret key for decryption /// Convert Ed25519 secret key to Curve25519 secret key for decryption
@@ -330,14 +340,16 @@ impl SigningKeyPair {
/// This allows decrypting messages that were encrypted using the /// This allows decrypting messages that were encrypted using the
/// converted public key. /// converted public key.
pub fn to_curve25519_sk(&self) -> Result<SecretKey, CryptoError> { pub fn to_curve25519_sk(&self) -> Result<SecretKey, CryptoError> {
ed25519::to_curve25519_sk(&self.secret_key) ed25519::to_curve25519_sk(&self.secret_key).map_err(|_| CryptoError::KeyConversionFailed)
.map_err(|_| CryptoError::KeyConversionFailed)
} }
} }
/// Verify a signed message /// Verify a signed message
/// Returns the original message if signature is valid /// Returns the original message if signature is valid
pub fn verify_signed(signed_message: &[u8], public_key: &sign::PublicKey) -> Result<Vec<u8>, CryptoError> { pub fn verify_signed(
signed_message: &[u8],
public_key: &sign::PublicKey,
) -> Result<Vec<u8>, CryptoError> {
sign::verify(signed_message, public_key).map_err(|_| CryptoError::SignatureVerificationFailed) sign::verify(signed_message, public_key).map_err(|_| CryptoError::SignatureVerificationFailed)
} }
@@ -374,7 +386,8 @@ mod tests {
let message = b"Hello, RustDesk!"; let message = b"Hello, RustDesk!";
let (nonce, ciphertext) = encrypt_box(message, &bob.public_key, &alice.secret_key); let (nonce, ciphertext) = encrypt_box(message, &bob.public_key, &alice.secret_key);
let plaintext = decrypt_box(&ciphertext, &nonce, &alice.public_key, &bob.secret_key).unwrap(); let plaintext =
decrypt_box(&ciphertext, &nonce, &alice.public_key, &bob.secret_key).unwrap();
assert_eq!(plaintext, message); assert_eq!(plaintext, message);
} }

View File

@@ -7,9 +7,8 @@ use bytes::Bytes;
use protobuf::Message as ProtobufMessage; use protobuf::Message as ProtobufMessage;
use super::protocol::hbb::message::{ use super::protocol::hbb::message::{
message as msg_union, misc as misc_union, video_frame as vf_union, message as msg_union, misc as misc_union, video_frame as vf_union, AudioFormat, AudioFrame,
AudioFormat, AudioFrame, CursorData, CursorPosition, CursorData, CursorPosition, EncodedVideoFrame, EncodedVideoFrames, Message, Misc, VideoFrame,
EncodedVideoFrame, EncodedVideoFrames, Message, Misc, VideoFrame,
}; };
/// Video codec type for RustDesk /// Video codec type for RustDesk
@@ -63,7 +62,12 @@ impl VideoFrameAdapter {
/// Convert encoded video data to RustDesk Message (zero-copy version) /// Convert encoded video data to RustDesk Message (zero-copy version)
/// ///
/// This version takes Bytes directly to avoid copying the frame data. /// This version takes Bytes directly to avoid copying the frame data.
pub fn encode_frame_from_bytes(&mut self, data: Bytes, is_keyframe: bool, timestamp_ms: u64) -> Message { pub fn encode_frame_from_bytes(
&mut self,
data: Bytes,
is_keyframe: bool,
timestamp_ms: u64,
) -> Message {
// Calculate relative timestamp // Calculate relative timestamp
if self.seq == 0 { if self.seq == 0 {
self.timestamp_base = timestamp_ms; self.timestamp_base = timestamp_ms;
@@ -104,13 +108,23 @@ impl VideoFrameAdapter {
/// Encode frame to bytes for sending (zero-copy version) /// Encode frame to bytes for sending (zero-copy version)
/// ///
/// Takes Bytes directly to avoid copying the frame data. /// Takes Bytes directly to avoid copying the frame data.
pub fn encode_frame_bytes_zero_copy(&mut self, data: Bytes, is_keyframe: bool, timestamp_ms: u64) -> Bytes { pub fn encode_frame_bytes_zero_copy(
&mut self,
data: Bytes,
is_keyframe: bool,
timestamp_ms: u64,
) -> Bytes {
let msg = self.encode_frame_from_bytes(data, is_keyframe, timestamp_ms); let msg = self.encode_frame_from_bytes(data, is_keyframe, timestamp_ms);
Bytes::from(msg.write_to_bytes().unwrap_or_default()) Bytes::from(msg.write_to_bytes().unwrap_or_default())
} }
/// Encode frame to bytes for sending /// Encode frame to bytes for sending
pub fn encode_frame_bytes(&mut self, data: &[u8], is_keyframe: bool, timestamp_ms: u64) -> Bytes { pub fn encode_frame_bytes(
&mut self,
data: &[u8],
is_keyframe: bool,
timestamp_ms: u64,
) -> Bytes {
self.encode_frame_bytes_zero_copy(Bytes::copy_from_slice(data), is_keyframe, timestamp_ms) self.encode_frame_bytes_zero_copy(Bytes::copy_from_slice(data), is_keyframe, timestamp_ms)
} }
@@ -234,15 +248,13 @@ mod tests {
let msg = adapter.encode_frame(&data, true, 0); let msg = adapter.encode_frame(&data, true, 0);
match &msg.union { match &msg.union {
Some(msg_union::Union::VideoFrame(vf)) => { Some(msg_union::Union::VideoFrame(vf)) => match &vf.union {
match &vf.union {
Some(vf_union::Union::H264s(frames)) => { Some(vf_union::Union::H264s(frames)) => {
assert_eq!(frames.frames.len(), 1); assert_eq!(frames.frames.len(), 1);
assert!(frames.frames[0].key); assert!(frames.frames[0].key);
} }
_ => panic!("Expected H264s"), _ => panic!("Expected H264s"),
} },
}
_ => panic!("Expected VideoFrame"), _ => panic!("Expected VideoFrame"),
} }
} }
@@ -256,15 +268,13 @@ mod tests {
assert!(adapter.format_sent()); assert!(adapter.format_sent());
match &msg.union { match &msg.union {
Some(msg_union::Union::Misc(misc)) => { Some(msg_union::Union::Misc(misc)) => match &misc.union {
match &misc.union {
Some(misc_union::Union::AudioFormat(fmt)) => { Some(misc_union::Union::AudioFormat(fmt)) => {
assert_eq!(fmt.sample_rate, 48000); assert_eq!(fmt.sample_rate, 48000);
assert_eq!(fmt.channels, 2); assert_eq!(fmt.channels, 2);
} }
_ => panic!("Expected AudioFormat"), _ => panic!("Expected AudioFormat"),
} },
}
_ => panic!("Expected Misc"), _ => panic!("Expected Misc"),
} }
} }

View File

@@ -2,13 +2,13 @@
//! //!
//! Converts RustDesk HID events (KeyEvent, MouseEvent) to One-KVM HID events. //! Converts RustDesk HID events (KeyEvent, MouseEvent) to One-KVM HID events.
use protobuf::Enum;
use crate::hid::{
KeyboardEvent, KeyboardModifiers, KeyEventType,
MouseButton, MouseEvent as OneKvmMouseEvent, MouseEventType,
};
use super::protocol::{KeyEvent, MouseEvent, ControlKey};
use super::protocol::hbb::message::key_event as ke_union; use super::protocol::hbb::message::key_event as ke_union;
use super::protocol::{ControlKey, KeyEvent, MouseEvent};
use crate::hid::{
KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent as OneKvmMouseEvent,
MouseEventType,
};
use protobuf::Enum;
/// Mouse event types from RustDesk protocol /// Mouse event types from RustDesk protocol
/// mask = (button << 3) | event_type /// mask = (button << 3) | event_type
@@ -32,7 +32,11 @@ pub mod mouse_button {
/// Convert RustDesk MouseEvent to One-KVM MouseEvent(s) /// Convert RustDesk MouseEvent to One-KVM MouseEvent(s)
/// Returns a Vec because a single RustDesk event may need multiple One-KVM events /// Returns a Vec because a single RustDesk event may need multiple One-KVM events
/// (e.g., move + button + scroll) /// (e.g., move + button + scroll)
pub fn convert_mouse_event(event: &MouseEvent, screen_width: u32, screen_height: u32) -> Vec<OneKvmMouseEvent> { pub fn convert_mouse_event(
event: &MouseEvent,
screen_width: u32,
screen_height: u32,
) -> Vec<OneKvmMouseEvent> {
let mut events = Vec::new(); let mut events = Vec::new();
// RustDesk uses absolute coordinates // RustDesk uses absolute coordinates
@@ -533,7 +537,9 @@ mod tests {
let events = convert_mouse_event(&event, 1920, 1080); let events = convert_mouse_event(&event, 1920, 1080);
assert!(events.len() >= 2); assert!(events.len() >= 2);
// Should have a button down event // Should have a button down event
assert!(events.iter().any(|e| e.event_type == MouseEventType::Down && e.button == Some(MouseButton::Left))); assert!(events
.iter()
.any(|e| e.event_type == MouseEventType::Down && e.button == Some(MouseButton::Left)));
} }
#[test] #[test]
@@ -542,7 +548,9 @@ mod tests {
let mut key_event = KeyEvent::new(); let mut key_event = KeyEvent::new();
key_event.down = true; key_event.down = true;
key_event.press = false; key_event.press = false;
key_event.union = Some(ke_union::Union::ControlKey(EnumOrUnknown::new(ControlKey::Return))); key_event.union = Some(ke_union::Union::ControlKey(EnumOrUnknown::new(
ControlKey::Return,
)));
let result = convert_key_event(&key_event); let result = convert_key_event(&key_event);
assert!(result.is_some()); assert!(result.is_some());

View File

@@ -205,7 +205,8 @@ impl RustDeskService {
self.connection_manager.set_audio(self.audio.clone()); self.connection_manager.set_audio(self.audio.clone());
// Set the video manager on connection manager for video streaming // Set the video manager on connection manager for video streaming
self.connection_manager.set_video_manager(self.video_manager.clone()); self.connection_manager
.set_video_manager(self.video_manager.clone());
*self.rendezvous.write() = Some(mediator.clone()); *self.rendezvous.write() = Some(mediator.clone());
@@ -231,7 +232,8 @@ impl RustDeskService {
let audio_punch = self.audio.clone(); let audio_punch = self.audio.clone();
let service_config_punch = self.config.clone(); let service_config_punch = self.config.clone();
mediator.set_punch_callback(Arc::new(move |peer_addr, rendezvous_addr, relay_server, uuid, socket_addr, device_id| { mediator.set_punch_callback(Arc::new(
move |peer_addr, rendezvous_addr, relay_server, uuid, socket_addr, device_id| {
let conn_mgr = connection_manager_punch.clone(); let conn_mgr = connection_manager_punch.clone();
let video = video_manager_punch.clone(); let video = video_manager_punch.clone();
let hid = hid_punch.clone(); let hid = hid_punch.clone();
@@ -274,14 +276,18 @@ impl RustDeskService {
video, video,
hid, hid,
audio, audio,
).await { )
.await
{
error!("Failed to handle relay request: {}", e); error!("Failed to handle relay request: {}", e);
} }
}); });
})); },
));
// Set the relay callback on the mediator // Set the relay callback on the mediator
mediator.set_relay_callback(Arc::new(move |rendezvous_addr, relay_server, uuid, socket_addr, device_id| { mediator.set_relay_callback(Arc::new(
move |rendezvous_addr, relay_server, uuid, socket_addr, device_id| {
let conn_mgr = connection_manager.clone(); let conn_mgr = connection_manager.clone();
let video = video_manager.clone(); let video = video_manager.clone();
let hid = hid.clone(); let hid = hid.clone();
@@ -306,15 +312,19 @@ impl RustDeskService {
video, video,
hid, hid,
audio, audio,
).await { )
.await
{
error!("Failed to handle relay request: {}", e); error!("Failed to handle relay request: {}", e);
} }
}); });
})); },
));
// Set the intranet callback on the mediator for same-LAN connections // Set the intranet callback on the mediator for same-LAN connections
let connection_manager2 = self.connection_manager.clone(); let connection_manager2 = self.connection_manager.clone();
mediator.set_intranet_callback(Arc::new(move |rendezvous_addr, peer_socket_addr, local_addr, relay_server, device_id| { mediator.set_intranet_callback(Arc::new(
move |rendezvous_addr, peer_socket_addr, local_addr, relay_server, device_id| {
let conn_mgr = connection_manager2.clone(); let conn_mgr = connection_manager2.clone();
tokio::spawn(async move { tokio::spawn(async move {
@@ -325,11 +335,14 @@ impl RustDeskService {
&relay_server, &relay_server,
&device_id, &device_id,
conn_mgr, conn_mgr,
).await { )
.await
{
error!("Failed to handle intranet request: {}", e); error!("Failed to handle intranet request: {}", e);
} }
}); });
})); },
));
// Spawn rendezvous task // Spawn rendezvous task
let status = self.status.clone(); let status = self.status.clone();
@@ -471,7 +484,9 @@ impl RustDeskService {
// Save signing keypair (Ed25519) // Save signing keypair (Ed25519)
let signing_pk = skp.public_key_base64(); let signing_pk = skp.public_key_base64();
let signing_sk = skp.secret_key_base64(); let signing_sk = skp.secret_key_base64();
if config.signing_public_key.as_ref() != Some(&signing_pk) || config.signing_private_key.as_ref() != Some(&signing_sk) { if config.signing_public_key.as_ref() != Some(&signing_pk)
|| config.signing_private_key.as_ref() != Some(&signing_sk)
{
config.signing_public_key = Some(signing_pk); config.signing_public_key = Some(signing_pk);
config.signing_private_key = Some(signing_sk); config.signing_private_key = Some(signing_sk);
changed = true; changed = true;
@@ -522,13 +537,18 @@ async fn handle_relay_request(
_hid: Arc<HidController>, _hid: Arc<HidController>,
_audio: Arc<AudioController>, _audio: Arc<AudioController>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
info!("Handling relay request: rendezvous={}, relay={}, uuid={}", rendezvous_addr, relay_server, uuid); info!(
"Handling relay request: rendezvous={}, relay={}, uuid={}",
rendezvous_addr, relay_server, uuid
);
// Step 1: Connect to RENDEZVOUS server and send RelayResponse // Step 1: Connect to RENDEZVOUS server and send RelayResponse
let rendezvous_socket_addr: SocketAddr = tokio::net::lookup_host(rendezvous_addr) let rendezvous_socket_addr: SocketAddr = tokio::net::lookup_host(rendezvous_addr)
.await? .await?
.next() .next()
.ok_or_else(|| anyhow::anyhow!("Failed to resolve rendezvous server: {}", rendezvous_addr))?; .ok_or_else(|| {
anyhow::anyhow!("Failed to resolve rendezvous server: {}", rendezvous_addr)
})?;
let mut rendezvous_stream = tokio::time::timeout( let mut rendezvous_stream = tokio::time::timeout(
Duration::from_millis(RELAY_CONNECT_TIMEOUT_MS), Duration::from_millis(RELAY_CONNECT_TIMEOUT_MS),
@@ -537,12 +557,17 @@ async fn handle_relay_request(
.await .await
.map_err(|_| anyhow::anyhow!("Rendezvous connection timeout"))??; .map_err(|_| anyhow::anyhow!("Rendezvous connection timeout"))??;
debug!("Connected to rendezvous server at {}", rendezvous_socket_addr); debug!(
"Connected to rendezvous server at {}",
rendezvous_socket_addr
);
// Send RelayResponse to rendezvous server with client's socket_addr // Send RelayResponse to rendezvous server with client's socket_addr
// IMPORTANT: Include our device ID so rendezvous server can look up and sign our public key // IMPORTANT: Include our device ID so rendezvous server can look up and sign our public key
let relay_response = make_relay_response(uuid, socket_addr, relay_server, device_id); let relay_response = make_relay_response(uuid, socket_addr, relay_server, device_id);
let bytes = relay_response.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let bytes = relay_response
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
bytes_codec::write_frame(&mut rendezvous_stream, &bytes).await?; bytes_codec::write_frame(&mut rendezvous_stream, &bytes).await?;
debug!("Sent RelayResponse to rendezvous server for uuid={}", uuid); debug!("Sent RelayResponse to rendezvous server for uuid={}", uuid);
@@ -568,7 +593,9 @@ async fn handle_relay_request(
// The licence_key is required if the relay server is configured with -k option // The licence_key is required if the relay server is configured with -k option
// The socket_addr is CRITICAL - the relay server uses it to match us with the peer // The socket_addr is CRITICAL - the relay server uses it to match us with the peer
let request_relay = make_request_relay(uuid, relay_key, socket_addr); let request_relay = make_request_relay(uuid, relay_key, socket_addr);
let bytes = request_relay.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let bytes = request_relay
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
bytes_codec::write_frame(&mut stream, &bytes).await?; bytes_codec::write_frame(&mut stream, &bytes).await?;
debug!("Sent RequestRelay to relay server for uuid={}", uuid); debug!("Sent RequestRelay to relay server for uuid={}", uuid);
@@ -576,8 +603,13 @@ async fn handle_relay_request(
let peer_addr = rendezvous::AddrMangle::decode(socket_addr).unwrap_or(relay_addr); let peer_addr = rendezvous::AddrMangle::decode(socket_addr).unwrap_or(relay_addr);
// Step 3: Accept connection - relay server bridges the connection // Step 3: Accept connection - relay server bridges the connection
connection_manager.accept_connection(stream, peer_addr).await?; connection_manager
info!("Relay connection established for uuid={}, peer={}", uuid, peer_addr); .accept_connection(stream, peer_addr)
.await?;
info!(
"Relay connection established for uuid={}, peer={}",
uuid, peer_addr
);
Ok(()) Ok(())
} }
@@ -608,14 +640,15 @@ async fn handle_intranet_request(
debug!("Peer address from FetchLocalAddr: {:?}", peer_addr); debug!("Peer address from FetchLocalAddr: {:?}", peer_addr);
// Connect to rendezvous server via TCP with timeout // Connect to rendezvous server via TCP with timeout
let mut stream = tokio::time::timeout( let mut stream =
Duration::from_secs(5), tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(rendezvous_addr))
TcpStream::connect(rendezvous_addr),
)
.await .await
.map_err(|_| anyhow::anyhow!("Timeout connecting to rendezvous server"))??; .map_err(|_| anyhow::anyhow!("Timeout connecting to rendezvous server"))??;
info!("Connected to rendezvous server for intranet: {}", rendezvous_addr); info!(
"Connected to rendezvous server for intranet: {}",
rendezvous_addr
);
// Build LocalAddr message with our local address (mangled) // Build LocalAddr message with our local address (mangled)
let local_addr_bytes = AddrMangle::encode(local_addr); let local_addr_bytes = AddrMangle::encode(local_addr);
@@ -626,7 +659,9 @@ async fn handle_intranet_request(
device_id, device_id,
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
); );
let bytes = msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let bytes = msg
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
// Send LocalAddr using RustDesk's variable-length framing // Send LocalAddr using RustDesk's variable-length framing
bytes_codec::write_frame(&mut stream, &bytes).await?; bytes_codec::write_frame(&mut stream, &bytes).await?;
@@ -640,11 +675,15 @@ async fn handle_intranet_request(
// Get peer address for logging/connection tracking // Get peer address for logging/connection tracking
let effective_peer_addr = peer_addr.unwrap_or_else(|| { let effective_peer_addr = peer_addr.unwrap_or_else(|| {
// If we can't decode the peer address, use the rendezvous server address // If we can't decode the peer address, use the rendezvous server address
rendezvous_addr.parse().unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap()) rendezvous_addr
.parse()
.unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap())
}); });
// Accept the connection - the stream is now a proxied connection to the client // Accept the connection - the stream is now a proxied connection to the client
connection_manager.accept_connection(stream, effective_peer_addr).await?; connection_manager
.accept_connection(stream, effective_peer_addr)
.await?;
info!("Intranet connection established via rendezvous server proxy"); info!("Intranet connection established via rendezvous server proxy");
Ok(()) Ok(())

View File

@@ -14,22 +14,20 @@ pub mod hbb {
// Re-export commonly used types // Re-export commonly used types
pub use hbb::rendezvous::{ pub use hbb::rendezvous::{
rendezvous_message, relay_response, punch_hole_response, punch_hole_response, relay_response, rendezvous_message, ConfigUpdate, ConnType,
ConnType, ConfigUpdate, FetchLocalAddr, HealthCheck, KeyExchange, LocalAddr, NatType, FetchLocalAddr, HealthCheck, KeyExchange, LocalAddr, NatType, OnlineRequest, OnlineResponse,
OnlineRequest, OnlineResponse, PeerDiscovery, PunchHole, PunchHoleRequest, PunchHoleResponse, PeerDiscovery, PunchHole, PunchHoleRequest, PunchHoleResponse, PunchHoleSent, RegisterPeer,
PunchHoleSent, RegisterPeer, RegisterPeerResponse, RegisterPk, RegisterPkResponse, RegisterPeerResponse, RegisterPk, RegisterPkResponse, RelayResponse, RendezvousMessage,
RelayResponse, RendezvousMessage, RequestRelay, SoftwareUpdate, TestNatRequest, RequestRelay, SoftwareUpdate, TestNatRequest, TestNatResponse,
TestNatResponse,
}; };
// Re-export message.proto types // Re-export message.proto types
pub use hbb::message::{ pub use hbb::message::{
message, misc, login_response, key_event, key_event, login_response, message, misc, AudioFormat, AudioFrame, Auth2FA, Clipboard,
AudioFormat, AudioFrame, Auth2FA, Clipboard, CursorData, CursorPosition, EncodedVideoFrame, ControlKey, CursorData, CursorPosition, DisplayInfo, EncodedVideoFrame, EncodedVideoFrames,
EncodedVideoFrames, Hash, IdPk, KeyEvent, LoginRequest, LoginResponse, MouseEvent, Misc, Features, Hash, IdPk, KeyEvent, LoginRequest, LoginResponse, Message as HbbMessage, Misc,
OptionMessage, PeerInfo, PublicKey, SignedId, SupportedDecoding, VideoFrame, TestDelay, MouseEvent, OptionMessage, PeerInfo, PublicKey, SignedId, SupportedDecoding, SupportedEncoding,
Features, SupportedResolutions, WindowsSessions, Message as HbbMessage, ControlKey, SupportedResolutions, TestDelay, VideoFrame, WindowsSessions,
DisplayInfo, SupportedEncoding,
}; };
/// Helper to create a RendezvousMessage with RegisterPeer /// Helper to create a RendezvousMessage with RegisterPeer
@@ -80,7 +78,12 @@ pub fn make_punch_hole_sent(
/// IMPORTANT: The union field should be `Id` (our device ID), NOT `Pk`. /// IMPORTANT: The union field should be `Id` (our device ID), NOT `Pk`.
/// The rendezvous server will look up our registered public key using this ID, /// The rendezvous server will look up our registered public key using this ID,
/// sign it with the server's private key, and set the `pk` field before forwarding to client. /// sign it with the server's private key, and set the `pk` field before forwarding to client.
pub fn make_relay_response(uuid: &str, socket_addr: &[u8], relay_server: &str, device_id: &str) -> RendezvousMessage { pub fn make_relay_response(
uuid: &str,
socket_addr: &[u8],
relay_server: &str,
device_id: &str,
) -> RendezvousMessage {
let mut rr = RelayResponse::new(); let mut rr = RelayResponse::new();
rr.socket_addr = socket_addr.to_vec().into(); rr.socket_addr = socket_addr.to_vec().into();
rr.uuid = uuid.to_string(); rr.uuid = uuid.to_string();

View File

@@ -69,10 +69,7 @@ impl PunchHoleHandler {
/// ///
/// Tries direct connection first, falls back to relay if needed. /// Tries direct connection first, falls back to relay if needed.
/// Returns true if direct connection succeeded, false if relay is needed. /// Returns true if direct connection succeeded, false if relay is needed.
pub async fn handle_punch_hole( pub async fn handle_punch_hole(&self, peer_addr: Option<SocketAddr>) -> bool {
&self,
peer_addr: Option<SocketAddr>,
) -> bool {
let peer_addr = match peer_addr { let peer_addr = match peer_addr {
Some(addr) => addr, Some(addr) => addr,
None => { None => {
@@ -84,7 +81,11 @@ impl PunchHoleHandler {
match try_direct_connection(peer_addr).await { match try_direct_connection(peer_addr).await {
PunchResult::DirectConnection(stream) => { PunchResult::DirectConnection(stream) => {
// Direct connection succeeded, accept it // Direct connection succeeded, accept it
match self.connection_manager.accept_connection(stream, peer_addr).await { match self
.connection_manager
.accept_connection(stream, peer_addr)
.await
{
Ok(_) => { Ok(_) => {
info!("P2P direct connection established with {}", peer_addr); info!("P2P direct connection established with {}", peer_addr);
true true

View File

@@ -18,8 +18,8 @@ use tracing::{debug, error, info, warn};
use super::config::RustDeskConfig; use super::config::RustDeskConfig;
use super::crypto::{KeyPair, SigningKeyPair}; use super::crypto::{KeyPair, SigningKeyPair};
use super::protocol::{ use super::protocol::{
rendezvous_message, make_punch_hole_sent, make_register_peer, decode_rendezvous_message, make_punch_hole_sent, make_register_peer, make_register_pk,
make_register_pk, NatType, RendezvousMessage, decode_rendezvous_message, rendezvous_message, NatType, RendezvousMessage,
}; };
/// Registration interval in milliseconds /// Registration interval in milliseconds
@@ -81,7 +81,8 @@ pub type RelayCallback = Arc<dyn Fn(String, String, String, Vec<u8>, String) + S
/// Callback type for P2P punch hole requests /// Callback type for P2P punch hole requests
/// Parameters: peer_addr (decoded), relay_callback_params (rendezvous_addr, relay_server, uuid, socket_addr, device_id) /// Parameters: peer_addr (decoded), relay_callback_params (rendezvous_addr, relay_server, uuid, socket_addr, device_id)
/// Returns: should call relay callback if P2P fails /// Returns: should call relay callback if P2P fails
pub type PunchCallback = Arc<dyn Fn(Option<SocketAddr>, String, String, String, Vec<u8>, String) + Send + Sync>; pub type PunchCallback =
Arc<dyn Fn(Option<SocketAddr>, String, String, String, Vec<u8>, String) + Send + Sync>;
/// Callback type for intranet/local address connections /// Callback type for intranet/local address connections
/// Parameters: rendezvous_addr, peer_socket_addr (mangled), local_addr, relay_server, device_id /// Parameters: rendezvous_addr, peer_socket_addr (mangled), local_addr, relay_server, device_id
@@ -232,7 +233,8 @@ impl RendezvousMediator {
if signing_guard.is_none() { if signing_guard.is_none() {
let config = self.config.read(); let config = self.config.read();
// Try to load from config first // Try to load from config first
if let (Some(pk), Some(sk)) = (&config.signing_public_key, &config.signing_private_key) { if let (Some(pk), Some(sk)) = (&config.signing_public_key, &config.signing_private_key)
{
if let Ok(skp) = SigningKeyPair::from_base64(pk, sk) { if let Ok(skp) = SigningKeyPair::from_base64(pk, sk) {
debug!("Loaded signing keypair from config"); debug!("Loaded signing keypair from config");
*signing_guard = Some(skp.clone()); *signing_guard = Some(skp.clone());
@@ -265,14 +267,20 @@ impl RendezvousMediator {
config.enabled, effective_server config.enabled, effective_server
); );
if !config.enabled || effective_server.is_empty() { if !config.enabled || effective_server.is_empty() {
info!("Rendezvous mediator not starting: enabled={}, server='{}'", config.enabled, effective_server); info!(
"Rendezvous mediator not starting: enabled={}, server='{}'",
config.enabled, effective_server
);
return Ok(()); return Ok(());
} }
*self.status.write() = RendezvousStatus::Connecting; *self.status.write() = RendezvousStatus::Connecting;
let addr = config.rendezvous_addr(); let addr = config.rendezvous_addr();
info!("Starting rendezvous mediator for {} to {}", config.device_id, addr); info!(
"Starting rendezvous mediator for {} to {}",
config.device_id, addr
);
// Resolve server address // Resolve server address
let server_addr: SocketAddr = tokio::net::lookup_host(&addr) let server_addr: SocketAddr = tokio::net::lookup_host(&addr)
@@ -376,7 +384,9 @@ impl RendezvousMediator {
let serial = *self.serial.read(); let serial = *self.serial.read();
let msg = make_register_peer(&id, serial); let msg = make_register_peer(&id, serial);
let bytes = msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let bytes = msg
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
socket.send(&bytes).await?; socket.send(&bytes).await?;
Ok(()) Ok(())
} }
@@ -393,7 +403,9 @@ impl RendezvousMediator {
debug!("Sending RegisterPk: id={}", id); debug!("Sending RegisterPk: id={}", id);
let msg = make_register_pk(&id, &uuid, pk, ""); let msg = make_register_pk(&id, &uuid, pk, "");
let bytes = msg.write_to_bytes().map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; let bytes = msg
.write_to_bytes()
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
socket.send(&bytes).await?; socket.send(&bytes).await?;
Ok(()) Ok(())
} }
@@ -570,9 +582,22 @@ impl RendezvousMediator {
// Use punch callback if set (tries P2P first, then relay) // Use punch callback if set (tries P2P first, then relay)
// Otherwise fall back to relay callback directly // Otherwise fall back to relay callback directly
if let Some(callback) = self.punch_callback.read().as_ref() { if let Some(callback) = self.punch_callback.read().as_ref() {
callback(peer_addr, rendezvous_addr, relay_server, uuid, ph.socket_addr.to_vec(), device_id); callback(
peer_addr,
rendezvous_addr,
relay_server,
uuid,
ph.socket_addr.to_vec(),
device_id,
);
} else if let Some(callback) = self.relay_callback.read().as_ref() { } else if let Some(callback) = self.relay_callback.read().as_ref() {
callback(rendezvous_addr, relay_server, uuid, ph.socket_addr.to_vec(), device_id); callback(
rendezvous_addr,
relay_server,
uuid,
ph.socket_addr.to_vec(),
device_id,
);
} }
} }
} }
@@ -591,7 +616,13 @@ impl RendezvousMediator {
let config = self.config.read().clone(); let config = self.config.read().clone();
let rendezvous_addr = config.rendezvous_addr(); let rendezvous_addr = config.rendezvous_addr();
let device_id = config.device_id.clone(); let device_id = config.device_id.clone();
callback(rendezvous_addr, relay_server, rr.uuid.clone(), rr.socket_addr.to_vec(), device_id); callback(
rendezvous_addr,
relay_server,
rr.uuid.clone(),
rr.socket_addr.to_vec(),
device_id,
);
} }
} }
Some(rendezvous_message::Union::FetchLocalAddr(fla)) => { Some(rendezvous_message::Union::FetchLocalAddr(fla)) => {
@@ -602,7 +633,8 @@ impl RendezvousMediator {
peer_addr, fla.socket_addr.len(), fla.relay_server peer_addr, fla.socket_addr.len(), fla.relay_server
); );
// Respond with our local address for same-LAN direct connection // Respond with our local address for same-LAN direct connection
self.send_local_addr(socket, &fla.socket_addr, &fla.relay_server).await?; self.send_local_addr(socket, &fla.socket_addr, &fla.relay_server)
.await?;
} }
Some(rendezvous_message::Union::ConfigureUpdate(cu)) => { Some(rendezvous_message::Union::ConfigureUpdate(cu)) => {
info!("Received ConfigureUpdate, serial={}", cu.serial); info!("Received ConfigureUpdate, serial={}", cu.serial);

View File

@@ -5,7 +5,10 @@ use crate::atx::AtxController;
use crate::audio::AudioController; use crate::audio::AudioController;
use crate::auth::{SessionStore, UserStore}; use crate::auth::{SessionStore, UserStore};
use crate::config::ConfigStore; use crate::config::ConfigStore;
use crate::events::{AtxDeviceInfo, AudioDeviceInfo, EventBus, HidDeviceInfo, MsdDeviceInfo, SystemEvent, VideoDeviceInfo}; use crate::events::{
AtxDeviceInfo, AudioDeviceInfo, EventBus, HidDeviceInfo, MsdDeviceInfo, SystemEvent,
VideoDeviceInfo,
};
use crate::extensions::ExtensionManager; use crate::extensions::ExtensionManager;
use crate::hid::HidController; use crate::hid::HidController;
use crate::msd::MsdController; use crate::msd::MsdController;

View File

@@ -12,8 +12,8 @@ use std::time::{Duration, Instant};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use crate::video::encoder::JpegEncoder;
use crate::video::encoder::traits::{Encoder, EncoderConfig}; use crate::video::encoder::traits::{Encoder, EncoderConfig};
use crate::video::encoder::JpegEncoder;
use crate::video::format::PixelFormat; use crate::video::format::PixelFormat;
use crate::video::VideoFrame; use crate::video::VideoFrame;
@@ -256,7 +256,10 @@ impl MjpegStreamHandler {
let config = EncoderConfig::jpeg(resolution, 85); let config = EncoderConfig::jpeg(resolution, 85);
match JpegEncoder::new(config) { match JpegEncoder::new(config) {
Ok(enc) => { Ok(enc) => {
debug!("Created JPEG encoder for MJPEG stream: {}x{}", resolution.width, resolution.height); debug!(
"Created JPEG encoder for MJPEG stream: {}x{}",
resolution.width, resolution.height
);
enc enc
} }
Err(e) => { Err(e) => {
@@ -270,37 +273,40 @@ impl MjpegStreamHandler {
// Check if resolution changed // Check if resolution changed
if encoder.config().resolution != resolution { if encoder.config().resolution != resolution {
debug!("Resolution changed, recreating JPEG encoder: {}x{}", resolution.width, resolution.height); debug!(
"Resolution changed, recreating JPEG encoder: {}x{}",
resolution.width, resolution.height
);
let config = EncoderConfig::jpeg(resolution, 85); let config = EncoderConfig::jpeg(resolution, 85);
*encoder = JpegEncoder::new(config).map_err(|e| format!("Failed to create encoder: {}", e))?; *encoder =
JpegEncoder::new(config).map_err(|e| format!("Failed to create encoder: {}", e))?;
} }
// Encode based on input format // Encode based on input format
let encoded = match frame.format { let encoded = match frame.format {
PixelFormat::Yuyv => { PixelFormat::Yuyv => encoder
encoder.encode_yuyv(frame.data(), sequence) .encode_yuyv(frame.data(), sequence)
.map_err(|e| format!("YUYV encode failed: {}", e))? .map_err(|e| format!("YUYV encode failed: {}", e))?,
} PixelFormat::Nv12 => encoder
PixelFormat::Nv12 => { .encode_nv12(frame.data(), sequence)
encoder.encode_nv12(frame.data(), sequence) .map_err(|e| format!("NV12 encode failed: {}", e))?,
.map_err(|e| format!("NV12 encode failed: {}", e))? PixelFormat::Rgb24 => encoder
} .encode_rgb(frame.data(), sequence)
PixelFormat::Rgb24 => { .map_err(|e| format!("RGB encode failed: {}", e))?,
encoder.encode_rgb(frame.data(), sequence) PixelFormat::Bgr24 => encoder
.map_err(|e| format!("RGB encode failed: {}", e))? .encode_bgr(frame.data(), sequence)
} .map_err(|e| format!("BGR encode failed: {}", e))?,
PixelFormat::Bgr24 => {
encoder.encode_bgr(frame.data(), sequence)
.map_err(|e| format!("BGR encode failed: {}", e))?
}
_ => { _ => {
return Err(format!("Unsupported format for JPEG encoding: {}", frame.format)); return Err(format!(
"Unsupported format for JPEG encoding: {}",
frame.format
));
} }
}; };
// Create new VideoFrame with JPEG data // Create new VideoFrame with JPEG data (zero-copy: Bytes -> Arc<Bytes>)
Ok(VideoFrame::from_vec( Ok(VideoFrame::new(
encoded.data.to_vec(), encoded.data,
resolution, resolution,
PixelFormat::Mjpeg, PixelFormat::Mjpeg,
0, // stride not relevant for JPEG 0, // stride not relevant for JPEG
@@ -333,7 +339,11 @@ impl MjpegStreamHandler {
pub fn register_client(&self, client_id: ClientId) { pub fn register_client(&self, client_id: ClientId) {
let session = ClientSession::new(client_id.clone()); let session = ClientSession::new(client_id.clone());
self.clients.write().insert(client_id.clone(), session); self.clients.write().insert(client_id.clone(), session);
info!("Client {} connected (total: {})", client_id, self.client_count()); info!(
"Client {} connected (total: {})",
client_id,
self.client_count()
);
} }
/// Unregister a client /// Unregister a client
@@ -391,7 +401,9 @@ impl MjpegStreamHandler {
*self.auto_pause_config.write() = config; *self.auto_pause_config.write() = config;
info!( info!(
"Auto-pause config updated: enabled={}, delay={}s, timeout={}s", "Auto-pause config updated: enabled={}, delay={}s, timeout={}s",
config_clone.enabled, config_clone.shutdown_delay_secs, config_clone.client_timeout_secs config_clone.enabled,
config_clone.shutdown_delay_secs,
config_clone.client_timeout_secs
); );
} }
@@ -440,10 +452,7 @@ impl ClientGuard {
/// Create a new client guard /// Create a new client guard
pub fn new(client_id: ClientId, handler: Arc<MjpegStreamHandler>) -> Self { pub fn new(client_id: ClientId, handler: Arc<MjpegStreamHandler>) -> Self {
handler.register_client(client_id.clone()); handler.register_client(client_id.clone());
Self { Self { client_id, handler }
client_id,
handler,
}
} }
/// Get client ID /// Get client ID
@@ -535,8 +544,8 @@ fn frames_are_identical(a: &VideoFrame, b: &VideoFrame) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use bytes::Bytes;
use crate::video::{format::Resolution, PixelFormat}; use crate::video::{format::Resolution, PixelFormat};
use bytes::Bytes;
#[tokio::test] #[tokio::test]
async fn test_stream_handler() { async fn test_stream_handler() {

View File

@@ -228,7 +228,8 @@ impl MjpegStreamer {
let device = self.current_device.read().await; let device = self.current_device.read().await;
let config = self.config.read().await; let config = self.config.read().await;
let (resolution, format, frames_captured) = if let Some(ref cap) = *self.capturer.read().await { let (resolution, format, frames_captured) =
if let Some(ref cap) = *self.capturer.read().await {
let stats = cap.stats().await; let stats = cap.stats().await;
( (
Some((config.resolution.width, config.resolution.height)), Some((config.resolution.width, config.resolution.height)),
@@ -286,7 +287,10 @@ impl MjpegStreamer {
/// Initialize with specific device /// Initialize with specific device
pub async fn init_with_device(self: &Arc<Self>, device: VideoDeviceInfo) -> Result<()> { pub async fn init_with_device(self: &Arc<Self>, device: VideoDeviceInfo) -> Result<()> {
info!("MjpegStreamer: Initializing with device: {}", device.path.display()); info!(
"MjpegStreamer: Initializing with device: {}",
device.path.display()
);
let config = self.config.read().await.clone(); let config = self.config.read().await.clone();
@@ -322,7 +326,9 @@ impl MjpegStreamer {
let _lock = self.start_lock.lock().await; let _lock = self.start_lock.lock().await;
if self.config_changing.load(Ordering::SeqCst) { if self.config_changing.load(Ordering::SeqCst) {
return Err(AppError::VideoError("Config change in progress".to_string())); return Err(AppError::VideoError(
"Config change in progress".to_string(),
));
} }
let state = *self.state.read().await; let state = *self.state.read().await;
@@ -332,7 +338,8 @@ impl MjpegStreamer {
// Get capturer // Get capturer
let capturer = self.capturer.read().await.clone(); let capturer = self.capturer.read().await.clone();
let capturer = capturer.ok_or_else(|| AppError::VideoError("Not initialized".to_string()))?; let capturer =
capturer.ok_or_else(|| AppError::VideoError("Not initialized".to_string()))?;
// Start capture // Start capture
capturer.start().await?; capturer.start().await?;
@@ -412,7 +419,9 @@ impl MjpegStreamer {
let device = devices let device = devices
.into_iter() .into_iter()
.find(|d| d.path == *path) .find(|d| d.path == *path)
.ok_or_else(|| AppError::VideoError(format!("Device not found: {}", path.display())))?; .ok_or_else(|| {
AppError::VideoError(format!("Device not found: {}", path.display()))
})?;
self.init_with_device(device).await?; self.init_with_device(device).await?;
} }

View File

@@ -13,5 +13,7 @@ pub mod mjpeg_streamer;
pub mod ws_hid; pub mod ws_hid;
pub use mjpeg::{ClientGuard, MjpegStreamHandler}; pub use mjpeg::{ClientGuard, MjpegStreamHandler};
pub use mjpeg_streamer::{MjpegStreamer, MjpegStreamerConfig, MjpegStreamerState, MjpegStreamerStats}; pub use mjpeg_streamer::{
MjpegStreamer, MjpegStreamerConfig, MjpegStreamerState, MjpegStreamerStats,
};
pub use ws_hid::WsHidHandler; pub use ws_hid::WsHidHandler;

View File

@@ -142,7 +142,9 @@ impl WsHidHandler {
shutdown_tx, shutdown_tx,
}); });
self.clients.write().insert(client_id.clone(), client.clone()); self.clients
.write()
.insert(client_id.clone(), client.clone());
info!( info!(
"WsHidHandler: Client {} connected (total: {})", "WsHidHandler: Client {} connected (total: {})",
client_id, client_id,
@@ -182,7 +184,11 @@ impl WsHidHandler {
let (mut sender, mut receiver) = socket.split(); let (mut sender, mut receiver) = socket.split();
// Send initial status as binary: 0x00 = ok, 0x01 = error // Send initial status as binary: 0x00 = ok, 0x01 = error
let status_byte = if self.is_hid_available() { 0x00u8 } else { 0x01u8 }; let status_byte = if self.is_hid_available() {
0x00u8
} else {
0x01u8
};
let _ = sender.send(Message::Binary(vec![status_byte].into())).await; let _ = sender.send(Message::Binary(vec![status_byte].into())).await;
loop { loop {
@@ -230,7 +236,10 @@ impl WsHidHandler {
let hid = self.hid_controller.read().clone(); let hid = self.hid_controller.read().clone();
if let Some(hid) = hid { if let Some(hid) = hid {
if let Err(e) = hid.reset().await { if let Err(e) = hid.reset().await {
warn!("WsHidHandler: Failed to reset HID on client {} disconnect: {}", client_id, e); warn!(
"WsHidHandler: Failed to reset HID on client {} disconnect: {}",
client_id, e
);
} else { } else {
debug!("WsHidHandler: HID reset on client {} disconnect", client_id); debug!("WsHidHandler: HID reset on client {} disconnect", client_id);
} }

View File

@@ -16,6 +16,7 @@ use std::time::{Duration, Instant};
/// ///
/// ```rust /// ```rust
/// use one_kvm::utils::LogThrottler; /// use one_kvm::utils::LogThrottler;
/// use std::time::Duration;
/// ///
/// let throttler = LogThrottler::new(Duration::from_secs(5)); /// let throttler = LogThrottler::new(Duration::from_secs(5));
/// ///

View File

@@ -231,7 +231,9 @@ impl VideoCapturer {
let last_error = self.last_error.clone(); let last_error = self.last_error.clone();
let handle = tokio::task::spawn_blocking(move || { let handle = tokio::task::spawn_blocking(move || {
capture_loop(config, state, stats, frame_tx, stop_flag, sequence, last_error); capture_loop(
config, state, stats, frame_tx, stop_flag, sequence, last_error,
);
}); });
*self.capture_handle.lock().await = Some(handle); *self.capture_handle.lock().await = Some(handle);
@@ -275,14 +277,7 @@ fn capture_loop(
sequence: Arc<AtomicU64>, sequence: Arc<AtomicU64>,
error_holder: Arc<parking_lot::RwLock<Option<(String, String)>>>, error_holder: Arc<parking_lot::RwLock<Option<(String, String)>>>,
) { ) {
let result = run_capture( let result = run_capture(&config, &state, &stats, &frame_tx, &stop_flag, &sequence);
&config,
&state,
&stats,
&frame_tx,
&stop_flag,
&sequence,
);
match result { match result {
Ok(_) => { Ok(_) => {
@@ -503,7 +498,10 @@ fn run_capture_inner(
// Validate frame // Validate frame
if frame_size < MIN_FRAME_SIZE { if frame_size < MIN_FRAME_SIZE {
debug!("Dropping small frame: {} bytes (bytesused={})", frame_size, meta.bytesused); debug!(
"Dropping small frame: {} bytes (bytesused={})",
frame_size, meta.bytesused
);
if let Ok(mut s) = stats.try_lock() { if let Ok(mut s) = stats.try_lock() {
s.frames_dropped += 1; s.frames_dropped += 1;
} }
@@ -606,16 +604,10 @@ impl FrameGrabber {
} }
/// Capture a single frame /// Capture a single frame
pub async fn grab( pub async fn grab(&self, resolution: Resolution, format: PixelFormat) -> Result<VideoFrame> {
&self,
resolution: Resolution,
format: PixelFormat,
) -> Result<VideoFrame> {
let device_path = self.device_path.clone(); let device_path = self.device_path.clone();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || grab_single_frame(&device_path, resolution, format))
grab_single_frame(&device_path, resolution, format)
})
.await .await
.map_err(|e| AppError::VideoError(format!("Grab task failed: {}", e)))? .map_err(|e| AppError::VideoError(format!("Grab task failed: {}", e)))?
} }
@@ -626,14 +618,13 @@ fn grab_single_frame(
resolution: Resolution, resolution: Resolution,
format: PixelFormat, format: PixelFormat,
) -> Result<VideoFrame> { ) -> Result<VideoFrame> {
let device = Device::with_path(device_path).map_err(|e| { let device = Device::with_path(device_path)
AppError::VideoError(format!("Failed to open device: {}", e)) .map_err(|e| AppError::VideoError(format!("Failed to open device: {}", e)))?;
})?;
let fmt = Format::new(resolution.width, resolution.height, format.to_fourcc()); let fmt = Format::new(resolution.width, resolution.height, format.to_fourcc());
let actual = device.set_format(&fmt).map_err(|e| { let actual = device
AppError::VideoError(format!("Failed to set format: {}", e)) .set_format(&fmt)
})?; .map_err(|e| AppError::VideoError(format!("Failed to set format: {}", e)))?;
let mut stream = MmapStream::with_buffers(&device, BufferType::VideoCapture, 2) let mut stream = MmapStream::with_buffers(&device, BufferType::VideoCapture, 2)
.map_err(|e| AppError::VideoError(format!("Failed to create stream: {}", e)))?; .map_err(|e| AppError::VideoError(format!("Failed to create stream: {}", e)))?;
@@ -643,8 +634,7 @@ fn grab_single_frame(
match stream.next() { match stream.next() {
Ok((buf, _meta)) => { Ok((buf, _meta)) => {
if buf.len() >= MIN_FRAME_SIZE { if buf.len() >= MIN_FRAME_SIZE {
let actual_format = let actual_format = PixelFormat::from_fourcc(actual.fourcc).unwrap_or(format);
PixelFormat::from_fourcc(actual.fourcc).unwrap_or(format);
return Ok(VideoFrame::new( return Ok(VideoFrame::new(
Bytes::copy_from_slice(buf), Bytes::copy_from_slice(buf),
@@ -657,16 +647,15 @@ fn grab_single_frame(
} }
Err(e) => { Err(e) => {
if attempt == 4 { if attempt == 4 {
return Err(AppError::VideoError(format!( return Err(AppError::VideoError(format!("Failed to grab frame: {}", e)));
"Failed to grab frame: {}",
e
)));
} }
} }
} }
} }
Err(AppError::VideoError("Failed to capture valid frame".to_string())) Err(AppError::VideoError(
"Failed to capture valid frame".to_string(),
))
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -233,6 +233,16 @@ impl PixelConverter {
} }
} }
/// Create a new converter for NV21 → YUV420P
pub fn nv21_to_yuv420p(resolution: Resolution) -> Self {
Self {
src_format: PixelFormat::Nv21,
dst_format: PixelFormat::Yuv420,
resolution,
output_buffer: Yuv420pBuffer::new(resolution),
}
}
/// Create a new converter for YVU420 → YUV420P (swap U and V planes) /// Create a new converter for YVU420 → YUV420P (swap U and V planes)
pub fn yvu420_to_yuv420p(resolution: Resolution) -> Self { pub fn yvu420_to_yuv420p(resolution: Resolution) -> Self {
Self { Self {
@@ -272,23 +282,39 @@ impl PixelConverter {
match (self.src_format, self.dst_format) { match (self.src_format, self.dst_format) {
(PixelFormat::Yuyv, PixelFormat::Yuv420) => { (PixelFormat::Yuyv, PixelFormat::Yuv420) => {
libyuv::yuy2_to_i420(input, self.output_buffer.as_bytes_mut(), width, height) libyuv::yuy2_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| AppError::VideoError(format!("libyuv conversion failed: {}", e)))?; .map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
} }
(PixelFormat::Uyvy, PixelFormat::Yuv420) => { (PixelFormat::Uyvy, PixelFormat::Yuv420) => {
libyuv::uyvy_to_i420(input, self.output_buffer.as_bytes_mut(), width, height) libyuv::uyvy_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| AppError::VideoError(format!("libyuv conversion failed: {}", e)))?; .map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
} }
(PixelFormat::Nv12, PixelFormat::Yuv420) => { (PixelFormat::Nv12, PixelFormat::Yuv420) => {
libyuv::nv12_to_i420(input, self.output_buffer.as_bytes_mut(), width, height) libyuv::nv12_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| AppError::VideoError(format!("libyuv conversion failed: {}", e)))?; .map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
}
(PixelFormat::Nv21, PixelFormat::Yuv420) => {
libyuv::nv21_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
} }
(PixelFormat::Rgb24, PixelFormat::Yuv420) => { (PixelFormat::Rgb24, PixelFormat::Yuv420) => {
libyuv::rgb24_to_i420(input, self.output_buffer.as_bytes_mut(), width, height) libyuv::rgb24_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| AppError::VideoError(format!("libyuv conversion failed: {}", e)))?; .map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
} }
(PixelFormat::Bgr24, PixelFormat::Yuv420) => { (PixelFormat::Bgr24, PixelFormat::Yuv420) => {
libyuv::bgr24_to_i420(input, self.output_buffer.as_bytes_mut(), width, height) libyuv::bgr24_to_i420(input, self.output_buffer.as_bytes_mut(), width, height)
.map_err(|e| AppError::VideoError(format!("libyuv conversion failed: {}", e)))?; .map_err(|e| {
AppError::VideoError(format!("libyuv conversion failed: {}", e))
})?;
} }
(PixelFormat::Yvyu, PixelFormat::Yuv420) => { (PixelFormat::Yvyu, PixelFormat::Yuv420) => {
// YVYU is not directly supported by libyuv, use software conversion // YVYU is not directly supported by libyuv, use software conversion
@@ -307,7 +333,9 @@ impl PixelConverter {
expected_size expected_size
))); )));
} }
self.output_buffer.as_bytes_mut().copy_from_slice(&input[..expected_size]); self.output_buffer
.as_bytes_mut()
.copy_from_slice(&input[..expected_size]);
} }
_ => { _ => {
return Err(AppError::VideoError(format!( return Err(AppError::VideoError(format!(
@@ -426,6 +454,8 @@ pub struct Nv12Converter {
resolution: Resolution, resolution: Resolution,
/// Output buffer (reused across conversions) /// Output buffer (reused across conversions)
output_buffer: Nv12Buffer, output_buffer: Nv12Buffer,
/// Optional I420 buffer for intermediate conversions
i420_buffer: Option<Yuv420pBuffer>,
} }
impl Nv12Converter { impl Nv12Converter {
@@ -435,6 +465,7 @@ impl Nv12Converter {
src_format: PixelFormat::Bgr24, src_format: PixelFormat::Bgr24,
resolution, resolution,
output_buffer: Nv12Buffer::new(resolution), output_buffer: Nv12Buffer::new(resolution),
i420_buffer: None,
} }
} }
@@ -444,6 +475,7 @@ impl Nv12Converter {
src_format: PixelFormat::Rgb24, src_format: PixelFormat::Rgb24,
resolution, resolution,
output_buffer: Nv12Buffer::new(resolution), output_buffer: Nv12Buffer::new(resolution),
i420_buffer: None,
} }
} }
@@ -453,6 +485,37 @@ impl Nv12Converter {
src_format: PixelFormat::Yuyv, src_format: PixelFormat::Yuyv,
resolution, resolution,
output_buffer: Nv12Buffer::new(resolution), output_buffer: Nv12Buffer::new(resolution),
i420_buffer: None,
}
}
/// Create a new converter for YUV420P (I420) → NV12
pub fn yuv420_to_nv12(resolution: Resolution) -> Self {
Self {
src_format: PixelFormat::Yuv420,
resolution,
output_buffer: Nv12Buffer::new(resolution),
i420_buffer: None,
}
}
/// Create a new converter for NV21 → NV12
pub fn nv21_to_nv12(resolution: Resolution) -> Self {
Self {
src_format: PixelFormat::Nv21,
resolution,
output_buffer: Nv12Buffer::new(resolution),
i420_buffer: Some(Yuv420pBuffer::new(resolution)),
}
}
/// Create a new converter for NV16 → NV12 (downsample chroma vertically)
pub fn nv16_to_nv12(resolution: Resolution) -> Self {
Self {
src_format: PixelFormat::Nv16,
resolution,
output_buffer: Nv12Buffer::new(resolution),
i420_buffer: None,
} }
} }
@@ -460,12 +523,45 @@ impl Nv12Converter {
pub fn convert(&mut self, input: &[u8]) -> Result<&[u8]> { pub fn convert(&mut self, input: &[u8]) -> Result<&[u8]> {
let width = self.resolution.width as i32; let width = self.resolution.width as i32;
let height = self.resolution.height as i32; let height = self.resolution.height as i32;
let dst = self.output_buffer.as_bytes_mut();
// Handle formats that need custom conversion without holding dst borrow
match self.src_format {
PixelFormat::Nv21 => {
let mut i420 = self.i420_buffer.take().ok_or_else(|| {
AppError::VideoError("NV21 I420 buffer not initialized".to_string())
})?;
{
let dst = self.output_buffer.as_bytes_mut();
Self::convert_nv21_to_nv12_with_dims(
self.resolution.width as usize,
self.resolution.height as usize,
input,
dst,
&mut i420,
)?;
}
self.i420_buffer = Some(i420);
return Ok(self.output_buffer.as_bytes());
}
PixelFormat::Nv16 => {
let dst = self.output_buffer.as_bytes_mut();
Self::convert_nv16_to_nv12_with_dims(
self.resolution.width as usize,
self.resolution.height as usize,
input,
dst,
)?;
return Ok(self.output_buffer.as_bytes());
}
_ => {}
}
let dst = self.output_buffer.as_bytes_mut();
let result = match self.src_format { let result = match self.src_format {
PixelFormat::Bgr24 => libyuv::bgr24_to_nv12(input, dst, width, height), PixelFormat::Bgr24 => libyuv::bgr24_to_nv12(input, dst, width, height),
PixelFormat::Rgb24 => libyuv::rgb24_to_nv12(input, dst, width, height), PixelFormat::Rgb24 => libyuv::rgb24_to_nv12(input, dst, width, height),
PixelFormat::Yuyv => libyuv::yuy2_to_nv12(input, dst, width, height), PixelFormat::Yuyv => libyuv::yuy2_to_nv12(input, dst, width, height),
PixelFormat::Yuv420 => libyuv::i420_to_nv12(input, dst, width, height),
_ => { _ => {
return Err(AppError::VideoError(format!( return Err(AppError::VideoError(format!(
"Unsupported conversion to NV12: {}", "Unsupported conversion to NV12: {}",
@@ -474,10 +570,71 @@ impl Nv12Converter {
} }
}; };
result.map_err(|e| AppError::VideoError(format!("libyuv NV12 conversion failed: {}", e)))?; result
.map_err(|e| AppError::VideoError(format!("libyuv NV12 conversion failed: {}", e)))?;
Ok(self.output_buffer.as_bytes()) Ok(self.output_buffer.as_bytes())
} }
fn convert_nv21_to_nv12_with_dims(
width: usize,
height: usize,
input: &[u8],
dst: &mut [u8],
yuv: &mut Yuv420pBuffer,
) -> Result<()> {
libyuv::nv21_to_i420(input, yuv.as_bytes_mut(), width as i32, height as i32)
.map_err(|e| AppError::VideoError(format!("libyuv NV21->I420 failed: {}", e)))?;
libyuv::i420_to_nv12(yuv.as_bytes(), dst, width as i32, height as i32)
.map_err(|e| AppError::VideoError(format!("libyuv I420->NV12 failed: {}", e)))?;
Ok(())
}
fn convert_nv16_to_nv12_with_dims(
width: usize,
height: usize,
input: &[u8],
dst: &mut [u8],
) -> Result<()> {
let y_size = width * height;
let uv_size_nv16 = y_size; // NV16 chroma plane is full height
let uv_size_nv12 = y_size / 2;
if input.len() < y_size + uv_size_nv16 {
return Err(AppError::VideoError(format!(
"NV16 data too small: {} < {}",
input.len(),
y_size + uv_size_nv16
)));
}
// Copy Y plane as-is
dst[..y_size].copy_from_slice(&input[..y_size]);
// Downsample chroma vertically: average pairs of rows
let src_uv = &input[y_size..y_size + uv_size_nv16];
let dst_uv = &mut dst[y_size..y_size + uv_size_nv12];
let src_row_bytes = width;
let dst_row_bytes = width;
let dst_rows = height / 2;
for row in 0..dst_rows {
let src_row0 =
&src_uv[row * 2 * src_row_bytes..row * 2 * src_row_bytes + src_row_bytes];
let src_row1 = &src_uv
[(row * 2 + 1) * src_row_bytes..(row * 2 + 1) * src_row_bytes + src_row_bytes];
let dst_row = &mut dst_uv[row * dst_row_bytes..row * dst_row_bytes + dst_row_bytes];
for i in 0..dst_row_bytes {
let sum = src_row0[i] as u16 + src_row1[i] as u16;
dst_row[i] = (sum / 2) as u8;
}
}
Ok(())
}
/// Get output buffer length /// Get output buffer length
pub fn output_len(&self) -> usize { pub fn output_len(&self) -> usize {
self.output_buffer.len() self.output_buffer.len()
@@ -542,10 +699,8 @@ mod tests {
// Create YUYV data (4x4 = 32 bytes) // Create YUYV data (4x4 = 32 bytes)
let yuyv = vec![ let yuyv = vec![
16, 128, 17, 129, 18, 130, 19, 131, 16, 128, 17, 129, 18, 130, 19, 131, 20, 132, 21, 133, 22, 134, 23, 135, 24, 136, 25,
20, 132, 21, 133, 22, 134, 23, 135, 137, 26, 138, 27, 139, 28, 140, 29, 141, 30, 142, 31, 143,
24, 136, 25, 137, 26, 138, 27, 139,
28, 140, 29, 141, 30, 142, 31, 143,
]; ];
let result = converter.convert(&yuyv).unwrap(); let result = converter.convert(&yuyv).unwrap();

View File

@@ -95,9 +95,10 @@ impl VideoDevice {
/// Get device capabilities /// Get device capabilities
pub fn capabilities(&self) -> Result<DeviceCapabilities> { pub fn capabilities(&self) -> Result<DeviceCapabilities> {
let caps = self.device.query_caps().map_err(|e| { let caps = self
AppError::VideoError(format!("Failed to query capabilities: {}", e)) .device
})?; .query_caps()
.map_err(|e| AppError::VideoError(format!("Failed to query capabilities: {}", e)))?;
Ok(DeviceCapabilities { Ok(DeviceCapabilities {
video_capture: caps.capabilities.contains(Flags::VIDEO_CAPTURE), video_capture: caps.capabilities.contains(Flags::VIDEO_CAPTURE),
@@ -110,9 +111,10 @@ impl VideoDevice {
/// Get detailed device information /// Get detailed device information
pub fn info(&self) -> Result<VideoDeviceInfo> { pub fn info(&self) -> Result<VideoDeviceInfo> {
let caps = self.device.query_caps().map_err(|e| { let caps = self
AppError::VideoError(format!("Failed to query capabilities: {}", e)) .device
})?; .query_caps()
.map_err(|e| AppError::VideoError(format!("Failed to query capabilities: {}", e)))?;
let capabilities = DeviceCapabilities { let capabilities = DeviceCapabilities {
video_capture: caps.capabilities.contains(Flags::VIDEO_CAPTURE), video_capture: caps.capabilities.contains(Flags::VIDEO_CAPTURE),
@@ -128,7 +130,8 @@ impl VideoDevice {
let is_capture_card = Self::detect_capture_card(&caps.card, &caps.driver, &formats); let is_capture_card = Self::detect_capture_card(&caps.card, &caps.driver, &formats);
// Calculate priority score // Calculate priority score
let priority = Self::calculate_priority(&caps.card, &caps.driver, &formats, is_capture_card); let priority =
Self::calculate_priority(&caps.card, &caps.driver, &formats, is_capture_card);
Ok(VideoDeviceInfo { Ok(VideoDeviceInfo {
path: self.path.clone(), path: self.path.clone(),
@@ -148,9 +151,10 @@ impl VideoDevice {
let mut formats = Vec::new(); let mut formats = Vec::new();
// Get supported formats // Get supported formats
let format_descs = self.device.enum_formats().map_err(|e| { let format_descs = self
AppError::VideoError(format!("Failed to enumerate formats: {}", e)) .device
})?; .enum_formats()
.map_err(|e| AppError::VideoError(format!("Failed to enumerate formats: {}", e)))?;
for desc in format_descs { for desc in format_descs {
// Try to convert FourCC to our PixelFormat // Try to convert FourCC to our PixelFormat
@@ -186,7 +190,9 @@ impl VideoDevice {
for size in sizes { for size in sizes {
match size.size { match size.size {
v4l::framesize::FrameSizeEnum::Discrete(d) => { v4l::framesize::FrameSizeEnum::Discrete(d) => {
let fps = self.enumerate_fps(fourcc, d.width, d.height).unwrap_or_default(); let fps = self
.enumerate_fps(fourcc, d.width, d.height)
.unwrap_or_default();
resolutions.push(ResolutionInfo::new(d.width, d.height, fps)); resolutions.push(ResolutionInfo::new(d.width, d.height, fps));
} }
v4l::framesize::FrameSizeEnum::Stepwise(s) => { v4l::framesize::FrameSizeEnum::Stepwise(s) => {
@@ -202,8 +208,11 @@ impl VideoDevice {
&& res.height >= s.min_height && res.height >= s.min_height
&& res.height <= s.max_height && res.height <= s.max_height
{ {
let fps = self.enumerate_fps(fourcc, res.width, res.height).unwrap_or_default(); let fps = self
resolutions.push(ResolutionInfo::new(res.width, res.height, fps)); .enumerate_fps(fourcc, res.width, res.height)
.unwrap_or_default();
resolutions
.push(ResolutionInfo::new(res.width, res.height, fps));
} }
} }
} }
@@ -263,9 +272,9 @@ impl VideoDevice {
/// Get current format /// Get current format
pub fn get_format(&self) -> Result<Format> { pub fn get_format(&self) -> Result<Format> {
self.device.format().map_err(|e| { self.device
AppError::VideoError(format!("Failed to get format: {}", e)) .format()
}) .map_err(|e| AppError::VideoError(format!("Failed to get format: {}", e)))
} }
/// Set capture format /// Set capture format
@@ -273,9 +282,10 @@ impl VideoDevice {
let fmt = Format::new(width, height, format.to_fourcc()); let fmt = Format::new(width, height, format.to_fourcc());
// Request the format // Request the format
let actual = self.device.set_format(&fmt).map_err(|e| { let actual = self
AppError::VideoError(format!("Failed to set format: {}", e)) .device
})?; .set_format(&fmt)
.map_err(|e| AppError::VideoError(format!("Failed to set format: {}", e)))?;
if actual.width != width || actual.height != height { if actual.width != width || actual.height != height {
warn!( warn!(
@@ -374,9 +384,9 @@ pub fn enumerate_devices() -> Result<Vec<VideoDeviceInfo>> {
let mut devices = Vec::new(); let mut devices = Vec::new();
// Scan /dev/video* devices // Scan /dev/video* devices
for entry in std::fs::read_dir("/dev").map_err(|e| { for entry in std::fs::read_dir("/dev")
AppError::VideoError(format!("Failed to read /dev: {}", e)) .map_err(|e| AppError::VideoError(format!("Failed to read /dev: {}", e)))?
})? { {
let entry = match entry { let entry = match entry {
Ok(e) => e, Ok(e) => e,
Err(_) => continue, Err(_) => continue,
@@ -432,9 +442,10 @@ pub fn enumerate_devices() -> Result<Vec<VideoDeviceInfo>> {
pub fn find_best_device() -> Result<VideoDeviceInfo> { pub fn find_best_device() -> Result<VideoDeviceInfo> {
let devices = enumerate_devices()?; let devices = enumerate_devices()?;
devices.into_iter().next().ok_or_else(|| { devices
AppError::VideoError("No video capture devices found".to_string()) .into_iter()
}) .next()
.ok_or_else(|| AppError::VideoError("No video capture devices found".to_string()))
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -99,8 +99,18 @@ pub enum H264InputFormat {
Yuv420p, Yuv420p,
/// NV12 - Y plane + interleaved UV plane (optimal for VAAPI) /// NV12 - Y plane + interleaved UV plane (optimal for VAAPI)
Nv12, Nv12,
/// NV21 - Y plane + interleaved VU plane
Nv21,
/// NV16 - Y plane + interleaved UV plane (4:2:2)
Nv16,
/// NV24 - Y plane + interleaved UV plane (4:4:4)
Nv24,
/// YUYV422 - packed YUV 4:2:2 format (optimal for RKMPP direct input) /// YUYV422 - packed YUV 4:2:2 format (optimal for RKMPP direct input)
Yuyv422, Yuyv422,
/// RGB24 - packed RGB format (RKMPP direct input)
Rgb24,
/// BGR24 - packed BGR format (RKMPP direct input)
Bgr24,
} }
impl Default for H264InputFormat { impl Default for H264InputFormat {
@@ -270,9 +280,8 @@ impl H264Encoder {
// Detect best encoder // Detect best encoder
let (_encoder_type, codec_name) = detect_best_encoder(width, height); let (_encoder_type, codec_name) = detect_best_encoder(width, height);
let codec_name = codec_name.ok_or_else(|| { let codec_name = codec_name
AppError::VideoError("No H.264 encoder available".to_string()) .ok_or_else(|| AppError::VideoError("No H.264 encoder available".to_string()))?;
})?;
Self::with_codec(config, &codec_name) Self::with_codec(config, &codec_name)
} }
@@ -287,8 +296,13 @@ impl H264Encoder {
// Select pixel format based on config // Select pixel format based on config
let pixfmt = match config.input_format { let pixfmt = match config.input_format {
H264InputFormat::Nv12 => AVPixelFormat::AV_PIX_FMT_NV12, H264InputFormat::Nv12 => AVPixelFormat::AV_PIX_FMT_NV12,
H264InputFormat::Nv21 => AVPixelFormat::AV_PIX_FMT_NV21,
H264InputFormat::Nv16 => AVPixelFormat::AV_PIX_FMT_NV16,
H264InputFormat::Nv24 => AVPixelFormat::AV_PIX_FMT_NV24,
H264InputFormat::Yuv420p => AVPixelFormat::AV_PIX_FMT_YUV420P, H264InputFormat::Yuv420p => AVPixelFormat::AV_PIX_FMT_YUV420P,
H264InputFormat::Yuyv422 => AVPixelFormat::AV_PIX_FMT_YUYV422, H264InputFormat::Yuyv422 => AVPixelFormat::AV_PIX_FMT_YUYV422,
H264InputFormat::Rgb24 => AVPixelFormat::AV_PIX_FMT_RGB24,
H264InputFormat::Bgr24 => AVPixelFormat::AV_PIX_FMT_BGR24,
}; };
info!( info!(
@@ -353,9 +367,9 @@ impl H264Encoder {
/// Update bitrate dynamically /// Update bitrate dynamically
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> { pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
self.inner.set_bitrate(bitrate_kbps as i32).map_err(|_| { self.inner
AppError::VideoError("Failed to set bitrate".to_string()) .set_bitrate(bitrate_kbps as i32)
})?; .map_err(|_| AppError::VideoError("Failed to set bitrate".to_string()))?;
self.config.bitrate_kbps = bitrate_kbps; self.config.bitrate_kbps = bitrate_kbps;
debug!("Bitrate updated to {} kbps", bitrate_kbps); debug!("Bitrate updated to {} kbps", bitrate_kbps);
Ok(()) Ok(())
@@ -394,16 +408,7 @@ impl H264Encoder {
Ok(owned_frames) Ok(owned_frames)
} }
Err(e) => { Err(e) => {
// For the first ~30 frames, x264 may fail due to initialization
// Log as warning instead of error to avoid alarming users
if self.frame_count <= 30 {
warn!(
"Encode failed during initialization (frame {}): {} - this is normal for x264",
self.frame_count, e
);
} else {
error!("Encode failed: {}", e); error!("Encode failed: {}", e);
}
Err(AppError::VideoError(format!("Encode failed: {}", e))) Err(AppError::VideoError(format!("Encode failed: {}", e)))
} }
} }
@@ -458,7 +463,9 @@ impl Encoder for H264Encoder {
if frames.is_empty() { if frames.is_empty() {
// Encoder needs more frames (shouldn't happen with our config) // Encoder needs more frames (shouldn't happen with our config)
warn!("Encoder returned no frames"); warn!("Encoder returned no frames");
return Err(AppError::VideoError("Encoder returned no frames".to_string())); return Err(AppError::VideoError(
"Encoder returned no frames".to_string(),
));
} }
// Take ownership of the first frame (zero-copy) // Take ownership of the first frame (zero-copy)
@@ -493,8 +500,13 @@ impl Encoder for H264Encoder {
// Check if the format matches our configured input format // Check if the format matches our configured input format
match self.config.input_format { match self.config.input_format {
H264InputFormat::Nv12 => matches!(format, PixelFormat::Nv12), H264InputFormat::Nv12 => matches!(format, PixelFormat::Nv12),
H264InputFormat::Nv21 => matches!(format, PixelFormat::Nv21),
H264InputFormat::Nv16 => matches!(format, PixelFormat::Nv16),
H264InputFormat::Nv24 => matches!(format, PixelFormat::Nv24),
H264InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420), H264InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420),
H264InputFormat::Yuyv422 => matches!(format, PixelFormat::Yuyv), H264InputFormat::Yuyv422 => matches!(format, PixelFormat::Yuyv),
H264InputFormat::Rgb24 => matches!(format, PixelFormat::Rgb24),
H264InputFormat::Bgr24 => matches!(format, PixelFormat::Bgr24),
} }
} }
} }
@@ -538,7 +550,11 @@ mod tests {
let config = H264Config::low_latency(Resolution::HD720, 2000); let config = H264Config::low_latency(Resolution::HD720, 2000);
match H264Encoder::new(config) { match H264Encoder::new(config) {
Ok(encoder) => { Ok(encoder) => {
println!("Created encoder: {} ({})", encoder.codec_name(), encoder.encoder_type()); println!(
"Created encoder: {} ({})",
encoder.codec_name(),
encoder.encoder_type()
);
} }
Err(e) => { Err(e) => {
println!("Failed to create encoder: {}", e); println!("Failed to create encoder: {}", e);

View File

@@ -92,8 +92,18 @@ pub enum H265InputFormat {
Yuv420p, Yuv420p,
/// NV12 - Y plane + interleaved UV plane (optimal for hardware encoders) /// NV12 - Y plane + interleaved UV plane (optimal for hardware encoders)
Nv12, Nv12,
/// NV21 - Y plane + interleaved VU plane
Nv21,
/// NV16 - Y plane + interleaved UV plane (4:2:2)
Nv16,
/// NV24 - Y plane + interleaved UV plane (4:4:4)
Nv24,
/// YUYV422 - packed YUV 4:2:2 format (optimal for RKMPP direct input) /// YUYV422 - packed YUV 4:2:2 format (optimal for RKMPP direct input)
Yuyv422, Yuyv422,
/// RGB24 - packed RGB format (RKMPP direct input)
Rgb24,
/// BGR24 - packed BGR format (RKMPP direct input)
Bgr24,
} }
impl Default for H265InputFormat { impl Default for H265InputFormat {
@@ -252,10 +262,7 @@ pub fn detect_best_h265_encoder(width: u32, height: u32) -> (H265EncoderType, Op
H265EncoderType::Software // Default to software for unknown H265EncoderType::Software // Default to software for unknown
}; };
info!( info!("Selected H.265 encoder: {} ({})", codec.name, encoder_type);
"Selected H.265 encoder: {} ({})",
codec.name, encoder_type
);
(encoder_type, Some(codec.name.clone())) (encoder_type, Some(codec.name.clone()))
} }
@@ -304,7 +311,8 @@ impl H265Encoder {
if encoder_type == H265EncoderType::None { if encoder_type == H265EncoderType::None {
return Err(AppError::VideoError( return Err(AppError::VideoError(
"No H.265 encoder available. Please ensure FFmpeg is built with libx265 support.".to_string(), "No H.265 encoder available. Please ensure FFmpeg is built with libx265 support."
.to_string(),
)); ));
} }
@@ -336,8 +344,17 @@ impl H265Encoder {
} else { } else {
match config.input_format { match config.input_format {
H265InputFormat::Nv12 => (AVPixelFormat::AV_PIX_FMT_NV12, H265InputFormat::Nv12), H265InputFormat::Nv12 => (AVPixelFormat::AV_PIX_FMT_NV12, H265InputFormat::Nv12),
H265InputFormat::Yuv420p => (AVPixelFormat::AV_PIX_FMT_YUV420P, H265InputFormat::Yuv420p), H265InputFormat::Nv21 => (AVPixelFormat::AV_PIX_FMT_NV21, H265InputFormat::Nv21),
H265InputFormat::Yuyv422 => (AVPixelFormat::AV_PIX_FMT_YUYV422, H265InputFormat::Yuyv422), H265InputFormat::Nv16 => (AVPixelFormat::AV_PIX_FMT_NV16, H265InputFormat::Nv16),
H265InputFormat::Nv24 => (AVPixelFormat::AV_PIX_FMT_NV24, H265InputFormat::Nv24),
H265InputFormat::Yuv420p => {
(AVPixelFormat::AV_PIX_FMT_YUV420P, H265InputFormat::Yuv420p)
}
H265InputFormat::Yuyv422 => {
(AVPixelFormat::AV_PIX_FMT_YUYV422, H265InputFormat::Yuyv422)
}
H265InputFormat::Rgb24 => (AVPixelFormat::AV_PIX_FMT_RGB24, H265InputFormat::Rgb24),
H265InputFormat::Bgr24 => (AVPixelFormat::AV_PIX_FMT_BGR24, H265InputFormat::Bgr24),
} }
}; };
@@ -407,9 +424,9 @@ impl H265Encoder {
/// Update bitrate dynamically /// Update bitrate dynamically
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> { pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
self.inner.set_bitrate(bitrate_kbps as i32).map_err(|_| { self.inner
AppError::VideoError("Failed to set H.265 bitrate".to_string()) .set_bitrate(bitrate_kbps as i32)
})?; .map_err(|_| AppError::VideoError("Failed to set H.265 bitrate".to_string()))?;
self.config.bitrate_kbps = bitrate_kbps; self.config.bitrate_kbps = bitrate_kbps;
debug!("H.265 bitrate updated to {} kbps", bitrate_kbps); debug!("H.265 bitrate updated to {} kbps", bitrate_kbps);
Ok(()) Ok(())
@@ -464,7 +481,10 @@ impl H265Encoder {
if keyframe || self.frame_count % 30 == 1 { if keyframe || self.frame_count % 30 == 1 {
debug!( debug!(
"[H265] Encoded frame #{}: output_size={}, keyframe={}, frame_count={}", "[H265] Encoded frame #{}: output_size={}, keyframe={}, frame_count={}",
self.frame_count, total_size, keyframe, owned_frames.len() self.frame_count,
total_size,
keyframe,
owned_frames.len()
); );
// Log first few bytes of keyframe for debugging // Log first few bytes of keyframe for debugging
@@ -477,7 +497,10 @@ impl H265Encoder {
} }
} }
} else { } else {
warn!("[H265] Encoder returned empty frame list for frame #{}", self.frame_count); warn!(
"[H265] Encoder returned empty frame list for frame #{}",
self.frame_count
);
} }
Ok(owned_frames) Ok(owned_frames)
@@ -567,8 +590,13 @@ impl Encoder for H265Encoder {
fn supports_format(&self, format: PixelFormat) -> bool { fn supports_format(&self, format: PixelFormat) -> bool {
match self.config.input_format { match self.config.input_format {
H265InputFormat::Nv12 => matches!(format, PixelFormat::Nv12), H265InputFormat::Nv12 => matches!(format, PixelFormat::Nv12),
H265InputFormat::Nv21 => matches!(format, PixelFormat::Nv21),
H265InputFormat::Nv16 => matches!(format, PixelFormat::Nv16),
H265InputFormat::Nv24 => matches!(format, PixelFormat::Nv24),
H265InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420), H265InputFormat::Yuv420p => matches!(format, PixelFormat::Yuv420),
H265InputFormat::Yuyv422 => matches!(format, PixelFormat::Yuyv), H265InputFormat::Yuyv422 => matches!(format, PixelFormat::Yuyv),
H265InputFormat::Rgb24 => matches!(format, PixelFormat::Rgb24),
H265InputFormat::Bgr24 => matches!(format, PixelFormat::Bgr24),
} }
} }
} }
@@ -580,7 +608,10 @@ mod tests {
#[test] #[test]
fn test_detect_h265_encoder() { fn test_detect_h265_encoder() {
let (encoder_type, codec_name) = detect_best_h265_encoder(1280, 720); let (encoder_type, codec_name) = detect_best_h265_encoder(1280, 720);
println!("Detected H.265 encoder: {:?} ({:?})", encoder_type, codec_name); println!(
"Detected H.265 encoder: {:?} ({:?})",
encoder_type, codec_name
);
} }
#[test] #[test]

View File

@@ -35,10 +35,12 @@ impl JpegEncoder {
// I420: Y = width*height, U = width*height/4, V = width*height/4 // I420: Y = width*height, U = width*height/4, V = width*height/4
let i420_size = width * height * 3 / 2; let i420_size = width * height * 3 / 2;
let mut compressor = turbojpeg::Compressor::new() let mut compressor = turbojpeg::Compressor::new().map_err(|e| {
.map_err(|e| AppError::VideoError(format!("Failed to create turbojpeg compressor: {}", e)))?; AppError::VideoError(format!("Failed to create turbojpeg compressor: {}", e))
})?;
compressor.set_quality(config.quality.min(100) as i32) compressor
.set_quality(config.quality.min(100) as i32)
.map_err(|e| AppError::VideoError(format!("Failed to set JPEG quality: {}", e)))?; .map_err(|e| AppError::VideoError(format!("Failed to set JPEG quality: {}", e)))?;
Ok(Self { Ok(Self {
@@ -56,7 +58,8 @@ impl JpegEncoder {
/// Set JPEG quality (1-100) /// Set JPEG quality (1-100)
pub fn set_quality(&mut self, quality: u32) -> Result<()> { pub fn set_quality(&mut self, quality: u32) -> Result<()> {
self.compressor.set_quality(quality.min(100) as i32) self.compressor
.set_quality(quality.min(100) as i32)
.map_err(|e| AppError::VideoError(format!("Failed to set JPEG quality: {}", e)))?; .map_err(|e| AppError::VideoError(format!("Failed to set JPEG quality: {}", e)))?;
self.config.quality = quality; self.config.quality = quality;
Ok(()) Ok(())
@@ -78,7 +81,9 @@ impl JpegEncoder {
}; };
// Compress YUV directly to JPEG (skips color space conversion!) // Compress YUV directly to JPEG (skips color space conversion!)
let jpeg_data = self.compressor.compress_yuv_to_vec(yuv_image) let jpeg_data = self
.compressor
.compress_yuv_to_vec(yuv_image)
.map_err(|e| AppError::VideoError(format!("JPEG compression failed: {}", e)))?; .map_err(|e| AppError::VideoError(format!("JPEG compression failed: {}", e)))?;
Ok(EncodedFrame::jpeg( Ok(EncodedFrame::jpeg(

View File

@@ -19,7 +19,9 @@ pub mod vp8;
pub mod vp9; pub mod vp9;
// Core traits and types // Core traits and types
pub use traits::{BitratePreset, EncodedFormat, EncodedFrame, Encoder, EncoderConfig, EncoderFactory}; pub use traits::{
BitratePreset, EncodedFormat, EncodedFrame, Encoder, EncoderConfig, EncoderFactory,
};
// WebRTC codec abstraction // WebRTC codec abstraction
pub use codec::{CodecFrame, VideoCodec, VideoCodecConfig, VideoCodecFactory, VideoCodecType}; pub use codec::{CodecFrame, VideoCodec, VideoCodecConfig, VideoCodecFactory, VideoCodecType};

View File

@@ -264,10 +264,7 @@ impl EncoderRegistry {
if let Some(encoder) = AvailableEncoder::from_codec_info(codec_info) { if let Some(encoder) = AvailableEncoder::from_codec_info(codec_info) {
debug!( debug!(
"Detected encoder: {} ({}) - {} priority={}", "Detected encoder: {} ({}) - {} priority={}",
encoder.codec_name, encoder.codec_name, encoder.format, encoder.backend, encoder.priority
encoder.format,
encoder.backend,
encoder.priority
); );
self.encoders self.encoders
@@ -336,13 +333,15 @@ impl EncoderRegistry {
format: VideoEncoderType, format: VideoEncoderType,
hardware_only: bool, hardware_only: bool,
) -> Option<&AvailableEncoder> { ) -> Option<&AvailableEncoder> {
self.encoders.get(&format)?.iter().find(|e| { self.encoders.get(&format)?.iter().find(
|e| {
if hardware_only { if hardware_only {
e.is_hardware e.is_hardware
} else { } else {
true true
} }
}) },
)
} }
/// Get all encoders for a format /// Get all encoders for a format
@@ -523,9 +522,6 @@ mod tests {
// Should have detected at least H264 (software fallback available) // Should have detected at least H264 (software fallback available)
println!("Available formats: {:?}", registry.available_formats(false)); println!("Available formats: {:?}", registry.available_formats(false));
println!( println!("Selectable formats: {:?}", registry.selectable_formats());
"Selectable formats: {:?}",
registry.selectable_formats()
);
} }
} }

View File

@@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize};
use std::time::Instant; use std::time::Instant;
use typeshare::typeshare; use typeshare::typeshare;
use crate::video::format::{PixelFormat, Resolution};
use crate::error::Result; use crate::error::Result;
use crate::video::format::{PixelFormat, Resolution};
/// Bitrate preset for video encoding /// Bitrate preset for video encoding
/// ///

View File

@@ -186,10 +186,7 @@ pub fn detect_best_vp8_encoder(width: u32, height: u32) -> (VP8EncoderType, Opti
VP8EncoderType::Software // Default to software for unknown VP8EncoderType::Software // Default to software for unknown
}; };
info!( info!("Selected VP8 encoder: {} ({})", codec.name, encoder_type);
"Selected VP8 encoder: {} ({})",
codec.name, encoder_type
);
(encoder_type, Some(codec.name.clone())) (encoder_type, Some(codec.name.clone()))
} }
@@ -238,7 +235,8 @@ impl VP8Encoder {
if encoder_type == VP8EncoderType::None { if encoder_type == VP8EncoderType::None {
return Err(AppError::VideoError( return Err(AppError::VideoError(
"No VP8 encoder available. Please ensure FFmpeg is built with libvpx support.".to_string(), "No VP8 encoder available. Please ensure FFmpeg is built with libvpx support."
.to_string(),
)); ));
} }
@@ -270,7 +268,9 @@ impl VP8Encoder {
} else { } else {
match config.input_format { match config.input_format {
VP8InputFormat::Nv12 => (AVPixelFormat::AV_PIX_FMT_NV12, VP8InputFormat::Nv12), VP8InputFormat::Nv12 => (AVPixelFormat::AV_PIX_FMT_NV12, VP8InputFormat::Nv12),
VP8InputFormat::Yuv420p => (AVPixelFormat::AV_PIX_FMT_YUV420P, VP8InputFormat::Yuv420p), VP8InputFormat::Yuv420p => {
(AVPixelFormat::AV_PIX_FMT_YUV420P, VP8InputFormat::Yuv420p)
}
} }
}; };
@@ -340,9 +340,9 @@ impl VP8Encoder {
/// Update bitrate dynamically /// Update bitrate dynamically
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> { pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
self.inner.set_bitrate(bitrate_kbps as i32).map_err(|_| { self.inner
AppError::VideoError("Failed to set VP8 bitrate".to_string()) .set_bitrate(bitrate_kbps as i32)
})?; .map_err(|_| AppError::VideoError("Failed to set VP8 bitrate".to_string()))?;
self.config.bitrate_kbps = bitrate_kbps; self.config.bitrate_kbps = bitrate_kbps;
debug!("VP8 bitrate updated to {} kbps", bitrate_kbps); debug!("VP8 bitrate updated to {} kbps", bitrate_kbps);
Ok(()) Ok(())
@@ -470,7 +470,10 @@ mod tests {
#[test] #[test]
fn test_detect_vp8_encoder() { fn test_detect_vp8_encoder() {
let (encoder_type, codec_name) = detect_best_vp8_encoder(1280, 720); let (encoder_type, codec_name) = detect_best_vp8_encoder(1280, 720);
println!("Detected VP8 encoder: {:?} ({:?})", encoder_type, codec_name); println!(
"Detected VP8 encoder: {:?} ({:?})",
encoder_type, codec_name
);
} }
#[test] #[test]

View File

@@ -186,10 +186,7 @@ pub fn detect_best_vp9_encoder(width: u32, height: u32) -> (VP9EncoderType, Opti
VP9EncoderType::Software // Default to software for unknown VP9EncoderType::Software // Default to software for unknown
}; };
info!( info!("Selected VP9 encoder: {} ({})", codec.name, encoder_type);
"Selected VP9 encoder: {} ({})",
codec.name, encoder_type
);
(encoder_type, Some(codec.name.clone())) (encoder_type, Some(codec.name.clone()))
} }
@@ -238,7 +235,8 @@ impl VP9Encoder {
if encoder_type == VP9EncoderType::None { if encoder_type == VP9EncoderType::None {
return Err(AppError::VideoError( return Err(AppError::VideoError(
"No VP9 encoder available. Please ensure FFmpeg is built with libvpx support.".to_string(), "No VP9 encoder available. Please ensure FFmpeg is built with libvpx support."
.to_string(),
)); ));
} }
@@ -270,7 +268,9 @@ impl VP9Encoder {
} else { } else {
match config.input_format { match config.input_format {
VP9InputFormat::Nv12 => (AVPixelFormat::AV_PIX_FMT_NV12, VP9InputFormat::Nv12), VP9InputFormat::Nv12 => (AVPixelFormat::AV_PIX_FMT_NV12, VP9InputFormat::Nv12),
VP9InputFormat::Yuv420p => (AVPixelFormat::AV_PIX_FMT_YUV420P, VP9InputFormat::Yuv420p), VP9InputFormat::Yuv420p => {
(AVPixelFormat::AV_PIX_FMT_YUV420P, VP9InputFormat::Yuv420p)
}
} }
}; };
@@ -340,9 +340,9 @@ impl VP9Encoder {
/// Update bitrate dynamically /// Update bitrate dynamically
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> { pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
self.inner.set_bitrate(bitrate_kbps as i32).map_err(|_| { self.inner
AppError::VideoError("Failed to set VP9 bitrate".to_string()) .set_bitrate(bitrate_kbps as i32)
})?; .map_err(|_| AppError::VideoError("Failed to set VP9 bitrate".to_string()))?;
self.config.bitrate_kbps = bitrate_kbps; self.config.bitrate_kbps = bitrate_kbps;
debug!("VP9 bitrate updated to {} kbps", bitrate_kbps); debug!("VP9 bitrate updated to {} kbps", bitrate_kbps);
Ok(()) Ok(())
@@ -470,7 +470,10 @@ mod tests {
#[test] #[test]
fn test_detect_vp9_encoder() { fn test_detect_vp9_encoder() {
let (encoder_type, codec_name) = detect_best_vp9_encoder(1280, 720); let (encoder_type, codec_name) = detect_best_vp9_encoder(1280, 720);
println!("Detected VP9 encoder: {:?} ({:?})", encoder_type, codec_name); println!(
"Detected VP9 encoder: {:?} ({:?})",
encoder_type, codec_name
);
} }
#[test] #[test]

View File

@@ -20,6 +20,8 @@ pub enum PixelFormat {
Uyvy, Uyvy,
/// NV12 semi-planar format (Y plane + interleaved UV) /// NV12 semi-planar format (Y plane + interleaved UV)
Nv12, Nv12,
/// NV21 semi-planar format (Y plane + interleaved VU)
Nv21,
/// NV16 semi-planar format /// NV16 semi-planar format
Nv16, Nv16,
/// NV24 semi-planar format /// NV24 semi-planar format
@@ -48,6 +50,7 @@ impl PixelFormat {
PixelFormat::Yvyu => fourcc::FourCC::new(b"YVYU"), PixelFormat::Yvyu => fourcc::FourCC::new(b"YVYU"),
PixelFormat::Uyvy => fourcc::FourCC::new(b"UYVY"), PixelFormat::Uyvy => fourcc::FourCC::new(b"UYVY"),
PixelFormat::Nv12 => fourcc::FourCC::new(b"NV12"), PixelFormat::Nv12 => fourcc::FourCC::new(b"NV12"),
PixelFormat::Nv21 => fourcc::FourCC::new(b"NV21"),
PixelFormat::Nv16 => fourcc::FourCC::new(b"NV16"), PixelFormat::Nv16 => fourcc::FourCC::new(b"NV16"),
PixelFormat::Nv24 => fourcc::FourCC::new(b"NV24"), PixelFormat::Nv24 => fourcc::FourCC::new(b"NV24"),
PixelFormat::Yuv420 => fourcc::FourCC::new(b"YU12"), PixelFormat::Yuv420 => fourcc::FourCC::new(b"YU12"),
@@ -69,6 +72,7 @@ impl PixelFormat {
b"YVYU" => Some(PixelFormat::Yvyu), b"YVYU" => Some(PixelFormat::Yvyu),
b"UYVY" => Some(PixelFormat::Uyvy), b"UYVY" => Some(PixelFormat::Uyvy),
b"NV12" => Some(PixelFormat::Nv12), b"NV12" => Some(PixelFormat::Nv12),
b"NV21" => Some(PixelFormat::Nv21),
b"NV16" => Some(PixelFormat::Nv16), b"NV16" => Some(PixelFormat::Nv16),
b"NV24" => Some(PixelFormat::Nv24), b"NV24" => Some(PixelFormat::Nv24),
b"YU12" | b"I420" => Some(PixelFormat::Yuv420), b"YU12" | b"I420" => Some(PixelFormat::Yuv420),
@@ -92,7 +96,9 @@ impl PixelFormat {
match self { match self {
PixelFormat::Mjpeg | PixelFormat::Jpeg => None, PixelFormat::Mjpeg | PixelFormat::Jpeg => None,
PixelFormat::Yuyv | PixelFormat::Yvyu | PixelFormat::Uyvy => Some(2), PixelFormat::Yuyv | PixelFormat::Yvyu | PixelFormat::Uyvy => Some(2),
PixelFormat::Nv12 | PixelFormat::Yuv420 | PixelFormat::Yvu420 => None, // Variable PixelFormat::Nv12 | PixelFormat::Nv21 | PixelFormat::Yuv420 | PixelFormat::Yvu420 => {
None
} // Variable
PixelFormat::Nv16 => None, PixelFormat::Nv16 => None,
PixelFormat::Nv24 => None, PixelFormat::Nv24 => None,
PixelFormat::Rgb565 => Some(2), PixelFormat::Rgb565 => Some(2),
@@ -108,7 +114,9 @@ impl PixelFormat {
match self { match self {
PixelFormat::Mjpeg | PixelFormat::Jpeg => None, PixelFormat::Mjpeg | PixelFormat::Jpeg => None,
PixelFormat::Yuyv | PixelFormat::Yvyu | PixelFormat::Uyvy => Some(pixels * 2), PixelFormat::Yuyv | PixelFormat::Yvyu | PixelFormat::Uyvy => Some(pixels * 2),
PixelFormat::Nv12 | PixelFormat::Yuv420 | PixelFormat::Yvu420 => Some(pixels * 3 / 2), PixelFormat::Nv12 | PixelFormat::Nv21 | PixelFormat::Yuv420 | PixelFormat::Yvu420 => {
Some(pixels * 3 / 2)
}
PixelFormat::Nv16 => Some(pixels * 2), PixelFormat::Nv16 => Some(pixels * 2),
PixelFormat::Nv24 => Some(pixels * 3), PixelFormat::Nv24 => Some(pixels * 3),
PixelFormat::Rgb565 => Some(pixels * 2), PixelFormat::Rgb565 => Some(pixels * 2),
@@ -125,6 +133,7 @@ impl PixelFormat {
PixelFormat::Jpeg => 99, PixelFormat::Jpeg => 99,
PixelFormat::Yuyv => 80, PixelFormat::Yuyv => 80,
PixelFormat::Nv12 => 75, PixelFormat::Nv12 => 75,
PixelFormat::Nv21 => 74,
PixelFormat::Yuv420 => 70, PixelFormat::Yuv420 => 70,
PixelFormat::Uyvy => 65, PixelFormat::Uyvy => 65,
PixelFormat::Yvyu => 64, PixelFormat::Yvyu => 64,
@@ -144,7 +153,10 @@ impl PixelFormat {
/// Software encoding prefers: YUYV > NV12 /// Software encoding prefers: YUYV > NV12
/// ///
/// Returns None if no suitable format is available /// Returns None if no suitable format is available
pub fn recommended_for_encoding(available: &[PixelFormat], is_hardware: bool) -> Option<PixelFormat> { pub fn recommended_for_encoding(
available: &[PixelFormat],
is_hardware: bool,
) -> Option<PixelFormat> {
if is_hardware { if is_hardware {
// Hardware encoding: NV12 > YUYV // Hardware encoding: NV12 > YUYV
if available.contains(&PixelFormat::Nv12) { if available.contains(&PixelFormat::Nv12) {
@@ -175,6 +187,7 @@ impl PixelFormat {
PixelFormat::Yvyu, PixelFormat::Yvyu,
PixelFormat::Uyvy, PixelFormat::Uyvy,
PixelFormat::Nv12, PixelFormat::Nv12,
PixelFormat::Nv21,
PixelFormat::Nv16, PixelFormat::Nv16,
PixelFormat::Nv24, PixelFormat::Nv24,
PixelFormat::Yuv420, PixelFormat::Yuv420,
@@ -196,6 +209,7 @@ impl fmt::Display for PixelFormat {
PixelFormat::Yvyu => "YVYU", PixelFormat::Yvyu => "YVYU",
PixelFormat::Uyvy => "UYVY", PixelFormat::Uyvy => "UYVY",
PixelFormat::Nv12 => "NV12", PixelFormat::Nv12 => "NV12",
PixelFormat::Nv21 => "NV21",
PixelFormat::Nv16 => "NV16", PixelFormat::Nv16 => "NV16",
PixelFormat::Nv24 => "NV24", PixelFormat::Nv24 => "NV24",
PixelFormat::Yuv420 => "YUV420", PixelFormat::Yuv420 => "YUV420",
@@ -220,6 +234,7 @@ impl std::str::FromStr for PixelFormat {
"YVYU" => Ok(PixelFormat::Yvyu), "YVYU" => Ok(PixelFormat::Yvyu),
"UYVY" => Ok(PixelFormat::Uyvy), "UYVY" => Ok(PixelFormat::Uyvy),
"NV12" => Ok(PixelFormat::Nv12), "NV12" => Ok(PixelFormat::Nv12),
"NV21" => Ok(PixelFormat::Nv21),
"NV16" => Ok(PixelFormat::Nv16), "NV16" => Ok(PixelFormat::Nv16),
"NV24" => Ok(PixelFormat::Nv24), "NV24" => Ok(PixelFormat::Nv24),
"YUV420" | "I420" => Ok(PixelFormat::Yuv420), "YUV420" | "I420" => Ok(PixelFormat::Yuv420),

View File

@@ -106,9 +106,9 @@ impl VideoFrame {
/// Get hash of frame data (computed once, cached) /// Get hash of frame data (computed once, cached)
/// Used for fast frame deduplication comparison /// Used for fast frame deduplication comparison
pub fn get_hash(&self) -> u64 { pub fn get_hash(&self) -> u64 {
*self.hash.get_or_init(|| { *self
xxhash_rust::xxh64::xxh64(self.data.as_ref(), 0) .hash
}) .get_or_init(|| xxhash_rust::xxh64::xxh64(self.data.as_ref(), 0))
} }
/// Check if format is JPEG/MJPEG /// Check if format is JPEG/MJPEG

View File

@@ -93,10 +93,7 @@ impl H264Pipeline {
pub fn new(config: H264PipelineConfig) -> Result<Self> { pub fn new(config: H264PipelineConfig) -> Result<Self> {
info!( info!(
"Creating H264 pipeline: {}x{} @ {} kbps, {} fps", "Creating H264 pipeline: {}x{} @ {} kbps, {} fps",
config.resolution.width, config.resolution.width, config.resolution.height, config.bitrate_kbps, config.fps
config.resolution.height,
config.bitrate_kbps,
config.fps
); );
// Determine encoder input format based on pipeline input // Determine encoder input format based on pipeline input
@@ -154,7 +151,7 @@ impl H264Pipeline {
// MJPEG/JPEG input - not supported (requires libjpeg for decoding) // MJPEG/JPEG input - not supported (requires libjpeg for decoding)
PixelFormat::Mjpeg | PixelFormat::Jpeg => { PixelFormat::Mjpeg | PixelFormat::Jpeg => {
return Err(AppError::VideoError( return Err(AppError::VideoError(
"MJPEG input format not supported in this build".to_string() "MJPEG input format not supported in this build".to_string(),
)); ));
} }
@@ -216,7 +213,10 @@ impl H264Pipeline {
} }
let _ = self.running.send(true); let _ = self.running.send(true);
info!("Starting H264 pipeline (input format: {})", self.config.input_format); info!(
"Starting H264 pipeline (input format: {})",
self.config.input_format
);
let encoder = self.encoder.lock().await.take(); let encoder = self.encoder.lock().await.take();
let nv12_converter = self.nv12_converter.lock().await.take(); let nv12_converter = self.nv12_converter.lock().await.take();

View File

@@ -18,11 +18,15 @@ pub mod video_session;
pub use capture::VideoCapturer; pub use capture::VideoCapturer;
pub use convert::{PixelConverter, Yuv420pBuffer}; pub use convert::{PixelConverter, Yuv420pBuffer};
pub use device::{VideoDevice, VideoDeviceInfo}; pub use device::{VideoDevice, VideoDeviceInfo};
pub use encoder::{JpegEncoder, H264Encoder, H264EncoderType}; pub use encoder::{H264Encoder, H264EncoderType, JpegEncoder};
pub use format::PixelFormat; pub use format::PixelFormat;
pub use frame::VideoFrame; pub use frame::VideoFrame;
pub use h264_pipeline::{H264Pipeline, H264PipelineBuilder, H264PipelineConfig}; pub use h264_pipeline::{H264Pipeline, H264PipelineBuilder, H264PipelineConfig};
pub use shared_video_pipeline::{EncodedVideoFrame, SharedVideoPipeline, SharedVideoPipelineConfig, SharedVideoPipelineStats}; pub use shared_video_pipeline::{
EncodedVideoFrame, SharedVideoPipeline, SharedVideoPipelineConfig, SharedVideoPipelineStats,
};
pub use stream_manager::VideoStreamManager; pub use stream_manager::VideoStreamManager;
pub use streamer::{Streamer, StreamerState}; pub use streamer::{Streamer, StreamerState};
pub use video_session::{VideoSessionManager, VideoSessionManagerConfig, VideoSessionInfo, VideoSessionState, CodecInfo}; pub use video_session::{
CodecInfo, VideoSessionInfo, VideoSessionManager, VideoSessionManagerConfig, VideoSessionState,
};

View File

@@ -28,8 +28,10 @@ const AUTO_STOP_GRACE_PERIOD_SECS: u64 = 3;
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
use crate::video::convert::{Nv12Converter, PixelConverter}; use crate::video::convert::{Nv12Converter, PixelConverter};
use crate::video::encoder::h264::{H264Config, H264Encoder}; use crate::video::encoder::h264::{detect_best_encoder, H264Config, H264Encoder, H264InputFormat};
use crate::video::encoder::h265::{H265Config, H265Encoder}; use crate::video::encoder::h265::{
detect_best_h265_encoder, H265Config, H265Encoder, H265InputFormat,
};
use crate::video::encoder::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType}; use crate::video::encoder::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType};
use crate::video::encoder::traits::EncoderConfig; use crate::video::encoder::traits::EncoderConfig;
use crate::video::encoder::vp8::{VP8Config, VP8Encoder}; use crate::video::encoder::vp8::{VP8Config, VP8Encoder};
@@ -157,7 +159,6 @@ pub struct SharedVideoPipelineStats {
pub subscribers: u64, pub subscribers: u64,
} }
/// Universal video encoder trait object /// Universal video encoder trait object
#[allow(dead_code)] #[allow(dead_code)]
trait VideoEncoderTrait: Send { trait VideoEncoderTrait: Send {
@@ -300,7 +301,7 @@ pub struct SharedVideoPipeline {
/// Whether the encoder needs YUV420P (true) or NV12 (false) /// Whether the encoder needs YUV420P (true) or NV12 (false)
encoder_needs_yuv420p: AtomicBool, encoder_needs_yuv420p: AtomicBool,
/// Whether YUYV direct input is enabled (RKMPP optimization) /// Whether YUYV direct input is enabled (RKMPP optimization)
yuyv_direct_input: AtomicBool, direct_input: AtomicBool,
frame_tx: broadcast::Sender<EncodedVideoFrame>, frame_tx: broadcast::Sender<EncodedVideoFrame>,
stats: Mutex<SharedVideoPipelineStats>, stats: Mutex<SharedVideoPipelineStats>,
running: watch::Sender<bool>, running: watch::Sender<bool>,
@@ -335,7 +336,7 @@ impl SharedVideoPipeline {
nv12_converter: Mutex::new(None), nv12_converter: Mutex::new(None),
yuv420p_converter: Mutex::new(None), yuv420p_converter: Mutex::new(None),
encoder_needs_yuv420p: AtomicBool::new(false), encoder_needs_yuv420p: AtomicBool::new(false),
yuyv_direct_input: AtomicBool::new(false), direct_input: AtomicBool::new(false),
frame_tx, frame_tx,
stats: Mutex::new(SharedVideoPipelineStats::default()), stats: Mutex::new(SharedVideoPipelineStats::default()),
running: running_tx, running: running_tx,
@@ -354,29 +355,108 @@ impl SharedVideoPipeline {
let registry = EncoderRegistry::global(); let registry = EncoderRegistry::global();
// Helper to get codec name for specific backend // Helper to get codec name for specific backend
let get_codec_name = |format: VideoEncoderType, backend: Option<EncoderBackend>| -> Option<String> { let get_codec_name =
|format: VideoEncoderType, backend: Option<EncoderBackend>| -> Option<String> {
match backend { match backend {
Some(b) => registry.encoder_with_backend(format, b).map(|e| e.codec_name.clone()), Some(b) => registry
None => registry.best_encoder(format, false).map(|e| e.codec_name.clone()), .encoder_with_backend(format, b)
.map(|e| e.codec_name.clone()),
None => registry
.best_encoder(format, false)
.map(|e| e.codec_name.clone()),
} }
}; };
// Check if RKMPP backend is available for YUYV direct input optimization // Check if RKMPP backend is available for direct input optimization
let is_rkmpp_available = registry.encoder_with_backend(VideoEncoderType::H264, EncoderBackend::Rkmpp).is_some(); let is_rkmpp_available = registry
.encoder_with_backend(VideoEncoderType::H264, EncoderBackend::Rkmpp)
.is_some();
let use_yuyv_direct = is_rkmpp_available && config.input_format == PixelFormat::Yuyv; let use_yuyv_direct = is_rkmpp_available && config.input_format == PixelFormat::Yuyv;
let use_rkmpp_direct = is_rkmpp_available
&& matches!(
config.input_format,
PixelFormat::Yuyv
| PixelFormat::Yuv420
| PixelFormat::Rgb24
| PixelFormat::Bgr24
| PixelFormat::Nv12
| PixelFormat::Nv16
| PixelFormat::Nv21
| PixelFormat::Nv24
);
if use_yuyv_direct { if use_yuyv_direct {
info!("RKMPP backend detected with YUYV input, enabling YUYV direct input optimization"); info!(
"RKMPP backend detected with YUYV input, enabling YUYV direct input optimization"
);
} else if use_rkmpp_direct {
info!(
"RKMPP backend detected with {} input, enabling direct input optimization",
config.input_format
);
} }
// Create encoder based on codec type // Create encoder based on codec type
let encoder: Box<dyn VideoEncoderTrait + Send> = match config.output_codec { let encoder: Box<dyn VideoEncoderTrait + Send> = match config.output_codec {
VideoEncoderType::H264 => { VideoEncoderType::H264 => {
// Determine H264 input format based on backend and input format let codec_name = if use_rkmpp_direct {
let h264_input_format = if use_yuyv_direct { // Force RKMPP backend for direct input
crate::video::encoder::h264::H264InputFormat::Yuyv422 get_codec_name(VideoEncoderType::H264, Some(EncoderBackend::Rkmpp)).ok_or_else(
|| {
AppError::VideoError(
"RKMPP backend not available for H.264".to_string(),
)
},
)?
} else if let Some(ref backend) = config.encoder_backend {
// Specific backend requested
get_codec_name(VideoEncoderType::H264, Some(*backend)).ok_or_else(|| {
AppError::VideoError(format!(
"Backend {:?} does not support H.264",
backend
))
})?
} else { } else {
crate::video::encoder::h264::H264InputFormat::Nv12 // Auto select best available encoder
let (_encoder_type, detected) =
detect_best_encoder(config.resolution.width, config.resolution.height);
detected.ok_or_else(|| {
AppError::VideoError("No H.264 encoder available".to_string())
})?
};
let is_rkmpp = codec_name.contains("rkmpp");
let direct_input_format = if is_rkmpp {
match config.input_format {
PixelFormat::Yuyv => Some(H264InputFormat::Yuyv422),
PixelFormat::Yuv420 => Some(H264InputFormat::Yuv420p),
PixelFormat::Rgb24 => Some(H264InputFormat::Rgb24),
PixelFormat::Bgr24 => Some(H264InputFormat::Bgr24),
PixelFormat::Nv12 => Some(H264InputFormat::Nv12),
PixelFormat::Nv16 => Some(H264InputFormat::Nv16),
PixelFormat::Nv21 => Some(H264InputFormat::Nv21),
PixelFormat::Nv24 => Some(H264InputFormat::Nv24),
_ => None,
}
} else if codec_name.contains("libx264") {
match config.input_format {
PixelFormat::Nv12 => Some(H264InputFormat::Nv12),
PixelFormat::Nv16 => Some(H264InputFormat::Nv16),
PixelFormat::Nv21 => Some(H264InputFormat::Nv21),
PixelFormat::Yuv420 => Some(H264InputFormat::Yuv420p),
_ => None,
}
} else {
None
};
// Choose input format: prefer direct input when supported
let h264_input_format = if let Some(fmt) = direct_input_format {
fmt
} else if codec_name.contains("libx264") {
H264InputFormat::Yuv420p
} else {
H264InputFormat::Nv12
}; };
let encoder_config = H264Config { let encoder_config = H264Config {
@@ -387,69 +467,124 @@ impl SharedVideoPipeline {
input_format: h264_input_format, input_format: h264_input_format,
}; };
let encoder = if use_yuyv_direct { if use_rkmpp_direct {
// Force RKMPP backend for YUYV direct input info!(
let codec_name = get_codec_name(VideoEncoderType::H264, Some(EncoderBackend::Rkmpp)) "Creating H264 encoder with RKMPP backend for {} direct input (codec: {})",
.ok_or_else(|| AppError::VideoError( config.input_format, codec_name
"RKMPP backend not available for H.264".to_string() );
))?;
info!("Creating H264 encoder with RKMPP backend for YUYV direct input (codec: {})", codec_name);
H264Encoder::with_codec(encoder_config, &codec_name)?
} else if let Some(ref backend) = config.encoder_backend { } else if let Some(ref backend) = config.encoder_backend {
// Specific backend requested info!(
let codec_name = get_codec_name(VideoEncoderType::H264, Some(*backend)) "Creating H264 encoder with backend {:?} (codec: {})",
.ok_or_else(|| AppError::VideoError(format!( backend, codec_name
"Backend {:?} does not support H.264", backend );
)))?; }
info!("Creating H264 encoder with backend {:?} (codec: {})", backend, codec_name);
H264Encoder::with_codec(encoder_config, &codec_name)? let encoder = H264Encoder::with_codec(encoder_config, &codec_name)?;
} else {
// Auto select
H264Encoder::new(encoder_config)?
};
info!("Created H264 encoder: {}", encoder.codec_name()); info!("Created H264 encoder: {}", encoder.codec_name());
Box::new(H264EncoderWrapper(encoder)) Box::new(H264EncoderWrapper(encoder))
} }
VideoEncoderType::H265 => { VideoEncoderType::H265 => {
// Determine H265 input format based on backend and input format let codec_name = if use_rkmpp_direct {
let encoder_config = if use_yuyv_direct { get_codec_name(VideoEncoderType::H265, Some(EncoderBackend::Rkmpp)).ok_or_else(
H265Config::low_latency_yuyv422(config.resolution, config.bitrate_kbps()) || {
AppError::VideoError(
"RKMPP backend not available for H.265".to_string(),
)
},
)?
} else if let Some(ref backend) = config.encoder_backend {
get_codec_name(VideoEncoderType::H265, Some(*backend)).ok_or_else(|| {
AppError::VideoError(format!(
"Backend {:?} does not support H.265",
backend
))
})?
} else { } else {
H265Config::low_latency(config.resolution, config.bitrate_kbps()) let (_encoder_type, detected) =
detect_best_h265_encoder(config.resolution.width, config.resolution.height);
detected.ok_or_else(|| {
AppError::VideoError("No H.265 encoder available".to_string())
})?
}; };
let encoder = if use_yuyv_direct { let is_rkmpp = codec_name.contains("rkmpp");
// Force RKMPP backend for YUYV direct input let direct_input_format = if is_rkmpp {
let codec_name = get_codec_name(VideoEncoderType::H265, Some(EncoderBackend::Rkmpp)) match config.input_format {
.ok_or_else(|| AppError::VideoError( PixelFormat::Yuyv => Some(H265InputFormat::Yuyv422),
"RKMPP backend not available for H.265".to_string() PixelFormat::Yuv420 => Some(H265InputFormat::Yuv420p),
))?; PixelFormat::Rgb24 => Some(H265InputFormat::Rgb24),
info!("Creating H265 encoder with RKMPP backend for YUYV direct input (codec: {})", codec_name); PixelFormat::Bgr24 => Some(H265InputFormat::Bgr24),
H265Encoder::with_codec(encoder_config, &codec_name)? PixelFormat::Nv12 => Some(H265InputFormat::Nv12),
} else if let Some(ref backend) = config.encoder_backend { PixelFormat::Nv16 => Some(H265InputFormat::Nv16),
let codec_name = get_codec_name(VideoEncoderType::H265, Some(*backend)) PixelFormat::Nv21 => Some(H265InputFormat::Nv21),
.ok_or_else(|| AppError::VideoError(format!( PixelFormat::Nv24 => Some(H265InputFormat::Nv24),
"Backend {:?} does not support H.265", backend _ => None,
)))?; }
info!("Creating H265 encoder with backend {:?} (codec: {})", backend, codec_name); } else if codec_name.contains("libx265") {
H265Encoder::with_codec(encoder_config, &codec_name)? match config.input_format {
PixelFormat::Yuv420 => Some(H265InputFormat::Yuv420p),
_ => None,
}
} else { } else {
H265Encoder::new(encoder_config)? None
}; };
let h265_input_format = if let Some(fmt) = direct_input_format {
fmt
} else if codec_name.contains("libx265") {
H265InputFormat::Yuv420p
} else {
H265InputFormat::Nv12
};
let encoder_config = H265Config {
base: EncoderConfig {
resolution: config.resolution,
input_format: config.input_format,
quality: config.bitrate_kbps(),
fps: config.fps,
gop_size: config.gop_size(),
},
bitrate_kbps: config.bitrate_kbps(),
gop_size: config.gop_size(),
fps: config.fps,
input_format: h265_input_format,
};
if use_rkmpp_direct {
info!(
"Creating H265 encoder with RKMPP backend for {} direct input (codec: {})",
config.input_format, codec_name
);
} else if let Some(ref backend) = config.encoder_backend {
info!(
"Creating H265 encoder with backend {:?} (codec: {})",
backend, codec_name
);
}
let encoder = H265Encoder::with_codec(encoder_config, &codec_name)?;
info!("Created H265 encoder: {}", encoder.codec_name()); info!("Created H265 encoder: {}", encoder.codec_name());
Box::new(H265EncoderWrapper(encoder)) Box::new(H265EncoderWrapper(encoder))
} }
VideoEncoderType::VP8 => { VideoEncoderType::VP8 => {
let encoder_config = VP8Config::low_latency(config.resolution, config.bitrate_kbps()); let encoder_config =
VP8Config::low_latency(config.resolution, config.bitrate_kbps());
let encoder = if let Some(ref backend) = config.encoder_backend { let encoder = if let Some(ref backend) = config.encoder_backend {
let codec_name = get_codec_name(VideoEncoderType::VP8, Some(*backend)) let codec_name = get_codec_name(VideoEncoderType::VP8, Some(*backend))
.ok_or_else(|| AppError::VideoError(format!( .ok_or_else(|| {
"Backend {:?} does not support VP8", backend AppError::VideoError(format!(
)))?; "Backend {:?} does not support VP8",
info!("Creating VP8 encoder with backend {:?} (codec: {})", backend, codec_name); backend
))
})?;
info!(
"Creating VP8 encoder with backend {:?} (codec: {})",
backend, codec_name
);
VP8Encoder::with_codec(encoder_config, &codec_name)? VP8Encoder::with_codec(encoder_config, &codec_name)?
} else { } else {
VP8Encoder::new(encoder_config)? VP8Encoder::new(encoder_config)?
@@ -459,14 +594,21 @@ impl SharedVideoPipeline {
Box::new(VP8EncoderWrapper(encoder)) Box::new(VP8EncoderWrapper(encoder))
} }
VideoEncoderType::VP9 => { VideoEncoderType::VP9 => {
let encoder_config = VP9Config::low_latency(config.resolution, config.bitrate_kbps()); let encoder_config =
VP9Config::low_latency(config.resolution, config.bitrate_kbps());
let encoder = if let Some(ref backend) = config.encoder_backend { let encoder = if let Some(ref backend) = config.encoder_backend {
let codec_name = get_codec_name(VideoEncoderType::VP9, Some(*backend)) let codec_name = get_codec_name(VideoEncoderType::VP9, Some(*backend))
.ok_or_else(|| AppError::VideoError(format!( .ok_or_else(|| {
"Backend {:?} does not support VP9", backend AppError::VideoError(format!(
)))?; "Backend {:?} does not support VP9",
info!("Creating VP9 encoder with backend {:?} (codec: {})", backend, codec_name); backend
))
})?;
info!(
"Creating VP9 encoder with backend {:?} (codec: {})",
backend, codec_name
);
VP9Encoder::with_codec(encoder_config, &codec_name)? VP9Encoder::with_codec(encoder_config, &codec_name)?
} else { } else {
VP9Encoder::new(encoder_config)? VP9Encoder::new(encoder_config)?
@@ -477,25 +619,71 @@ impl SharedVideoPipeline {
} }
}; };
// Determine if encoder needs YUV420P (software encoders) or NV12 (hardware encoders) // Determine if encoder can take direct input without conversion
let codec_name = encoder.codec_name(); let codec_name = encoder.codec_name();
let needs_yuv420p = codec_name.contains("libvpx") || codec_name.contains("libx265"); let use_direct_input = if codec_name.contains("rkmpp") {
matches!(
config.input_format,
PixelFormat::Yuyv
| PixelFormat::Yuv420
| PixelFormat::Rgb24
| PixelFormat::Bgr24
| PixelFormat::Nv12
| PixelFormat::Nv16
| PixelFormat::Nv21
| PixelFormat::Nv24
)
} else if codec_name.contains("libx264") {
matches!(
config.input_format,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv21 | PixelFormat::Yuv420
)
} else {
false
};
// Determine if encoder needs YUV420P (software encoders) or NV12 (hardware encoders)
let needs_yuv420p = if codec_name.contains("libx264") {
!matches!(
config.input_format,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv21 | PixelFormat::Yuv420
)
} else {
codec_name.contains("libvpx") || codec_name.contains("libx265")
};
info!( info!(
"Encoder {} needs {} format", "Encoder {} needs {} format",
codec_name, codec_name,
if use_yuyv_direct { "YUYV422 (direct)" } else if needs_yuv420p { "YUV420P" } else { "NV12" } if use_direct_input {
"direct"
} else if needs_yuv420p {
"YUV420P"
} else {
"NV12"
}
); );
// Create converter or decoder based on input format and encoder needs // Create converter or decoder based on input format and encoder needs
info!("Initializing input format handler for: {} -> {}", info!(
"Initializing input format handler for: {} -> {}",
config.input_format, config.input_format,
if use_yuyv_direct { "YUYV422 (direct)" } else if needs_yuv420p { "YUV420P" } else { "NV12" }); if use_direct_input {
"direct"
} else if needs_yuv420p {
"YUV420P"
} else {
"NV12"
}
);
let (nv12_converter, yuv420p_converter) = if use_yuyv_direct { let (nv12_converter, yuv420p_converter) = if use_yuyv_direct {
// RKMPP with YUYV direct input - skip all conversion // RKMPP with YUYV direct input - skip all conversion
info!("YUYV direct input enabled for RKMPP, skipping format conversion"); info!("YUYV direct input enabled for RKMPP, skipping format conversion");
(None, None) (None, None)
} else if use_direct_input {
info!("Direct input enabled, skipping format conversion");
(None, None)
} else if needs_yuv420p { } else if needs_yuv420p {
// Software encoder needs YUV420P // Software encoder needs YUV420P
match config.input_format { match config.input_format {
@@ -505,19 +693,38 @@ impl SharedVideoPipeline {
} }
PixelFormat::Yuyv => { PixelFormat::Yuyv => {
info!("Using YUYV->YUV420P converter"); info!("Using YUYV->YUV420P converter");
(None, Some(PixelConverter::yuyv_to_yuv420p(config.resolution))) (
None,
Some(PixelConverter::yuyv_to_yuv420p(config.resolution)),
)
} }
PixelFormat::Nv12 => { PixelFormat::Nv12 => {
info!("Using NV12->YUV420P converter"); info!("Using NV12->YUV420P converter");
(None, Some(PixelConverter::nv12_to_yuv420p(config.resolution))) (
None,
Some(PixelConverter::nv12_to_yuv420p(config.resolution)),
)
}
PixelFormat::Nv21 => {
info!("Using NV21->YUV420P converter");
(
None,
Some(PixelConverter::nv21_to_yuv420p(config.resolution)),
)
} }
PixelFormat::Rgb24 => { PixelFormat::Rgb24 => {
info!("Using RGB24->YUV420P converter"); info!("Using RGB24->YUV420P converter");
(None, Some(PixelConverter::rgb24_to_yuv420p(config.resolution))) (
None,
Some(PixelConverter::rgb24_to_yuv420p(config.resolution)),
)
} }
PixelFormat::Bgr24 => { PixelFormat::Bgr24 => {
info!("Using BGR24->YUV420P converter"); info!("Using BGR24->YUV420P converter");
(None, Some(PixelConverter::bgr24_to_yuv420p(config.resolution))) (
None,
Some(PixelConverter::bgr24_to_yuv420p(config.resolution)),
)
} }
_ => { _ => {
return Err(AppError::VideoError(format!( return Err(AppError::VideoError(format!(
@@ -537,6 +744,18 @@ impl SharedVideoPipeline {
info!("Using YUYV->NV12 converter"); info!("Using YUYV->NV12 converter");
(Some(Nv12Converter::yuyv_to_nv12(config.resolution)), None) (Some(Nv12Converter::yuyv_to_nv12(config.resolution)), None)
} }
PixelFormat::Nv21 => {
info!("Using NV21->NV12 converter");
(Some(Nv12Converter::nv21_to_nv12(config.resolution)), None)
}
PixelFormat::Nv16 => {
info!("Using NV16->NV12 converter");
(Some(Nv12Converter::nv16_to_nv12(config.resolution)), None)
}
PixelFormat::Yuv420 => {
info!("Using YUV420P->NV12 converter");
(Some(Nv12Converter::yuv420_to_nv12(config.resolution)), None)
}
PixelFormat::Rgb24 => { PixelFormat::Rgb24 => {
info!("Using RGB24->NV12 converter"); info!("Using RGB24->NV12 converter");
(Some(Nv12Converter::rgb24_to_nv12(config.resolution)), None) (Some(Nv12Converter::rgb24_to_nv12(config.resolution)), None)
@@ -557,8 +776,9 @@ impl SharedVideoPipeline {
*self.encoder.lock().await = Some(encoder); *self.encoder.lock().await = Some(encoder);
*self.nv12_converter.lock().await = nv12_converter; *self.nv12_converter.lock().await = nv12_converter;
*self.yuv420p_converter.lock().await = yuv420p_converter; *self.yuv420p_converter.lock().await = yuv420p_converter;
self.encoder_needs_yuv420p.store(needs_yuv420p, Ordering::Release); self.encoder_needs_yuv420p
self.yuyv_direct_input.store(use_yuyv_direct, Ordering::Release); .store(needs_yuv420p, Ordering::Release);
self.direct_input.store(use_direct_input, Ordering::Release);
Ok(()) Ok(())
} }
@@ -646,7 +866,10 @@ impl SharedVideoPipeline {
} }
/// Start the pipeline /// Start the pipeline
pub async fn start(self: &Arc<Self>, mut frame_rx: broadcast::Receiver<VideoFrame>) -> Result<()> { pub async fn start(
self: &Arc<Self>,
mut frame_rx: broadcast::Receiver<VideoFrame>,
) -> Result<()> {
if *self.running_rx.borrow() { if *self.running_rx.borrow() {
warn!("Pipeline already running"); warn!("Pipeline already running");
return Ok(()); return Ok(());
@@ -657,7 +880,10 @@ impl SharedVideoPipeline {
let config = self.config.read().await.clone(); let config = self.config.read().await.clone();
let gop_size = config.gop_size(); let gop_size = config.gop_size();
info!("Starting {} pipeline (GOP={})", config.output_codec, gop_size); info!(
"Starting {} pipeline (GOP={})",
config.output_codec, gop_size
);
let pipeline = self.clone(); let pipeline = self.clone();
@@ -674,7 +900,6 @@ impl SharedVideoPipeline {
let mut local_errors: u64 = 0; let mut local_errors: u64 = 0;
let mut local_dropped: u64 = 0; let mut local_dropped: u64 = 0;
let mut local_skipped: u64 = 0; let mut local_skipped: u64 = 0;
// Track when we last had subscribers for auto-stop feature // Track when we last had subscribers for auto-stop feature
let mut no_subscribers_since: Option<Instant> = None; let mut no_subscribers_since: Option<Instant> = None;
let grace_period = Duration::from_secs(AUTO_STOP_GRACE_PERIOD_SECS); let grace_period = Duration::from_secs(AUTO_STOP_GRACE_PERIOD_SECS);
@@ -790,7 +1015,11 @@ impl SharedVideoPipeline {
} }
/// Encode a single frame /// Encode a single frame
async fn encode_frame(&self, frame: &VideoFrame, frame_count: u64) -> Result<Option<EncodedVideoFrame>> { async fn encode_frame(
&self,
frame: &VideoFrame,
frame_count: u64,
) -> Result<Option<EncodedVideoFrame>> {
let config = self.config.read().await; let config = self.config.read().await;
let raw_frame = frame.data(); let raw_frame = frame.data();
let fps = config.fps; let fps = config.fps;
@@ -835,9 +1064,9 @@ impl SharedVideoPipeline {
let needs_yuv420p = self.encoder_needs_yuv420p.load(Ordering::Acquire); let needs_yuv420p = self.encoder_needs_yuv420p.load(Ordering::Acquire);
let mut encoder_guard = self.encoder.lock().await; let mut encoder_guard = self.encoder.lock().await;
let encoder = encoder_guard.as_mut().ok_or_else(|| { let encoder = encoder_guard
AppError::VideoError("Encoder not initialized".to_string()) .as_mut()
})?; .ok_or_else(|| AppError::VideoError("Encoder not initialized".to_string()))?;
// Check and consume keyframe request (atomic, no lock contention) // Check and consume keyframe request (atomic, no lock contention)
if self.keyframe_requested.swap(false, Ordering::AcqRel) { if self.keyframe_requested.swap(false, Ordering::AcqRel) {
@@ -848,13 +1077,15 @@ impl SharedVideoPipeline {
let encode_result = if needs_yuv420p && yuv420p_converter.is_some() { let encode_result = if needs_yuv420p && yuv420p_converter.is_some() {
// Software encoder with direct input conversion to YUV420P // Software encoder with direct input conversion to YUV420P
let conv = yuv420p_converter.as_mut().unwrap(); let conv = yuv420p_converter.as_mut().unwrap();
let yuv420p_data = conv.convert(raw_frame) let yuv420p_data = conv
.convert(raw_frame)
.map_err(|e| 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 nv12_converter.is_some() { } else if nv12_converter.is_some() {
// Hardware encoder with input conversion to NV12 // Hardware encoder with input conversion to NV12
let conv = nv12_converter.as_mut().unwrap(); let conv = nv12_converter.as_mut().unwrap();
let nv12_data = conv.convert(raw_frame) let nv12_data = conv
.convert(raw_frame)
.map_err(|e| AppError::VideoError(format!("NV12 conversion failed: {}", e)))?; .map_err(|e| AppError::VideoError(format!("NV12 conversion failed: {}", e)))?;
encoder.encode_raw(nv12_data, pts_ms) encoder.encode_raw(nv12_data, pts_ms)
} else { } else {
@@ -871,7 +1102,6 @@ impl SharedVideoPipeline {
if !frames.is_empty() { if !frames.is_empty() {
let encoded = frames.into_iter().next().unwrap(); let encoded = frames.into_iter().next().unwrap();
let is_keyframe = encoded.key == 1; let is_keyframe = encoded.key == 1;
let sequence = self.sequence.fetch_add(1, Ordering::Relaxed) + 1; let sequence = self.sequence.fetch_add(1, Ordering::Relaxed) + 1;
// Debug log for H265 encoded frame // Debug log for H265 encoded frame
@@ -901,17 +1131,23 @@ impl SharedVideoPipeline {
})) }))
} else { } else {
if codec == VideoEncoderType::H265 { if codec == VideoEncoderType::H265 {
warn!("[Pipeline-H265] Encoder returned no frames for frame #{}", frame_count); warn!(
"[Pipeline-H265] Encoder returned no frames for frame #{}",
frame_count
);
} }
Ok(None) Ok(None)
} }
} }
Err(e) => { Err(e) => {
if codec == VideoEncoderType::H265 { if codec == VideoEncoderType::H265 {
error!("[Pipeline-H265] Encode error at frame #{}: {}", frame_count, e); error!(
"[Pipeline-H265] Encode error at frame #{}: {}",
frame_count, e
);
} }
Err(e) Err(e)
}, }
} }
} }
@@ -924,7 +1160,10 @@ impl SharedVideoPipeline {
} }
/// Set bitrate using preset /// Set bitrate using preset
pub async fn set_bitrate_preset(&self, preset: crate::video::encoder::BitratePreset) -> Result<()> { pub async fn set_bitrate_preset(
&self,
preset: crate::video::encoder::BitratePreset,
) -> Result<()> {
let bitrate_kbps = preset.bitrate_kbps(); let bitrate_kbps = preset.bitrate_kbps();
if let Some(ref mut encoder) = *self.encoder.lock().await { if let Some(ref mut encoder) = *self.encoder.lock().await {
encoder.set_bitrate(bitrate_kbps)?; encoder.set_bitrate(bitrate_kbps)?;
@@ -965,11 +1204,7 @@ fn parse_h265_nal_types(data: &[u8]) -> Vec<(u8, usize)> {
&& data[i + 3] == 1 && data[i + 3] == 1
{ {
i + 4 i + 4
} else if i + 3 <= data.len() } else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
&& data[i] == 0
&& data[i + 1] == 0
&& data[i + 2] == 1
{
i + 3 i + 3
} else { } else {
i += 1; i += 1;

View File

@@ -30,6 +30,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use uuid::Uuid;
use crate::config::{ConfigStore, StreamMode}; use crate::config::{ConfigStore, StreamMode};
use crate::error::Result; use crate::error::Result;
@@ -55,6 +56,17 @@ pub struct StreamManagerConfig {
pub fps: u32, pub fps: u32,
} }
/// Result of a mode switch request.
#[derive(Debug, Clone)]
pub struct ModeSwitchTransaction {
/// Whether this request started a new switch.
pub accepted: bool,
/// Whether a switch is currently in progress after handling this request.
pub switching: bool,
/// Transition ID if a switch is/was in progress.
pub transition_id: Option<String>,
}
impl Default for StreamManagerConfig { impl Default for StreamManagerConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -90,6 +102,8 @@ pub struct VideoStreamManager {
config_store: RwLock<Option<ConfigStore>>, config_store: RwLock<Option<ConfigStore>>,
/// Mode switching lock to prevent concurrent switch requests /// Mode switching lock to prevent concurrent switch requests
switching: AtomicBool, switching: AtomicBool,
/// Current mode switch transaction ID (set while switching=true)
transition_id: RwLock<Option<String>>,
} }
impl VideoStreamManager { impl VideoStreamManager {
@@ -105,6 +119,7 @@ impl VideoStreamManager {
events: RwLock::new(None), events: RwLock::new(None),
config_store: RwLock::new(None), config_store: RwLock::new(None),
switching: AtomicBool::new(false), switching: AtomicBool::new(false),
transition_id: RwLock::new(None),
}) })
} }
@@ -113,6 +128,11 @@ impl VideoStreamManager {
self.switching.load(Ordering::SeqCst) self.switching.load(Ordering::SeqCst)
} }
/// Get current mode switch transition ID, if any
pub async fn current_transition_id(&self) -> Option<String> {
self.transition_id.read().await.clone()
}
/// Set event bus for notifications /// Set event bus for notifications
pub async fn set_event_bus(&self, events: Arc<EventBus>) { pub async fn set_event_bus(&self, events: Arc<EventBus>) {
*self.events.write().await = Some(events); *self.events.write().await = Some(events);
@@ -188,7 +208,9 @@ impl VideoStreamManager {
"Reconnecting frame source to WebRTC after init: {}x{} {:?} @ {}fps (receiver_count={})", "Reconnecting frame source to WebRTC after init: {}x{} {:?} @ {}fps (receiver_count={})",
resolution.width, resolution.height, format, fps, frame_tx.receiver_count() resolution.width, resolution.height, format, fps, frame_tx.receiver_count()
); );
self.webrtc_streamer.update_video_config(resolution, format, fps).await; self.webrtc_streamer
.update_video_config(resolution, format, fps)
.await;
self.webrtc_streamer.set_video_source(frame_tx).await; self.webrtc_streamer.set_video_source(frame_tx).await;
} }
@@ -204,6 +226,18 @@ impl VideoStreamManager {
/// 4. Start the new mode (ensuring video capture runs for WebRTC) /// 4. Start the new mode (ensuring video capture runs for WebRTC)
/// 5. Update configuration /// 5. Update configuration
pub async fn switch_mode(self: &Arc<Self>, new_mode: StreamMode) -> Result<()> { pub async fn switch_mode(self: &Arc<Self>, new_mode: StreamMode) -> Result<()> {
let _ = self.switch_mode_transaction(new_mode).await?;
Ok(())
}
/// Switch streaming mode with a transaction ID for correlating events
///
/// If a switch is already in progress, returns `accepted=false` with the
/// current `transition_id` (if known) and does not start a new switch.
pub async fn switch_mode_transaction(
self: &Arc<Self>,
new_mode: StreamMode,
) -> Result<ModeSwitchTransaction> {
let current_mode = self.mode.read().await.clone(); let current_mode = self.mode.read().await.clone();
if current_mode == new_mode { if current_mode == new_mode {
@@ -212,19 +246,85 @@ impl VideoStreamManager {
if new_mode == StreamMode::WebRTC { if new_mode == StreamMode::WebRTC {
self.ensure_video_capture_running().await?; self.ensure_video_capture_running().await?;
} }
return Ok(()); return Ok(ModeSwitchTransaction {
accepted: false,
switching: false,
transition_id: None,
});
} }
// Acquire switching lock - prevent concurrent switch requests // Acquire switching lock - prevent concurrent switch requests
if self.switching.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() { if self
.switching
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
debug!("Mode switch already in progress, ignoring duplicate request"); debug!("Mode switch already in progress, ignoring duplicate request");
return Ok(()); return Ok(ModeSwitchTransaction {
accepted: false,
switching: true,
transition_id: self.transition_id.read().await.clone(),
});
} }
// Use a helper to ensure we release the lock when done let transition_id = Uuid::new_v4().to_string();
let result = self.do_switch_mode(current_mode, new_mode.clone()).await; *self.transition_id.write().await = Some(transition_id.clone());
self.switching.store(false, Ordering::SeqCst);
result // Publish transaction start event
let from_mode_str = self.mode_to_string(&current_mode).await;
let to_mode_str = self.mode_to_string(&new_mode).await;
self.publish_event(SystemEvent::StreamModeSwitching {
transition_id: transition_id.clone(),
to_mode: to_mode_str,
from_mode: from_mode_str,
})
.await;
// Perform the switch asynchronously so the HTTP handler can return
// immediately and clients can reliably wait for WebSocket events.
let manager = Arc::clone(self);
let transition_id_for_task = transition_id.clone();
tokio::spawn(async move {
let result = manager
.do_switch_mode(current_mode, new_mode, transition_id_for_task.clone())
.await;
if let Err(e) = result {
error!(
"Mode switch transaction {} failed: {}",
transition_id_for_task, e
);
}
// Publish transaction end marker with best-effort actual mode
let actual_mode = manager.mode.read().await.clone();
let actual_mode_str = manager.mode_to_string(&actual_mode).await;
manager
.publish_event(SystemEvent::StreamModeReady {
transition_id: transition_id_for_task.clone(),
mode: actual_mode_str,
})
.await;
*manager.transition_id.write().await = None;
manager.switching.store(false, Ordering::SeqCst);
});
Ok(ModeSwitchTransaction {
accepted: true,
switching: true,
transition_id: Some(transition_id),
})
}
async fn mode_to_string(&self, mode: &StreamMode) -> String {
match mode {
StreamMode::Mjpeg => "mjpeg".to_string(),
StreamMode::WebRTC => {
let codec = self.webrtc_streamer.current_video_codec().await;
codec_to_string(codec)
}
}
} }
/// Ensure video capture is running (for WebRTC mode) /// Ensure video capture is running (for WebRTC mode)
@@ -257,7 +357,9 @@ impl VideoStreamManager {
"Reconnecting frame source to WebRTC: {}x{} {:?} @ {}fps", "Reconnecting frame source to WebRTC: {}x{} {:?} @ {}fps",
resolution.width, resolution.height, format, fps resolution.width, resolution.height, format, fps
); );
self.webrtc_streamer.update_video_config(resolution, format, fps).await; self.webrtc_streamer
.update_video_config(resolution, format, fps)
.await;
self.webrtc_streamer.set_video_source(frame_tx).await; self.webrtc_streamer.set_video_source(frame_tx).await;
} }
@@ -265,7 +367,12 @@ impl VideoStreamManager {
} }
/// Internal implementation of mode switching (called with lock held) /// Internal implementation of mode switching (called with lock held)
async fn do_switch_mode(self: &Arc<Self>, current_mode: StreamMode, new_mode: StreamMode) -> Result<()> { async fn do_switch_mode(
self: &Arc<Self>,
current_mode: StreamMode,
new_mode: StreamMode,
transition_id: String,
) -> Result<()> {
info!("Switching video mode: {:?} -> {:?}", current_mode, new_mode); info!("Switching video mode: {:?} -> {:?}", current_mode, new_mode);
// Get the actual mode strings (with codec info for WebRTC) // Get the actual mode strings (with codec info for WebRTC)
@@ -286,6 +393,7 @@ impl VideoStreamManager {
// 1. Publish mode change event (clients should prepare to reconnect) // 1. Publish mode change event (clients should prepare to reconnect)
self.publish_event(SystemEvent::StreamModeChanged { self.publish_event(SystemEvent::StreamModeChanged {
transition_id: Some(transition_id.clone()),
mode: new_mode_str, mode: new_mode_str,
previous_mode: previous_mode_str, previous_mode: previous_mode_str,
}) })
@@ -320,15 +428,26 @@ impl VideoStreamManager {
// Auto-switch to MJPEG format if device supports it // Auto-switch to MJPEG format if device supports it
if let Some(device) = self.streamer.current_device().await { if let Some(device) = self.streamer.current_device().await {
let (current_format, resolution, fps) = self.streamer.current_video_config().await; let (current_format, resolution, fps) =
let available_formats: Vec<PixelFormat> = device.formats.iter().map(|f| f.format).collect(); self.streamer.current_video_config().await;
let available_formats: Vec<PixelFormat> =
device.formats.iter().map(|f| f.format).collect();
// If current format is not MJPEG and device supports MJPEG, switch to it // If current format is not MJPEG and device supports MJPEG, switch to it
if current_format != PixelFormat::Mjpeg && available_formats.contains(&PixelFormat::Mjpeg) { if current_format != PixelFormat::Mjpeg
&& available_formats.contains(&PixelFormat::Mjpeg)
{
info!("Auto-switching to MJPEG format for MJPEG mode"); info!("Auto-switching to MJPEG format for MJPEG mode");
let device_path = device.path.to_string_lossy().to_string(); let device_path = device.path.to_string_lossy().to_string();
if let Err(e) = self.streamer.apply_video_config(&device_path, PixelFormat::Mjpeg, resolution, fps).await { if let Err(e) = self
warn!("Failed to auto-switch to MJPEG format: {}, keeping current format", e); .streamer
.apply_video_config(&device_path, PixelFormat::Mjpeg, resolution, fps)
.await
{
warn!(
"Failed to auto-switch to MJPEG format: {}, keeping current format",
e
);
} }
} }
} }
@@ -353,21 +472,29 @@ impl VideoStreamManager {
// Auto-switch to non-compressed format if current format is MJPEG/JPEG // Auto-switch to non-compressed format if current format is MJPEG/JPEG
if let Some(device) = self.streamer.current_device().await { if let Some(device) = self.streamer.current_device().await {
let (current_format, resolution, fps) = self.streamer.current_video_config().await; let (current_format, resolution, fps) =
self.streamer.current_video_config().await;
if current_format.is_compressed() { if current_format.is_compressed() {
let available_formats: Vec<PixelFormat> = device.formats.iter().map(|f| f.format).collect(); let available_formats: Vec<PixelFormat> =
device.formats.iter().map(|f| f.format).collect();
// Determine if using hardware encoding // Determine if using hardware encoding
let is_hardware = self.webrtc_streamer.is_hardware_encoding().await; let is_hardware = self.webrtc_streamer.is_hardware_encoding().await;
if let Some(recommended) = PixelFormat::recommended_for_encoding(&available_formats, is_hardware) { if let Some(recommended) =
PixelFormat::recommended_for_encoding(&available_formats, is_hardware)
{
info!( info!(
"Auto-switching from {:?} to {:?} for WebRTC encoding (hardware={})", "Auto-switching from {:?} to {:?} for WebRTC encoding (hardware={})",
current_format, recommended, is_hardware current_format, recommended, is_hardware
); );
let device_path = device.path.to_string_lossy().to_string(); let device_path = device.path.to_string_lossy().to_string();
if let Err(e) = self.streamer.apply_video_config(&device_path, recommended, resolution, fps).await { if let Err(e) = self
.streamer
.apply_video_config(&device_path, recommended, resolution, fps)
.await
{
warn!("Failed to auto-switch format for WebRTC: {}, keeping current format", e); warn!("Failed to auto-switch format for WebRTC: {}, keeping current format", e);
} }
} }
@@ -394,33 +521,24 @@ impl VideoStreamManager {
"Connecting frame source to WebRTC pipeline: {}x{} {:?} @ {}fps", "Connecting frame source to WebRTC pipeline: {}x{} {:?} @ {}fps",
resolution.width, resolution.height, format, fps resolution.width, resolution.height, format, fps
); );
self.webrtc_streamer.update_video_config(resolution, format, fps).await; self.webrtc_streamer
self.webrtc_streamer.set_video_source(frame_tx).await; .update_video_config(resolution, format, fps)
// Get device path for events
let device_path = self.streamer.current_device().await
.map(|d| d.path.to_string_lossy().to_string())
.unwrap_or_default();
// Publish StreamConfigApplied event - clients can now safely connect
self.publish_event(SystemEvent::StreamConfigApplied {
device: device_path,
resolution: (resolution.width, resolution.height),
format: format!("{:?}", format).to_lowercase(),
fps,
})
.await; .await;
self.webrtc_streamer.set_video_source(frame_tx).await;
// Publish WebRTCReady event - frame source is now connected // Publish WebRTCReady event - frame source is now connected
let codec = self.webrtc_streamer.current_video_codec().await; let codec = self.webrtc_streamer.current_video_codec().await;
let is_hardware = self.webrtc_streamer.is_hardware_encoding().await; let is_hardware = self.webrtc_streamer.is_hardware_encoding().await;
self.publish_event(SystemEvent::WebRTCReady { self.publish_event(SystemEvent::WebRTCReady {
transition_id: Some(transition_id.clone()),
codec: codec_to_string(codec), codec: codec_to_string(codec),
hardware: is_hardware, hardware: is_hardware,
}) })
.await; .await;
} else { } else {
warn!("No frame source available for WebRTC - sessions may fail to receive video"); warn!(
"No frame source available for WebRTC - sessions may fail to receive video"
);
} }
info!("WebRTC mode activated (sessions created on-demand)"); info!("WebRTC mode activated (sessions created on-demand)");
@@ -483,13 +601,16 @@ impl VideoStreamManager {
if let Some(frame_tx) = self.streamer.frame_sender().await { if let Some(frame_tx) = self.streamer.frame_sender().await {
// Note: update_video_config was already called above with the requested config, // Note: update_video_config was already called above with the requested config,
// but verify that actual capture matches // but verify that actual capture matches
let (actual_format, actual_resolution, actual_fps) = self.streamer.current_video_config().await; let (actual_format, actual_resolution, actual_fps) =
self.streamer.current_video_config().await;
if actual_format != format || actual_resolution != resolution || actual_fps != fps { if actual_format != format || actual_resolution != resolution || actual_fps != fps {
info!( info!(
"Actual capture config differs from requested, updating WebRTC: {}x{} {:?} @ {}fps", "Actual capture config differs from requested, updating WebRTC: {}x{} {:?} @ {}fps",
actual_resolution.width, actual_resolution.height, actual_format, actual_fps actual_resolution.width, actual_resolution.height, actual_format, actual_fps
); );
self.webrtc_streamer.update_video_config(actual_resolution, actual_format, actual_fps).await; self.webrtc_streamer
.update_video_config(actual_resolution, actual_format, actual_fps)
.await;
} }
info!("Reconnecting frame source to WebRTC after config change"); info!("Reconnecting frame source to WebRTC after config change");
self.webrtc_streamer.set_video_source(frame_tx).await; self.webrtc_streamer.set_video_source(frame_tx).await;
@@ -522,7 +643,9 @@ impl VideoStreamManager {
if let Some(frame_tx) = self.streamer.frame_sender().await { if let Some(frame_tx) = self.streamer.frame_sender().await {
// Synchronize WebRTC config with actual capture format // Synchronize WebRTC config with actual capture format
let (format, resolution, fps) = self.streamer.current_video_config().await; let (format, resolution, fps) = self.streamer.current_video_config().await;
self.webrtc_streamer.update_video_config(resolution, format, fps).await; self.webrtc_streamer
.update_video_config(resolution, format, fps)
.await;
self.webrtc_streamer.set_video_source(frame_tx).await; self.webrtc_streamer.set_video_source(frame_tx).await;
} }
} }
@@ -620,7 +743,9 @@ impl VideoStreamManager {
// ========================================================================= // =========================================================================
/// List available video devices /// List available video devices
pub async fn list_devices(&self) -> crate::error::Result<Vec<crate::video::device::VideoDeviceInfo>> { pub async fn list_devices(
&self,
) -> crate::error::Result<Vec<crate::video::device::VideoDeviceInfo>> {
self.streamer.list_devices().await self.streamer.list_devices().await
} }
@@ -640,7 +765,9 @@ impl VideoStreamManager {
} }
/// Get frame sender for video frames /// Get frame sender for video frames
pub async fn frame_sender(&self) -> Option<tokio::sync::broadcast::Sender<crate::video::frame::VideoFrame>> { pub async fn frame_sender(
&self,
) -> Option<tokio::sync::broadcast::Sender<crate::video::frame::VideoFrame>> {
self.streamer.frame_sender().await self.streamer.frame_sender().await
} }
@@ -654,12 +781,17 @@ 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::broadcast::Receiver<crate::video::shared_video_pipeline::EncodedVideoFrame>> { ) -> Option<
tokio::sync::broadcast::Receiver<crate::video::shared_video_pipeline::EncodedVideoFrame>,
> {
// 1. Ensure video capture is initialized // 1. Ensure video capture is initialized
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");
if let Err(e) = self.streamer.init_auto().await { if let Err(e) = self.streamer.init_auto().await {
tracing::error!("Failed to initialize video capture for encoded frames: {}", e); tracing::error!(
"Failed to initialize video capture for encoded frames: {}",
e
);
return None; return None;
} }
} }
@@ -688,13 +820,22 @@ impl VideoStreamManager {
let (format, resolution, fps) = self.streamer.current_video_config().await; let (format, resolution, fps) = self.streamer.current_video_config().await;
tracing::info!( tracing::info!(
"Connecting encoded frame subscription: {}x{} {:?} @ {}fps", "Connecting encoded frame subscription: {}x{} {:?} @ {}fps",
resolution.width, resolution.height, format, fps resolution.width,
resolution.height,
format,
fps
); );
self.webrtc_streamer.update_video_config(resolution, format, fps).await; self.webrtc_streamer
.update_video_config(resolution, format, fps)
.await;
// 5. Use WebRtcStreamer to ensure the shared video pipeline is running // 5. Use WebRtcStreamer to ensure the shared video pipeline is running
// This will create the pipeline if needed // This will create the pipeline if needed
match self.webrtc_streamer.ensure_video_pipeline_for_external(frame_tx).await { match self
.webrtc_streamer
.ensure_video_pipeline_for_external(frame_tx)
.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);
@@ -704,7 +845,9 @@ impl VideoStreamManager {
} }
/// Get the current video encoding configuration from the shared pipeline /// Get the current video encoding configuration from the shared pipeline
pub async fn get_encoding_config(&self) -> Option<crate::video::shared_video_pipeline::SharedVideoPipelineConfig> { pub async fn get_encoding_config(
&self,
) -> Option<crate::video::shared_video_pipeline::SharedVideoPipelineConfig> {
self.webrtc_streamer.get_pipeline_config().await self.webrtc_streamer.get_pipeline_config().await
} }
@@ -712,7 +855,10 @@ impl VideoStreamManager {
/// ///
/// This allows external consumers (like RustDesk) to set the video codec /// This allows external consumers (like RustDesk) to set the video codec
/// before subscribing to encoded frames. /// before subscribing to encoded frames.
pub async fn set_video_codec(&self, codec: crate::video::encoder::VideoCodecType) -> crate::error::Result<()> { pub async fn set_video_codec(
&self,
codec: crate::video::encoder::VideoCodecType,
) -> crate::error::Result<()> {
self.webrtc_streamer.set_video_codec(codec).await self.webrtc_streamer.set_video_codec(codec).await
} }
@@ -720,7 +866,10 @@ impl VideoStreamManager {
/// ///
/// This allows external consumers (like RustDesk) to adjust the video quality /// This allows external consumers (like RustDesk) to adjust the video quality
/// based on client preferences. /// based on client preferences.
pub async fn set_bitrate_preset(&self, preset: crate::video::encoder::BitratePreset) -> crate::error::Result<()> { pub async fn set_bitrate_preset(
&self,
preset: crate::video::encoder::BitratePreset,
) -> crate::error::Result<()> {
self.webrtc_streamer.set_bitrate_preset(preset).await self.webrtc_streamer.set_bitrate_preset(preset).await
} }

View File

@@ -133,7 +133,12 @@ impl Streamer {
/// Get current state as SystemEvent /// Get current state as SystemEvent
pub async fn current_state_event(&self) -> SystemEvent { pub async fn current_state_event(&self) -> SystemEvent {
let state = *self.state.read().await; let state = *self.state.read().await;
let device = self.current_device.read().await.as_ref().map(|d| d.path.display().to_string()); let device = self
.current_device
.read()
.await
.as_ref()
.map(|d| d.path.display().to_string());
SystemEvent::StreamStateChanged { SystemEvent::StreamStateChanged {
state: match state { state: match state {
@@ -162,7 +167,8 @@ impl Streamer {
/// Check if config is currently being changed /// Check if config is currently being changed
/// When true, auto-start should be blocked to prevent device busy errors /// When true, auto-start should be blocked to prevent device busy errors
pub fn is_config_changing(&self) -> bool { pub fn is_config_changing(&self) -> bool {
self.config_changing.load(std::sync::atomic::Ordering::SeqCst) self.config_changing
.load(std::sync::atomic::Ordering::SeqCst)
} }
/// Get MJPEG handler for stream endpoints /// Get MJPEG handler for stream endpoints
@@ -209,13 +215,17 @@ impl Streamer {
fps: u32, fps: u32,
) -> Result<()> { ) -> Result<()> {
// Set config_changing flag to prevent frontend mode sync during config change // Set config_changing flag to prevent frontend mode sync during config change
self.config_changing.store(true, std::sync::atomic::Ordering::SeqCst); self.config_changing
.store(true, std::sync::atomic::Ordering::SeqCst);
let result = self.apply_video_config_inner(device_path, format, resolution, fps).await; let result = self
.apply_video_config_inner(device_path, format, resolution, fps)
.await;
// Clear the flag after config change is complete // Clear the flag after config change is complete
// The stream will be started by MJPEG client connection, not here // The stream will be started by MJPEG client connection, not here
self.config_changing.store(false, std::sync::atomic::Ordering::SeqCst); self.config_changing
.store(false, std::sync::atomic::Ordering::SeqCst);
result result
} }
@@ -230,6 +240,7 @@ impl Streamer {
) -> Result<()> { ) -> Result<()> {
// Publish "config changing" event // Publish "config changing" event
self.publish_event(SystemEvent::StreamConfigChanging { self.publish_event(SystemEvent::StreamConfigChanging {
transition_id: None,
reason: "device_switch".to_string(), reason: "device_switch".to_string(),
}) })
.await; .await;
@@ -254,7 +265,9 @@ impl Streamer {
.iter() .iter()
.any(|r| r.width == resolution.width && r.height == resolution.height) .any(|r| r.width == resolution.width && r.height == resolution.height)
{ {
return Err(AppError::VideoError("Requested resolution not supported".to_string())); return Err(AppError::VideoError(
"Requested resolution not supported".to_string(),
));
} }
// IMPORTANT: Disconnect all MJPEG clients FIRST before stopping capture // IMPORTANT: Disconnect all MJPEG clients FIRST before stopping capture
@@ -277,7 +290,6 @@ impl Streamer {
// Explicitly drop the capturer to release V4L2 resources // Explicitly drop the capturer to release V4L2 resources
drop(capturer); drop(capturer);
} }
} }
// Update config // Update config
@@ -305,9 +317,12 @@ impl Streamer {
*self.state.write().await = StreamerState::Ready; *self.state.write().await = StreamerState::Ready;
// Publish "config applied" event // Publish "config applied" event
info!("Publishing StreamConfigApplied event: {}x{} {:?} @ {}fps", info!(
resolution.width, resolution.height, format, fps); "Publishing StreamConfigApplied event: {}x{} {:?} @ {}fps",
resolution.width, resolution.height, format, fps
);
self.publish_event(SystemEvent::StreamConfigApplied { self.publish_event(SystemEvent::StreamConfigApplied {
transition_id: None,
device: device_path.to_string(), device: device_path.to_string(),
resolution: (resolution.width, resolution.height), resolution: (resolution.width, resolution.height),
format: format!("{:?}", format), format: format!("{:?}", format),
@@ -381,7 +396,11 @@ impl Streamer {
} }
/// Select best format for device /// Select best format for device
fn select_format(&self, device: &VideoDeviceInfo, preferred: PixelFormat) -> Result<PixelFormat> { fn select_format(
&self,
device: &VideoDeviceInfo,
preferred: PixelFormat,
) -> Result<PixelFormat> {
// Check if preferred format is available // Check if preferred format is available
if device.formats.iter().any(|f| f.format == preferred) { if device.formats.iter().any(|f| f.format == preferred) {
return Ok(preferred); return Ok(preferred);
@@ -410,9 +429,10 @@ impl Streamer {
// Check if preferred resolution is available // Check if preferred resolution is available
if format_info.resolutions.is_empty() if format_info.resolutions.is_empty()
|| format_info.resolutions.iter().any(|r| { || format_info
r.width == preferred.width && r.height == preferred.height .resolutions
}) .iter()
.any(|r| r.width == preferred.width && r.height == preferred.height)
{ {
return Ok(preferred); return Ok(preferred);
} }
@@ -528,7 +548,10 @@ impl Streamer {
// Stop the streamer // Stop the streamer
if let Some(streamer) = state_ref.upgrade() { if let Some(streamer) = state_ref.upgrade() {
if let Err(e) = streamer.stop().await { if let Err(e) = streamer.stop().await {
warn!("Failed to stop streamer during idle cleanup: {}", e); warn!(
"Failed to stop streamer during idle cleanup: {}",
e
);
} }
} }
break; break;
@@ -609,8 +632,14 @@ impl Streamer {
// Start background tasks only once per Streamer instance // Start background tasks only once per Streamer instance
// Use compare_exchange to atomically check and set the flag // Use compare_exchange to atomically check and set the flag
if self.background_tasks_started if self
.compare_exchange(false, true, std::sync::atomic::Ordering::SeqCst, std::sync::atomic::Ordering::SeqCst) .background_tasks_started
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
)
.is_ok() .is_ok()
{ {
info!("Starting background tasks (stats, cleanup, monitor)"); info!("Starting background tasks (stats, cleanup, monitor)");
@@ -626,10 +655,12 @@ impl Streamer {
let clients_stat = streamer.mjpeg_handler().get_clients_stat(); let clients_stat = streamer.mjpeg_handler().get_clients_stat();
let clients = clients_stat.len() as u64; let clients = clients_stat.len() as u64;
streamer.publish_event(SystemEvent::StreamStatsUpdate { streamer
.publish_event(SystemEvent::StreamStatsUpdate {
clients, clients,
clients_stat, clients_stat,
}).await; })
.await;
} else { } else {
break; break;
} }
@@ -649,7 +680,9 @@ impl Streamer {
loop { loop {
interval.tick().await; interval.tick().await;
let Some(streamer) = monitor_ref.upgrade() else { break; }; let Some(streamer) = monitor_ref.upgrade() else {
break;
};
// Check auto-pause configuration // Check auto-pause configuration
let config = monitor_handler.auto_pause_config(); let config = monitor_handler.auto_pause_config();
@@ -663,10 +696,16 @@ impl Streamer {
if count == 0 { if count == 0 {
if zero_since.is_none() { if zero_since.is_none() {
zero_since = Some(std::time::Instant::now()); zero_since = Some(std::time::Instant::now());
info!("No clients connected, starting shutdown timer ({}s)", config.shutdown_delay_secs); info!(
"No clients connected, starting shutdown timer ({}s)",
config.shutdown_delay_secs
);
} else if let Some(since) = zero_since { } else if let Some(since) = zero_since {
if since.elapsed().as_secs() >= config.shutdown_delay_secs { if since.elapsed().as_secs() >= config.shutdown_delay_secs {
info!("Auto-pausing stream (no clients for {}s)", config.shutdown_delay_secs); info!(
"Auto-pausing stream (no clients for {}s)",
config.shutdown_delay_secs
);
if let Err(e) = streamer.stop().await { if let Err(e) = streamer.stop().await {
error!("Auto-pause failed: {}", e); error!("Auto-pause failed: {}", e);
} }
@@ -734,8 +773,14 @@ impl Streamer {
clients: self.mjpeg_handler.client_count(), clients: self.mjpeg_handler.client_count(),
target_fps: config.fps, target_fps: config.fps,
fps: capture_stats.as_ref().map(|s| s.current_fps).unwrap_or(0.0), fps: capture_stats.as_ref().map(|s| s.current_fps).unwrap_or(0.0),
frames_captured: capture_stats.as_ref().map(|s| s.frames_captured).unwrap_or(0), frames_captured: capture_stats
frames_dropped: capture_stats.as_ref().map(|s| s.frames_dropped).unwrap_or(0), .as_ref()
.map(|s| s.frames_captured)
.unwrap_or(0),
frames_dropped: capture_stats
.as_ref()
.map(|s| s.frames_dropped)
.unwrap_or(0),
} }
} }
@@ -776,7 +821,10 @@ impl Streamer {
/// until the device is recovered. /// until the device is recovered.
async fn start_device_recovery_internal(self: &Arc<Self>) { async fn start_device_recovery_internal(self: &Arc<Self>) {
// Check if recovery is already in progress // Check if recovery is already in progress
if self.recovery_in_progress.swap(true, std::sync::atomic::Ordering::SeqCst) { if self
.recovery_in_progress
.swap(true, std::sync::atomic::Ordering::SeqCst)
{
debug!("Device recovery already in progress, skipping"); debug!("Device recovery already in progress, skipping");
return; return;
} }
@@ -786,7 +834,9 @@ impl Streamer {
let capturer = self.capturer.read().await; let capturer = self.capturer.read().await;
if let Some(cap) = capturer.as_ref() { if let Some(cap) = capturer.as_ref() {
cap.last_error().unwrap_or_else(|| { cap.last_error().unwrap_or_else(|| {
let device_path = self.current_device.blocking_read() let device_path = self
.current_device
.blocking_read()
.as_ref() .as_ref()
.map(|d| d.path.display().to_string()) .map(|d| d.path.display().to_string())
.unwrap_or_else(|| "unknown".to_string()); .unwrap_or_else(|| "unknown".to_string());
@@ -800,13 +850,15 @@ impl Streamer {
// Store error info // Store error info
*self.last_lost_device.write().await = Some(device.clone()); *self.last_lost_device.write().await = Some(device.clone());
*self.last_lost_reason.write().await = Some(reason.clone()); *self.last_lost_reason.write().await = Some(reason.clone());
self.recovery_retry_count.store(0, std::sync::atomic::Ordering::Relaxed); self.recovery_retry_count
.store(0, std::sync::atomic::Ordering::Relaxed);
// Publish device lost event // Publish device lost event
self.publish_event(SystemEvent::StreamDeviceLost { self.publish_event(SystemEvent::StreamDeviceLost {
device: device.clone(), device: device.clone(),
reason: reason.clone(), reason: reason.clone(),
}).await; })
.await;
// Start recovery task // Start recovery task
let streamer = Arc::clone(self); let streamer = Arc::clone(self);
@@ -814,11 +866,16 @@ impl Streamer {
let device_path = device.clone(); let device_path = device.clone();
loop { loop {
let attempt = streamer.recovery_retry_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; let attempt = streamer
.recovery_retry_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
+ 1;
// Check if still in device lost state // Check if still in device lost state
let current_state = *streamer.state.read().await; let current_state = *streamer.state.read().await;
if current_state != StreamerState::DeviceLost && current_state != StreamerState::Recovering { if current_state != StreamerState::DeviceLost
&& current_state != StreamerState::Recovering
{
info!("Stream state changed during recovery, stopping recovery task"); info!("Stream state changed during recovery, stopping recovery task");
break; break;
} }
@@ -828,11 +885,16 @@ impl Streamer {
// 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 % 5 == 0 {
streamer.publish_event(SystemEvent::StreamReconnecting { streamer
.publish_event(SystemEvent::StreamReconnecting {
device: device_path.clone(), device: device_path.clone(),
attempt, attempt,
}).await; })
info!("Attempting to recover video device {} (attempt {})", device_path, attempt); .await;
info!(
"Attempting to recover video device {} (attempt {})",
device_path, attempt
);
} }
// Wait before retry (1 second) // Wait before retry (1 second)
@@ -848,13 +910,20 @@ impl Streamer {
// Try to restart capture // Try to restart capture
match streamer.restart_capturer().await { match streamer.restart_capturer().await {
Ok(_) => { Ok(_) => {
info!("Video device {} recovered after {} attempts", device_path, attempt); info!(
streamer.recovery_in_progress.store(false, std::sync::atomic::Ordering::SeqCst); "Video device {} recovered after {} attempts",
device_path, attempt
);
streamer
.recovery_in_progress
.store(false, std::sync::atomic::Ordering::SeqCst);
// Publish recovered event // Publish recovered event
streamer.publish_event(SystemEvent::StreamRecovered { streamer
.publish_event(SystemEvent::StreamRecovered {
device: device_path.clone(), device: device_path.clone(),
}).await; })
.await;
// Clear error info // Clear error info
*streamer.last_lost_device.write().await = None; *streamer.last_lost_device.write().await = None;
@@ -867,7 +936,9 @@ impl Streamer {
} }
} }
streamer.recovery_in_progress.store(false, std::sync::atomic::Ordering::SeqCst); streamer
.recovery_in_progress
.store(false, std::sync::atomic::Ordering::SeqCst);
}); });
} }
} }

View File

@@ -234,10 +234,7 @@ impl VideoSessionManager {
let mut sessions = self.sessions.write().await; let mut sessions = self.sessions.write().await;
sessions.insert(session_id.clone(), session); sessions.insert(session_id.clone(), session);
info!( info!("Video session created: {} (codec: {})", session_id, codec);
"Video session created: {} (codec: {})",
session_id, codec
);
Ok(session_id) Ok(session_id)
} }
@@ -428,8 +425,7 @@ impl VideoSessionManager {
sessions sessions
.iter() .iter()
.filter(|(_, s)| { .filter(|(_, s)| {
(s.state == VideoSessionState::Paused (s.state == VideoSessionState::Paused || s.state == VideoSessionState::Created)
|| s.state == VideoSessionState::Created)
&& now.duration_since(s.last_activity) > timeout && now.duration_since(s.last_activity) > timeout
}) })
.map(|(id, _)| id.clone()) .map(|(id, _)| id.clone())

View File

@@ -31,15 +31,14 @@ pub async fn apply_video_config(
.format .format
.as_ref() .as_ref()
.and_then(|f| { .and_then(|f| {
serde_json::from_value::<crate::video::format::PixelFormat>( serde_json::from_value::<crate::video::format::PixelFormat>(serde_json::Value::String(
serde_json::Value::String(f.clone()), f.clone(),
) ))
.ok() .ok()
}) })
.unwrap_or(crate::video::format::PixelFormat::Mjpeg); .unwrap_or(crate::video::format::PixelFormat::Mjpeg);
let resolution = let resolution = crate::video::format::Resolution::new(new_config.width, new_config.height);
crate::video::format::Resolution::new(new_config.width, new_config.height);
// Step 1: 更新 WebRTC streamer 配置(停止现有 pipeline 和 sessions // Step 1: 更新 WebRTC streamer 配置(停止现有 pipeline 和 sessions
state state
@@ -162,9 +161,16 @@ pub async fn apply_hid_config(
// 如果描述符变更且当前使用 OTG 后端,需要重建 Gadget // 如果描述符变更且当前使用 OTG 后端,需要重建 Gadget
if descriptor_changed && new_config.backend == HidBackend::Otg { if descriptor_changed && new_config.backend == HidBackend::Otg {
tracing::info!("OTG descriptor changed, updating gadget..."); tracing::info!("OTG descriptor changed, updating gadget...");
if let Err(e) = state.otg_service.update_descriptor(&new_config.otg_descriptor).await { if let Err(e) = state
.otg_service
.update_descriptor(&new_config.otg_descriptor)
.await
{
tracing::error!("Failed to update OTG descriptor: {}", e); tracing::error!("Failed to update OTG descriptor: {}", e);
return Err(AppError::Config(format!("OTG descriptor update failed: {}", e))); return Err(AppError::Config(format!(
"OTG descriptor update failed: {}",
e
)));
} }
tracing::info!("OTG descriptor updated successfully"); tracing::info!("OTG descriptor updated successfully");
} }
@@ -197,7 +203,10 @@ pub async fn apply_hid_config(
.await .await
.map_err(|e| AppError::Config(format!("HID reload failed: {}", e)))?; .map_err(|e| AppError::Config(format!("HID reload failed: {}", e)))?;
tracing::info!("HID backend reloaded successfully: {:?}", new_config.backend); tracing::info!(
"HID backend reloaded successfully: {:?}",
new_config.backend
);
// When switching to OTG backend, automatically enable MSD if not already enabled // When switching to OTG backend, automatically enable MSD if not already enabled
// OTG HID and MSD share the same USB gadget, so it makes sense to enable both // OTG HID and MSD share the same USB gadget, so it makes sense to enable both
@@ -245,7 +254,11 @@ pub async fn apply_msd_config(
let old_msd_enabled = old_config.enabled; let old_msd_enabled = old_config.enabled;
let new_msd_enabled = new_config.enabled; let new_msd_enabled = new_config.enabled;
tracing::info!("MSD enabled: old={}, new={}", old_msd_enabled, new_msd_enabled); tracing::info!(
"MSD enabled: old={}, new={}",
old_msd_enabled,
new_msd_enabled
);
if old_msd_enabled != new_msd_enabled { if old_msd_enabled != new_msd_enabled {
if new_msd_enabled { if new_msd_enabled {
@@ -257,9 +270,9 @@ pub async fn apply_msd_config(
&new_config.images_path, &new_config.images_path,
&new_config.drive_path, &new_config.drive_path,
); );
msd.init().await.map_err(|e| { msd.init()
AppError::Config(format!("MSD initialization failed: {}", e)) .await
})?; .map_err(|e| AppError::Config(format!("MSD initialization failed: {}", e)))?;
// Set event bus // Set event bus
let events = state.events.clone(); let events = state.events.clone();
@@ -429,7 +442,10 @@ pub async fn apply_rustdesk_config(
if let Err(e) = service.restart(new_config.clone()).await { if let Err(e) = service.restart(new_config.clone()).await {
tracing::error!("Failed to restart RustDesk service: {}", e); tracing::error!("Failed to restart RustDesk service: {}", e);
} else { } else {
tracing::info!("RustDesk service restarted with ID: {}", new_config.device_id); tracing::info!(
"RustDesk service restarted with ID: {}",
new_config.device_id
);
// Save generated keypair and UUID to config // Save generated keypair and UUID to config
credentials_to_save = service.save_credentials(); credentials_to_save = service.save_credentials();
} }

View File

@@ -19,26 +19,26 @@
pub(crate) mod apply; pub(crate) mod apply;
mod types; mod types;
pub(crate) mod video;
mod stream;
mod hid;
mod msd;
mod atx; mod atx;
mod audio; mod audio;
mod hid;
mod msd;
mod rustdesk; mod rustdesk;
mod stream;
pub(crate) mod video;
mod web; mod web;
// 导出 handler 函数 // 导出 handler 函数
pub use video::{get_video_config, update_video_config};
pub use stream::{get_stream_config, update_stream_config};
pub use hid::{get_hid_config, update_hid_config};
pub use msd::{get_msd_config, update_msd_config};
pub use atx::{get_atx_config, update_atx_config}; pub use atx::{get_atx_config, update_atx_config};
pub use audio::{get_audio_config, update_audio_config}; pub use audio::{get_audio_config, update_audio_config};
pub use hid::{get_hid_config, update_hid_config};
pub use msd::{get_msd_config, update_msd_config};
pub use rustdesk::{ pub use rustdesk::{
get_rustdesk_config, get_rustdesk_status, update_rustdesk_config, get_device_password, get_rustdesk_config, get_rustdesk_status, regenerate_device_id,
regenerate_device_id, regenerate_device_password, get_device_password, regenerate_device_password, update_rustdesk_config,
}; };
pub use stream::{get_stream_config, update_stream_config};
pub use video::{get_video_config, update_video_config};
pub use web::{get_web_config, update_web_config}; pub use web::{get_web_config, update_web_config};
// 保留全局配置查询(向后兼容) // 保留全局配置查询(向后兼容)

View File

@@ -48,12 +48,16 @@ pub struct RustDeskStatusResponse {
} }
/// 获取 RustDesk 配置 /// 获取 RustDesk 配置
pub async fn get_rustdesk_config(State(state): State<Arc<AppState>>) -> Json<RustDeskConfigResponse> { pub async fn get_rustdesk_config(
State(state): State<Arc<AppState>>,
) -> Json<RustDeskConfigResponse> {
Json(RustDeskConfigResponse::from(&state.config.get().rustdesk)) Json(RustDeskConfigResponse::from(&state.config.get().rustdesk))
} }
/// 获取 RustDesk 完整状态(配置 + 服务状态) /// 获取 RustDesk 完整状态(配置 + 服务状态)
pub async fn get_rustdesk_status(State(state): State<Arc<AppState>>) -> Json<RustDeskStatusResponse> { pub async fn get_rustdesk_status(
State(state): State<Arc<AppState>>,
) -> Json<RustDeskStatusResponse> {
let config = state.config.get().rustdesk.clone(); let config = state.config.get().rustdesk.clone();
// 获取服务状态 // 获取服务状态

View File

@@ -1,9 +1,9 @@
use serde::Deserialize;
use typeshare::typeshare;
use crate::config::*; use crate::config::*;
use crate::error::AppError; use crate::error::AppError;
use crate::rustdesk::config::RustDeskConfig; use crate::rustdesk::config::RustDeskConfig;
use crate::video::encoder::BitratePreset; use crate::video::encoder::BitratePreset;
use serde::Deserialize;
use typeshare::typeshare;
// ===== Video Config ===== // ===== Video Config =====
#[typeshare] #[typeshare]
@@ -21,12 +21,16 @@ impl VideoConfigUpdate {
pub fn validate(&self) -> crate::error::Result<()> { pub fn validate(&self) -> crate::error::Result<()> {
if let Some(width) = self.width { if let Some(width) = self.width {
if !(320..=7680).contains(&width) { if !(320..=7680).contains(&width) {
return Err(AppError::BadRequest("Invalid width: must be 320-7680".into())); return Err(AppError::BadRequest(
"Invalid width: must be 320-7680".into(),
));
} }
} }
if let Some(height) = self.height { if let Some(height) = self.height {
if !(240..=4320).contains(&height) { if !(240..=4320).contains(&height) {
return Err(AppError::BadRequest("Invalid height: must be 240-4320".into())); return Err(AppError::BadRequest(
"Invalid height: must be 240-4320".into(),
));
} }
} }
if let Some(fps) = self.fps { if let Some(fps) = self.fps {
@@ -36,7 +40,9 @@ impl VideoConfigUpdate {
} }
if let Some(quality) = self.quality { if let Some(quality) = self.quality {
if !(1..=100).contains(&quality) { if !(1..=100).contains(&quality) {
return Err(AppError::BadRequest("Invalid quality: must be 1-100".into())); return Err(AppError::BadRequest(
"Invalid quality: must be 1-100".into(),
));
} }
} }
Ok(()) Ok(())
@@ -126,7 +132,8 @@ impl StreamConfigUpdate {
if let Some(ref stun) = self.stun_server { if let Some(ref stun) = self.stun_server {
if !stun.is_empty() && !stun.starts_with("stun:") { if !stun.is_empty() && !stun.starts_with("stun:") {
return Err(AppError::BadRequest( return Err(AppError::BadRequest(
"STUN server must start with 'stun:' (e.g., stun:stun.l.google.com:19302)".into(), "STUN server must start with 'stun:' (e.g., stun:stun.l.google.com:19302)"
.into(),
)); ));
} }
} }
@@ -153,16 +160,32 @@ impl StreamConfigUpdate {
} }
// STUN/TURN settings - empty string means clear (use public servers), Some("value") means set custom // STUN/TURN settings - empty string means clear (use public servers), Some("value") means set custom
if let Some(ref stun) = self.stun_server { if let Some(ref stun) = self.stun_server {
config.stun_server = if stun.is_empty() { None } else { Some(stun.clone()) }; config.stun_server = if stun.is_empty() {
None
} else {
Some(stun.clone())
};
} }
if let Some(ref turn) = self.turn_server { if let Some(ref turn) = self.turn_server {
config.turn_server = if turn.is_empty() { None } else { Some(turn.clone()) }; config.turn_server = if turn.is_empty() {
None
} else {
Some(turn.clone())
};
} }
if let Some(ref username) = self.turn_username { if let Some(ref username) = self.turn_username {
config.turn_username = if username.is_empty() { None } else { Some(username.clone()) }; config.turn_username = if username.is_empty() {
None
} else {
Some(username.clone())
};
} }
if let Some(ref password) = self.turn_password { if let Some(ref password) = self.turn_password {
config.turn_password = if password.is_empty() { None } else { Some(password.clone()) }; config.turn_password = if password.is_empty() {
None
} else {
Some(password.clone())
};
} }
} }
} }
@@ -185,19 +208,25 @@ impl OtgDescriptorConfigUpdate {
// Validate manufacturer string length // Validate manufacturer string length
if let Some(ref s) = self.manufacturer { if let Some(ref s) = self.manufacturer {
if s.len() > 126 { if s.len() > 126 {
return Err(AppError::BadRequest("Manufacturer string too long (max 126 chars)".into())); return Err(AppError::BadRequest(
"Manufacturer string too long (max 126 chars)".into(),
));
} }
} }
// Validate product string length // Validate product string length
if let Some(ref s) = self.product { if let Some(ref s) = self.product {
if s.len() > 126 { if s.len() > 126 {
return Err(AppError::BadRequest("Product string too long (max 126 chars)".into())); return Err(AppError::BadRequest(
"Product string too long (max 126 chars)".into(),
));
} }
} }
// Validate serial number string length // Validate serial number string length
if let Some(ref s) = self.serial_number { if let Some(ref s) = self.serial_number {
if s.len() > 126 { if s.len() > 126 {
return Err(AppError::BadRequest("Serial number string too long (max 126 chars)".into())); return Err(AppError::BadRequest(
"Serial number string too long (max 126 chars)".into(),
));
} }
} }
Ok(()) Ok(())
@@ -469,7 +498,8 @@ impl RustDeskConfigUpdate {
if let Some(ref server) = self.rendezvous_server { if let Some(ref server) = self.rendezvous_server {
if !server.is_empty() && !server.contains(':') { if !server.is_empty() && !server.contains(':') {
return Err(AppError::BadRequest( return Err(AppError::BadRequest(
"Rendezvous server must be in format 'host:port' (e.g., rs.example.com:21116)".into(), "Rendezvous server must be in format 'host:port' (e.g., rs.example.com:21116)"
.into(),
)); ));
} }
} }
@@ -477,7 +507,8 @@ impl RustDeskConfigUpdate {
if let Some(ref server) = self.relay_server { if let Some(ref server) = self.relay_server {
if !server.is_empty() && !server.contains(':') { if !server.is_empty() && !server.contains(':') {
return Err(AppError::BadRequest( return Err(AppError::BadRequest(
"Relay server must be in format 'host:port' (e.g., rs.example.com:21117)".into(), "Relay server must be in format 'host:port' (e.g., rs.example.com:21117)"
.into(),
)); ));
} }
} }
@@ -500,10 +531,18 @@ impl RustDeskConfigUpdate {
config.rendezvous_server = server.clone(); config.rendezvous_server = server.clone();
} }
if let Some(ref server) = self.relay_server { if let Some(ref server) = self.relay_server {
config.relay_server = if server.is_empty() { None } else { Some(server.clone()) }; config.relay_server = if server.is_empty() {
None
} else {
Some(server.clone())
};
} }
if let Some(ref key) = self.relay_key { if let Some(ref key) = self.relay_key {
config.relay_key = if key.is_empty() { None } else { Some(key.clone()) }; config.relay_key = if key.is_empty() {
None
} else {
Some(key.clone())
};
} }
if let Some(ref password) = self.device_password { if let Some(ref password) = self.device_password {
if !password.is_empty() { if !password.is_empty() {

View File

@@ -10,8 +10,8 @@ use typeshare::typeshare;
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
use crate::extensions::{ use crate::extensions::{
EasytierConfig, EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs, EasytierConfig, EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs, ExtensionsStatus,
ExtensionsStatus, GostcConfig, GostcInfo, TtydConfig, TtydInfo, GostcConfig, GostcInfo, TtydConfig, TtydInfo,
}; };
use crate::state::AppState; use crate::state::AppState;
@@ -108,9 +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) mgr.stop(ext_id).await.map_err(|e| AppError::Internal(e))?;
.await
.map_err(|e| AppError::Internal(e))?;
// Return updated status // Return updated status
Ok(Json(ExtensionInfo { Ok(Json(ExtensionInfo {

View File

@@ -124,8 +124,7 @@ pub async fn system_info(State(state): State<Arc<AppState>>) -> Json<SystemInfo>
backend: if config.atx.enabled { backend: if config.atx.enabled {
Some(format!( Some(format!(
"power: {:?}, reset: {:?}", "power: {:?}, reset: {:?}",
config.atx.power.driver, config.atx.power.driver, config.atx.reset.driver
config.atx.reset.driver
)) ))
} else { } else {
None None
@@ -208,7 +207,8 @@ fn get_cpu_model() -> String {
} }
/// CPU usage state for calculating usage between samples /// CPU usage state for calculating usage between samples
static CPU_PREV_STATS: std::sync::OnceLock<std::sync::Mutex<(u64, u64)>> = std::sync::OnceLock::new(); static CPU_PREV_STATS: std::sync::OnceLock<std::sync::Mutex<(u64, u64)>> =
std::sync::OnceLock::new();
/// Get CPU usage percentage (0.0 - 100.0) /// Get CPU usage percentage (0.0 - 100.0)
fn get_cpu_usage() -> f32 { fn get_cpu_usage() -> f32 {
@@ -268,7 +268,12 @@ struct MemInfo {
fn get_meminfo() -> MemInfo { fn get_meminfo() -> MemInfo {
let content = match std::fs::read_to_string("/proc/meminfo") { let content = match std::fs::read_to_string("/proc/meminfo") {
Ok(c) => c, Ok(c) => c,
Err(_) => return MemInfo { total: 0, available: 0 }, Err(_) => {
return MemInfo {
total: 0,
available: 0,
}
}
}; };
let mut total = 0u64; let mut total = 0u64;
@@ -276,11 +281,19 @@ fn get_meminfo() -> MemInfo {
for line in content.lines() { for line in content.lines() {
if line.starts_with("MemTotal:") { if line.starts_with("MemTotal:") {
if let Some(kb) = line.split_whitespace().nth(1).and_then(|v| v.parse::<u64>().ok()) { if let Some(kb) = line
.split_whitespace()
.nth(1)
.and_then(|v| v.parse::<u64>().ok())
{
total = kb * 1024; total = kb * 1024;
} }
} else if line.starts_with("MemAvailable:") { } else if line.starts_with("MemAvailable:") {
if let Some(kb) = line.split_whitespace().nth(1).and_then(|v| v.parse::<u64>().ok()) { if let Some(kb) = line
.split_whitespace()
.nth(1)
.and_then(|v| v.parse::<u64>().ok())
{
available = kb * 1024; available = kb * 1024;
} }
} }
@@ -312,10 +325,7 @@ fn get_network_addresses() -> Vec<NetworkAddress> {
if !ipv4_map.contains_key(&ifaddr.interface_name) { if !ipv4_map.contains_key(&ifaddr.interface_name) {
if let Some(addr) = ifaddr.address { if let Some(addr) = ifaddr.address {
if let Some(sockaddr_in) = addr.as_sockaddr_in() { if let Some(sockaddr_in) = addr.as_sockaddr_in() {
ipv4_map.insert( ipv4_map.insert(ifaddr.interface_name.clone(), sockaddr_in.ip().to_string());
ifaddr.interface_name.clone(),
sockaddr_in.ip().to_string(),
);
} }
} }
} }
@@ -624,10 +634,7 @@ pub async fn setup_init(
if new_config.extensions.ttyd.enabled { if new_config.extensions.ttyd.enabled {
if let Err(e) = state if let Err(e) = state
.extensions .extensions
.start( .start(crate::extensions::ExtensionId::Ttyd, &new_config.extensions)
crate::extensions::ExtensionId::Ttyd,
&new_config.extensions,
)
.await .await
{ {
tracing::warn!("Failed to start ttyd during setup: {}", e); tracing::warn!("Failed to start ttyd during setup: {}", e);
@@ -658,7 +665,10 @@ pub async fn setup_init(
if let Err(e) = state.audio.update_config(audio_config).await { if let Err(e) = state.audio.update_config(audio_config).await {
tracing::warn!("Failed to start audio during setup: {}", e); tracing::warn!("Failed to start audio during setup: {}", e);
} else { } else {
tracing::info!("Audio started during setup: device={}", new_config.audio.device); tracing::info!(
"Audio started during setup: device={}",
new_config.audio.device
);
} }
// Also enable WebRTC audio // Also enable WebRTC audio
if let Err(e) = state.stream_manager.set_webrtc_audio_enabled(true).await { if let Err(e) = state.stream_manager.set_webrtc_audio_enabled(true).await {
@@ -666,7 +676,10 @@ pub async fn setup_init(
} }
} }
tracing::info!("System initialized successfully with admin user: {}", req.username); tracing::info!(
"System initialized successfully with admin user: {}",
req.username
);
Ok(Json(LoginResponse { Ok(Json(LoginResponse {
success: true, success: true,
@@ -798,10 +811,19 @@ pub async fn update_config(
if let Some(frame_tx) = state.stream_manager.frame_sender().await { if let Some(frame_tx) = state.stream_manager.frame_sender().await {
let receiver_count = frame_tx.receiver_count(); let receiver_count = frame_tx.receiver_count();
// Use WebRtcStreamer (new unified interface) // Use WebRtcStreamer (new unified interface)
state.stream_manager.webrtc_streamer().set_video_source(frame_tx).await; state
tracing::info!("WebRTC streamer frame source updated with new capturer (receiver_count={})", receiver_count); .stream_manager
.webrtc_streamer()
.set_video_source(frame_tx)
.await;
tracing::info!(
"WebRTC streamer frame source updated with new capturer (receiver_count={})",
receiver_count
);
} else { } else {
tracing::warn!("No frame source available after config change - streamer may not be running"); tracing::warn!(
"No frame source available after config change - streamer may not be running"
);
} }
} }
@@ -831,8 +853,11 @@ pub async fn update_config(
.await .await
.ok(); // Ignore error if no active stream .ok(); // Ignore error if no active stream
tracing::info!("Stream config applied: encoder={:?}, bitrate={}", tracing::info!(
new_config.stream.encoder, new_config.stream.bitrate_preset); "Stream config applied: encoder={:?}, bitrate={}",
new_config.stream.encoder,
new_config.stream.bitrate_preset
);
} }
// HID config processing - always reload if section was sent // HID config processing - always reload if section was sent
@@ -860,7 +885,10 @@ pub async fn update_config(
})); }));
} }
tracing::info!("HID backend reloaded successfully: {:?}", new_config.hid.backend); tracing::info!(
"HID backend reloaded successfully: {:?}",
new_config.hid.backend
);
} }
// Audio config processing - always reload if section was sent // Audio config processing - always reload if section was sent
@@ -888,7 +916,11 @@ pub async fn update_config(
} }
// Also update WebRTC audio enabled state // Also update WebRTC audio enabled state
if let Err(e) = state.stream_manager.set_webrtc_audio_enabled(new_config.audio.enabled).await { if let Err(e) = state
.stream_manager
.set_webrtc_audio_enabled(new_config.audio.enabled)
.await
{
tracing::warn!("Failed to update WebRTC audio state: {}", e); tracing::warn!("Failed to update WebRTC audio state: {}", e);
} else { } else {
tracing::info!("WebRTC audio enabled: {}", new_config.audio.enabled); tracing::info!("WebRTC audio enabled: {}", new_config.audio.enabled);
@@ -911,7 +943,11 @@ pub async fn update_config(
let old_msd_enabled = old_config.msd.enabled; let old_msd_enabled = old_config.msd.enabled;
let new_msd_enabled = new_config.msd.enabled; let new_msd_enabled = new_config.msd.enabled;
tracing::info!("MSD enabled: old={}, new={}", old_msd_enabled, new_msd_enabled); tracing::info!(
"MSD enabled: old={}, new={}",
old_msd_enabled,
new_msd_enabled
);
if old_msd_enabled != new_msd_enabled { if old_msd_enabled != new_msd_enabled {
if new_msd_enabled { if new_msd_enabled {
@@ -953,7 +989,10 @@ pub async fn update_config(
tracing::info!("MSD shutdown complete"); tracing::info!("MSD shutdown complete");
} }
} else { } else {
tracing::info!("MSD enabled state unchanged ({}), no reload needed", new_msd_enabled); tracing::info!(
"MSD enabled state unchanged ({}), no reload needed",
new_msd_enabled
);
} }
} }
@@ -1060,7 +1099,12 @@ fn extract_usb_bus_from_bus_info(bus_info: &str) -> Option<String> {
if parts.len() == 2 { if parts.len() == 2 {
let port = parts[0]; let port = parts[0];
// Verify it looks like a USB port (starts with digit) // Verify it looks like a USB port (starts with digit)
if port.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) { if port
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
return Some(port.to_string()); return Some(port.to_string());
} }
} }
@@ -1115,7 +1159,10 @@ pub async fn list_devices(State(state): State<Arc<AppState>>) -> Json<DeviceList
None => continue, None => continue,
}; };
// Check if matches any prefix // Check if matches any prefix
if serial_prefixes.iter().any(|prefix| name.starts_with(prefix)) { if serial_prefixes
.iter()
.any(|prefix| name.starts_with(prefix))
{
let path = entry.path(); let path = entry.path();
if let Some(p) = path.to_str() { if let Some(p) = path.to_str() {
serial_devices.push(SerialDevice { serial_devices.push(SerialDevice {
@@ -1156,7 +1203,9 @@ pub async fn list_devices(State(state): State<Arc<AppState>>) -> Json<DeviceList
}; };
// Check extension availability // Check extension availability
let ttyd_available = state.extensions.check_available(crate::extensions::ExtensionId::Ttyd); let ttyd_available = state
.extensions
.check_available(crate::extensions::ExtensionId::Ttyd);
Json(DeviceList { Json(DeviceList {
video: video_devices, video: video_devices,
@@ -1174,12 +1223,12 @@ pub async fn list_devices(State(state): State<Arc<AppState>>) -> Json<DeviceList
// Stream Control // Stream Control
// ============================================================================ // ============================================================================
use crate::video::streamer::StreamerStats;
use axum::{ use axum::{
body::Body, body::Body,
http::{header, StatusCode}, http::{header, StatusCode},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use crate::video::streamer::StreamerStats;
/// Get stream state /// Get stream state
pub async fn stream_state(State(state): State<Arc<AppState>>) -> Json<StreamerStats> { pub async fn stream_state(State(state): State<Arc<AppState>>) -> Json<StreamerStats> {
@@ -1216,6 +1265,9 @@ pub struct SetStreamModeRequest {
pub struct StreamModeResponse { pub struct StreamModeResponse {
pub success: bool, pub success: bool,
pub mode: String, pub mode: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub transition_id: Option<String>,
pub switching: bool,
pub message: Option<String>, pub message: Option<String>,
} }
@@ -1223,12 +1275,27 @@ pub struct StreamModeResponse {
pub async fn stream_mode_get(State(state): State<Arc<AppState>>) -> Json<StreamModeResponse> { pub async fn stream_mode_get(State(state): State<Arc<AppState>>) -> Json<StreamModeResponse> {
let mode = state.stream_manager.current_mode().await; let mode = state.stream_manager.current_mode().await;
let mode_str = match mode { let mode_str = match mode {
StreamMode::Mjpeg => "mjpeg", StreamMode::Mjpeg => "mjpeg".to_string(),
StreamMode::WebRTC => "webrtc", StreamMode::WebRTC => {
use crate::video::encoder::VideoCodecType;
let codec = state
.stream_manager
.webrtc_streamer()
.current_video_codec()
.await;
match codec {
VideoCodecType::H264 => "h264".to_string(),
VideoCodecType::H265 => "h265".to_string(),
VideoCodecType::VP8 => "vp8".to_string(),
VideoCodecType::VP9 => "vp9".to_string(),
}
}
}; };
Json(StreamModeResponse { Json(StreamModeResponse {
success: true, success: true,
mode: mode_str.to_string(), mode: mode_str,
transition_id: state.stream_manager.current_transition_id().await,
switching: state.stream_manager.is_switching(),
message: None, message: None,
}) })
} }
@@ -1258,15 +1325,24 @@ pub async fn stream_mode_set(
// Set video codec if switching to WebRTC mode with specific codec // Set video codec if switching to WebRTC mode with specific codec
if let Some(codec) = video_codec { if let Some(codec) = video_codec {
info!("Setting WebRTC video codec to {:?}", codec); info!("Setting WebRTC video codec to {:?}", codec);
if let Err(e) = state.stream_manager.webrtc_streamer().set_video_codec(codec).await { if let Err(e) = state
.stream_manager
.webrtc_streamer()
.set_video_codec(codec)
.await
{
warn!("Failed to set video codec: {}", e); warn!("Failed to set video codec: {}", e);
} }
} }
state.stream_manager.switch_mode(new_mode.clone()).await?; let tx = state
.stream_manager
.switch_mode_transaction(new_mode.clone())
.await?;
// Return the actual codec being used // Return the requested codec identifier (for UI display). The actual active mode
let mode_str = match (&new_mode, &video_codec) { // may differ if the request was rejected due to an in-progress switch.
let requested_mode_str = match (&new_mode, &video_codec) {
(StreamMode::Mjpeg, _) => "mjpeg", (StreamMode::Mjpeg, _) => "mjpeg",
(StreamMode::WebRTC, Some(VideoCodecType::H264)) => "h264", (StreamMode::WebRTC, Some(VideoCodecType::H264)) => "h264",
(StreamMode::WebRTC, Some(VideoCodecType::H265)) => "h265", (StreamMode::WebRTC, Some(VideoCodecType::H265)) => "h265",
@@ -1275,10 +1351,39 @@ pub async fn stream_mode_set(
(StreamMode::WebRTC, None) => "webrtc", (StreamMode::WebRTC, None) => "webrtc",
}; };
let active_mode_str = match state.stream_manager.current_mode().await {
StreamMode::Mjpeg => "mjpeg".to_string(),
StreamMode::WebRTC => {
let codec = state
.stream_manager
.webrtc_streamer()
.current_video_codec()
.await;
match codec {
VideoCodecType::H264 => "h264".to_string(),
VideoCodecType::H265 => "h265".to_string(),
VideoCodecType::VP8 => "vp8".to_string(),
VideoCodecType::VP9 => "vp9".to_string(),
}
}
};
Ok(Json(StreamModeResponse { Ok(Json(StreamModeResponse {
success: true, success: tx.accepted,
mode: mode_str.to_string(), mode: if tx.accepted {
message: Some(format!("Switched to {} mode", mode_str)), requested_mode_str.to_string()
} else {
active_mode_str
},
transition_id: tx.transition_id,
switching: tx.switching,
message: Some(if tx.accepted {
format!("Switching to {} mode", requested_mode_str)
} else if tx.switching {
"Mode switch already in progress".to_string()
} else {
"No switch needed".to_string()
}),
})) }))
} }
@@ -1470,7 +1575,9 @@ pub async fn mjpeg_stream(
return axum::response::Response::builder() return axum::response::Response::builder()
.status(axum::http::StatusCode::SERVICE_UNAVAILABLE) .status(axum::http::StatusCode::SERVICE_UNAVAILABLE)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(axum::body::Body::from(r#"{"error":"MJPEG mode not active. Current mode is WebRTC."}"#)) .body(axum::body::Body::from(
r#"{"error":"MJPEG mode not active. Current mode is WebRTC."}"#,
))
.unwrap(); .unwrap();
} }
@@ -1479,7 +1586,9 @@ pub async fn mjpeg_stream(
return axum::response::Response::builder() return axum::response::Response::builder()
.status(axum::http::StatusCode::SERVICE_UNAVAILABLE) .status(axum::http::StatusCode::SERVICE_UNAVAILABLE)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(axum::body::Body::from(r#"{"error":"Video configuration is being changed. Please retry shortly."}"#)) .body(axum::body::Body::from(
r#"{"error":"Video configuration is being changed. Please retry shortly."}"#,
))
.unwrap(); .unwrap();
} }
@@ -1493,7 +1602,8 @@ pub async fn mjpeg_stream(
let handler = state.stream_manager.mjpeg_handler(); let handler = state.stream_manager.mjpeg_handler();
// Use provided client ID or generate a new one // Use provided client ID or generate a new one
let client_id = query.client_id let client_id = query
.client_id
.filter(|id| !id.is_empty() && id.len() <= 64) // Validate: non-empty, max 64 chars .filter(|id| !id.is_empty() && id.len() <= 64) // Validate: non-empty, max 64 chars
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
@@ -1538,10 +1648,8 @@ pub async fn mjpeg_stream(
} }
// Wait for new frame notification with timeout // Wait for new frame notification with timeout
let result = tokio::time::timeout( let result =
std::time::Duration::from_secs(5), tokio::time::timeout(std::time::Duration::from_secs(5), notify_rx.recv()).await;
notify_rx.recv()
).await;
match result { match result {
Ok(Ok(())) => { Ok(Ok(())) => {
@@ -1622,7 +1730,10 @@ pub async fn mjpeg_stream(
Response::builder() Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header(header::CONTENT_TYPE, "multipart/x-mixed-replace; boundary=frame") .header(
header::CONTENT_TYPE,
"multipart/x-mixed-replace; boundary=frame",
)
.header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate") .header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate")
.header(header::PRAGMA, "no-cache") .header(header::PRAGMA, "no-cache")
.header(header::EXPIRES, "0") .header(header::EXPIRES, "0")
@@ -1636,14 +1747,12 @@ pub async fn snapshot(State(state): State<Arc<AppState>>) -> impl IntoResponse {
let handler = state.stream_manager.mjpeg_handler(); let handler = state.stream_manager.mjpeg_handler();
match handler.current_frame() { match handler.current_frame() {
Some(frame) if frame.is_valid_jpeg() => { Some(frame) if frame.is_valid_jpeg() => Response::builder()
Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header(header::CONTENT_TYPE, "image/jpeg") .header(header::CONTENT_TYPE, "image/jpeg")
.header(header::CACHE_CONTROL, "no-cache") .header(header::CACHE_CONTROL, "no-cache")
.body(Body::from(frame.data_bytes())) .body(Body::from(frame.data_bytes()))
.unwrap() .unwrap(),
}
_ => Response::builder() _ => Response::builder()
.status(StatusCode::SERVICE_UNAVAILABLE) .status(StatusCode::SERVICE_UNAVAILABLE)
.body(Body::from("No frame available")) .body(Body::from("No frame available"))
@@ -1674,7 +1783,7 @@ fn create_mjpeg_part(jpeg_data: &[u8]) -> bytes::Bytes {
// WebRTC // WebRTC
// ============================================================================ // ============================================================================
use crate::webrtc::signaling::{IceCandidateRequest, OfferRequest, AnswerResponse}; use crate::webrtc::signaling::{AnswerResponse, IceCandidateRequest, OfferRequest};
/// Create WebRTC session /// Create WebRTC session
#[derive(Serialize)] #[derive(Serialize)]
@@ -1692,7 +1801,11 @@ pub async fn webrtc_create_session(
)); ));
} }
let session_id = state.stream_manager.webrtc_streamer().create_session().await?; let session_id = state
.stream_manager
.webrtc_streamer()
.create_session()
.await?;
Ok(Json(CreateSessionResponse { session_id })) Ok(Json(CreateSessionResponse { session_id }))
} }
@@ -1986,7 +2099,9 @@ pub async fn msd_image_upload(
// Use streaming upload - chunks are written directly to disk // Use streaming upload - chunks are written directly to disk
// This avoids loading the entire file into memory // This avoids loading the entire file into memory
let image = manager.create_from_multipart_field(&filename, field).await?; let image = manager
.create_from_multipart_field(&filename, field)
.await?;
return Ok(Json(image)); return Ok(Json(image));
} }
} }
@@ -2033,9 +2148,7 @@ pub async fn msd_image_download(
.as_ref() .as_ref()
.ok_or_else(|| AppError::Internal("MSD not initialized".to_string()))?; .ok_or_else(|| AppError::Internal("MSD not initialized".to_string()))?;
let progress = controller let progress = controller.download_image(req.url, req.filename).await?;
.download_image(req.url, req.filename)
.await?;
Ok(Json(progress)) Ok(Json(progress))
} }
@@ -2076,9 +2189,9 @@ pub async fn msd_connect(
match req.mode { match req.mode {
MsdMode::Image => { MsdMode::Image => {
let image_id = req let image_id = req.image_id.ok_or_else(|| {
.image_id AppError::BadRequest("image_id required for image mode".to_string())
.ok_or_else(|| AppError::BadRequest("image_id required for image mode".to_string()))?; })?;
// Get image info from ImageManager // Get image info from ImageManager
let images_path = std::path::PathBuf::from(&config.msd.images_path); let images_path = std::path::PathBuf::from(&config.msd.images_path);
@@ -2170,9 +2283,8 @@ pub async fn msd_drive_delete(State(state): State<Arc<AppState>>) -> Result<Json
// Delete the drive file // Delete the drive file
let drive_path = std::path::PathBuf::from(&config.msd.drive_path); let drive_path = std::path::PathBuf::from(&config.msd.drive_path);
if drive_path.exists() { if drive_path.exists() {
std::fs::remove_file(&drive_path).map_err(|e| { std::fs::remove_file(&drive_path)
AppError::Internal(format!("Failed to delete drive file: {}", e)) .map_err(|e| AppError::Internal(format!("Failed to delete drive file: {}", e)))?;
})?;
} }
Ok(Json(LoginResponse { Ok(Json(LoginResponse {
@@ -2227,7 +2339,9 @@ pub async fn msd_drive_upload(
// Use streaming upload - chunks are written directly to disk // Use streaming upload - chunks are written directly to disk
// This avoids loading the entire file into memory // This avoids loading the entire file into memory
drive.write_file_from_multipart_field(&file_path, field).await?; drive
.write_file_from_multipart_field(&file_path, field)
.await?;
return Ok(Json(LoginResponse { return Ok(Json(LoginResponse {
success: true, success: true,
@@ -2561,7 +2675,10 @@ pub async fn list_users(
Extension(session): Extension<Session>, Extension(session): Extension<Session>,
) -> Result<Json<Vec<UserResponse>>> { ) -> Result<Json<Vec<UserResponse>>> {
// Check if current user is admin // Check if current user is admin
let current_user = state.users.get(&session.user_id).await? let current_user = state
.users
.get(&session.user_id)
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?; .ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
if !current_user.is_admin { if !current_user.is_admin {
@@ -2588,7 +2705,10 @@ pub async fn create_user(
Json(req): Json<CreateUserRequest>, Json(req): Json<CreateUserRequest>,
) -> Result<Json<UserResponse>> { ) -> Result<Json<UserResponse>> {
// Check if current user is admin // Check if current user is admin
let current_user = state.users.get(&session.user_id).await? let current_user = state
.users
.get(&session.user_id)
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?; .ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
if !current_user.is_admin { if !current_user.is_admin {
@@ -2597,13 +2717,20 @@ pub async fn create_user(
// Validate input // Validate input
if req.username.len() < 2 { if req.username.len() < 2 {
return Err(AppError::BadRequest("Username must be at least 2 characters".to_string())); return Err(AppError::BadRequest(
"Username must be at least 2 characters".to_string(),
));
} }
if req.password.len() < 4 { if req.password.len() < 4 {
return Err(AppError::BadRequest("Password must be at least 4 characters".to_string())); return Err(AppError::BadRequest(
"Password must be at least 4 characters".to_string(),
));
} }
let user = state.users.create(&req.username, &req.password, req.is_admin).await?; let user = state
.users
.create(&req.username, &req.password, req.is_admin)
.await?;
info!("User created: {} (admin: {})", user.username, user.is_admin); info!("User created: {} (admin: {})", user.username, user.is_admin);
Ok(Json(UserResponse::from(user))) Ok(Json(UserResponse::from(user)))
} }
@@ -2623,7 +2750,10 @@ pub async fn update_user(
Json(req): Json<UpdateUserRequest>, Json(req): Json<UpdateUserRequest>,
) -> Result<Json<UserResponse>> { ) -> Result<Json<UserResponse>> {
// Check if current user is admin // Check if current user is admin
let current_user = state.users.get(&session.user_id).await? let current_user = state
.users
.get(&session.user_id)
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?; .ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
if !current_user.is_admin { if !current_user.is_admin {
@@ -2631,13 +2761,18 @@ pub async fn update_user(
} }
// Get target user // Get target user
let mut user = state.users.get(&user_id).await? let mut user = state
.users
.get(&user_id)
.await?
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?; .ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
// Update fields if provided // Update fields if provided
if let Some(username) = req.username { if let Some(username) = req.username {
if username.len() < 2 { if username.len() < 2 {
return Err(AppError::BadRequest("Username must be at least 2 characters".to_string())); return Err(AppError::BadRequest(
"Username must be at least 2 characters".to_string(),
));
} }
user.username = username; user.username = username;
} }
@@ -2647,7 +2782,9 @@ pub async fn update_user(
// Note: We need to add an update method to UserStore // Note: We need to add an update method to UserStore
// For now, return error // For now, return error
Err(AppError::Internal("User update not yet implemented".to_string())) Err(AppError::Internal(
"User update not yet implemented".to_string(),
))
} }
/// Delete user (admin only) /// Delete user (admin only)
@@ -2657,7 +2794,10 @@ pub async fn delete_user(
Path(user_id): Path<String>, Path(user_id): Path<String>,
) -> Result<Json<LoginResponse>> { ) -> Result<Json<LoginResponse>> {
// Check if current user is admin // Check if current user is admin
let current_user = state.users.get(&session.user_id).await? let current_user = state
.users
.get(&session.user_id)
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?; .ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
if !current_user.is_admin { if !current_user.is_admin {
@@ -2666,17 +2806,24 @@ pub async fn delete_user(
// Prevent deleting self // Prevent deleting self
if user_id == session.user_id { if user_id == session.user_id {
return Err(AppError::BadRequest("Cannot delete your own account".to_string())); return Err(AppError::BadRequest(
"Cannot delete your own account".to_string(),
));
} }
// Check if this is the last admin // Check if this is the last admin
let users = state.users.list().await?; let users = state.users.list().await?;
let admin_count = users.iter().filter(|u| u.is_admin).count(); let admin_count = users.iter().filter(|u| u.is_admin).count();
let target_user = state.users.get(&user_id).await? let target_user = state
.users
.get(&user_id)
.await?
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?; .ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
if target_user.is_admin && admin_count <= 1 { if target_user.is_admin && admin_count <= 1 {
return Err(AppError::BadRequest("Cannot delete the last admin user".to_string())); return Err(AppError::BadRequest(
"Cannot delete the last admin user".to_string(),
));
} }
state.users.delete(&user_id).await?; state.users.delete(&user_id).await?;
@@ -2703,30 +2850,45 @@ pub async fn change_user_password(
Json(req): Json<ChangePasswordRequest>, Json(req): Json<ChangePasswordRequest>,
) -> Result<Json<LoginResponse>> { ) -> Result<Json<LoginResponse>> {
// Check if current user is admin or changing own password // Check if current user is admin or changing own password
let current_user = state.users.get(&session.user_id).await? let current_user = state
.users
.get(&session.user_id)
.await?
.ok_or_else(|| AppError::AuthError("User not found".to_string()))?; .ok_or_else(|| AppError::AuthError("User not found".to_string()))?;
let is_self = user_id == session.user_id; let is_self = user_id == session.user_id;
let is_admin = current_user.is_admin; let is_admin = current_user.is_admin;
if !is_self && !is_admin { if !is_self && !is_admin {
return Err(AppError::Forbidden("Cannot change other user's password".to_string())); return Err(AppError::Forbidden(
"Cannot change other user's password".to_string(),
));
} }
// Validate new password // Validate new password
if req.new_password.len() < 4 { if req.new_password.len() < 4 {
return Err(AppError::BadRequest("Password must be at least 4 characters".to_string())); return Err(AppError::BadRequest(
"Password must be at least 4 characters".to_string(),
));
} }
// If changing own password, verify current password // If changing own password, verify current password
if is_self { if is_self {
let verified = state.users.verify(&current_user.username, &req.current_password).await?; let verified = state
.users
.verify(&current_user.username, &req.current_password)
.await?;
if verified.is_none() { if verified.is_none() {
return Err(AppError::AuthError("Current password is incorrect".to_string())); return Err(AppError::AuthError(
"Current password is incorrect".to_string(),
));
} }
} }
state.users.update_password(&user_id, &req.new_password).await?; state
.users
.update_password(&user_id, &req.new_password)
.await?;
info!("Password changed for user ID: {}", user_id); info!("Password changed for user ID: {}", user_id);
Ok(Json(LoginResponse { Ok(Json(LoginResponse {

View File

@@ -14,9 +14,7 @@ use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream; use tokio::net::UnixStream;
use tokio_tungstenite::tungstenite::{ use tokio_tungstenite::tungstenite::{
client::IntoClientRequest, client::IntoClientRequest, http::HeaderValue, Message as TungsteniteMessage,
http::HeaderValue,
Message as TungsteniteMessage,
}; };
use crate::error::AppError; use crate::error::AppError;
@@ -60,10 +58,9 @@ async fn handle_terminal_websocket(client_ws: WebSocket, query_string: String) {
} }
}; };
request.headers_mut().insert( request
"Sec-WebSocket-Protocol", .headers_mut()
HeaderValue::from_static("tty"), .insert("Sec-WebSocket-Protocol", HeaderValue::from_static("tty"));
);
// Create WebSocket connection to ttyd // Create WebSocket connection to ttyd
let ws_stream = match tokio_tungstenite::client_async(request, unix_stream).await { let ws_stream = match tokio_tungstenite::client_async(request, unix_stream).await {
@@ -143,7 +140,11 @@ pub async fn terminal_proxy(
// Build HTTP request to forward // Build HTTP request to forward
let method = req.method().as_str(); let method = req.method().as_str();
let query = req.uri().query().map(|q| format!("?{}", q)).unwrap_or_default(); let query = req
.uri()
.query()
.map(|q| format!("?{}", q))
.unwrap_or_default();
let uri_path = if path_str.is_empty() { let uri_path = if path_str.is_empty() {
format!("/api/terminal/{}", query) format!("/api/terminal/{}", query)
} else { } else {
@@ -203,7 +204,8 @@ pub async fn terminal_proxy(
.unwrap_or(200); .unwrap_or(200);
// Build response // Build response
let mut builder = Response::builder().status(StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK)); let mut builder =
Response::builder().status(StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK));
// Forward response headers // Forward response headers
for line in headers_part.lines().skip(1) { for line in headers_part.lines().skip(1) {

View File

@@ -1,6 +1,6 @@
mod audio_ws; mod audio_ws;
mod routes;
mod handlers; mod handlers;
mod routes;
mod static_files; mod static_files;
mod ws; mod ws;

View File

@@ -78,9 +78,15 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/config", get(handlers::config::get_all_config)) .route("/config", get(handlers::config::get_all_config))
.route("/config", post(handlers::update_config)) .route("/config", post(handlers::update_config))
.route("/config/video", get(handlers::config::get_video_config)) .route("/config/video", get(handlers::config::get_video_config))
.route("/config/video", patch(handlers::config::update_video_config)) .route(
"/config/video",
patch(handlers::config::update_video_config),
)
.route("/config/stream", get(handlers::config::get_stream_config)) .route("/config/stream", get(handlers::config::get_stream_config))
.route("/config/stream", patch(handlers::config::update_stream_config)) .route(
"/config/stream",
patch(handlers::config::update_stream_config),
)
.route("/config/hid", get(handlers::config::get_hid_config)) .route("/config/hid", get(handlers::config::get_hid_config))
.route("/config/hid", patch(handlers::config::update_hid_config)) .route("/config/hid", patch(handlers::config::update_hid_config))
.route("/config/msd", get(handlers::config::get_msd_config)) .route("/config/msd", get(handlers::config::get_msd_config))
@@ -88,14 +94,35 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/config/atx", get(handlers::config::get_atx_config)) .route("/config/atx", get(handlers::config::get_atx_config))
.route("/config/atx", patch(handlers::config::update_atx_config)) .route("/config/atx", patch(handlers::config::update_atx_config))
.route("/config/audio", get(handlers::config::get_audio_config)) .route("/config/audio", get(handlers::config::get_audio_config))
.route("/config/audio", patch(handlers::config::update_audio_config)) .route(
"/config/audio",
patch(handlers::config::update_audio_config),
)
// RustDesk configuration endpoints // RustDesk configuration endpoints
.route("/config/rustdesk", get(handlers::config::get_rustdesk_config)) .route(
.route("/config/rustdesk", patch(handlers::config::update_rustdesk_config)) "/config/rustdesk",
.route("/config/rustdesk/status", get(handlers::config::get_rustdesk_status)) get(handlers::config::get_rustdesk_config),
.route("/config/rustdesk/password", get(handlers::config::get_device_password)) )
.route("/config/rustdesk/regenerate-id", post(handlers::config::regenerate_device_id)) .route(
.route("/config/rustdesk/regenerate-password", post(handlers::config::regenerate_device_password)) "/config/rustdesk",
patch(handlers::config::update_rustdesk_config),
)
.route(
"/config/rustdesk/status",
get(handlers::config::get_rustdesk_status),
)
.route(
"/config/rustdesk/password",
get(handlers::config::get_device_password),
)
.route(
"/config/rustdesk/regenerate-id",
post(handlers::config::regenerate_device_id),
)
.route(
"/config/rustdesk/regenerate-password",
post(handlers::config::regenerate_device_password),
)
// Web server configuration // Web server configuration
.route("/config/web", get(handlers::config::get_web_config)) .route("/config/web", get(handlers::config::get_web_config))
.route("/config/web", patch(handlers::config::update_web_config)) .route("/config/web", patch(handlers::config::update_web_config))
@@ -105,7 +132,10 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/msd/status", get(handlers::msd_status)) .route("/msd/status", get(handlers::msd_status))
.route("/msd/images", get(handlers::msd_images_list)) .route("/msd/images", get(handlers::msd_images_list))
.route("/msd/images/download", post(handlers::msd_image_download)) .route("/msd/images/download", post(handlers::msd_image_download))
.route("/msd/images/download/cancel", post(handlers::msd_image_download_cancel)) .route(
"/msd/images/download/cancel",
post(handlers::msd_image_download_cancel),
)
.route("/msd/images/{id}", get(handlers::msd_image_get)) .route("/msd/images/{id}", get(handlers::msd_image_get))
.route("/msd/images/{id}", delete(handlers::msd_image_delete)) .route("/msd/images/{id}", delete(handlers::msd_image_delete))
.route("/msd/connect", post(handlers::msd_connect)) .route("/msd/connect", post(handlers::msd_connect))
@@ -115,8 +145,14 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/msd/drive", delete(handlers::msd_drive_delete)) .route("/msd/drive", delete(handlers::msd_drive_delete))
.route("/msd/drive/init", post(handlers::msd_drive_init)) .route("/msd/drive/init", post(handlers::msd_drive_init))
.route("/msd/drive/files", get(handlers::msd_drive_files)) .route("/msd/drive/files", get(handlers::msd_drive_files))
.route("/msd/drive/files/{*path}", get(handlers::msd_drive_download)) .route(
.route("/msd/drive/files/{*path}", delete(handlers::msd_drive_file_delete)) "/msd/drive/files/{*path}",
get(handlers::msd_drive_download),
)
.route(
"/msd/drive/files/{*path}",
delete(handlers::msd_drive_file_delete),
)
.route("/msd/drive/mkdir/{*path}", post(handlers::msd_drive_mkdir)) .route("/msd/drive/mkdir/{*path}", post(handlers::msd_drive_mkdir))
// ATX (Power Control) endpoints // ATX (Power Control) endpoints
.route("/atx/status", get(handlers::atx_status)) .route("/atx/status", get(handlers::atx_status))
@@ -132,13 +168,34 @@ pub fn create_router(state: Arc<AppState>) -> Router {
// Extension management endpoints // Extension management endpoints
.route("/extensions", get(handlers::extensions::list_extensions)) .route("/extensions", get(handlers::extensions::list_extensions))
.route("/extensions/{id}", get(handlers::extensions::get_extension)) .route("/extensions/{id}", get(handlers::extensions::get_extension))
.route("/extensions/{id}/start", post(handlers::extensions::start_extension)) .route(
.route("/extensions/{id}/stop", post(handlers::extensions::stop_extension)) "/extensions/{id}/start",
.route("/extensions/{id}/logs", get(handlers::extensions::get_extension_logs)) post(handlers::extensions::start_extension),
.route("/extensions/ttyd/config", patch(handlers::extensions::update_ttyd_config)) )
.route("/extensions/ttyd/status", get(handlers::extensions::get_ttyd_status)) .route(
.route("/extensions/gostc/config", patch(handlers::extensions::update_gostc_config)) "/extensions/{id}/stop",
.route("/extensions/easytier/config", patch(handlers::extensions::update_easytier_config)) post(handlers::extensions::stop_extension),
)
.route(
"/extensions/{id}/logs",
get(handlers::extensions::get_extension_logs),
)
.route(
"/extensions/ttyd/config",
patch(handlers::extensions::update_ttyd_config),
)
.route(
"/extensions/ttyd/status",
get(handlers::extensions::get_ttyd_status),
)
.route(
"/extensions/gostc/config",
patch(handlers::extensions::update_gostc_config),
)
.route(
"/extensions/easytier/config",
patch(handlers::extensions::update_easytier_config),
)
// Terminal (ttyd) reverse proxy - WebSocket and HTTP // Terminal (ttyd) reverse proxy - WebSocket and HTTP
.route("/terminal", get(handlers::terminal::terminal_index)) .route("/terminal", get(handlers::terminal::terminal_index))
.route("/terminal/", get(handlers::terminal::terminal_index)) .route("/terminal/", get(handlers::terminal::terminal_index))
@@ -148,9 +205,7 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.layer(middleware::from_fn_with_state(state.clone(), require_admin)); .layer(middleware::from_fn_with_state(state.clone(), require_admin));
// Combine protected routes (user + admin) // Combine protected routes (user + admin)
let protected_routes = Router::new() let protected_routes = Router::new().merge(user_routes).merge(admin_routes);
.merge(user_routes)
.merge(admin_routes);
// Stream endpoints (accessible with auth, but typically embedded in pages) // Stream endpoints (accessible with auth, but typically embedded in pages)
let stream_routes = Router::new() let stream_routes = Router::new()

View File

@@ -26,7 +26,8 @@ pub struct StaticAssets;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
fn get_static_base_dir() -> PathBuf { fn get_static_base_dir() -> PathBuf {
static BASE_DIR: OnceLock<PathBuf> = OnceLock::new(); static BASE_DIR: OnceLock<PathBuf> = OnceLock::new();
BASE_DIR.get_or_init(|| { BASE_DIR
.get_or_init(|| {
// Try to get executable directory // Try to get executable directory
if let Ok(exe_path) = std::env::current_exe() { if let Ok(exe_path) = std::env::current_exe() {
if let Some(exe_dir) = exe_path.parent() { if let Some(exe_dir) = exe_path.parent() {
@@ -35,7 +36,8 @@ fn get_static_base_dir() -> PathBuf {
} }
// Fallback to current directory // Fallback to current directory
PathBuf::from("web/dist") PathBuf::from("web/dist")
}).clone() })
.clone()
} }
/// Create router for static file serving /// Create router for static file serving

View File

@@ -228,7 +228,8 @@ impl H265Payloader {
let fragment_size = remaining.min(max_fragment_size); let fragment_size = remaining.min(max_fragment_size);
// Create FU packet // Create FU packet
let mut packet = BytesMut::with_capacity(H265_NAL_HEADER_SIZE + H265_FU_HEADER_SIZE + fragment_size); let mut packet =
BytesMut::with_capacity(H265_NAL_HEADER_SIZE + H265_FU_HEADER_SIZE + fragment_size);
// NAL header for FU (2 bytes) // NAL header for FU (2 bytes)
// Preserve F bit (bit 7) and LayerID MSB (bit 0) from original, set Type to 49 // Preserve F bit (bit 7) and LayerID MSB (bit 0) from original, set Type to 49

View File

@@ -42,5 +42,7 @@ pub use rtp::{H264VideoTrack, H264VideoTrackConfig, OpusAudioTrack};
pub use session::WebRtcSessionManager; pub use session::WebRtcSessionManager;
pub use signaling::{ConnectionState, IceCandidate, SdpAnswer, SdpOffer, SignalingMessage}; pub use signaling::{ConnectionState, IceCandidate, SdpAnswer, SdpOffer, SignalingMessage};
pub use universal_session::{UniversalSession, UniversalSessionConfig, UniversalSessionInfo}; pub use universal_session::{UniversalSession, UniversalSessionConfig, UniversalSessionInfo};
pub use video_track::{UniversalVideoTrack, UniversalVideoTrackConfig, VideoCodec, VideoTrackStats}; pub use video_track::{
UniversalVideoTrack, UniversalVideoTrackConfig, VideoCodec, VideoTrackStats,
};
pub use webrtc_streamer::{SessionInfo, WebRtcStreamer, WebRtcStreamerConfig, WebRtcStreamerStats}; pub use webrtc_streamer::{SessionInfo, WebRtcStreamer, WebRtcStreamerConfig, WebRtcStreamerStats};

View File

@@ -92,10 +92,9 @@ impl PeerConnection {
}; };
// Create peer connection // Create peer connection
let pc = api let pc = api.new_peer_connection(rtc_config).await.map_err(|e| {
.new_peer_connection(rtc_config) AppError::VideoError(format!("Failed to create peer connection: {}", e))
.await })?;
.map_err(|e| AppError::VideoError(format!("Failed to create peer connection: {}", e)))?;
let pc = Arc::new(pc); let pc = Arc::new(pc);
@@ -125,7 +124,8 @@ impl PeerConnection {
let session_id = self.session_id.clone(); let session_id = self.session_id.clone();
// Connection state change handler // Connection state change handler
self.pc.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { self.pc
.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| {
let state = state.clone(); let state = state.clone();
let session_id = session_id.clone(); let session_id = session_id.clone();
@@ -147,14 +147,13 @@ impl PeerConnection {
// ICE candidate handler // ICE candidate handler
let ice_candidates = self.ice_candidates.clone(); let ice_candidates = self.ice_candidates.clone();
self.pc.on_ice_candidate(Box::new(move |candidate: Option<RTCIceCandidate>| { self.pc
.on_ice_candidate(Box::new(move |candidate: Option<RTCIceCandidate>| {
let ice_candidates = ice_candidates.clone(); let ice_candidates = ice_candidates.clone();
Box::pin(async move { Box::pin(async move {
if let Some(c) = candidate { if let Some(c) = candidate {
let candidate_str = c.to_json() let candidate_str = c.to_json().map(|j| j.candidate).unwrap_or_default();
.map(|j| j.candidate)
.unwrap_or_default();
debug!("ICE candidate: {}", candidate_str); debug!("ICE candidate: {}", candidate_str);
@@ -171,7 +170,8 @@ impl PeerConnection {
// Data channel handler - note: HID processing is done when hid_controller is set // Data channel handler - note: HID processing is done when hid_controller is set
let data_channel = self.data_channel.clone(); let data_channel = self.data_channel.clone();
self.pc.on_data_channel(Box::new(move |dc: Arc<RTCDataChannel>| { self.pc
.on_data_channel(Box::new(move |dc: Arc<RTCDataChannel>| {
let data_channel = data_channel.clone(); let data_channel = data_channel.clone();
Box::pin(async move { Box::pin(async move {
@@ -206,7 +206,11 @@ impl PeerConnection {
let is_hid_channel = label == "hid" || label == "hid-unreliable"; let is_hid_channel = label == "hid" || label == "hid-unreliable";
if is_hid_channel { if is_hid_channel {
info!("HID DataChannel opened: {} (unreliable: {})", label, label == "hid-unreliable"); info!(
"HID DataChannel opened: {} (unreliable: {})",
label,
label == "hid-unreliable"
);
// Store the reliable data channel for sending responses // Store the reliable data channel for sending responses
if label == "hid" { if label == "hid" {
@@ -291,10 +295,9 @@ impl PeerConnection {
let sdp = RTCSessionDescription::offer(offer.sdp) let sdp = RTCSessionDescription::offer(offer.sdp)
.map_err(|e| AppError::VideoError(format!("Invalid SDP offer: {}", e)))?; .map_err(|e| AppError::VideoError(format!("Invalid SDP offer: {}", e)))?;
self.pc self.pc.set_remote_description(sdp).await.map_err(|e| {
.set_remote_description(sdp) AppError::VideoError(format!("Failed to set remote description: {}", e))
.await })?;
.map_err(|e| AppError::VideoError(format!("Failed to set remote description: {}", e)))?;
// Create answer // Create answer
let answer = self let answer = self
@@ -373,7 +376,11 @@ impl PeerConnection {
// Reset HID state to release any held keys/buttons // Reset HID state to release any held keys/buttons
if let Some(ref hid) = self.hid_controller { if let Some(ref hid) = self.hid_controller {
if let Err(e) = hid.reset().await { if let Err(e) = hid.reset().await {
tracing::warn!("Failed to reset HID on peer {} close: {}", self.session_id, e); tracing::warn!(
"Failed to reset HID on peer {} close: {}",
self.session_id,
e
);
} else { } else {
tracing::debug!("HID reset on peer {} close", self.session_id); tracing::debug!("HID reset on peer {} close", self.session_id);
} }

View File

@@ -21,9 +21,9 @@ use tokio::sync::Mutex;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use webrtc::media::io::h264_reader::H264Reader; use webrtc::media::io::h264_reader::H264Reader;
use webrtc::media::Sample; use webrtc::media::Sample;
use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability;
use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample; use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample;
use webrtc::track::track_local::TrackLocal; use webrtc::track::track_local::TrackLocal;
use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability;
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
use crate::video::format::Resolution; use crate::video::format::Resolution;
@@ -168,7 +168,12 @@ impl H264VideoTrack {
/// * `data` - H264 Annex B encoded frame data /// * `data` - H264 Annex B encoded frame data
/// * `duration` - Frame duration (typically 1/fps seconds) /// * `duration` - Frame duration (typically 1/fps seconds)
/// * `is_keyframe` - Whether this is a keyframe (IDR frame) /// * `is_keyframe` - Whether this is a keyframe (IDR frame)
pub async fn write_frame(&self, data: &[u8], _duration: Duration, is_keyframe: bool) -> Result<()> { pub async fn write_frame(
&self,
data: &[u8],
_duration: Duration,
is_keyframe: bool,
) -> Result<()> {
if data.is_empty() { if data.is_empty() {
return Ok(()); return Ok(());
} }
@@ -324,9 +329,9 @@ impl H264VideoTrack {
let mut payloader = self.payloader.lock().await; let mut payloader = self.payloader.lock().await;
let bytes = Bytes::copy_from_slice(data); let bytes = Bytes::copy_from_slice(data);
payloader.payload(mtu, &bytes).map_err(|e| { payloader
AppError::VideoError(format!("H264 packetization failed: {}", e)) .payload(mtu, &bytes)
}) .map_err(|e| AppError::VideoError(format!("H264 packetization failed: {}", e)))
} }
/// Get configuration /// Get configuration
@@ -423,7 +428,10 @@ impl OpusAudioTrack {
let mut stats = self.stats.lock().await; let mut stats = self.stats.lock().await;
stats.errors += 1; stats.errors += 1;
error!("Failed to write Opus sample: {}", e); error!("Failed to write Opus sample: {}", e);
Err(AppError::WebRtcError(format!("Failed to write audio sample: {}", e))) Err(AppError::WebRtcError(format!(
"Failed to write audio sample: {}",
e
)))
} }
} }
} }

View File

@@ -4,9 +4,9 @@ use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use tokio::sync::{broadcast, watch, Mutex}; use tokio::sync::{broadcast, watch, Mutex};
use tracing::{debug, error, info}; use tracing::{debug, error, info};
use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability;
use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP;
use webrtc::track::track_local::TrackLocalWriter; use webrtc::track::track_local::TrackLocalWriter;
use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability;
use crate::video::frame::VideoFrame; use crate::video::frame::VideoFrame;
@@ -56,7 +56,9 @@ impl VideoCodecType {
pub fn sdp_fmtp(&self) -> &'static str { pub fn sdp_fmtp(&self) -> &'static str {
match self { match self {
VideoCodecType::H264 => "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", VideoCodecType::H264 => {
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f"
}
VideoCodecType::VP8 => "", VideoCodecType::VP8 => "",
VideoCodecType::VP9 => "profile-id=0", VideoCodecType::VP9 => "profile-id=0",
} }
@@ -156,10 +158,7 @@ impl VideoTrack {
} }
/// Start sending frames from a broadcast receiver /// Start sending frames from a broadcast receiver
pub async fn start_sending( pub async fn start_sending(&self, mut frame_rx: broadcast::Receiver<VideoFrame>) {
&self,
mut frame_rx: broadcast::Receiver<VideoFrame>,
) {
let _ = self.running.send(true); let _ = self.running.send(true);
let track = self.track.clone(); let track = self.track.clone();
let stats = self.stats.clone(); let stats = self.stats.clone();

View File

@@ -18,7 +18,9 @@ use webrtc::peer_connection::configuration::RTCConfiguration;
use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState;
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
use webrtc::peer_connection::RTCPeerConnection; use webrtc::peer_connection::RTCPeerConnection;
use webrtc::rtp_transceiver::rtp_codec::{RTCRtpCodecCapability, RTCRtpCodecParameters, RTPCodecType}; use webrtc::rtp_transceiver::rtp_codec::{
RTCRtpCodecCapability, RTCRtpCodecParameters, RTPCodecType,
};
use webrtc::rtp_transceiver::RTCPFeedback; use webrtc::rtp_transceiver::RTCPFeedback;
use super::config::WebRtcConfig; use super::config::WebRtcConfig;
@@ -192,7 +194,8 @@ impl UniversalSession {
clock_rate: 90000, clock_rate: 90000,
channels: 0, channels: 0,
// Match browser's fmtp format for profile-id=1 // Match browser's fmtp format for profile-id=1
sdp_fmtp_line: "level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST".to_owned(), sdp_fmtp_line: "level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST"
.to_owned(),
rtcp_feedback: video_rtcp_feedback.clone(), rtcp_feedback: video_rtcp_feedback.clone(),
}, },
payload_type: 49, // Use same payload type as browser payload_type: 49, // Use same payload type as browser
@@ -200,7 +203,9 @@ impl UniversalSession {
}, },
RTPCodecType::Video, RTPCodecType::Video,
) )
.map_err(|e| AppError::VideoError(format!("Failed to register H.265 codec: {}", e)))?; .map_err(|e| {
AppError::VideoError(format!("Failed to register H.265 codec: {}", e))
})?;
// Also register profile-id=2 (Main 10) variant // Also register profile-id=2 (Main 10) variant
media_engine media_engine
@@ -210,7 +215,8 @@ impl UniversalSession {
mime_type: MIME_TYPE_H265.to_owned(), mime_type: MIME_TYPE_H265.to_owned(),
clock_rate: 90000, clock_rate: 90000,
channels: 0, channels: 0,
sdp_fmtp_line: "level-id=180;profile-id=2;tier-flag=0;tx-mode=SRST".to_owned(), sdp_fmtp_line: "level-id=180;profile-id=2;tier-flag=0;tx-mode=SRST"
.to_owned(),
rtcp_feedback: video_rtcp_feedback, rtcp_feedback: video_rtcp_feedback,
}, },
payload_type: 51, payload_type: 51,
@@ -218,7 +224,12 @@ impl UniversalSession {
}, },
RTPCodecType::Video, RTPCodecType::Video,
) )
.map_err(|e| AppError::VideoError(format!("Failed to register H.265 codec (profile 2): {}", e)))?; .map_err(|e| {
AppError::VideoError(format!(
"Failed to register H.265 codec (profile 2): {}",
e
))
})?;
info!("Registered H.265/HEVC codec for session {}", session_id); info!("Registered H.265/HEVC codec for session {}", session_id);
} }
@@ -269,10 +280,9 @@ impl UniversalSession {
..Default::default() ..Default::default()
}; };
let pc = api let pc = api.new_peer_connection(rtc_config).await.map_err(|e| {
.new_peer_connection(rtc_config) AppError::VideoError(format!("Failed to create peer connection: {}", e))
.await })?;
.map_err(|e| AppError::VideoError(format!("Failed to create peer connection: {}", e)))?;
let pc = Arc::new(pc); let pc = Arc::new(pc);
@@ -291,7 +301,10 @@ impl UniversalSession {
pc.add_track(audio.as_track_local()) pc.add_track(audio.as_track_local())
.await .await
.map_err(|e| AppError::AudioError(format!("Failed to add audio track: {}", e)))?; .map_err(|e| AppError::AudioError(format!("Failed to add audio track: {}", e)))?;
info!("Opus audio track added to peer connection (session {})", session_id); info!(
"Opus audio track added to peer connection (session {})",
session_id
);
} }
// Create state channel // Create state channel
@@ -479,11 +492,13 @@ impl UniversalSession {
&self, &self,
mut frame_rx: broadcast::Receiver<EncodedVideoFrame>, mut frame_rx: broadcast::Receiver<EncodedVideoFrame>,
on_connected: F, on_connected: F,
) ) where
where
F: FnOnce() + Send + 'static, F: FnOnce() + Send + 'static,
{ {
info!("Starting {} session {} with shared encoder", self.codec, self.session_id); info!(
"Starting {} session {} with shared encoder",
self.codec, self.session_id
);
let video_track = self.video_track.clone(); let video_track = self.video_track.clone();
let mut state_rx = self.state_rx.clone(); let mut state_rx = self.state_rx.clone();
@@ -492,7 +507,10 @@ impl UniversalSession {
let expected_codec = self.codec; let expected_codec = self.codec;
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
info!("Video receiver waiting for connection for session {}", session_id); info!(
"Video receiver waiting for connection for session {}",
session_id
);
// Wait for Connected state before sending frames // Wait for Connected state before sending frames
loop { loop {
@@ -500,7 +518,10 @@ impl UniversalSession {
if current_state == ConnectionState::Connected { if current_state == ConnectionState::Connected {
break; break;
} }
if matches!(current_state, ConnectionState::Closed | ConnectionState::Failed) { if matches!(
current_state,
ConnectionState::Closed | ConnectionState::Failed
) {
info!("Session {} closed before connecting", session_id); info!("Session {} closed before connecting", session_id);
return; return;
} }
@@ -509,7 +530,10 @@ impl UniversalSession {
} }
} }
info!("Video receiver started for session {} (ICE connected)", session_id); info!(
"Video receiver started for session {} (ICE connected)",
session_id
);
// Request keyframe now that connection is established // Request keyframe now that connection is established
on_connected(); on_connected();
@@ -592,7 +616,10 @@ impl UniversalSession {
} }
} }
info!("Video receiver stopped for session {} (sent {} frames)", session_id, frames_sent); info!(
"Video receiver stopped for session {} (sent {} frames)",
session_id, frames_sent
);
}); });
*self.video_receiver_handle.lock().await = Some(handle); *self.video_receiver_handle.lock().await = Some(handle);
@@ -620,7 +647,10 @@ impl UniversalSession {
if current_state == ConnectionState::Connected { if current_state == ConnectionState::Connected {
break; break;
} }
if matches!(current_state, ConnectionState::Closed | ConnectionState::Failed) { if matches!(
current_state,
ConnectionState::Closed | ConnectionState::Failed
) {
info!("Session {} closed before audio could start", session_id); info!("Session {} closed before audio could start", session_id);
return; return;
} }
@@ -629,7 +659,10 @@ impl UniversalSession {
} }
} }
info!("Audio receiver started for session {} (ICE connected)", session_id); info!(
"Audio receiver started for session {} (ICE connected)",
session_id
);
let mut packets_sent: u64 = 0; let mut packets_sent: u64 = 0;
@@ -673,7 +706,10 @@ impl UniversalSession {
} }
} }
info!("Audio receiver stopped for session {} (sent {} packets)", session_id, packets_sent); info!(
"Audio receiver stopped for session {} (sent {} packets)",
session_id, packets_sent
);
}); });
*self.audio_receiver_handle.lock().await = Some(handle); *self.audio_receiver_handle.lock().await = Some(handle);
@@ -697,8 +733,7 @@ impl UniversalSession {
|| offer.sdp.to_lowercase().contains("hevc"); || offer.sdp.to_lowercase().contains("hevc");
info!( info!(
"[SDP] Session {} offer contains H.265: {}", "[SDP] Session {} offer contains H.265: {}",
self.session_id, self.session_id, has_h265
has_h265
); );
if !has_h265 { if !has_h265 {
warn!("[SDP] Browser offer does not include H.265 codec! Session may fail."); warn!("[SDP] Browser offer does not include H.265 codec! Session may fail.");
@@ -708,10 +743,9 @@ impl UniversalSession {
let sdp = RTCSessionDescription::offer(offer.sdp) let sdp = RTCSessionDescription::offer(offer.sdp)
.map_err(|e| AppError::VideoError(format!("Invalid SDP offer: {}", e)))?; .map_err(|e| AppError::VideoError(format!("Invalid SDP offer: {}", e)))?;
self.pc self.pc.set_remote_description(sdp).await.map_err(|e| {
.set_remote_description(sdp) AppError::VideoError(format!("Failed to set remote description: {}", e))
.await })?;
.map_err(|e| AppError::VideoError(format!("Failed to set remote description: {}", e)))?;
let answer = self let answer = self
.pc .pc
@@ -725,8 +759,7 @@ impl UniversalSession {
|| answer.sdp.to_lowercase().contains("hevc"); || answer.sdp.to_lowercase().contains("hevc");
info!( info!(
"[SDP] Session {} answer contains H.265: {}", "[SDP] Session {} answer contains H.265: {}",
self.session_id, self.session_id, has_h265
has_h265
); );
if !has_h265 { if !has_h265 {
warn!("[SDP] Answer does not include H.265! Codec negotiation may have failed."); warn!("[SDP] Answer does not include H.265! Codec negotiation may have failed.");
@@ -821,9 +854,21 @@ mod tests {
#[test] #[test]
fn test_encoder_type_to_video_codec() { fn test_encoder_type_to_video_codec() {
assert_eq!(encoder_type_to_video_codec(VideoEncoderType::H264), VideoCodec::H264); assert_eq!(
assert_eq!(encoder_type_to_video_codec(VideoEncoderType::H265), VideoCodec::H265); encoder_type_to_video_codec(VideoEncoderType::H264),
assert_eq!(encoder_type_to_video_codec(VideoEncoderType::VP8), VideoCodec::VP8); VideoCodec::H264
assert_eq!(encoder_type_to_video_codec(VideoEncoderType::VP9), VideoCodec::VP9); );
assert_eq!(
encoder_type_to_video_codec(VideoEncoderType::H265),
VideoCodec::H265
);
assert_eq!(
encoder_type_to_video_codec(VideoEncoderType::VP8),
VideoCodec::VP8
);
assert_eq!(
encoder_type_to_video_codec(VideoEncoderType::VP9),
VideoCodec::VP9
);
} }
} }

View File

@@ -41,12 +41,14 @@ use crate::audio::shared_pipeline::{SharedAudioPipeline, SharedAudioPipelineConf
use crate::audio::{AudioController, OpusFrame}; use crate::audio::{AudioController, OpusFrame};
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
use crate::hid::HidController; use crate::hid::HidController;
use crate::video::encoder::registry::VideoEncoderType;
use crate::video::encoder::registry::EncoderBackend; use crate::video::encoder::registry::EncoderBackend;
use crate::video::encoder::registry::VideoEncoderType;
use crate::video::encoder::VideoCodecType; use crate::video::encoder::VideoCodecType;
use crate::video::format::{PixelFormat, Resolution}; use crate::video::format::{PixelFormat, Resolution};
use crate::video::frame::VideoFrame; use crate::video::frame::VideoFrame;
use crate::video::shared_video_pipeline::{SharedVideoPipeline, SharedVideoPipelineConfig, SharedVideoPipelineStats}; use crate::video::shared_video_pipeline::{
SharedVideoPipeline, SharedVideoPipelineConfig, SharedVideoPipelineStats,
};
use super::config::{TurnServer, WebRtcConfig}; use super::config::{TurnServer, WebRtcConfig};
use super::signaling::{ConnectionState, IceCandidate, SdpAnswer, SdpOffer}; use super::signaling::{ConnectionState, IceCandidate, SdpAnswer, SdpOffer};
@@ -489,7 +491,9 @@ impl WebRtcStreamer {
} }
} }
} else { } else {
info!("No video pipeline exists yet, frame source will be used when pipeline is created"); info!(
"No video pipeline exists yet, frame source will be used when pipeline is created"
);
} }
} }
@@ -517,24 +521,21 @@ impl WebRtcStreamer {
/// Only restarts the encoding pipeline if configuration actually changed. /// Only restarts the encoding pipeline if configuration actually changed.
/// This allows multiple consumers (WebRTC, RustDesk) to share the same pipeline /// This allows multiple consumers (WebRTC, RustDesk) to share the same pipeline
/// without interrupting each other when they call this method with the same config. /// without interrupting each other when they call this method with the same config.
pub async fn update_video_config( pub async fn update_video_config(&self, resolution: Resolution, format: PixelFormat, fps: u32) {
&self,
resolution: Resolution,
format: PixelFormat,
fps: u32,
) {
// Check if configuration actually changed // Check if configuration actually changed
let config = self.config.read().await; let config = self.config.read().await;
let config_changed = config.resolution != resolution let config_changed =
|| config.input_format != format config.resolution != resolution || config.input_format != format || config.fps != fps;
|| config.fps != fps;
drop(config); drop(config);
if !config_changed { if !config_changed {
// Configuration unchanged, no need to restart pipeline // Configuration unchanged, no need to restart pipeline
trace!( trace!(
"Video config unchanged: {}x{} {:?} @ {} fps", "Video config unchanged: {}x{} {:?} @ {} fps",
resolution.width, resolution.height, format, fps resolution.width,
resolution.height,
format,
fps
); );
return; return;
} }
@@ -554,7 +555,10 @@ impl WebRtcStreamer {
// Close all existing sessions - they need to reconnect // Close all existing sessions - they need to reconnect
let session_count = self.close_all_sessions().await; let session_count = self.close_all_sessions().await;
if session_count > 0 { if session_count > 0 {
info!("Closed {} existing sessions due to config change", session_count); info!(
"Closed {} existing sessions due to config change",
session_count
);
} }
// Update config (preserve user-configured bitrate) // Update config (preserve user-configured bitrate)
@@ -581,17 +585,17 @@ impl WebRtcStreamer {
// Close all existing sessions - they need to reconnect with new encoder // Close all existing sessions - they need to reconnect with new encoder
let session_count = self.close_all_sessions().await; let session_count = self.close_all_sessions().await;
if session_count > 0 { if session_count > 0 {
info!("Closed {} existing sessions due to encoder backend change", session_count); info!(
"Closed {} existing sessions due to encoder backend change",
session_count
);
} }
// Update config // Update config
let mut config = self.config.write().await; let mut config = self.config.write().await;
config.encoder_backend = encoder_backend; config.encoder_backend = encoder_backend;
info!( info!("WebRTC encoder backend updated: {:?}", encoder_backend);
"WebRTC encoder backend updated: {:?}",
encoder_backend
);
} }
/// Check if current encoder configuration uses hardware encoding /// Check if current encoder configuration uses hardware encoding
@@ -694,7 +698,11 @@ impl WebRtcStreamer {
let codec = *self.video_codec.read().await; let codec = *self.video_codec.read().await;
// Ensure video pipeline is running // Ensure video pipeline is running
let frame_tx = self.video_frame_tx.read().await.clone() let frame_tx = self
.video_frame_tx
.read()
.await
.clone()
.ok_or_else(|| AppError::VideoError("No video frame source".to_string()))?; .ok_or_else(|| AppError::VideoError("No video frame source".to_string()))?;
let pipeline = self.ensure_video_pipeline(frame_tx).await?; let pipeline = self.ensure_video_pipeline(frame_tx).await?;
@@ -729,15 +737,20 @@ impl WebRtcStreamer {
// Request keyframe after ICE connection is established (via callback) // Request keyframe after ICE connection is established (via callback)
let pipeline_for_callback = pipeline.clone(); let pipeline_for_callback = pipeline.clone();
let session_id_for_callback = session_id.clone(); let session_id_for_callback = session_id.clone();
session.start_from_video_pipeline(pipeline.subscribe(), move || { session
.start_from_video_pipeline(pipeline.subscribe(), move || {
// Spawn async task to request keyframe // Spawn async task to request keyframe
let pipeline = pipeline_for_callback; let pipeline = pipeline_for_callback;
let sid = session_id_for_callback; let sid = session_id_for_callback;
tokio::spawn(async move { tokio::spawn(async move {
info!("Requesting keyframe for session {} after ICE connected", sid); info!(
"Requesting keyframe for session {} after ICE connected",
sid
);
pipeline.request_keyframe().await; pipeline.request_keyframe().await;
}); });
}).await; })
.await;
// Start audio if enabled // Start audio if enabled
if session_config.audio_enabled { if session_config.audio_enabled {
@@ -863,7 +876,9 @@ impl WebRtcStreamer {
.filter(|(_, s)| { .filter(|(_, s)| {
matches!( matches!(
s.state(), s.state(),
ConnectionState::Closed | ConnectionState::Failed | ConnectionState::Disconnected ConnectionState::Closed
| ConnectionState::Failed
| ConnectionState::Disconnected
) )
}) })
.map(|(id, _)| id.clone()) .map(|(id, _)| id.clone())
@@ -967,10 +982,7 @@ impl WebRtcStreamer {
}; };
if pipeline_running { if pipeline_running {
info!( info!("Restarting video pipeline to apply new bitrate: {}", preset);
"Restarting video pipeline to apply new bitrate: {}",
preset
);
// Save video_frame_tx BEFORE stopping pipeline (monitor task will clear it) // Save video_frame_tx BEFORE stopping pipeline (monitor task will clear it)
let saved_frame_tx = self.video_frame_tx.read().await.clone(); let saved_frame_tx = self.video_frame_tx.read().await.clone();
@@ -1005,13 +1017,18 @@ impl WebRtcStreamer {
info!("Reconnecting session {} to new pipeline", session_id); info!("Reconnecting session {} to new pipeline", session_id);
let pipeline_for_callback = pipeline.clone(); let pipeline_for_callback = pipeline.clone();
let sid = session_id.clone(); let sid = session_id.clone();
session.start_from_video_pipeline(pipeline.subscribe(), move || { session
.start_from_video_pipeline(pipeline.subscribe(), move || {
let pipeline = pipeline_for_callback; let pipeline = pipeline_for_callback;
tokio::spawn(async move { tokio::spawn(async move {
info!("Requesting keyframe for session {} after reconnect", sid); info!(
"Requesting keyframe for session {} after reconnect",
sid
);
pipeline.request_keyframe().await; pipeline.request_keyframe().await;
}); });
}).await; })
.await;
} }
} }

Some files were not shown because too many files have changed in this diff Show More