# Web Media Renderer - Architecture
## Vision
Système de Media Renderer pilotable à distance via UPnP, exposant un flux audio vers différents types de lecteurs physiques.
## Architecture globale en 4 parties
```mermaid
flowchart LR
A[Media Server] -->|flux audio| B[Control Point]
B -->|commandes| C[Web Media Renderer]
C -->|flux + contrôles| D[Device physique]
subgraph Devices physiques
D1[Browser]
D2[Android Auto]
D3[Apple CarPlay]
D4[Sonos multipoint]
D5[Chromecast]
end
D --> D1
D --> D2
D --> D3
D --> D4
D --> D5
```
### Rôles
1. **Media Server** - Source audio (le flux OGG-FLAC existant)
2. **Control Point** - Interface UI qui envoie les commandes (pause, play, seek, next, prev)
3. **Web Media Renderer** - Hub qui expose le flux et traduit les commandes selon le device
4. **Physical Device** - Lecteur final (browser, voiture, Sonos, Chromecast...)
## Web Media Renderer - Rôle central
```mermaid
blockdiag
{
block = Commandes UPnP
block -> "Web Media Renderer" -> Adaptation selon device
"Web Media Renderer" -> Device-specific protocols
}
```
### Rôle central: Adaptateur
Le Web Media Renderer est un **adaptateur** qui:
- **Reçoit le flux** du Media Server (OGG-FLAC)
- **Reçoit les commandes** du Control Point (UPnP)
- **Les traduit** vers les devices physiques
- **Expose une API de contrôle** commune
### Ce qui est COMMUN (factorisé)
| Layer | Description |
|-------|-------------|
| **API contrôle** | pause, resume, seek, next, prev, flush, stop |
| **Métadonnées** | /nowplaying, /metadata, /state |
| **Flux audio** | OGG-FLAC (identique pour tous) |
| **StreamType** | Continuous vs Finite |
### Ce qui est SPÉCIFIQUE (par device)
| Device | Transport | Buffer Management | Sync |
|--------|-----------|-------------------|------|
| Browser | HTTP/WebSocket | JS flush | N/A |
| Android Auto | AA API | native | varies |
| CarPlay | CP API | native | varies |
| Sonos | UPnP | none | UPnP |
| Chromecast | Cast API | none | Cast |
### Problème du buffer (Browser)
Le browser buffer cause des delais de reaction:
- **Pause**: delai de 5 secondes
- **Seek**: cherche dans le buffer, pas dans le nouveau flux
- **Next/Prev**: changement reporte
**Solutions**:
1. Web Audio API - `audioContext.suspend()/resume()` - plus petit buffer (~50ms)
2. Frontend flush buffer
3. Chaque client manage son propre buffer, Web Media Renderer juste expose API
## Implémentation actuelle
### Faits
- Flux audio OGG-FLAC ✓
- Pause/Resume ✓ (via `OggFlacStreamHandle`)
- TrackBoundary pour OGG segments
- StreamType (Continuous vs Finite)
### À faire
- seek/next/prev API
- WebSocket pour temps réel
- Metadata endpoint (/nowplaying JSON)
- MPV integration (multi-point/multi-room)
## Code actuel - Pause/Resume
```rust
// OggFlacStreamHandle - méthodes de contrôle
pub fn pause(&self) {
self.inner.is_paused.store(true, Ordering::SeqCst);
}
pub fn resume(&self) {
self.inner.is_paused.store(false, Ordering::SeqCst);
}
pub fn is_paused(&self) -> bool {
self.inner.is_paused.load(Ordering::SeqCst)
}
```
### Différences Continuous vs Finite
- **Continuous** (radio): pause -> sends silence, drops incoming chunks
- **Finite** (tracks): pause -> don't receive chunks (backpressure), loops sending silence
## Schéma d'intégration
```mermaid
sequenceDiagram
participant CP as Control Point
participant WMR as Web Media Renderer
participant FS as Flux Server
participant D as Device
CP->>WMR: pause()
WMR->>FS: commande pause
FS->>FS: pause state change
FS->>WMR: silence (continuous) / blocked (finite)
WMR->>D: flux avec silence
D-->>CP: audio joué (avec delay si buffer)
```
## Le Web Media Renderer - Adaptateur
Le rôle central du Web Media Renderer est de **convertir des ordres UPnP en actions spécifiques** selon le device cible:
```
UPnP orders → [Web Media Renderer] → Device-specific actions
```
## Browser Player - Composant web invisible
Pour s'entraîner, on peut se focaliser sur un composant web qui:
- Est **complètement invisible** (pas de UI)
- Est **télécommandable** par le Web Media Renderer
- Joue la musique dans le navigateur
### Specifications
| Requirement | Description |
|-------------|-------------|
| Invisible | Pas de UI, pas de controls, pas de visuel |
| Remote control | Reçoit commandes via WebSocket/HTTP |
| Auto-reconnect | Reconnection si stream coupé |
| Buffer management | Flush commandée |
| Audio format | OGG-FLAC stream |
### Architecture en 2 parties
| Partie | Langage | Rôle |
|--------|---------|------|
| Backend | Rust (pmoaudio-ext) | Contrôle, flux OGG-FLAC |
| Frontend | JavaScript | Player invisible dans le browser |
#### Backend (Rust)
- Expose le flux audio (`/stream`)
- API contrôle (`/pause`, `/resume`, `/seek`, `/flush`, `/stop`)
- WebSocket pour temps réel (`/ws`)
- **Reçoit les rapports de position/state**
```rust
// Endpoints existants
POST /pause
POST /resume
POST /seek?t={timestamp}
POST /flush
POST /stop
// Stream
GET /stream
// WebSocket messages REÇUS du player:
{
"type": "position",
"position_sec": 125.5,
"duration_sec": 240.0,
"state": "playing"
}
{
"type": "track",
"id": "...",
"title": "...",
"artist": "..."
}
{
"type": "ready_state",
"ready_state": "canplay"
}
```
#### Frontend (JavaScript)
Composant minimal (~100 lignes):
```javascript
class RemotePlayer {
constructor(wsUrl) {
this.ws = new WebSocket(wsUrl);
this.audio = new Audio();
this.ac = new AudioContext();
this.ws.onmessage = (e) => this.handle(e.data);
}
handle(msg) {
switch(msg.type) {
case 'stream': this.load(msg.url); break;
case 'play': this.play(); break;
case 'pause': this.pause(); break;
case 'seek': this.seek(msg.timestamp); break;
case 'flush': this.flush(); break;
case 'stop': this.stop(); break;
}
}
load(url) {
this.audio.src = url;
}
play() {
this.audio.play();
}
pause() {
this.audio.pause();
}
seek(ts) {
this.audio.currentTime = ts;
}
flush() {
// Flush buffer immediatement
this.audio.pause();
this.audio.currentTime = 0;
this.audio.src = '';
this.ac.suspend();
}
stop() {
this.flush();
}
}
```
**Usage:**
```html
```
### Fichier à créer
`pmoapp/webapp/src/services/PMOPlayer.ts`
### Endpoints HTTP
| Endpoint | Methode | Description |
|----------|---------|-------------|
| `/api/webrenderer/register` | POST | Enregistre instance |
| `/api/webrenderer/{id}/stream` | GET | Flux audio OGG-FLAC |
| `/api/webrenderer/{id}/position` | POST | Rapporte position |
| `/api/webrenderer/{id}/report` | POST | Rapporte etat player |
| `/api/webrenderer/{id}/command` | GET | Recupere commande pending |
| `/api/webrenderer/{id}` | DELETE | Desenregistre |
### Architecture
```
Player (Browser) <--HTTP--> Backend
- Report: position/state via POST /report
- Poll: command via GET /command (500ms)
- Stream: GET /stream
```
### Réactivité
Pour maximiser la réactivité:
| Technique | Impact |
|-----------|--------|
| WebSocket | Temps réel vs HTTP polling |
| AudioContext.suspend() | Buffer ~50ms au lieu de ~5s |
| Flush command | Vide le buffer immediatement |
| Native HTML5 audio | Le plus simple = le plus stable |
### Schéma
```mermaid
sequenceDiagram
participant WMR as Web Media Renderer
participant BP as Browser Player
WMR->>BP: stream(url)
BP->>BP: audio.src = url; play()
WMR->>BP: pause()
BP->>BP: audio.pause()
WMR->>BP: seek(timestamp)
BP->>BP: audio.currentTime = timestamp
WMR->>BP: flush()
BP->>BP: audioContext.suspend()
```
### Mapping orders → actions par device
| UPnP order | Browser | Android Auto | CarPlay | Sonos | Chromecast |
|-----------|---------|-------------|--------|------|-------------|
| Play | `audio.play()` | AA play | CP play | UPnP Play | Cast play |
| Pause | `audio.pause()` | AA pause | CP pause | UPnP Pause | Cast pause |
| Resume | `audio.play()` | AA play | CP play | UPnP Play | Cast play |
| Seek | `audio.currentTime=t` | AA seek | CP seek | UPnP Seek | Cast seek |
| Next | fetch new stream | AA next | CP next | UPnP Next | Cast next |
| Prev | fetch new stream | AA prev | CP prev | UPnP Prev | Cast prev |
| Flush | JS `audioContext.suspend()` | AA flush | CP flush | N/A | Cast load |
| Stop | `audio.stop()` | AA stop | CP stop | UPnP Stop | Cast stop |
### Protocole de contrôle
Le Web Media Renderer expose une API de contrôle uniforme qui est traduite selon le device:
### Commandes
| Commande | Description |
|---------|-------------|
| `play` | Lecture |
| `pause` | Pause (silence ou backpressure) |
| `resume` | Reprise |
| `seek(t)` | Seek vers timestamp t |
| `next` | Track suivante |
| `prev` | Track précédente |
| `flush` | **Flush buffer** - ordre critique pour reponse rapide |
| `stop` | Arrêt total |
### Métadonnées
| Endpoint | Description |
|----------|-------------|
| `/nowplaying` | Track actuelle, timestamp, is_paused |
| `/metadata` | TITLE, ARTIST, ALBUM, COVER |
| `/state` | État complet (position, duration, volume...) |
### Ordres spéciaux pour devices avec buffer
Pour les devices типа Android Auto, CarPlay, Browser:
- `flush` = vide le buffer immédiatement
- `stop` = arrête + flush
- Ces ordres doivent être traités en priorité
```mermaid
sequenceDiagram
participant CP as Control Point
participant WMR as Web Media Renderer
participant D as Device (Android Auto, Browser...)
CP->>WMR: flush()
Note over WMR: Priorité haute - immédiat
WMR->>D: FLUSH order
D-->>WMR: ack
WMR-->>CP: flushed
CP->>WMR: pause()
Note over WMR: Standard
WMR->>D: flux with silence
```
## multipoint/multi-room
```mermaid
flowchart TB
WMR[Web Media Renderer] -->|flux| R1[Renderer 1]
WMR -->|flux| R2[Renderer 2]
WMR -->|flux| R3[Renderer N]
R1 -->|sync| R2
R2 -->|sync| R3
```
Possibilités:
- UPnP pour renderers UPnP
- Cast API pour Chromecast
- Serveur temps réel pour sync
## Notes techniques
### AudioChunk::silence()
```rust
impl AudioChunk {
pub fn silence(frames: usize, sample_rate: u32) -> Self {
AudioChunk::I32(AudioChunkData::::silence(frames, sample_rate))
}
}
impl AudioChunkData {
pub fn silence(frames: usize, sample_rate: u32) -> Arc {
Self::new(vec![[T::ZERO; 2]; frames], sample_rate, 0.0)
}
}
```
### StreamType
```rust
pub enum StreamType {
Continuous, // radio - silence pendant pause
Finite, // tracks - backpressure pendant pause
}
```
### TrackBoundary avec StreamType
```rust
SyncMarker::TrackBoundary {
metadata: ...,
stream_type: StreamType
}
```