Flux video temps reel : l'iPhone pousse, MediaMTX restitue
L'iPhone n'est joignable depuis nulle part - CGNAT operateur en 4G, NAT de la box sur le WiFi du studio, UPnP indisponible. Le serveur RTSP de l'application camera est donc inutilisable de l'exterieur. Seule une connexion sortante traverse les deux NAT : c'est le telephone qui pousse. - MediaMTX en conteneur : SRT/RTMP en entree, WebRTC (WHEP) et HLS en sortie. La video ne traverse jamais la JVM - 800 kbps continus dans Spring Boot couteraient un thread par spectateur sans rien apporter. - SRT privilegie sur RTMP : RTMP est du TCP, un paquet perdu sur un lien mobile bloque tout le flux. SRT retransmet selectivement en UDP. - Lecture publique (choix assume), publication authentifiee : sans ca n'importe qui pousserait sa propre video sur le flux "biblio". - Console : client WHEP maison avec repli HLS automatique, et mise en pause quand l'onglet passe en arriere-plan. Correction d'un bug trouve en deployant : le @ExceptionHandler(Exception) avalait les exceptions que Spring sait deja traduire, si bien que toute URL inconnue renvoyait 500 au lieu de 404 - ce qui noyait les vraies pannes au milieu des 500 provoques par les crawlers. Test de regression ajoute. 35 tests au vert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
94e893af97
commit
d70892dce0
12 changed files with 827 additions and 13 deletions
104
README.md
104
README.md
|
|
@ -29,6 +29,39 @@ tests plutôt que par la discipline.
|
|||
|
||||
## Le flux
|
||||
|
||||
### Deux flux, deux chemins
|
||||
|
||||
Le système transporte deux choses de nature très différente, et elles ne
|
||||
suivent volontairement pas le même chemin :
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ iPhone (capteur) │
|
||||
└──────────┬──────────┘
|
||||
événements │ vidéo
|
||||
(quelques octets) │ (800 kbps continu)
|
||||
┌──────────┴──────────┐
|
||||
▼ ▼
|
||||
┌───────────────┐ ┌────────────────┐
|
||||
│ Spring Boot │ │ MediaMTX │
|
||||
│ Kafka · JPA │ │ SRT → WebRTC │
|
||||
│ domaine │ │ transport pur │
|
||||
└───────┬───────┘ └────────┬───────┘
|
||||
└──────────┬───────────┘
|
||||
▼
|
||||
Console web (une seule page)
|
||||
```
|
||||
|
||||
**La vidéo ne traverse jamais la JVM.** Faire transiter 800 kbps continus par
|
||||
Spring Boot n'apporterait rien et coûterait un thread par spectateur. Le média
|
||||
est un problème de transport, résolu par un service dédié de 83 Mo ; la JVM
|
||||
garde le domaine métier. Les deux se rejoignent seulement dans le navigateur.
|
||||
|
||||
Corollaire pratique : MediaMTX peut tomber sans que l'alarme cesse de
|
||||
fonctionner, et inversement.
|
||||
|
||||
### Le flux d'événements en détail
|
||||
|
||||
```
|
||||
┌──────────────────┐
|
||||
│ iPhone 6s │ Détection de mouvement
|
||||
|
|
@ -166,6 +199,74 @@ implémentation de `AlarmReadModelPort`.
|
|||
|
||||
---
|
||||
|
||||
## Le flux vidéo
|
||||
|
||||
### Le téléphone pousse, on ne l'interroge pas
|
||||
|
||||
C'est la contrainte qui a dicté toute cette partie. Un iPhone n'a **aucune
|
||||
adresse joignable depuis Internet** : en 4G il est derrière le CGNAT de
|
||||
l'opérateur, sur le WiFi du studio derrière le NAT de la box, et l'application
|
||||
signale `UPnP unavailable`. Le serveur RTSP intégré à l'application caméra est
|
||||
donc inutilisable de l'extérieur, quelle que soit la configuration.
|
||||
|
||||
La seule direction qui traverse ces deux NAT est la sortante. Le téléphone
|
||||
**pousse** son flux vers le serveur :
|
||||
|
||||
```
|
||||
iPhone ──SRT (UDP 8890)──▶ MediaMTX ──┬──▶ WebRTC < 1 s
|
||||
└─RTMP (TCP 1935)──▶ └──▶ HLS ~ 6 s
|
||||
```
|
||||
|
||||
**SRT plutôt que RTMP** quand le choix est possible : RTMP est du TCP, et sur
|
||||
un lien mobile un paquet perdu déclenche une retransmission qui bloque tout le
|
||||
reste. SRT transporte en UDP avec sa propre retransmission sélective — c'est
|
||||
exactement le protocole conçu pour ce cas.
|
||||
|
||||
**WebRTC plutôt que HLS** côté lecture : voir quelqu'un six secondes après son
|
||||
passage n'a pas le même intérêt. La console tente WebRTC (WHEP) d'abord et
|
||||
bascule sur HLS si l'UDP est filtré — ce qui arrive précisément sur les réseaux
|
||||
d'entreprise.
|
||||
|
||||
### Ce qui est protégé, ce qui ne l'est pas
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Lecture** | publique — le flux est une vitrine, c'est assumé |
|
||||
| **Publication** | authentifiée — sans ça, n'importe qui sur Internet pousserait sa propre vidéo sur le flux `biblio` |
|
||||
|
||||
### Brancher un vrai téléphone
|
||||
|
||||
Application : **IP Camera Lite** (iOS), menu *Live Push*.
|
||||
|
||||
```
|
||||
SRT srt://<serveur>:8890?streamid=publish:biblio:iphone:<mot-de-passe>
|
||||
RTMP rtmp://<serveur>:1935/biblio?user=iphone&pass=<mot-de-passe>
|
||||
```
|
||||
|
||||
Le mot de passe est généré au déploiement dans le `.env` du serveur
|
||||
(`STREAM_PUBLISH_PASSWORD`) et n'existe nulle part dans ce dépôt.
|
||||
|
||||
Pour valider la chaîne sans téléphone, une mire suffit :
|
||||
|
||||
```bash
|
||||
docker run --rm --network iot-alarm-internal jrottenberg/ffmpeg:6.1-alpine \
|
||||
-re -f lavfi -i testsrc=size=640x360:rate=25 \
|
||||
-c:v libx264 -preset ultrafast -tune zerolatency -b:v 800k -pix_fmt yuv420p \
|
||||
-f flv "rtmp://mediamtx:1935/biblio?user=iphone&pass=$PASS"
|
||||
```
|
||||
|
||||
### Le piège du reverse proxy
|
||||
|
||||
MediaMTX répond d'abord une redirection vers un chemin **absolu**
|
||||
(`/biblio/index.m3u8?cookieCheck=1`). Derrière un `handle_path /hls/*` qui
|
||||
retire le préfixe, le navigateur suivait cette redirection jusqu'à
|
||||
l'application Spring Boot et recevait du JSON là où il attendait une playlist.
|
||||
Le `header_down Location` du [snippet Caddy](deploy/Caddyfile.snippet) réécrit
|
||||
le préfixe. Symptôme trompeur : `curl -I` renvoyait un 404 sans rapport, le
|
||||
serveur HLS ne gérant simplement pas la méthode HEAD.
|
||||
|
||||
---
|
||||
|
||||
## Les décisions non évidentes
|
||||
|
||||
Les choix ci-dessous ne sont pas les plus courts. Ils sont justifiés ici parce
|
||||
|
|
@ -259,6 +360,8 @@ mvn verify -Pintegration # Testcontainers (Kafka + PostgreSQL réels)
|
|||
| `GET` | `/api/v1/alarm/status` | **Requête** — état courant |
|
||||
| `GET` | `/api/v1/alerts?limit=50` | **Requête** — historique |
|
||||
| `WS` | `/ws` → `/topic/alerts` | Push temps réel (STOMP) |
|
||||
| `POST` | `/webrtc/biblio/whep` | Négociation WebRTC (servi par MediaMTX) |
|
||||
| `GET` | `/hls/biblio/index.m3u8` | Repli HLS (servi par MediaMTX) |
|
||||
|
||||
Documentation OpenAPI : `/swagger-ui.html`
|
||||
|
||||
|
|
@ -310,6 +413,7 @@ services.
|
|||
| Messagerie | Apache Kafka 3.9 (KRaft) |
|
||||
| Persistance | PostgreSQL 16 · Spring Data JPA · Flyway |
|
||||
| Temps réel | WebSocket / STOMP / SockJS |
|
||||
| Vidéo | MediaMTX · SRT / RTMP en entrée · WebRTC (WHEP) / HLS en sortie |
|
||||
| Build | Maven · Docker multi-stage |
|
||||
| Tests | JUnit 5 · AssertJ · **ArchUnit** · Testcontainers |
|
||||
| Observabilité | Actuator · Micrometer / Prometheus |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue