Files
pyramid/lib/core/ice_servers.dart
Bernd Steckmeister 25ed765a03 wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.)
als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen
Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
2026-07-03 05:47:18 +02:00

66 lines
2.2 KiB
Dart

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<Map<String, dynamic>> _fallback = [
{
'urls': [
'stun:stun.l.google.com:19302',
'stun:stun.cloudflare.com:3478',
],
},
];
static List<Map<String, dynamic>>? _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<List<Map<String, dynamic>>> 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>()
.map((e) => e.cast<String, dynamic>())
.toList();
if (list.isNotEmpty) {
_cached = list;
_fetchedAt = DateTime.now();
return list;
}
}
} catch (_) {
// Offline/Endpoint fehlt — Fallback unten.
}
return _cached ?? _fallback;
}
}