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

@@ -58,6 +58,7 @@ pub struct ReactionInfo {
#[derive(Debug, Clone)]
pub enum MediaInfo {
Photo(PhotoInfo),
Voice(VoiceInfo),
}
/// Информация о фотографии в сообщении
@@ -78,6 +79,26 @@ pub enum PhotoDownloadState {
Error(String),
}
/// Информация о голосовом сообщении
#[derive(Debug, Clone)]
pub struct VoiceInfo {
pub file_id: i32,
pub duration: i32, // seconds
pub mime_type: String,
/// Waveform данные для визуализации (base64-encoded строка амплитуд)
pub waveform: String,
pub download_state: VoiceDownloadState,
}
/// Состояние загрузки голосового сообщения
#[derive(Debug, Clone)]
pub enum VoiceDownloadState {
NotDownloaded,
Downloading,
Downloaded(String), // path to cached OGG file
Error(String),
}
/// Метаданные сообщения (ID, отправитель, время)
#[derive(Debug, Clone)]
pub struct MessageMetadata {
@@ -251,6 +272,27 @@ impl MessageInfo {
}
}
/// Проверяет, содержит ли сообщение голосовое
pub fn has_voice(&self) -> bool {
matches!(self.content.media, Some(MediaInfo::Voice(_)))
}
/// Возвращает ссылку на VoiceInfo (если есть)
pub fn voice_info(&self) -> Option<&VoiceInfo> {
match &self.content.media {
Some(MediaInfo::Voice(info)) => Some(info),
_ => None,
}
}
/// Возвращает мутабельную ссылку на VoiceInfo (если есть)
pub fn voice_info_mut(&mut self) -> Option<&mut VoiceInfo> {
match &mut self.content.media {
Some(MediaInfo::Voice(info)) => Some(info),
_ => None,
}
}
pub fn reply_to(&self) -> Option<&ReplyInfo> {
self.interactions.reply_to.as_ref()
}
@@ -646,3 +688,28 @@ pub struct ImageModalState {
/// Высота оригинального изображения
pub photo_height: i32,
}
/// Состояние воспроизведения голосового сообщения
#[derive(Debug, Clone)]
pub struct PlaybackState {
/// ID сообщения, которое воспроизводится
pub message_id: MessageId,
/// Статус воспроизведения
pub status: PlaybackStatus,
/// Текущая позиция (секунды)
pub position: f32,
/// Общая длительность (секунды)
pub duration: f32,
/// Громкость (0.0 - 1.0)
pub volume: f32,
}
/// Статус воспроизведения
#[derive(Debug, Clone, PartialEq)]
pub enum PlaybackStatus {
Playing,
Paused,
Stopped,
Loading,
Error(String),
}