25ed765a03
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
66 lines
2.2 KiB
Dart
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;
|
|
}
|
|
}
|