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
14 KiB
Pyramid – Entwicklungsprotokoll
Stand: 24.04.2026
Projektübersicht
Pyramid ist ein Flutter-basierter Matrix-Client, modelliert nach einem FluffyChat-Fork, mit zwei parallelen Call-Systemen:
| System | Einsatz | Datei |
|---|---|---|
LiveKit (livekit_client) |
Gruppen-Sprachräume | lib/core/livekit_call_manager.dart |
Matrix VoIP (matrix SDK) |
1:1 Direktnachrichten | lib/core/voip_manager.dart |
Server-Infrastruktur läuft auf einem Raspberry Pi (steggi-matrix.work), LiveKit unter
livekit.steggi-matrix.work.
Erledigte Änderungen
1. LiveKit – Qualitäts-Bugs behoben (livekit_call_manager.dart)
Problem: Streamer und Zuschauer konnten die Übertragungsqualität nicht ändern.
Ursachen:
_screenShareEncoding()hatte einen hardcodiertenswitch ('balanced')— gab immer halbe Bitrate zurück, unabhängig vom eingestellten Qualitäts-Key.setSubscribeQuality('auto')sprang vorzeitig raus und setzte die Qualität nie.
Fix:
VideoEncoding _screenShareEncoding() {
return _screenShareEncodings[_localQualityKey] ??
const VideoEncoding(maxBitrate: 1_500_000, maxFramerate: 15);
}
setSubscribeQuality wendet jetzt immer die Qualität an, auch bei 'auto'.
2. LiveKit – Track-Events lösen UI-Rebuild aus (livekit_call_manager.dart)
Problem: Kamera-Feed wurde nach dem Publishen nicht angezeigt, weil kein Widget-Rebuild ausgelöst wurde.
Fix: Handler für alle Track-Events hinzugefügt:
..on<LocalTrackPublishedEvent>((_) => notifyListeners())
..on<TrackPublishedEvent>((_) => notifyListeners())
..on<TrackUnpublishedEvent>((_) => notifyListeners())
..on<TrackSubscribedEvent>((_) => notifyListeners())
..on<TrackUnsubscribedEvent>((_) => notifyListeners())
..on<ParticipantConnectedEvent>((_) => notifyListeners())
..on<ParticipantDisconnectedEvent>((_) => notifyListeners())
3. LiveKit – Optimistic Mute (livekit_call_manager.dart)
Problem: Mute-Button hatte eine spürbare Verzögerung (async API-Call).
Fix: notifyListeners() wird jetzt vor dem await aufgerufen:
Future<void> toggleMute() async {
isMuted = !isMuted;
notifyListeners(); // Sofort
await _room?.localParticipant?.setMicrophoneEnabled(!isMuted);
}
4. VoIP – Kamera-Fix (voip_manager.dart)
Problem: Kamera-Tracks wurden nicht korrekt zugewiesen, weil der Renderer bereits belegt wurde bevor Video-Daten verfügbar waren.
Fix:
- Track-Zuweisung erfolgt jetzt erst wenn
getVideoTracks().isNotEmpty— kein Renderer-Update auf leere Streams mehr. _isCameraMutedwird bei eingehenden Calls korrekt aus dem Call-Typ gesetzt:_isCameraMuted = call.type == CallType.kVoice;- Bei
kConnected-State wirdsetLocalVideoMuted(false)nochmals aufgerufen falls Kamera aktiv sein soll. - Poll-Intervall von 1500ms auf 1000ms reduziert für schnellere Feed-Erkennung.
5. VoIP – Optimistic Mic/Camera Toggle (voip_manager.dart)
Problem: toggleMic() und toggleCamera() nutzten safeAction() mit 1500ms Delay.
Fix:
toggleMic()flippt State sofort, reverted bei Fehler automatisch.toggleCamera()flippt_isCameraMutedsofort (optimistic), ruft_updateRenderers()mehrfach nach kurzen Delays auf.
6. VoIP – Desktop Speakerphone Fix (voip_manager.dart)
Problem: MissingPluginException: No implementation found for method enableSpeakerphone
auf Windows/Desktop.
Fix: Speakerphone-Callback wird im Konstruktor unterdrückt:
try {
(voip as dynamic).onConfigSpeakerphone = (bool _) async {};
} catch (_) {}
7. SQLite-Stabilität auf Windows
Problem: Sporadische "Database is locked"-Fehler durch parallele Zugriffe.
Fix: WAL-Modus aktiviert und busy_timeout gesetzt, um Locking-Konflikte zu verhindern.
8. Einheitliches Call-UI (voice_channel.dart) – Kompletter Rewrite
Die gesamte voice_channel.dart wurde neu strukturiert mit einer einheitlichen Architektur
für LiveKit und VoIP.
8a. Unified Participant Model
class _ParticipantData {
final String id, name;
final Widget? videoWidget;
final bool isSpeaking, isMuted, hasVideo, isScreenShare;
final Color avatarColor;
final dynamic originalParticipant;
}
8b. Adaptive Seitenverhältnisse (voice_channel.dart)
Problem: Video-Tiles waren auf AspectRatio(16/9) festgelegt — Hochkant-Videos vom
Handy wurden stark beschnitten und falsch dargestellt.
Fix:
_ParticipantDatabekommt einaspectRatio-Feld (Default16.0/9.0).- VoIP: echte Dimension aus
RTCVideoRenderer.videoWidth/videoHeightausgelesen. - LiveKit: echte Dimension aus
(videoTrack as dynamic).currentDimensionsausgelesen. - Alle Renderer nutzen
VideoViewFit.contain/RTCVideoViewObjectFitContainstattcover— kein Beschnitt mehr. _ParticipantGridwechselt vonGridView(childAspectRatio: 16/9)zuWrapmit individuell berechneterSizedBox(width, height)pro Tile._ParticipantStrippasst Tile-Breite an:101 * p.aspectRatiofür Landscape-Tiles.
8c. Hero/Spotlight-System
- Auto-Spotlight: Screenshares werden automatisch als Hero angezeigt
- Manuelles Pinnen: Klick auf Tile → wird Hero; erneuter Klick = unpin
- Gear-Icon im Hero-Overlay öffnet das Qualitätsmenü (hover-reveal)
- Fullscreen-Button öffnet eine eigene Fullscreen-Seite via
Navigator.push
8d. Aktive Sprecher
Pulsierende Rahmen-Animation (AnimationController) für Teilnehmer, die gerade sprechen.
8e. Teilnehmer-Sortierung
Screensharer → Kamera-an → Sprechend → Audio-only
8f. Responsive Layouts
Portrait: Hero oben (Expanded) + horizontaler Teilnehmer-Strip unten (120px) Landscape: Hero links (Expanded) + vertikale Sidebar rechts (200px)
Beide Layouts sind einklappbar (Swipe-Geste + Pfeil-Button).
8g. Zentrales Mirroring
_MirrorWrapper spiegelt nur die eigene Kamera (isLocal && !isScreenShare).
Screenshares und Remote-Teilnehmer bleiben ungespiegelt.
8h. Grid-Layout mit einheitlichen Tiles
SliverGridDelegateWithFixedCrossAxisCount:
- < 600px → 2 Spalten
- 600–900px → 3 Spalten
-
900px → 4 Spalten
8i. Steuerungs-Reihenfolge (alle Views)
[ Mic ] [ Deaf ] [ Kamera ] [ Screen ] | [ Auflegen ]
9. Mini-Call-Widget – Steuerungs-Reihenfolge (mini_call_widget.dart)
Gleiche Reihenfolge wie Haupt-Call-Ansicht: Mic → Deaf → Kamera → Screen.
10. 4K Support
4K (2160p) als Qualitätsoption in LiveKit- und VoIP-Qualitätsmenüs hinzugefügt.
11. App-Icon – Pyramid-Logo (flutter_launcher_icons.yaml)
Problem: App zeigte das Flutter-Standard-Icon statt dem Pyramid-Logo.
Fix:
flutter_launcher_icons.yamlaufassets/logo.png(goldene 3D-Pyramide, 1024px) umgestellt.- macOS-Generierung explizit aktiviert (
macos: generate: true). - Windows ICO nach
dart run flutter_launcher_iconsmit Pillow neu generiert (16/32/48/64/128/256px multi-size ICO, verhindert das 48px-Limit des Packages).
12. Lade-Animation – 3D Pyramiden-Painter (pyramid_loader.dart)
Problem: Lottie-JSON zeigte inkorrekte Farben, SVG wurde nur statisch gerendert (flutter_svg führt kein JavaScript aus).
Lösung: JavaScript-Animations-Logik aus dem Original-SVG exakt nach Dart portiert
als CustomPainter:
_ease(t, exp): kubische Ease-In/Out wie im JS-Original_yawAt(t): Yaw-Rotation mit Hold-Phase (80% Rotation, 20% Pause bei π/4)_PyramidPainter: 4 Flächen, Y-Rotation, Z-Clipping, orthografische Projektion, 20°-Tilt, Depth-Sorting (Painter's Algorithm)- Fill:
darken(accent, 0.45), Stroke:accent, strokeWidth:1.4 * (size/96) useLottie: falseist nun der Standard — kein Lottie-Asset mehr nötig
Loader Preview in den Appearance Settings (zwei 120px-Boxen: Custom Painter + Lottie-Fallback).
13. Chat-Scroll-Stabilisierung (chat_view.dart)
Problem: Der Chat zuckte und zitterte je weiter man nach oben scrollte, besonders wenn neue History nachgeladen wurde.
Ursache: DateDivider und MessageGroup waren in einem gemeinsamen Column-Widget
mit einem einzigen Key zusammengefasst. Wenn beim Nachladen von History die showDate-
Eigenschaft einer Gruppe sich änderte (z.B. weil sie nicht mehr die älteste Gruppe war),
änderte sich die Höhe des Items — direkt in dem Bereich, den der User gerade betrachtete.
Fix:
DateDividerundMessageGroupsind jetzt separate List-Items mit unabhängigen Keys:- Gruppen:
ValueKey('g:<firstEventId>') - Datumstrennlinien:
ValueKey('d:<YYYY-MM-DD>')
- Gruppen:
findChildIndexCallbacknutzt eine vorberechneteMap<String, int>für O(1)-Lookup statt O(n)-indexWhere.- 200ms Cooldown nach
requestHistoryverhindert sofortige Re-Trigger durch veraltetesmaxScrollExtent.
14. Startup-Flash Fix (app.dart)
Problem: Beim App-Start flackerte kurz die Server-Eingabe-Seite auf, bevor der
korrekte Redirect zu /app stattfand.
Ursache: isLoggedInProvider (StreamProvider) gibt beim ersten Build AsyncLoading
zurück. valueOrNull ?? false ergibt false → Router leitet sofort zu /server weiter.
Kurz danach resolvet der Stream und der Router erstellt sich neu mit isLoggedIn = true.
Fix: PyramidApp prüft isLoggedInProvider zuerst. Solange noch isLoading:
vollbild PyramidLoader statt Router — die Pyramiden-Animation überbrückt den kurzen
Moment, bis der Auth-State bekannt ist.
15. Eigenes Profilbild (user_panel.dart)
Problem: Das eigene Profilbild wurde unten links (UserPanel) nicht angezeigt — nur ein Farbverlauf-Platzhalter mit Initial-Buchstabe.
Fix:
_ownProfileProvider(FutureProvider): ruft einmaligclient.getProfileFromUserId(userID)auf und cached das Ergebnis.UserPanelzeigt nunMxcAvatarmit dem echten Profilbild.- Anzeigename kommt aus
profile.displayNamestatt nur aus der UserID. - Farbverlauf-Initial-Avatar bleibt als Fallback (
_InitialAvatar) während des Ladens oder wenn kein Bild gesetzt ist.
16. QR-Code-Verifikation (app_shell.dart, settings_modal.dart)
Problem: Geräteverifikation unterstützte nur Emoji-SAS — kein QR-Code-Flow.
Fix:
KeyVerificationMethod.qrShowzuverificationMethodsinmatrix_client.darthinzugefügt.pubspec.yaml:pretty_qr_code: ^3.6.0undqr: ^3.0.2ergänzt.- Eingehender Verifizierungsdialog (
app_shell.dart) und ausgehender Dialog (settings_modal.dart) unterstützen jetzt beide 3 neue States:askChoice→ Methodenwahl (Emoji oder QR)confirmQRScan→ QR-Code anzeigen (QrCode.fromUint8List+PrettyQrView) + "Gescannt bestätigen"-ButtonshowQRSuccess→ Erfolgsanzeige
Technischer Hinweis: Matrix QR-Codes sind Binärdaten (qrDataRawBytes: Uint8Buffer).
Rendering über QrCode.fromUint8List(data: rawBytes, errorCorrectLevel: QrErrorCorrectLevel.L).
17. Wiederherstellungsschlüssel ändern (settings_modal.dart)
Problem: Kein Feature zum Wechseln des SSSS-Wiederherstellungsschlüssels vorhanden.
Implementierung: _ChangeRecoveryKeyDialog mit Bootstrap-State-Machine:
Flow:
- Laden — Bootstrap startet,
wipeSsss(true)→newSsss()generiert neuen Schlüssel - Schlüssel anzeigen — Schlüssel selektierbar + Kopier-Button; wartet auf Nutzer
- Bestätigen — Nutzer tippt Schlüssel zur Bestätigung ein (
_ConfirmKeyBody) - Aktivieren — wartet auf Bootstrap
done(Cross-Signing + Key Backup im Hintergrund) - Fertig — Erfolgsmeldung
Kritischer Fix (BootstrapBadStateException):
newSsss()ruft interncheckCrossSigning()auf → State geht sofort zuaskWipeCrossSigning.openExistingSsss()ist nachnewSsss()nicht mehr erreichbar — wurde entfernt.- Alle Post-
newSsss-States werden ohne_step-Guards auto-advanced. - Bootstrap-
donesetzt_stepnur wenn bereits inapplying— verhindert vorzeitigen Dialog-Schluss.
Offene Punkte
⚠️ Screenshare auf Windows (LiveKit)
startScreenShare() wählt auf Windows automatisch die erste verfügbare Screen-Source.
Der ScreenSharePicker ist implementiert, muss noch verifiziert werden.
ℹ️ Profilbilder in Chats – Lazy Loading
Profilbilder in der Raumliste und im Chat werden erst beim Öffnen des jeweiligen Raums geladen (kein Preloading). Ist akzeptables Verhalten, kein Fehler.
📋 Noch nicht implementiert (Settings)
Alle erledigt in Sitzung 3 (26.04.2026): Gerät umbenennen, Alle anderen Sitzungen abmelden, Ed25519-Fingerabdruck, Blockierte Nutzer, Passwort ändern, Konto löschen, Logout-Button
💡 Nice-to-have
- QR-Express-Login (MSC4108): Desktop zeigt QR → Handy scannt → eingeloggt + verifiziert. Voraussetzung: Rendezvous-Server + SDK-Unterstützung (matrix-dart-sdk ≥ 7.x erwartet). Aktuell blockiert durch fehlende SDK-API in 6.2.0.
Geänderte Dateien (vollständig)
| Datei | Inhalt |
|---|---|
lib/core/livekit_call_manager.dart |
Quality-Bug-Fix, Track-Events, Optimistic Mute, 4K |
lib/core/voip_manager.dart |
Kamera-Fix, Optimistic Toggle, Speakerphone-Fix, 4K |
lib/features/call/voice_channel.dart |
Kompletter Rewrite: Unified Architecture, adaptive AR, Hero, Mirroring, Grid, Sortierung, Controls |
lib/features/call/mini_call_widget.dart |
Steuerungs-Reihenfolge |
lib/core/matrix_client.dart |
WAL-Modus, busy_timeout, qrShow in verificationMethods |
lib/widgets/pyramid_loader.dart |
3D CustomPainter (JS-Port), useLottie=false Standard |
lib/core/app.dart |
Startup-Flash-Fix: PyramidLoader während isLoggedInProvider lädt |
lib/features/chat/chat_view.dart |
Scroll-Stabilisierung: flache Item-Liste, O(1)-Lookup, Cooldown |
lib/features/rooms/user_panel.dart |
Eigenes Profilbild via _ownProfileProvider + MxcAvatar |
lib/widgets/settings_modal.dart |
QR-Verifikation, Wiederherstellungsschlüssel ändern, Loader-Preview |
lib/layout/app_shell.dart |
QR-Verifikation im eingehenden Verifizierungsdialog |
pubspec.yaml |
pretty_qr_code, qr hinzugefügt |
flutter_launcher_icons.yaml |
Pyramid-Logo für alle Plattformen |
windows/runner/resources/app_icon.ico |
Multi-Size ICO (16/32/48/64/128/256px) via Pillow |
assets/logo.png |
Pyramid-Logo (1024px) |
assets/pyramid-loader.json |
Lottie-Fallback (aus Ordner mit Leerzeichen verschoben) |
Backups
| Datum | Pfad |
|---|---|
| 24.04.2026 (Sitzung 1) | E:\pyramid_backup_20260424 |
| 24.04.2026 (Sitzung 2) | E:\pyramid_backup_20260424_2 |
| 26.04.2026 | E:\pyramid_backup_20260426 |