Lenteur détection des serveurs.

This commit is contained in:
2025-12-27 17:07:18 +01:00
parent 46625c20ec
commit dec48a3130
5 changed files with 920 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
//! Simple Chromecast info retrieval test
//!
//! This is the most minimal test - just connects and gets device status.
//!
//! Usage:
//! cargo run --example chromecast_info -- <chromecast_ip>
use std::env;
use std::sync::Once;
use rust_cast::CastDevice;
const DEFAULT_DESTINATION_ID: &str = "receiver-0";
const DEFAULT_PORT: u16 = 8009;
/// Ensures the Rustls CryptoProvider is initialized exactly once.
fn ensure_crypto_provider_initialized() {
static INIT: Once = Once::new();
INIT.call_once(|| {
let _ = rustls::crypto::CryptoProvider::install_default(
rustls::crypto::aws_lc_rs::default_provider()
);
});
}
fn main() {
// Parse command line arguments
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <chromecast_ip>", args[0]);
eprintln!("\nExample:");
eprintln!(" {} 192.168.1.100", args[0]);
std::process::exit(1);
}
let chromecast_ip = &args[1];
println!("═══════════════════════════════════════════════════════");
println!(" Chromecast Info Test");
println!("═══════════════════════════════════════════════════════");
println!();
// Initialize crypto provider
ensure_crypto_provider_initialized();
// Connect to the device
println!("→ Connecting to {}:{}...", chromecast_ip, DEFAULT_PORT);
let cast_device = match CastDevice::connect_without_host_verification(chromecast_ip, DEFAULT_PORT) {
Ok(device) => {
println!(" ✓ Connected");
device
}
Err(e) => {
eprintln!(" ✗ Failed: {}", e);
std::process::exit(1);
}
};
// Connect to receiver channel
println!();
println!("→ Connecting to receiver channel...");
if let Err(e) = cast_device.connection.connect(DEFAULT_DESTINATION_ID.to_string()) {
eprintln!(" ✗ Failed: {}", e);
std::process::exit(1);
}
println!(" ✓ Channel connected");
// Send initial ping
println!();
println!("→ Sending initial ping...");
if let Err(e) = cast_device.heartbeat.ping() {
eprintln!(" ✗ Failed: {}", e);
std::process::exit(1);
}
println!(" ✓ Ping sent");
// Get receiver status
println!();
println!("→ Getting receiver status...");
match cast_device.receiver.get_status() {
Ok(status) => {
println!(" ✓ Status retrieved\n");
// Volume
if let Some(level) = status.volume.level {
println!(" Volume: {:.0}%", level * 100.0);
}
if let Some(muted) = status.volume.muted {
println!(" Muted: {}", muted);
}
// Applications
println!("\n Running applications: {}", status.applications.len());
for (i, app) in status.applications.iter().enumerate() {
println!("\n App #{}:", i + 1);
println!(" Display Name: {}", app.display_name);
println!(" App ID: {}", app.app_id);
println!(" Session ID: {}", app.session_id);
println!(" Transport ID: {}", app.transport_id);
println!(" Status: {}", app.status_text);
println!(" Namespaces: {}", app.namespaces.len());
}
if status.applications.is_empty() {
println!(" (no apps currently running)");
}
}
Err(e) => {
eprintln!(" ✗ Failed: {}", e);
std::process::exit(1);
}
}
println!();
println!("═══════════════════════════════════════════════════════");
println!(" Test completed successfully!");
println!("═══════════════════════════════════════════════════════");
}

View File

