diff --git a/src-tauri/src/ffmpeg/mod.rs b/src-tauri/src/ffmpeg/mod.rs index 0027db5..f35c671 100644 --- a/src-tauri/src/ffmpeg/mod.rs +++ b/src-tauri/src/ffmpeg/mod.rs @@ -183,6 +183,68 @@ pub async fn trim_video( Ok(()) } +/// Extract a sample audio from the video file for waveform display +pub async fn extract_audio_sample(file: &Path) -> Result { + // ffmpeg -i fixed_\[30655592\]1742887114_0325084106_81.5.mp4 -ar 16000 test.wav + log::info!("Extract audio sample task start: {}", file.display()); + let output_path = file.with_extension("opus"); + let mut extract_error = None; + + let mut ffmpeg_process = tokio::process::Command::new(ffmpeg_path()); + #[cfg(target_os = "windows")] + ffmpeg_process.creation_flags(CREATE_NO_WINDOW); + + let child = ffmpeg_process + .args(["-i", file.to_str().unwrap()]) + .args(["-c:a", "libopus"]) + .args(["-ar", "16000"]) + .args(["-ac", "1"]) + .args(["-vn"]) + .args(["-b:a", "12k"]) + .args(["-vbr", "on"]) + .args(["-compression_level", "10"]) + .args([output_path.to_str().unwrap()]) + .args(["-y"]) + .args(["-progress", "pipe:2"]) + .stderr(Stdio::piped()) + .spawn(); + + if let Err(e) = child { + return Err(e.to_string()); + } + + let mut child = child.unwrap(); + let stderr = child.stderr.take().unwrap(); + let reader = BufReader::new(stderr); + let mut parser = FfmpegLogParser::new(reader); + while let Ok(event) = parser.parse_next_event().await { + match event { + FfmpegEvent::Error(e) => { + log::error!("Extract audio sample error: {e}"); + extract_error = Some(e.to_string()); + } + FfmpegEvent::LogEOF => break, + FfmpegEvent::Progress(p) => { + log::info!("Extract audio sample progress: {}", p.time); + } + FfmpegEvent::Log(_level, _content) => {} + _ => {} + } + } + + if let Err(e) = child.wait().await { + log::error!("Extract audio sample error: {e}"); + return Err(e.to_string()); + } + + if let Some(error) = extract_error { + log::error!("Extract audio sample error: {error}"); + Err(error) + } else { + log::info!("Extract audio sample task end: {}", output_path.display()); + Ok(output_path) + } +} pub async fn extract_audio_chunks(file: &Path, format: &str) -> Result { // ffmpeg -i fixed_\[30655190\]1742887114_0325084106_81.5.mp4 -ar 16000 test.wav log::info!("Extract audio task start: {}", file.display()); diff --git a/src-tauri/src/handlers/video.rs b/src-tauri/src/handlers/video.rs index ecb4b57..0cc6a59 100644 --- a/src-tauri/src/handlers/video.rs +++ b/src-tauri/src/handlers/video.rs @@ -436,6 +436,7 @@ async fn clip_range_inner( if cover_generate_ffmpeg { ffmpeg::generate_thumbnail(&file, 0.0).await?; } + let _ = crate::ffmpeg::extract_audio_sample(&file).await?; // get filename from path let filename = Path::new(&file) .file_name() @@ -714,6 +715,8 @@ pub async fn delete_video(state: state_type!(), id: i64) -> Result<(), String> { let _ = tokio::fs::remove_file(wav_path).await; let mp3_path = file.with_extension("mp3"); let _ = tokio::fs::remove_file(mp3_path).await; + let opus_path = file.with_extension("opus"); + let _ = tokio::fs::remove_file(opus_path).await; let cover_path = Path::new(&config.output).join(&video.cover); let _ = tokio::fs::remove_file(cover_path).await; @@ -1452,3 +1455,14 @@ pub async fn get_import_progress( Ok(None) } + +#[cfg_attr(feature = "gui", tauri::command)] +pub async fn generate_audio_sample(state: state_type!(), video_id: i64) -> Result<(), String> { + let video = state.db.get_video(video_id).await?; + let video_path = Path::new(&state.config.read().await.output).join(&video.file); + let opus_path = video_path.with_extension("opus"); + if !opus_path.exists() { + let _ = crate::ffmpeg::extract_audio_sample(&video_path).await?; + } + Ok(()) +} diff --git a/src-tauri/src/http_server/api_server.rs b/src-tauri/src/http_server/api_server.rs index 91d36f7..50d7aca 100644 --- a/src-tauri/src/http_server/api_server.rs +++ b/src-tauri/src/http_server/api_server.rs @@ -32,10 +32,11 @@ use crate::{ utils::{console_log, get_disk_info, list_folder, sanitize_filename_advanced, DiskInfo}, video::{ batch_import_external_videos, cancel, clip_range, clip_video, delete_video, - encode_video_subtitle, generate_video_subtitle, generic_ffmpeg_command, get_all_videos, - get_file_size, get_import_progress, get_video, get_video_cover, get_video_subtitle, - get_video_typelist, get_videos, import_external_video, update_video_cover, - update_video_note, update_video_subtitle, upload_procedure, + encode_video_subtitle, generate_audio_sample, generate_video_subtitle, + generic_ffmpeg_command, get_all_videos, get_file_size, get_import_progress, get_video, + get_video_cover, get_video_subtitle, get_video_typelist, get_videos, + import_external_video, update_video_cover, update_video_note, update_video_subtitle, + upload_procedure, }, AccountInfo, }, @@ -756,6 +757,20 @@ async fn handler_get_video( Ok(Json(ApiResponse::success(video))) } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct GenerateAudioSampleRequest { + video_id: i64, +} + +async fn handler_generate_audio_sample( + state: axum::extract::State, + Json(param): Json, +) -> Result>, ApiError> { + generate_audio_sample(state.0, param.video_id).await?; + Ok(Json(ApiResponse::success(()))) +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct GetVideosRequest { @@ -1816,6 +1831,10 @@ pub async fn start_api_server(state: State) { // Video commands .route("/api/clip_range", post(handler_clip_range)) .route("/api/get_video", post(handler_get_video)) + .route( + "/api/generate_audio_sample", + post(handler_generate_audio_sample), + ) .route("/api/get_videos", post(handler_get_videos)) .route("/api/get_video_cover", post(handler_get_video_cover)) .route("/api/get_all_videos", post(handler_get_all_videos)) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 70e1a6c..97f7f17 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -691,6 +691,7 @@ fn setup_invoke_handlers(builder: tauri::Builder) -> tauri::Builder< crate::handlers::video::clip_video, crate::handlers::video::get_file_size, crate::handlers::video::get_import_progress, + crate::handlers::video::generate_audio_sample, crate::handlers::task::get_tasks, crate::handlers::task::delete_task, crate::handlers::utils::show_in_folder, diff --git a/src-tauri/src/recorder_manager.rs b/src-tauri/src/recorder_manager.rs index afae49e..e66314a 100644 --- a/src-tauri/src/recorder_manager.rs +++ b/src-tauri/src/recorder_manager.rs @@ -1437,6 +1437,7 @@ impl RecorderManager { } let _ = crate::ffmpeg::generate_thumbnail(Path::new(&output_path), 0.0).await; + let _ = crate::ffmpeg::extract_audio_sample(Path::new(&output_path)).await; let video = self .db diff --git a/src-tauri/src/static_server/mod.rs b/src-tauri/src/static_server/mod.rs index 50f862e..9bce1fa 100644 --- a/src-tauri/src/static_server/mod.rs +++ b/src-tauri/src/static_server/mod.rs @@ -44,9 +44,9 @@ pub async fn start_static_server( .allow_methods(Any) .allow_headers(Any); let router = Router::new() - .layer(cors) .nest_service("/output", ServeDir::new(output_path)) - .nest_service("/cache", ServeDir::new(cache_path)); + .nest_service("/cache", ServeDir::new(cache_path)) + .layer(cors); if let Err(e) = axum::serve(listener, router).await { log::error!("Server error: {}", e); diff --git a/src/lib/components/VideoPreview.svelte b/src/lib/components/VideoPreview.svelte index 24217b4..668d1e5 100644 --- a/src/lib/components/VideoPreview.svelte +++ b/src/lib/components/VideoPreview.svelte @@ -153,14 +153,14 @@ isWaveformLoading = true; try { - createWaveSurfer(); + await createWaveSurfer(); } catch (error) { console.error("Failed to initialize WaveSurfer.js:", error); isWaveformLoading = false; } } - function createWaveSurfer() { + async function createWaveSurfer() { // 使用更稳定的容器查找方式 const container = document.querySelector( "[data-waveform-container]" @@ -203,9 +203,13 @@ plugins: [], }); - console.log("WaveSurfer created, loading file:", video.file); // 加载音频 - wavesurfer.load(video.file); + await invoke("generate_audio_sample", { + videoId: video.id, + }); + let opus_file = video.file.replace(".mp4", ".opus"); + console.log("WaveSurfer created, loading file:", opus_file); + wavesurfer.load(opus_file); // 监听加载完成 wavesurfer.on("ready", () => { @@ -1738,7 +1742,9 @@ on:wheel|preventDefault={handleWheel} > {#if isWaveformLoading} -
+