diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 01:31:11 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 01:31:11 +0100 |
| commit | b9eaa579dac2ae912e3be9a3aa3e73ddcb3416e1 (patch) | |
| tree | 6082f23150d0c64ae1ac8aeebc5393468263ad82 | |
| parent | tts: attach the generated WAV without a temp file (diff) | |
| download | senator-sperma-b9eaa579dac2ae912e3be9a3aa3e73ddcb3416e1.tar.gz senator-sperma-b9eaa579dac2ae912e3be9a3aa3e73ddcb3416e1.zip | |
music: decode compressed playback without a temp file
| -rw-r--r-- | src/music.rs | 10 | ||||
| -rw-r--r-- | src/voice/mod.rs | 36 |
2 files changed, 34 insertions, 12 deletions
diff --git a/src/music.rs b/src/music.rs index ac88631..acec278 100644 --- a/src/music.rs +++ b/src/music.rs | |||
| @@ -575,14 +575,8 @@ async fn build_input(request: &TrackRequest, http: &reqwest::Client) -> Result<I | |||
| 575 | info!("compressed to {} bytes", compressed_bytes.len()); | 575 | info!("compressed to {} bytes", compressed_bytes.len()); |
| 576 | 576 | ||
| 577 | // Decode the compressed OGG via ffmpeg (symphonia lacks Opus support). | 577 | // Decode the compressed OGG via ffmpeg (symphonia lacks Opus support). |
| 578 | let compressed_path = format!( | 578 | // Piping keeps the intermediate out of TEMP_PATH entirely. |
| 579 | "{}/play_compressed_{}.ogg", | 579 | let input = voice::ffmpeg_decode_bytes(compressed_bytes).await?; |
| 580 | crate::config::TEMP_PATH, | ||
| 581 | Uuid::new_v4() | ||
| 582 | ); | ||
| 583 | let _ = tokio::fs::create_dir_all(crate::config::TEMP_PATH).await; | ||
| 584 | tokio::fs::write(&compressed_path, &compressed_bytes).await?; | ||
| 585 | let input = voice::ffmpeg_decode_file(&compressed_path).await?; | ||
| 586 | Ok(input) | 580 | Ok(input) |
| 587 | } else { | 581 | } else { |
| 588 | match request.kind { | 582 | match request.kind { |
diff --git a/src/voice/mod.rs b/src/voice/mod.rs index d8308eb..2866f54 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs | |||
| @@ -342,15 +342,17 @@ impl tokio::io::AsyncRead for FfmpegChild { | |||
| 342 | } | 342 | } |
| 343 | } | 343 | } |
| 344 | 344 | ||
| 345 | /// Decode a local audio file via ffmpeg to raw f32 PCM at 48 kHz stereo. | 345 | /// Decode an in-memory audio file via ffmpeg to raw f32 PCM at 48 kHz stereo. |
| 346 | /// No reconnect flags (those are for HTTP streams only). | 346 | /// |
| 347 | pub async fn ffmpeg_decode_file(path: &str) -> Result<songbird::input::Input> { | 347 | /// Used for compressed playback so the intermediate file never has to touch |
| 348 | /// disk (and therefore cannot leak into `TEMP_PATH`). | ||
| 349 | pub async fn ffmpeg_decode_bytes(data: Vec<u8>) -> Result<songbird::input::Input> { | ||
| 348 | use std::process::Stdio; | 350 | use std::process::Stdio; |
| 349 | 351 | ||
| 350 | let mut child = tokio::process::Command::new("ffmpeg") | 352 | let mut child = tokio::process::Command::new("ffmpeg") |
| 351 | .args([ | 353 | .args([ |
| 352 | "-i", | 354 | "-i", |
| 353 | path, | 355 | "pipe:0", |
| 354 | "-vn", | 356 | "-vn", |
| 355 | "-f", | 357 | "-f", |
| 356 | "f32le", | 358 | "f32le", |
| @@ -360,17 +362,43 @@ pub async fn ffmpeg_decode_file(path: &str) -> Result<songbird::input::Input> { | |||
| 360 | "2", | 362 | "2", |
| 361 | "pipe:1", | 363 | "pipe:1", |
| 362 | ]) | 364 | ]) |
| 365 | .stdin(Stdio::piped()) | ||
| 363 | .stdout(Stdio::piped()) | 366 | .stdout(Stdio::piped()) |
| 364 | .stderr(Stdio::piped()) | 367 | .stderr(Stdio::piped()) |
| 365 | .kill_on_drop(true) | 368 | .kill_on_drop(true) |
| 366 | .spawn() | 369 | .spawn() |
| 367 | .map_err(|e| Error::Custom(format!("ffmpeg decode spawn: {e}")))?; | 370 | .map_err(|e| Error::Custom(format!("ffmpeg decode spawn: {e}")))?; |
| 368 | 371 | ||
| 372 | let mut stdin = child | ||
| 373 | .stdin | ||
| 374 | .take() | ||
| 375 | .ok_or_else(|| Error::Custom("ffmpeg stdin not captured".into()))?; | ||
| 369 | let stdout = child | 376 | let stdout = child |
| 370 | .stdout | 377 | .stdout |
| 371 | .take() | 378 | .take() |
| 372 | .ok_or_else(|| Error::Custom("ffmpeg stdout not captured".into()))?; | 379 | .ok_or_else(|| Error::Custom("ffmpeg stdout not captured".into()))?; |
| 373 | 380 | ||
| 381 | tokio::spawn(async move { | ||
| 382 | use tokio::io::AsyncWriteExt; | ||
| 383 | let _ = stdin.write_all(&data).await; | ||
| 384 | // Dropping stdin gives ffmpeg EOF. | ||
| 385 | }); | ||
| 386 | |||
| 387 | if let Some(stderr) = child.stderr.take() { | ||
| 388 | tokio::spawn(async move { | ||
| 389 | use tokio::io::AsyncReadExt; | ||
| 390 | let mut buf = Vec::with_capacity(4096); | ||
| 391 | let mut reader = tokio::io::BufReader::new(stderr); | ||
| 392 | let _ = reader.read_to_end(&mut buf).await; | ||
| 393 | if !buf.is_empty() { | ||
| 394 | warn!( | ||
| 395 | "ffmpeg (decode) stderr: {}", | ||
| 396 | String::from_utf8_lossy(&buf).trim_end() | ||
| 397 | ); | ||
| 398 | } | ||
| 399 | }); | ||
| 400 | } | ||
| 401 | |||
| 374 | let ffmpeg_child = FfmpegChild { | 402 | let ffmpeg_child = FfmpegChild { |
| 375 | _child: child, | 403 | _child: child, |
| 376 | stdout, | 404 | stdout, |
