feat(calls): add consent-based signaling to existing ChatHub

Wire shared PostIt call signaling, account-derived relay policy and temporary TURN credentials. Keep calls disabled pending native Android/Linux media integration. Document experimental Android crash and laboratory removal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul Schneider 2026-10-26 01:00:19 +00:00
commit 5e48831d6d
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
20 changed files with 1368 additions and 16 deletions

View file

@ -2,12 +2,14 @@
## Statut et périmètre
Ce document décrit un **plan d'implémentation** : les appels ne sont pas encore
disponibles. La préférence de confidentialité est persistée et éditable dans
le profil, mais ne pilote aucun transport média à ce stade.
Ce document décrit l'architecture cible et l'implémentation commencée :
les appels ne sont pas encore disponibles. La préférence de confidentialité
est persistée et éditable dans le profil. Les premiers contrats de
signalisation sont intégrés au chat, désactivés par défaut ; aucun transport
média natif n'est encore raccordé.
Il complète la [topologie de PostIt](postit.md) et les
[mécanismes de notification existants](notifications.md).
œ
La cible couvre :
- les appels audio-vidéo bidirectionnels entre deux utilisateurs ;
@ -29,8 +31,11 @@ La cible couvre :
| Appels de groupe | SFU compatible WebRTC | Réception et redistribution sélective des flux |
SignalR transporte les messages de contrôle, **jamais les flux audio-vidéo**.
La signalisation peut utiliser l'infrastructure SignalR existante, mais les
contrats d'appel et leurs autorisations doivent être distincts du chat.
La signalisation doit réutiliser le [ChatHub existant](../../src/Yavsc.Server/Hubs/ChatHub.cs),
déjà exposé par [Yavsc.Api](../../src/Yavsc.Api/Program.cs), et ses salles de
discussion. Il n'est pas nécessaire de créer un second service de salles.
Les contrats d'appel et leurs autorisations restent à définir explicitement :
l'appartenance à une salle ne vaut pas acceptation d'un appel.
Le choix des bibliothèques natives, du SFU et des codecs effectivement
disponibles reste à valider par un prototype multi-plateforme.
@ -212,4 +217,140 @@ la signalisation des premiers appels à deux.
- Activation du masquage pendant un appel : suspension des médias,
remplacement vérifié du transport et retour d'état fidèle dans l'interface.
- Échecs de permissions, de signalisation, de TURN et du SFU visibles,
ressources caméra/micro et connexions libérées à la fin de l'appel.
ressources caméra/micro et connexions libérées à la fin de l'appel.
## Première tranche intégrée : signalisation Android et Desktop Linux
Le périmètre initial retenu est **Android + Desktop Linux**. Il n'ajoute
aucun serveur ni aucune salle : les méthodes suivantes appartiennent au
[ChatHub](../../src/Yavsc.Server/Hubs/ChatHub.Calls.cs) existant.
Les [contrats partagés](../../src/Yavsc.Abstract/Chat/Calls/CallContracts.cs)
sont utilisés par le serveur et PostIt.
| Méthode | Effet |
|---|---|
| `InviteCall(roomName, callee)` | Invite un autre compte présent dans la salle et retourne un identifiant d'appel |
| `AcceptCall(callId)` | Accepte sur un seul appareil, notifie les deux pairs et ferme l'invitation sur les autres appareils |
| `SendCallSignal(callId, signal)` | Transmet l'offre, la réponse ou un candidat ICE uniquement à l'autre appareil accepté |
| `EndCall(callId)` | Annule, refuse ou termine l'appel depuis un appareil autorisé |
Événements : `CallInvitation`, `CallAccepted`, `CallSignal(callId, signal)`
et `CallEnded`. **Accepté ne veut pas dire connecté** : l'événement accepté
exprime le consentement et fournit la politique ICE et les paramètres
STUN/TURN, pas une réussite de l'échange média.
Le serveur vérifie les comptes, la présence dans la salle et le blocage
entre utilisateurs. Les appareils d'un compte reçoivent l'invitation ;
seul le premier qui accepte peut ensuite émettre la réponse et les
candidats. L'appelant reste lié à sa connexion d'origine. Les médias
ne sont jamais diffusés au groupe SignalR de la salle.
La préférence persistée est lue à l'invitation et relue à l'acceptation.
Pour cette première version à deux, si l'un des comptes demande le
masquage, **les deux transports doivent être relayés**, règle conservatrice
plus stricte que la politique individuelle cible. L'acceptation ne peut
pas diminuer la protection annoncée à l'invitation. Sans TURN configuré,
un appel imposant le relais échoue explicitement.
La signalisation refuse les candidats non relayés, les adresses associées
non masquées et les adresses directes dans les champs SDP contrôlés lorsque
le relais est imposé. Ces contrôles de forme ne constituent pas un audit
exhaustif de toutes les extensions SDP possibles : la non-divulgation doit
encore être prouvée avec les SDP et statistiques des moteurs réellement
intégrés. Aucun SDP n'est réécrit.
L'activation du masquage ou le changement de pseudo par l'API de profil
termine l'appel existant ; le client devra libérer les médias et proposer
un nouvel appel. La renégociation transparente du transport n'est pas
implémentée. La déconnexion d'un pair sélectionné, le départ ou l'exclusion
de la salle terminent également l'appel. Pas de reprise automatique après
reconnexion : elle crée une nouvelle identité de connexion.
Les appels sont temporaires, en mémoire et **sur une seule instance API**.
Une invitation expire après 45 secondes, une session acceptée après une
heure ; les identifiants TURN expirent cinq minutes plus tard. Cette borne
initiale ne remplace pas un futur renouvellement d'identifiants pour les
appels longs. Un compte ne peut avoir qu'un appel réservé ou accepté à la
fois. Le registre est limité à 128 appels et 256 candidats par appel.
La tranche ne permet qu'une offre initiale de l'appelant et une réponse ;
les redémarrages ICE et renégociations restent à implémenter.
Configuration serveur, section `AudioVideoCalls` :
- `Enabled` : `false` par défaut, à conserver tant que les moteurs ne sont
pas validés ;
- `StunUrls` : tableau d'URI `stun:` ou `stuns:` ;
- `TurnUrls` : tableau d'URI `turn:` ou `turns:`, avec transport optionnel ;
- `TurnSharedSecret` : secret REST coturn, **uniquement côté serveur**,
à fournir par le gestionnaire de secrets ou la variable d'environnement
`AudioVideoCalls__TurnSharedSecret`, jamais dans le dépôt.
Les credentials TURN temporaires sont calculés côté serveur avec le
format REST coturn (expiration, nonce aléatoire et HMAC-SHA1). Aucun secret
de longue durée n'est envoyé aux clients. Aucun service TURN n'a été
déployé ou configuré par cette tranche.
Le [transport PostIt](../../src/PostIt/PostIt/Services/CallSignalingClient.cs)
utilise la connexion authentifiée du `ChatViewModel`, sans seconde connexion
ni interrogation HTTP. Il expose les commandes et événements typés et
signale les interruptions ; il ne capture aucun média. L'interface ne
propose pas encore de bouton d'appel ; une éventuelle invitation est
signalée comme non utilisable faute de moteur natif, sans acceptation
automatique.
### Travail natif restant avant activation
- Android : intégrer un adaptateur libwebrtc dans l'application existante,
isoler le problème JNI observé, vérifier les versions Java/natives et
conserver les diagnostics fatals utiles, sans journaliser SDP ou secrets.
- Desktop Linux : intégrer un moteur WebRTC natif dans PostIt.Desktop
avec capture réelle et rendu Avalonia ; un processus Python de test
GStreamer ne vaut pas cette intégration.
- Côté partagé : relier consentement, états réels du moteur, mise en
sourdine, arrêt et libération des ressources à la signalisation.
- Valider un appel Android ↔ Desktop Linux avec caméra avant et microphone
réels, rendu et audio distants, VP8/Opus, puis transport TURN sélectionné
dans les statistiques, sans repli direct lorsque le masquage est demandé.
Les tests de cette tranche portent sur les contrats, autorisations, courses
d'acceptation, expirations, limites, interruption et préférence persistée.
La compilation des applications Android et Desktop ne prouve pas le
fonctionnement de WebRTC natif : le crash antérieur n'est pas résolu.
## Retour sur l'essai Android du 25 octobre 2026
Le laboratoire indépendant a été supprimé ; il ne constitue pas une base
de salles de discussion ni une implémentation de PostIt. La suite doit
réutiliser le ChatHub existant pour la signalisation.
L'essai en échec opposait un téléphone Android 15 arm64 à Chromium,
en mode TURN uniquement, avec signalisation HTTP locale et TURN/TCP
accessibles par redirections USB adb. Il n'utilisait ni ChatHub ni
PostIt.Desktop Linux.
Les observations doivent être distinguées :
- Camera2, la capture audio et un premier rendu EGL local ont démarré.
Cela ne valide ni le transport ni le rendu distant.
- Le processus Android a subi un SIGABRT dans
`libjingle_peerconnection_so.so`. Une première pile passait par
`PeerConnection.nativeAddIceCandidate` ; après modification du traitement
des candidats, un arrêt natif persistait sur `network_thread`.
- Le test a attendu un échange média qui n'est jamais arrivé. Le dernier
rapport Browser indiquait `connection: new`, zéro octet audio/vidéo et
aucune paire ICE sélectionnée ; aucun rapport Android n'a validé l'appel.
- Le problème a été reproduit avec les SDK libwebrtc 150.7871.04 et
144.7559.15. L'attente des descriptions distantes, la résolution du
`sdpMid` et l'ajout asynchrone des candidats n'ont pas suffi.
Il s'agit donc d'un **crash natif pendant la tentative d'établissement**,
pas d'un refus de codec ou d'un échec TURN identifié. Le dernier appel JNI
visible ne prouve pas la cause du crash. Sans message fatal détaillé ni
pile native symbolisée, un défaut du code expérimental, de son intégration
Java/JNI ou de libwebrtc reste à départager. Rien ne démontre une
impossibilité de WebRTC sur Android ou une défaillance du ChatHub.
Les essais Browser/GStreamer réussis utilisaient des sources synthétiques
et un décodage sans affichage côté GStreamer : ils ne valident pas
PostIt.Desktop Linux, ses périphériques réels ou son rendu Avalonia.
La validation multi-plateforme demeure incomplète.