feat: Call-Robustheit + Standalone-Voice-Channel per Klick beitretbar (M3)

Mit zwei echten Test-Accounts (zwei GUI-Instanzen via PYRAMID_PROFILE_DIR)
durchgeklickt: Beitreten, Teilnehmerliste, bidirektionaler Screenshare,
Live-Qualitaetswechsel (Full HD 60fps), sauberes Verlassen.

- rooms_list.dart: Voice-Channel in der normalen Raumliste war per Klick
  nicht beitretbar (oeffnete nur Chat) -> _toggleVoiceChannel wie Space-Liste.
- livekit_call_manager.dart: _callGeneration entwertet laufenden connect
  bei Auflegen/Wechsel (keine Geisterverbindung mit offenem Mikro);
  Fehlerpfade setzen error sichtbar statt still zu verschwinden.
- rooms_panel_core.dart: raeumt activeVoiceRoomIdProvider auf, wenn der
  Voice-Service inaktiv wird, + Fehler-SnackBar.
- test/live_call_setup_test.dart: gegateter Setup-Helfer fuer den Testraum.

UNGETESTET: echtes 2-Peer-Audio, Netz-Abbruch mitten im Call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bernd Steckmeister
2026-07-06 22:50:58 +02:00
parent ffd70caffd
commit f8f5332d90
7 changed files with 407 additions and 10 deletions
@@ -73,6 +73,12 @@ class LiveKitCallManager extends VoiceChannelService {
Timer? _statsTimer;
/// Entwertet laufende `startCall`-Verbindungsaufbauten: Wer während des
/// (mehrere Sekunden dauernden) `connect` auflegt oder den Kanal wechselt,
/// darf KEINE Geisterverbindung zurücklassen vorher blieb in dem Fall
/// das Mikro im alten Kanal offen, während die UI „nicht verbunden" zeigte.
int _callGeneration = 0;
@override
bool get isActive => _room != null || isConnecting;
@override
@@ -93,6 +99,7 @@ class LiveKitCallManager extends VoiceChannelService {
void _onRoomChanged() => notifyListeners();
void _cleanUpAfterDisconnect() {
_callGeneration++;
_stopStatsLogging();
_presenceHeartbeat?.cancel();
_presenceHeartbeat = null;
@@ -105,7 +112,9 @@ class LiveKitCallManager extends VoiceChannelService {
_room = null;
_displayName = '';
isConnecting = false;
error = null;
// `error` bewusst NICHT löschen: Ein unerwarteter Abbruch/Join-Fehler
// muss für die UI sichtbar bleiben (kein stiller Fehlerpfad). Manuelles
// Auflegen (hangUp) setzt error selbst zurück.
isMuted = false;
isCameraOff = false;
isScreenSharing = false;
@@ -162,6 +171,7 @@ class LiveKitCallManager extends VoiceChannelService {
String? matrixRoomId,
}) async {
if (isActive) return;
final gen = ++_callGeneration;
final url = 'wss://livekit.steggi-matrix.work';
@@ -223,6 +233,17 @@ class LiveKitCallManager extends VoiceChannelService {
connectOptions: ConnectOptions(rtcConfiguration: rtcConfig),
);
if (gen != _callGeneration) {
// Während des Verbindens wurde aufgelegt oder der Kanal gewechselt
// diese Verbindung gehört niemandem mehr: sofort wieder trennen,
// sonst bleibt das Mikro unsichtbar im alten Kanal offen.
room.removeListener(_onRoomChanged);
try {
await room.disconnect();
} catch (_) {}
return;
}
_room = room;
isConnecting = false;
notifyListeners();
@@ -263,7 +284,11 @@ class LiveKitCallManager extends VoiceChannelService {
..on<TrackUnpublishedEvent>((_) => notifyListeners())
..on<ParticipantConnectedEvent>((_) => notifyListeners())
..on<ParticipantDisconnectedEvent>((_) => notifyListeners())
..on<RoomDisconnectedEvent>((_) {
..on<RoomDisconnectedEvent>((event) {
// Kommt nur bei UNERWARTETEN Abbrüchen an (manuelles hangUp
// entsorgt den Listener vorher) Grund sichtbar machen statt
// still aus dem Kanal zu verschwinden.
error = 'Verbindung getrennt (${event.reason?.name ?? 'unbekannt'})';
_cleanUpAfterDisconnect();
});
@@ -318,9 +343,16 @@ class LiveKitCallManager extends VoiceChannelService {
_startStatsLogging();
} catch (e) {
debugPrint('[VOIP] Start call error: $e');
if (gen != _callGeneration) {
// Inzwischen wurde aufgelegt/neu verbunden der Zustand gehört
// schon dem neuen Aufbau, hier nichts mehr anfassen.
return;
}
isConnecting = false;
error = e.toString();
_cleanUpAfterDisconnect();
// NACH dem Aufräumen setzen, damit der Join-Fehler sichtbar bleibt.
error = 'Beitritt fehlgeschlagen: $e';
notifyListeners();
}
}
@@ -556,6 +588,8 @@ class LiveKitCallManager extends VoiceChannelService {
@override
Future<void> hangUp() async {
// Entwertet einen evtl. noch laufenden Verbindungsaufbau (s. _callGeneration).
_callGeneration++;
_stopStatsLogging();
_presenceHeartbeat?.cancel();
_presenceHeartbeat = null;