mirror of
https://github.com/Xinrea/bili-shadowreplay.git
synced 2025-11-24 20:15:34 +08:00
feat: special audio file for waveform (#226)
This commit is contained in:
@@ -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<PathBuf, String> {
|
||||
// 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<PathBuf, String> {
|
||||
// ffmpeg -i fixed_\[30655190\]1742887114_0325084106_81.5.mp4 -ar 16000 test.wav
|
||||
log::info!("Extract audio task start: {}", file.display());
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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<State>,
|
||||
Json(param): Json<GenerateAudioSampleRequest>,
|
||||
) -> Result<Json<ApiResponse<()>>, 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))
|
||||
|
||||
@@ -691,6 +691,7 @@ fn setup_invoke_handlers(builder: tauri::Builder<tauri::Wry>) -> 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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
<div class="flex items-center space-x-2 text-gray-400 w-full">
|
||||
<div
|
||||
class="flex items-center justify-center gap-2 text-gray-400 w-full h-full text-center"
|
||||
>
|
||||
<svg
|
||||
class="animate-spin h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
Reference in New Issue
Block a user