@@ -0,0 +1,284 @@
//! Simple Chromecast playback test
//!
//! This example tests basic Chromecast functionality by:
//! 1. Connecting to a Chromecast device
//! 2. Launching the DefaultMediaReceiver app
//! 3. Loading and playing a test media URL
//! 4. Maintaining the heartbeat loop
//!
//! Usage:
//! cargo run --example chromecast_playback_test -- <chromecast_ip> [media_url]
//!
//! Example:
//! cargo run --example chromecast_playback_test -- 192.168.1.100
use std::env;
use std::sync::Once;
use rust_cast::{
CastDevice, ChannelMessage,
channels::{
heartbeat::HeartbeatResponse,
media::{Media, StreamType},
receiver::CastDeviceApp,
},
};
const DEFAULT_DESTINATION_ID: &str = "receiver-0";
const DEFAULT_PORT: u16 = 8009;
// Test media URLs (public domain audio files)
const TEST_MEDIA_URL: &str = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3";
/// Ensures the Rustls CryptoProvider is initialized exactly once.
fn ensure_crypto_provider_initialized() {
static INIT: Once = Once::new();
INIT.call_once(|| {
let _ = rustls::crypto::CryptoProvider::install_default(
rustls::crypto::aws_lc_rs::default_provider()
);
println!("✓ Rustls CryptoProvider initialized");
});
}
/// Detects content type from URL path
fn detect_content_type(url: &str) -> String {
// Detect from URL path - check if path contains /flac/, /mp3/, etc.
if url.contains("/flac/") || url.contains(".flac") {
println!(" ✓ Detected FLAC from URL path");
return "audio/flac".to_string();
}
if url.contains("/mp3/") || url.contains(".mp3") {
println!(" ✓ Detected MP3 from URL path");
return "audio/mpeg".to_string();
}
if url.contains(".m4a") || url.contains(".mp4") || url.contains(".aac") {
println!(" ✓ Detected AAC/M4A from URL path");
return "audio/mp4".to_string();
}
if url.contains("/ogg/") || url.contains(".ogg") {
println!(" ✓ Detected OGG from URL path");
return "audio/ogg".to_string();
}
if url.contains(".opus") {
println!(" ✓ Detected Opus from URL path");
return "audio/opus".to_string();
}
if url.contains(".wav") {
println!(" ✓ Detected WAV from URL path");
return "audio/wav".to_string();
}
// Fallback
println!(" ⚠ Could not detect type, using audio/mpeg as fallback");
"audio/mpeg".to_string()
}
fn main() {
// Setup logging
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
// Parse command line arguments
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <chromecast_ip> [media_url]", args[0]);
eprintln!("\nExample:");
eprintln!(" {} 192.168.1.100", args[0]);
eprintln!("\nIf no media URL is provided, will use: {}", TEST_MEDIA_URL);
std::process::exit(1);
}
let chromecast_ip = &args[1];
let media_url = if args.len() > 2 {
&args[2]
} else {
TEST_MEDIA_URL
};
println!("╔════════════════════════════════════════════════════════════╗");
println!("║ Chromecast Playback Test (rust_cast) ║");
println!("╚════════════════════════════════════════════════════════════╝");
println!();
println!("Target: {}:{}", chromecast_ip, DEFAULT_PORT);
println!("Media URL: {}", media_url);
println!();
println!("Detecting media content type...");
let media_type = detect_content_type(media_url);
println!();
// Initialize crypto provider
ensure_crypto_provider_initialized();
// Step 1: Connect to the device
println!("──────────────────────────────────────────────────────────");
println!("STEP 1: Connecting to Chromecast...");
let cast_device = match CastDevice::connect_without_host_verification(chromecast_ip, DEFAULT_PORT) {
Ok(device) => {
println!("✓ Connected to Chromecast");
device
}
Err(e) => {
eprintln!("✗ Failed to connect: {}", e);
std::process::exit(1);
}
};
// Step 2: Connect to the default receiver channel
println!();
println!("STEP 2: Connecting to receiver channel...");
if let Err(e) = cast_device.connection.connect(DEFAULT_DESTINATION_ID.to_string()) {
eprintln!("✗ Failed to connect channel: {}", e);
std::process::exit(1);
}
println!("✓ Channel connected");
// Step 3: Send initial ping (CRITICAL per rust_caster.rs)
println!();
println!("STEP 3: Sending initial heartbeat ping...");
if let Err(e) = cast_device.heartbeat.ping() {
eprintln!("✗ Failed to send initial ping: {}", e);
std::process::exit(1);
}
println!("✓ Initial ping sent");
// Step 4: Get receiver status
println!();
println!("STEP 4: Getting receiver status...");
let status = match cast_device.receiver.get_status() {
Ok(status) => {
println!("✓ Receiver status obtained");
println!(" - Volume: {:.0}%", status.volume.level.unwrap_or(0.5) * 100.0);
println!(" - Muted: {}", status.volume.muted.unwrap_or(false));
println!(" - Running apps: {}", status.applications.len());
status
}
Err(e) => {
eprintln!("✗ Failed to get status: {}", e);
std::process::exit(1);
}
};
// Step 5: Launch DefaultMediaReceiver
println!();
println!("STEP 5: Launching DefaultMediaReceiver app...");
let app = match cast_device.receiver.launch_app(&CastDeviceApp::DefaultMediaReceiver) {
Ok(app) => {
println!("✓ App launched successfully");
println!(" - App ID: {}", app.app_id);
println!(" - Display Name: {}", app.display_name);
println!(" - Session ID: {}", app.session_id);
println!(" - Transport ID: {}", app.transport_id);
app
}
Err(e) => {
eprintln!("✗ Failed to launch app: {}", e);
std::process::exit(1);
}
};
// Step 6: Connect to the app's transport
println!();
println!("STEP 6: Connecting to app transport...");
if let Err(e) = cast_device.connection.connect(app.transport_id.as_str()) {
eprintln!("✗ Failed to connect to app transport: {}", e);
std::process::exit(1);
}
println!("✓ Connected to app transport: {}", app.transport_id);
// Step 7: Load the media
println!();
println!("STEP 7: Loading media...");
println!(" Content-Type: {}", media_type);
let media = Media {
content_id: media_url.to_string(),
content_type: media_type,
stream_type: StreamType::Buffered,
duration: None,
metadata: None,
};
match cast_device.media.load(
app.transport_id.as_str(),
app.session_id.as_str(),
&media,
) {
Ok(status) => {
println!("✓ Media loaded successfully!");
println!(" - Media status entries: {}", status.entries.len());
if let Some(entry) = status.entries.first() {
println!(" - Player state: {:?}", entry.player_state);
println!(" - Media session ID: {}", entry.media_session_id);
if let Some(ref media) = entry.media {
println!(" - Content ID: {}", media.content_id);
println!(" - Stream type: {:?}", media.stream_type);
}
}
}
Err(e) => {
eprintln!("✗ Failed to load media: {}", e);
std::process::exit(1);
}
}
// Step 8: Enter heartbeat loop
println!();
println!("──────────────────────────────────────────────────────────");
println!("STEP 8: Entering heartbeat loop (Ctrl+C to exit)...");
println!("──────────────────────────────────────────────────────────");
println!();
let mut heartbeat_count = 0;
let mut media_message_count = 0;
loop {
match cast_device.receive() {
Ok(ChannelMessage::Heartbeat(response)) => {
if let HeartbeatResponse::Ping = response {
heartbeat_count += 1;
println!("[Heartbeat #{:3}] Received Ping, sending Pong...", heartbeat_count);
if let Err(e) = cast_device.heartbeat.pong() {
eprintln!("✗ Failed to send pong: {}", e);
break;
}
} else {
println!("[Heartbeat] {:?}", response);
}
}
Ok(ChannelMessage::Media(response)) => {
media_message_count += 1;
println!("[Media #{:3}] {:?}", media_message_count, response);
}
Ok(ChannelMessage::Receiver(response)) => {
println!("[Receiver] {:?}", response);
}
Ok(ChannelMessage::Connection(response)) => {
println!("[Connection] {:?}", response);
}
Ok(ChannelMessage::Raw(response)) => {
println!("[Raw] Unsupported message type: {:?}", response);
}
Err(e) => {
eprintln!("✗ Error receiving message: {}", e);
break;
}
}
}
println!();
println!("──────────────────────────────────────────────────────────");
println!("Test completed.");
println!("Total heartbeats: {}", heartbeat_count);
println!("Total media messages: {}", media_message_count);
}

