import 'dart:convert'; import 'package:http/http.dart' as http; /// Zentrale Quelle für ICE-Server (STUN/TURN), die in ALLE WebRTC-Verbindungen /// injiziert werden (Matrix-1:1-Calls und LiveKit). /// /// Hintergrund: Der Heimserver hängt hinter CGNAT (keine Portfreigabe möglich). /// Direkte Verbindungen brauchen deshalb ICE-Hole-Punching mit STUN auf beiden /// Seiten; für harte Fälle (symmetrische Mobilfunk-NAT) können später /// TURN-Server (z. B. Cloudflare TURN, 1 TB/Monat gratis) ergänzt werden — /// dafür muss nur die ice.json auf dem Pi aktualisiert werden, ohne App-Update. class IceServers { /// Wird vom Pi ausgeliefert und kann dort per Cron mit frischen /// TURN-Credentials befüllt werden (siehe /home/steggi/matrix/ice.json). static const _remoteUrl = 'https://steggi-matrix.work/.well-known/pyramid/ice.json'; /// Fallback, falls die ice.json nicht erreichbar ist. static const List> _fallback = [ { 'urls': [ 'stun:stun.l.google.com:19302', 'stun:stun.cloudflare.com:3478', ], }, ]; static List>? _cached; static DateTime? _fetchedAt; /// Liefert die ICE-Server als WebRTC-`iceServers`-Maps /// (`{'urls': [...], 'username': ..., 'credential': ...}`). /// Ergebnis wird 30 Minuten gecached; TURN-Credentials in der ice.json /// sollten deutlich länger gültig sein. static Future>> get() async { final cached = _cached; if (cached != null && _fetchedAt != null && DateTime.now().difference(_fetchedAt!) < const Duration(minutes: 30)) { return cached; } try { final resp = await http .get(Uri.parse(_remoteUrl)) .timeout(const Duration(seconds: 4)); if (resp.statusCode == 200) { final body = jsonDecode(resp.body); final list = (body['ice_servers'] as List) .whereType() .map((e) => e.cast()) .toList(); if (list.isNotEmpty) { _cached = list; _fetchedAt = DateTime.now(); return list; } } } catch (_) { // Offline/Endpoint fehlt — Fallback unten. } return _cached ?? _fallback; } }