mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 11:42:02 +08:00
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:
@@ -99,7 +99,10 @@ impl MsdController {
|
||||
initialized: true,
|
||||
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
|
||||
/// * `cdrom` - Mount as CD-ROM (read-only, removable)
|
||||
/// * `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
|
||||
let _op_guard = self.operation_lock.write().await;
|
||||
|
||||
@@ -154,7 +162,9 @@ impl MsdController {
|
||||
|
||||
if !state.available {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -167,7 +177,9 @@ impl MsdController {
|
||||
// Verify image exists
|
||||
if !image.path.exists() {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -182,12 +194,16 @@ impl MsdController {
|
||||
if let Some(ref msd) = *self.msd_function.read().await {
|
||||
if let Err(e) = msd.configure_lun_async(&gadget_path, 0, &config).await {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -236,7 +252,9 @@ impl MsdController {
|
||||
|
||||
if !state.available {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -248,10 +266,11 @@ impl MsdController {
|
||||
|
||||
// Check drive exists
|
||||
if !self.drive_path.exists() {
|
||||
let err = AppError::Internal(
|
||||
"Virtual drive not initialized. Call init first.".to_string(),
|
||||
);
|
||||
self.monitor.report_error("Virtual drive not initialized", "drive_not_found").await;
|
||||
let err =
|
||||
AppError::Internal("Virtual drive not initialized. Call init first.".to_string());
|
||||
self.monitor
|
||||
.report_error("Virtual drive not initialized", "drive_not_found")
|
||||
.await;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
@@ -262,12 +281,16 @@ impl MsdController {
|
||||
if let Some(ref msd) = *self.msd_function.read().await {
|
||||
if let Err(e) = msd.configure_lun_async(&gadget_path, 0, &config).await {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -381,12 +404,9 @@ impl MsdController {
|
||||
}
|
||||
|
||||
// Extract filename for initial response
|
||||
let display_filename = filename.clone().unwrap_or_else(|| {
|
||||
url.rsplit('/')
|
||||
.next()
|
||||
.unwrap_or("download")
|
||||
.to_string()
|
||||
});
|
||||
let display_filename = filename
|
||||
.clone()
|
||||
.unwrap_or_else(|| url.rsplit('/').next().unwrap_or("download").to_string());
|
||||
|
||||
// Create initial progress
|
||||
let initial_progress = DownloadProgress {
|
||||
|
||||
@@ -42,9 +42,8 @@ impl ImageManager {
|
||||
|
||||
/// Ensure images directory exists
|
||||
pub fn ensure_dir(&self) -> Result<()> {
|
||||
fs::create_dir_all(&self.images_path).map_err(|e| {
|
||||
AppError::Internal(format!("Failed to create images directory: {}", e))
|
||||
})?;
|
||||
fs::create_dir_all(&self.images_path)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to create images directory: {}", e)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -54,9 +53,9 @@ impl ImageManager {
|
||||
|
||||
let mut images = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(&self.images_path).map_err(|e| {
|
||||
AppError::Internal(format!("Failed to read images directory: {}", e))
|
||||
})? {
|
||||
for entry in fs::read_dir(&self.images_path)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to read images directory: {}", e)))?
|
||||
{
|
||||
let entry = entry.map_err(|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| {
|
||||
AppError::Internal(format!("Failed to create image file: {}", e))
|
||||
})?;
|
||||
let mut file = File::create(&path)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to create image file: {}", e)))?;
|
||||
|
||||
file.write_all(data).map_err(|e| {
|
||||
// Try to clean up on error
|
||||
@@ -193,9 +191,8 @@ impl ImageManager {
|
||||
}
|
||||
|
||||
// Create file and copy data
|
||||
let mut file = File::create(&path).map_err(|e| {
|
||||
AppError::Internal(format!("Failed to create image file: {}", e))
|
||||
})?;
|
||||
let mut file = File::create(&path)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to create image file: {}", e)))?;
|
||||
|
||||
let bytes_written = io::copy(reader, &mut file).map_err(|e| {
|
||||
let _ = fs::remove_file(&path);
|
||||
@@ -244,9 +241,11 @@ impl ImageManager {
|
||||
let mut bytes_written: u64 = 0;
|
||||
|
||||
// Stream chunks directly to disk
|
||||
while let Some(chunk) = field.chunk().await.map_err(|e| {
|
||||
AppError::Internal(format!("Failed to read upload chunk: {}", e))
|
||||
})? {
|
||||
while let Some(chunk) = field
|
||||
.chunk()
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(format!("Failed to read upload chunk: {}", e)))?
|
||||
{
|
||||
// Check size limit
|
||||
bytes_written += chunk.len() as u64;
|
||||
if bytes_written > MAX_IMAGE_SIZE {
|
||||
@@ -260,15 +259,15 @@ impl ImageManager {
|
||||
}
|
||||
|
||||
// Write chunk to file
|
||||
file.write_all(&chunk).await.map_err(|e| {
|
||||
AppError::Internal(format!("Failed to write chunk: {}", e))
|
||||
})?;
|
||||
file.write_all(&chunk)
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(format!("Failed to write chunk: {}", e)))?;
|
||||
}
|
||||
|
||||
// Flush and close file
|
||||
file.flush().await.map_err(|e| {
|
||||
AppError::Internal(format!("Failed to flush file: {}", e))
|
||||
})?;
|
||||
file.flush()
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(format!("Failed to flush file: {}", e)))?;
|
||||
drop(file);
|
||||
|
||||
// Move temp file to final location
|
||||
@@ -279,7 +278,10 @@ impl ImageManager {
|
||||
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)
|
||||
}
|
||||
@@ -288,9 +290,8 @@ impl ImageManager {
|
||||
pub fn delete(&self, id: &str) -> Result<()> {
|
||||
let image = self.get(id)?;
|
||||
|
||||
fs::remove_file(&image.path).map_err(|e| {
|
||||
AppError::Internal(format!("Failed to delete image: {}", e))
|
||||
})?;
|
||||
fs::remove_file(&image.path)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to delete image: {}", e)))?;
|
||||
|
||||
info!("Deleted image: {}", image.name);
|
||||
Ok(())
|
||||
@@ -304,9 +305,8 @@ impl ImageManager {
|
||||
return Err(AppError::NotFound(format!("Image not found: {}", name)));
|
||||
}
|
||||
|
||||
fs::remove_file(&path).map_err(|e| {
|
||||
AppError::Internal(format!("Failed to delete image: {}", e))
|
||||
})?;
|
||||
fs::remove_file(&path)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to delete image: {}", e)))?;
|
||||
|
||||
info!("Deleted image: {}", name);
|
||||
Ok(())
|
||||
@@ -414,7 +414,9 @@ impl ImageManager {
|
||||
};
|
||||
|
||||
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
|
||||
@@ -468,16 +470,14 @@ impl ImageManager {
|
||||
progress_callback(0, content_length);
|
||||
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let chunk = chunk_result
|
||||
.map_err(|e| AppError::Internal(format!("Download error: {}", e)))?;
|
||||
let chunk =
|
||||
chunk_result.map_err(|e| AppError::Internal(format!("Download error: {}", e)))?;
|
||||
|
||||
file.write_all(&chunk)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Cleanup on error
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
AppError::Internal(format!("Failed to write data: {}", e))
|
||||
})?;
|
||||
file.write_all(&chunk).await.map_err(|e| {
|
||||
// Cleanup on error
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
AppError::Internal(format!("Failed to write data: {}", e))
|
||||
})?;
|
||||
|
||||
downloaded += chunk.len() as u64;
|
||||
|
||||
|
||||
@@ -15,19 +15,19 @@
|
||||
//! ```
|
||||
|
||||
pub mod controller;
|
||||
pub mod ventoy_drive;
|
||||
pub mod image;
|
||||
pub mod monitor;
|
||||
pub mod types;
|
||||
pub mod ventoy_drive;
|
||||
|
||||
pub use controller::MsdController;
|
||||
pub use ventoy_drive::VentoyDrive;
|
||||
pub use image::ImageManager;
|
||||
pub use monitor::{MsdHealthMonitor, MsdHealthStatus, MsdMonitorConfig};
|
||||
pub use types::{
|
||||
DownloadProgress, DownloadStatus, DriveFile, DriveInfo, DriveInitRequest, ImageDownloadRequest,
|
||||
ImageInfo, MsdConnectRequest, MsdMode, MsdState,
|
||||
};
|
||||
pub use ventoy_drive::VentoyDrive;
|
||||
|
||||
// Re-export from otg module for backward compatibility
|
||||
pub use crate::otg::{MsdFunction, MsdLunConfig};
|
||||
|
||||
@@ -120,7 +120,10 @@ impl MsdHealthMonitor {
|
||||
// Log with throttling (always log if error type changed)
|
||||
let throttle_key = format!("msd_{}", error_code);
|
||||
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
|
||||
|
||||
@@ -71,13 +71,11 @@ impl VentoyDrive {
|
||||
|
||||
// Run Ventoy creation in blocking task
|
||||
let info = tokio::task::spawn_blocking(move || {
|
||||
VentoyImage::create(&path, &size_str, DEFAULT_LABEL)
|
||||
.map_err(ventoy_to_app_error)?;
|
||||
VentoyImage::create(&path, &size_str, DEFAULT_LABEL).map_err(ventoy_to_app_error)?;
|
||||
|
||||
// Get file metadata for DriveInfo
|
||||
let metadata = std::fs::metadata(&path).map_err(|e| {
|
||||
AppError::Internal(format!("Failed to read drive metadata: {}", e))
|
||||
})?;
|
||||
let metadata = std::fs::metadata(&path)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to read drive metadata: {}", e)))?;
|
||||
|
||||
Ok::<DriveInfo, AppError>(DriveInfo {
|
||||
size: metadata.len(),
|
||||
@@ -104,16 +102,13 @@ impl VentoyDrive {
|
||||
let _lock = self.lock.read().await; // Read lock for info query
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let metadata = std::fs::metadata(&path).map_err(|e| {
|
||||
AppError::Internal(format!("Failed to read drive metadata: {}", e))
|
||||
})?;
|
||||
let metadata = std::fs::metadata(&path)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to read drive metadata: {}", e)))?;
|
||||
|
||||
// Open image to get file list and calculate used space
|
||||
let image = VentoyImage::open(&path).map_err(ventoy_to_app_error)?;
|
||||
|
||||
let files = image
|
||||
.list_files_recursive()
|
||||
.map_err(ventoy_to_app_error)?;
|
||||
let files = image.list_files_recursive().map_err(ventoy_to_app_error)?;
|
||||
|
||||
let used: u64 = files
|
||||
.iter()
|
||||
@@ -190,9 +185,11 @@ impl VentoyDrive {
|
||||
|
||||
let mut bytes_written: u64 = 0;
|
||||
|
||||
while let Some(chunk) = field.chunk().await.map_err(|e| {
|
||||
AppError::Internal(format!("Failed to read upload chunk: {}", e))
|
||||
})? {
|
||||
while let Some(chunk) = field
|
||||
.chunk()
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(format!("Failed to read upload chunk: {}", e)))?
|
||||
{
|
||||
bytes_written += chunk.len() as u64;
|
||||
tokio::io::AsyncWriteExt::write_all(&mut temp_file, &chunk)
|
||||
.await
|
||||
@@ -248,9 +245,7 @@ impl VentoyDrive {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let image = VentoyImage::open(&path).map_err(ventoy_to_app_error)?;
|
||||
|
||||
image
|
||||
.read_file(&file_path)
|
||||
.map_err(ventoy_to_app_error)
|
||||
image.read_file(&file_path).map_err(ventoy_to_app_error)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))?
|
||||
@@ -321,7 +316,8 @@ impl VentoyDrive {
|
||||
let lock = self.lock.clone();
|
||||
|
||||
// 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
|
||||
tokio::task::spawn_blocking(move || {
|
||||
@@ -404,20 +400,14 @@ fn ventoy_to_app_error(err: VentoyError) -> AppError {
|
||||
match err {
|
||||
VentoyError::Io(e) => AppError::Io(e),
|
||||
VentoyError::InvalidSize(s) => AppError::BadRequest(format!("Invalid size: {}", s)),
|
||||
VentoyError::SizeParseError(s) => {
|
||||
AppError::BadRequest(format!("Size parse error: {}", s))
|
||||
}
|
||||
VentoyError::FilesystemError(s) => {
|
||||
AppError::Internal(format!("Filesystem error: {}", s))
|
||||
}
|
||||
VentoyError::SizeParseError(s) => AppError::BadRequest(format!("Size parse error: {}", s)),
|
||||
VentoyError::FilesystemError(s) => AppError::Internal(format!("Filesystem error: {}", s)),
|
||||
VentoyError::ImageError(s) => AppError::Internal(format!("Image error: {}", s)),
|
||||
VentoyError::FileNotFound(s) => AppError::NotFound(format!("File not found: {}", s)),
|
||||
VentoyError::ResourceNotFound(s) => {
|
||||
AppError::Internal(format!("Resource not found: {}", s))
|
||||
}
|
||||
VentoyError::PartitionError(s) => {
|
||||
AppError::Internal(format!("Partition error: {}", s))
|
||||
}
|
||||
VentoyError::PartitionError(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 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;
|
||||
|
||||
if self.buffer.len() >= STREAM_CHUNK_SIZE {
|
||||
@@ -512,10 +503,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Path to ventoy resources directory
|
||||
static RESOURCE_DIR: &str = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../ventoy-img-rs/resources"
|
||||
);
|
||||
static RESOURCE_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../ventoy-img-rs/resources");
|
||||
|
||||
/// Initialize ventoy resources once
|
||||
fn init_ventoy_resources() -> bool {
|
||||
@@ -561,7 +549,10 @@ mod tests {
|
||||
if !output.status.success() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("xz decompress failed: {}", String::from_utf8_lossy(&output.stderr)),
|
||||
format!(
|
||||
"xz decompress failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user