Files
pmomusic/pmocontrol/rust-cast-async-analysis.md

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>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-rustls différente de rustls::StreamOwned → peut nécessiter refactoring profond
  • Gestion des locks async (Mutexasync_lock::Mutex ou tokio::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 :

  1. Effort minimal : 2-8 heures vs 27-45h pour async
  2. Risque minimal : Garde l'architecture validée
  3. Compatibilité : Pas de changement dans PMOMusic
  4. 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 Mutex par async_lock::Mutex
  • Gérer correctement le Unpin trait 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 !