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

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

View File

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

View File

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

View File

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

View File

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