12 KiB
Analyse : Rendre rust-cast asynchrone vs autres options
Date : 2025-12-27 Question : Est-il plus simple d'intégrer rust-cast et le modifier pour le rendre asynchrone ?
1. Analyse de la codebase rust-cast
Taille et complexité
Total : ~5800 lignes de code Rust
Structure modulaire :
├── src/lib.rs (~570 lignes)
├── src/message_manager.rs (~300 lignes)
├── src/channels/
│ ├── media.rs (~800 lignes)
│ ├── receiver.rs (~400 lignes)
│ ├── heartbeat.rs (~100 lignes)
│ └── connection.rs (~100 lignes)
├── src/cast/
│ ├── cast_channel.rs (généré par protobuf)
│ └── proxies.rs (~500 lignes)
└── src/errors.rs, utils.rs (~200 lignes)
Conclusion : Codebase de taille modeste et bien structurée.
2. Points bloquants identifiés
2.1 I/O synchrone bloquant
Tous les I/O passent par MessageManager<S> où S: Read + Write :
// message_manager.rs:246-253
fn read(&self) -> Result<CastMessage, Error> {
let mut buffer: [u8; 4] = [0; 4];
let reader = &mut *self.stream.borrow_mut();
reader.read_exact(&mut buffer)?; // ❌ BLOQUANT
let length = utils::read_u32_from_buffer(&buffer)?;
let mut buffer: Vec<u8> = Vec::with_capacity(length as usize);
let mut limited_reader = reader.take(u64::from(length));
limited_reader.read_to_end(&mut buffer)?; // ❌ BLOQUANT
...
}
// message_manager.rs:138-141
pub fn send(&self, message: CastMessage) -> Result<(), Error> {
...
let writer = &mut *self.stream.borrow_mut();
writer.write_all(&message_length_buffer)?; // ❌ BLOQUANT
writer.write_all(&message_content_buffer)?; // ❌ BLOQUANT
...
}
2.2 Connexion TLS
// lib.rs:125
let stream = StreamOwned::new(
conn,
TcpStream::connect((host.as_ref(), port))? // ❌ BLOQUANT
);
Total : 5 points bloquants critiques (connect, read_exact, read_to_end, 2x write_all)
3. Effort pour rendre rust-cast asynchrone
3.1 Modifications requises
A. Remplacer la stack réseau
Avant (sync) :
use std::net::TcpStream;
use rustls::{ClientConnection, StreamOwned};
type TlsStream = StreamOwned<ClientConnection, TcpStream>;
Après (async) :
use async_io::Async;
use std::net::TcpStream;
use async_rustls::{TlsConnector, client::TlsStream};
// OU avec tokio :
use tokio::net::TcpStream;
use tokio_rustls::{TlsConnector, client::TlsStream};
⚠️ PROBLÈME : rustls::StreamOwned n'existe pas en version async native. Il faut utiliser :
async-rustls(pour async-std/smol)tokio-rustls(pour tokio)
Ces crates ont une API différente de rustls::StreamOwned.
B. Modifier MessageManager
- pub struct MessageManager<S> where S: Write + Read {
+ pub struct MessageManager<S> where S: AsyncWrite + AsyncRead + Unpin {
- pub fn send(&self, message: CastMessage) -> Result<(), Error> {
+ pub async fn send(&self, message: CastMessage) -> Result<(), Error> {
...
- writer.write_all(&message_length_buffer)?;
+ writer.write_all(&message_length_buffer).await?;
}
- pub fn receive(&self) -> Result<CastMessage, Error> {
+ pub async fn receive(&self) -> Result<CastMessage, Error> {
...
}
- fn read(&self) -> Result<CastMessage, Error> {
+ async fn read(&self) -> Result<CastMessage, Error> {
- reader.read_exact(&mut buffer)?;
+ reader.read_exact(&mut buffer).await?;
- limited_reader.read_to_end(&mut buffer)?;
+ limited_reader.read_to_end(&mut buffer).await?;
}
}
C. Propager async dans tous les channels
Avant :
// channels/media.rs
impl<'a, S> MediaChannel<'a, S> where S: Write + Read {
pub fn play(&self, ...) -> Result<(), Error> {
self.message_manager.send(...)?;
self.message_manager.receive_find_map(...)
}
}
Après :
impl<'a, S> MediaChannel<'a, S> where S: AsyncWrite + AsyncRead + Unpin {
pub async fn play(&self, ...) -> Result<(), Error> {
self.message_manager.send(...).await?;
self.message_manager.receive_find_map(...).await
}
}
Impact : TOUS les channels (media, receiver, heartbeat, connection) deviennent async.
D. Modifier CastDevice
impl<'a> CastDevice<'a> {
- pub fn connect<S>(host: S, port: u16) -> Result<CastDevice<'a>, Error>
+ pub async fn connect<S>(host: S, port: u16) -> Result<CastDevice<'a>, Error>
{
...
- let stream = TcpStream::connect((host.as_ref(), port))?;
+ let stream = TcpStream::connect((host.as_ref(), port)).await?;
...
}
- pub fn receive(&self) -> Result<ChannelMessage, Error> {
+ pub async fn receive(&self) -> Result<ChannelMessage, Error> {
- let cast_message = self.message_manager.receive()?;
+ let cast_message = self.message_manager.receive().await?;
...
}
}
3.2 Estimation de l'effort
| Tâche | Fichiers touchés | Complexité | Temps estimé |
|---|---|---|---|
| Choisir stack async (smol vs tokio) | - | Faible | 1h |
| Migrer vers async-rustls/tokio-rustls | lib.rs | MOYENNE | 4-6h |
| Rendre MessageManager async | message_manager.rs | MOYENNE | 4-6h |
| Rendre tous les channels async | 4 fichiers | MOYENNE-ÉLEVÉE | 8-12h |
| Mettre à jour CastDevice | lib.rs | Moyenne | 2-4h |
| Tests et debug | Tous | ÉLEVÉE | 8-16h |
| TOTAL | ~10 fichiers | ÉLEVÉE | 27-45 heures |
⚠️ RISQUES :
- API
async-rustlsdifférente derustls::StreamOwned→ peut nécessiter refactoring profond - Gestion des locks async (
Mutex→async_lock::Mutexoutokio::sync::Mutex) - Bugs subtils liés à la concurrence async
- Tests nécessaires pour valider la stabilité
4. Comparaison des 4 options
Option 1 : ✅ Rester avec rust-cast sync et corriger le TLS
Effort : FAIBLE (2-8 heures)
Actions :
- Investiguer les erreurs TLS prématurées
- Ajouter retry logic sur les reconnexions
- Améliorer la gestion d'erreur dans chromecast_renderer.rs
- Peut-être ajuster les timeouts de lecture
Avantages :
- ✅ Garde l'API sync compatible avec PMOMusic
- ✅ Risque minimal
- ✅ Solution rapide
Inconvénients :
- ⚠️ Ne résout peut-être pas tous les problèmes TLS
Option 2 : 🔧 Forker rust-cast et moderniser le TLS (reste sync)
Effort : MOYEN (8-16 heures)
Actions :
- Forker rust-cast sur GitHub/GitLab
- Améliorer la gestion TLS (retry, reconnexion automatique)
- Ajouter logs détaillés
- Corriger les bugs TLS identifiés
- Maintenir un fork privé
Avantages :
- ✅ Garde l'API sync
- ✅ Contrôle total sur les correctifs
- ✅ Peut merger les améliorations de upstream
Inconvénients :
- ⚠️ Maintenance du fork à long terme
- ⚠️ Doit suivre les mises à jour de rustls
Option 3 : 🔄 Rendre rust-cast asynchrone
Effort : ÉLEVÉ (27-45 heures)
Actions :
- Migrer vers async-rustls ou tokio-rustls
- Rendre tout le code async (MessageManager, channels, CastDevice)
- Adapter PMOMusic pour wrapper les appels async
Avantages :
- ✅ Architecture moderne
- ✅ Potentiellement meilleure performance pour gérer plusieurs devices
- ✅ Résout probablement les problèmes TLS via stack moderne
Inconvénients :
- ❌ Effort très élevé
- ❌ Risque de bugs subtils
- ❌ PMOMusic doit wrapper tous les appels avec
smol::block_on() - ❌ Overhead de conversion sync→async→sync
⚠️ PARADOXE : Rendre rust-cast async pour ensuite le wrapper en sync dans PMOMusic = surcharge inutile
Option 4 : ❌ Migrer vers cast-sender (déjà async)
Effort : TRÈS ÉLEVÉ (40-80 heures)
Problèmes critiques :
- ❌ API incomplète (pas de get_status, pas de seek)
- ❌ Nécessite architecture stateful complexe
- ❌ Documentation insuffisante (23%)
Voir : cast-sender-evaluation.md
5. Analyse détaillée : Async est-il vraiment utile ?
5.1 Cas d'usage PMOMusic
Architecture actuelle :
- 1 thread par Chromecast actif (pour le heartbeat)
- Opérations de contrôle (play, pause, volume) : sporadiques
- Pas de gestion massive de connexions simultanées
Bénéfice de async :
- ❌ FAIBLE : PMOMusic n'a pas besoin de gérer 100+ connexions simultanées
- ❌ OVERHEAD : Wrapping sync→async→sync ajoute de la complexité
5.2 Vraie cause des problèmes TLS ?
Les problèmes de "fermeture TLS prématurée" sont probablement dus à :
- Timeout réseau trop court
- Gestion d'erreur insuffisante lors des reconnexions
- Bugs spécifiques de certaines versions de rustls
Async ne résout PAS directement ces problèmes !
6. Recommandation finale
🏆 Option recommandée : Option 1 (Corriger rust-cast sync)
Raisons :
- Effort minimal : 2-8 heures vs 27-45h pour async
- Risque minimal : Garde l'architecture validée
- Compatibilité : Pas de changement dans PMOMusic
- Pragmatique : Résout le problème réel (TLS) sans over-engineering
Plan d'action concret :
// Améliorer la fonction connect_to_device
fn connect_to_device(host: &str, port: u16) -> Result<CastDevice> {
const MAX_RETRIES: u32 = 3;
const RETRY_DELAY_MS: u64 = 1000;
for attempt in 1..=MAX_RETRIES {
match try_connect(host, port) {
Ok(device) => return Ok(device),
Err(e) if attempt < MAX_RETRIES => {
tracing::warn!(
"Connection attempt {} failed: {}. Retrying in {}ms...",
attempt, e, RETRY_DELAY_MS
);
std::thread::sleep(Duration::from_millis(RETRY_DELAY_MS));
}
Err(e) => return Err(e),
}
}
unreachable!()
}
// Ajouter timeout configurable pour les read operations
// Ajouter meilleure gestion d'erreur dans le heartbeat loop
🥈 Alternative : Option 2 (Fork rust-cast)
Si l'Option 1 ne suffit pas après investigation, forker permet :
- Corrections TLS plus profondes
- Ajout de fonctionnalités manquantes
- Contrôle total
Pas besoin de rendre async !
🚫 Options déconseillées :
- ❌ Option 3 (Async rust-cast) : Effort 5-10x supérieur pour bénéfice marginal
- ❌ Option 4 (cast-sender) : API incomplète, effort encore plus élevé
7. Conclusion
NON, rendre rust-cast asynchrone n'est PAS plus simple.
Comparaison des efforts :
| Option | Effort (heures) | Complexité | Risque |
|---|---|---|---|
| 1. Corriger rust-cast sync | 2-8 | Faible | Minimal |
| 2. Forker rust-cast | 8-16 | Moyenne | Faible |
| 3. Async rust-cast | 27-45 | Élevée | Élevé |
| 4. Migrer cast-sender | 40-80 | Très élevée | Très élevé |
Le ratio effort/bénéfice de l'option async est défavorable :
- 5-10x plus d'effort que corriger le code sync
- Bénéfice minimal pour l'architecture actuelle de PMOMusic
- Risques élevés de bugs de concurrence async
Recommandation : Commencer par l'Option 1, investiguer les vrais problèmes TLS, et envisager l'Option 2 (fork) uniquement si nécessaire. Éviter absolument l'Option 3 (async) sauf changement radical d'architecture de PMOMusic.
Annexe : Si vous vouliez quand même faire async...
Stack recommandée
Pour PMOMusic (déjà avec smol) :
[dependencies]
async-io = "2.3"
async-rustls = "0.4"
futures-lite = "2.1"
Points d'attention :
- Remplacer tous les
Mutexparasync_lock::Mutex - Gérer correctement le
Unpintrait pour les streams - Tester intensivement la gestion des erreurs async
- Prévoir 2-3 semaines de développement + tests
Mais encore une fois : le jeu n'en vaut pas la chandelle !