View File

@@ -0,0 +1,409 @@
# 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é
```bash
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` :
```rust
// 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
...
}
```
```rust
// 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
```rust
// 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) :**
```rust
use std::net::TcpStream;
use rustls::{ClientConnection, StreamOwned};
type TlsStream = StreamOwned<ClientConnection, TcpStream>;
```
**Après (async) :**
```rust
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`
```diff
- 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 :**
```rust
// 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 :**
```rust
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`
```diff
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 (`Mutex``async_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](pmocontrol/src/chromecast_renderer.rs:86-104)
- 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](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 :**
```rust
// 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) :**
```toml
[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 !**

View File

@@ -94,6 +94,12 @@ fn connect_to_device<'a>(host: &'a str, port: u16) -> Result<CastDevice<'a>> {
.connect(DEFAULT_DESTINATION_ID.to_string()) .connect(DEFAULT_DESTINATION_ID.to_string())
.map_err(|e| anyhow!("Failed to connect channel: {}", e))?; .map_err(|e| anyhow!("Failed to connect channel: {}", e))?;
// Send initial gre to establish heartbeat communication
// This is critical per rust_caster.rs example
device.heartbeat
.ping()
.map_err(|e| anyhow!("Failed to send initial heartbeat ping: {}", e))?;
Ok(device) Ok(device)
} }

