feat: complete Phase 12 — voice playback ticker, cache, config, and UI
Add playback position ticker in event loop with 1s UI refresh rate, integrate VoiceCache for downloaded voice files, add [audio] config section (cache_size_mb, auto_download_voice), and render progress bar with waveform visualization in message bubbles. Fix race conditions in AudioPlayer: add `starting` flag to prevent false `is_stopped()` during ffplay startup, guard pid cleanup so old threads don't overwrite newer process pids. Implement `resume_from()` with ffplay `-ss` for real audio seek on unpause (-1s rewind). Kill ffplay on app exit via `stop_playback()` in shutdown + Drop impl. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -504,11 +504,21 @@ async fn handle_toggle_voice_playback<T: TdClientTrait>(app: &mut App<T>) {
|
||||
PlaybackStatus::Playing => {
|
||||
player.pause();
|
||||
playback.status = PlaybackStatus::Paused;
|
||||
app.last_playback_tick = None;
|
||||
app.status_message = Some("⏸ Пауза".to_string());
|
||||
}
|
||||
PlaybackStatus::Paused => {
|
||||
player.resume();
|
||||
// Откатываем на 1 секунду для контекста
|
||||
let resume_pos = (playback.position - 1.0).max(0.0);
|
||||
// Перезапускаем ffplay с нужной позиции (-ss)
|
||||
if player.resume_from(resume_pos).is_ok() {
|
||||
playback.position = resume_pos;
|
||||
} else {
|
||||
// Fallback: простой SIGCONT без перемотки
|
||||
player.resume();
|
||||
}
|
||||
playback.status = PlaybackStatus::Playing;
|
||||
app.last_playback_tick = Some(Instant::now());
|
||||
app.status_message = Some("▶ Воспроизведение".to_string());
|
||||
}
|
||||
_ => {}
|
||||
@@ -612,6 +622,7 @@ async fn handle_play_voice_from_path<T: TdClientTrait>(
|
||||
duration: voice.duration as f32,
|
||||
volume: player.volume(),
|
||||
});
|
||||
app.last_playback_tick = Some(Instant::now());
|
||||
app.status_message = Some(format!("▶ Воспроизведение ({:.0}s)", voice.duration));
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
@@ -658,7 +669,12 @@ async fn handle_play_voice<T: TdClientTrait>(app: &mut App<T>) {
|
||||
for entry in entries.flatten() {
|
||||
let entry_name = entry.file_name();
|
||||
if entry_name.to_string_lossy().starts_with(&stem.to_string_lossy().to_string()) {
|
||||
return handle_play_voice_from_path(app, &entry.path().to_string_lossy(), &voice, &msg).await;
|
||||
let found_path = entry.path().to_string_lossy().to_string();
|
||||
// Кэшируем найденный файл
|
||||
if let Some(ref mut cache) = app.voice_cache {
|
||||
let _ = cache.store(&file_id.to_string(), Path::new(&found_path));
|
||||
}
|
||||
return handle_play_voice_from_path(app, &found_path, &voice, &msg).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -669,37 +685,35 @@ async fn handle_play_voice<T: TdClientTrait>(app: &mut App<T>) {
|
||||
}
|
||||
};
|
||||
|
||||
// Кэшируем файл если ещё не в кэше
|
||||
if let Some(ref mut cache) = app.voice_cache {
|
||||
let _ = cache.store(&file_id.to_string(), Path::new(&audio_path));
|
||||
}
|
||||
|
||||
handle_play_voice_from_path(app, &audio_path, &voice, &msg).await;
|
||||
}
|
||||
VoiceDownloadState::Downloading => {
|
||||
app.status_message = Some("Загрузка голосового...".to_string());
|
||||
}
|
||||
VoiceDownloadState::NotDownloaded => {
|
||||
use crate::tdlib::{PlaybackState, PlaybackStatus};
|
||||
// Проверяем кэш перед загрузкой
|
||||
let cache_key = file_id.to_string();
|
||||
if let Some(cached_path) = app.voice_cache.as_mut().and_then(|c| c.get(&cache_key)) {
|
||||
let path_str = cached_path.to_string_lossy().to_string();
|
||||
handle_play_voice_from_path(app, &path_str, &voice, &msg).await;
|
||||
return;
|
||||
}
|
||||
|
||||
// Начинаем загрузку
|
||||
app.status_message = Some("Загрузка голосового...".to_string());
|
||||
match app.td_client.download_voice_note(file_id).await {
|
||||
Ok(path) => {
|
||||
// Пытаемся воспроизвести после загрузки
|
||||
if let Some(ref player) = app.audio_player {
|
||||
match player.play(&path) {
|
||||
Ok(_) => {
|
||||
app.playback_state = Some(PlaybackState {
|
||||
message_id: msg.id(),
|
||||
status: PlaybackStatus::Playing,
|
||||
position: 0.0,
|
||||
duration: voice.duration as f32,
|
||||
volume: player.volume(),
|
||||
});
|
||||
app.status_message = Some(format!("▶ Воспроизведение ({:.0}s)", voice.duration));
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
Err(e) => {
|
||||
app.error_message = Some(format!("Ошибка воспроизведения: {}", e));
|
||||
}
|
||||
}
|
||||
// Кэшируем загруженный файл
|
||||
if let Some(ref mut cache) = app.voice_cache {
|
||||
let _ = cache.store(&cache_key, std::path::Path::new(&path));
|
||||
}
|
||||
|
||||
handle_play_voice_from_path(app, &path, &voice, &msg).await;
|
||||
}
|
||||
Err(e) => {
|
||||
app.error_message = Some(format!("Ошибка загрузки: {}", e));
|
||||
|
||||
Reference in New Issue
Block a user