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

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