View File

@@ -19,6 +19,8 @@ use crate::control_point::ControlPoint;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
use crate::model::{MediaServerEvent, RendererEvent}; use crate::model::{MediaServerEvent, RendererEvent};
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
use crate::registry::DeviceRegistryRead;
#[cfg(feature = "pmoserver")]
use async_stream::stream; use async_stream::stream;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
use axum::{ use axum::{
@@ -168,6 +170,32 @@ pub async fn renderer_events_sse(
}); });
let stream = stream! { let stream = stream! {
// INITIAL SNAPSHOT: Send Online events for all currently discovered renderers
// This ensures clients see devices that were discovered before they connected
let initial_renderers = {
let registry = control_point.registry();
let reg = registry.read().unwrap();
reg.list_renderers()
};
for info in initial_renderers {
if info.online {
let timestamp = chrono::Utc::now();
let payload = RendererEventPayload::Online {
renderer_id: info.id.0.clone(),
friendly_name: info.friendly_name.clone(),
model_name: info.model_name.clone(),
manufacturer: info.manufacturer.clone(),
timestamp,
};
if let Ok(json) = serde_json::to_string(&payload) {
yield Ok::<_, axum::Error>(Event::default().event("renderer").data(json));
}
}
}
// Then stream future events
while let Some(event) = rx_tokio.recv().await { while let Some(event) = rx_tokio.recv().await {
let timestamp = chrono::Utc::now(); let timestamp = chrono::Utc::now();
@@ -285,6 +313,32 @@ pub async fn media_server_events_sse(
}); });
let stream = stream! { let stream = stream! {
// INITIAL SNAPSHOT: Send Online events for all currently discovered media servers
// This ensures clients see servers that were discovered before they connected
let initial_servers = {
let registry = control_point.registry();
let reg = registry.read().unwrap();
reg.list_servers()
};
for info in initial_servers {
if info.online {
let timestamp = chrono::Utc::now();
let payload = MediaServerEventPayload::Online {
server_id: info.id.0.clone(),
friendly_name: info.friendly_name.clone(),
model_name: info.model_name.clone(),
manufacturer: info.manufacturer.clone(),
timestamp,
};
if let Ok(json) = serde_json::to_string(&payload) {
yield Ok::<_, axum::Error>(Event::default().event("media_server").data(json));
}
}
}
// Then stream future events
while let Some(event) = rx_tokio.recv().await { while let Some(event) = rx_tokio.recv().await {
let timestamp = chrono::Utc::now(); let timestamp = chrono::Utc::now();
@@ -370,6 +424,53 @@ pub async fn all_events_sse(State(control_point): State<Arc<ControlPoint>>) -> i
}); });
let stream = stream! { let stream = stream! {
// INITIAL SNAPSHOT: Send Online events for all currently discovered devices
// This ensures clients see devices that were discovered before they connected
let (initial_renderers, initial_servers) = {
let registry = control_point.registry();
let reg = registry.read().unwrap();
(reg.list_renderers(), reg.list_servers())
};
// Send renderer Online events
for info in initial_renderers {
if info.online {
let timestamp = chrono::Utc::now();
let renderer_payload = RendererEventPayload::Online {
renderer_id: info.id.0.clone(),
friendly_name: info.friendly_name.clone(),
model_name: info.model_name.clone(),
manufacturer: info.manufacturer.clone(),
timestamp,
};
let payload = UnifiedEventPayload::Renderer(renderer_payload);
if let Ok(json) = serde_json::to_string(&payload) {
yield Ok::<_, axum::Error>(Event::default().event("control").data(json));
}
}
}
// Send server Online events
for info in initial_servers {
if info.online {
let timestamp = chrono::Utc::now();
let server_payload = MediaServerEventPayload::Online {
server_id: info.id.0.clone(),
friendly_name: info.friendly_name.clone(),
model_name: info.model_name.clone(),
manufacturer: info.manufacturer.clone(),
timestamp,
};
let payload = UnifiedEventPayload::MediaServer(server_payload);
if let Ok(json) = serde_json::to_string(&payload) {
yield Ok::<_, axum::Error>(Event::default().event("control").data(json));
}
}
}
// Then stream future events
loop { loop {
tokio::select! { tokio::select! {
Some(event) = renderer_rx_tokio.recv() => { Some(event) = renderer_rx_tokio.recv() => {