feat: implement Phase 12 — voice message playback with ffplay

Add voice message playback infrastructure:
- AudioPlayer using ffplay subprocess with SIGSTOP/SIGCONT for pause/resume
- VoiceCache with LRU eviction (100 MB limit)
- TDLib integration: VoiceInfo, VoiceDownloadState, PlaybackState types
- download_voice_note() in TdClientTrait
- Keybindings: Space (play/pause), ←/→ (seek ±5s)
- Auto-stop playback on message navigation
- Remove debug_log module
This commit is contained in:
Mikhail Kilin
2026-02-09 02:35:49 +03:00
parent 2a5fd6aa35
commit 7bc264198f
17 changed files with 750 additions and 103 deletions

View File

@@ -47,6 +47,7 @@ impl<T: TdClientTrait> MessageMethods<T> for App<T> {
if let ChatState::MessageSelection { selected_index } = &mut self.chat_state {
if *selected_index > 0 {
*selected_index -= 1;
self.stop_playback();
}
}
}
@@ -59,9 +60,11 @@ impl<T: TdClientTrait> MessageMethods<T> for App<T> {
if let ChatState::MessageSelection { selected_index } = &mut self.chat_state {
if *selected_index < total - 1 {
*selected_index += 1;
self.stop_playback();
} else {
// Дошли до самого нового сообщения - выходим из режима выбора
self.chat_state = ChatState::Normal;
self.stop_playback();
}
}
}

View File

@@ -100,6 +100,13 @@ pub struct App<T: TdClientTrait = TdClient> {
/// Время последнего рендеринга изображений (для throttling до 15 FPS)
#[cfg(feature = "images")]
pub last_image_render_time: Option<std::time::Instant>,
// Voice playback
/// Аудиопроигрыватель для голосовых сообщений (rodio)
pub audio_player: Option<crate::audio::AudioPlayer>,
/// Кэш голосовых файлов (LRU, max 100 MB)
pub voice_cache: Option<crate::audio::VoiceCache>,
/// Состояние текущего воспроизведения
pub playback_state: Option<crate::tdlib::PlaybackState>,
}
impl<T: TdClientTrait> App<T> {
@@ -160,6 +167,10 @@ impl<T: TdClientTrait> App<T> {
image_modal: None,
#[cfg(feature = "images")]
last_image_render_time: None,
// Voice playback
audio_player: crate::audio::AudioPlayer::new().ok(),
voice_cache: crate::audio::VoiceCache::new().ok(),
playback_state: None,
}
}
@@ -181,6 +192,15 @@ impl<T: TdClientTrait> App<T> {
self.selected_chat_id.map(|id| id.as_i64())
}
/// Останавливает воспроизведение голосового и сбрасывает состояние
pub fn stop_playback(&mut self) {
if let Some(ref player) = self.audio_player {
player.stop();
}
self.playback_state = None;
self.status_message = None;
}
/// Get the selected chat info
pub fn get_selected_chat(&self) -> Option<&ChatInfo> {
self.selected_chat_id