Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5204bb81bb | ||
|
|
f46cc22aae | ||
|
|
2bbb45fe19 |
+41
@@ -13,6 +13,47 @@ Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-10-08 (Teil 2) – Anrufe im Live-Test nachgestellt: Teilen, Verbindung, Sprech-Ring → 0.1.40
|
||||||
|
|
||||||
|
**Erledigt (8074928, Version 064d730, Gitea-Vorabversion v0.1.40):**
|
||||||
|
- **Neuer Live-Anruftest** `integration_test/live_call_test.dart`: App
|
||||||
|
(pyramidtest1) + zweiter Matrix-Client (pyramidtest2, kurzlebiges Gerät)
|
||||||
|
im selben Prozess, eigener unverschlüsselter Raum. Teil 1: Gegenseite ruft
|
||||||
|
an, App nimmt über den echten Knopf an; Ton beide Richtungen (Bytes),
|
||||||
|
Bildschirm der Gegenseite erscheint in der App, App teilt über den echten
|
||||||
|
Quellen-Auswähler. Teil 2: App ruft an. In `scripts/test.ps1 -Ui`.
|
||||||
|
- **Bildschirm teilen** wurde nie angezeigt: `_updateRenderers` rief ein
|
||||||
|
SDK-`getFeeds()` auf, das es nicht gibt (Fehler geschluckt). Jetzt
|
||||||
|
typisierte Zugriffe, `stopped` beachtet.
|
||||||
|
- **Verbindungsaufbau:** matrix-SDK 6.2 verwirft Kandidaten, die vor der
|
||||||
|
fertig verarbeiteten Einladung (eigener Listener je Sync, nicht
|
||||||
|
serialisiert) bzw. während `answer()` eintreffen; Windows-
|
||||||
|
`getRemoteDescription` wirft statt null. Pyramid puffert Kandidaten der
|
||||||
|
Gegenseite (`_bufferRemoteCandidates`) und reicht sie direkt an die
|
||||||
|
PeerConnection nach. Vorher im Test NIE verbunden, jetzt 0,5 s / 2,7 s.
|
||||||
|
- **SDK-Zone** in `voip_manager.dart`: unbehandelte SDK/Plugin-Fehler
|
||||||
|
werden protokolliert; `_inSdkZone` reicht Ergebnis/Fehler per Completer
|
||||||
|
über die Zonengrenze (sonst würde der Aufrufer bei Fehlern ewig warten).
|
||||||
|
- **Sprech-Ring** bei 1:1-Anrufen (audioLevel aus getStats, Hysterese);
|
||||||
|
großes Bild springt nicht mehr beim Sprechen.
|
||||||
|
- **Trenner** mitten in einer Gruppe (Inline-Variante) blieb stehen → nur mit
|
||||||
|
Ungelesenem, blendet aus, trägt den Sprungschlüssel; Live-Test Teil 3 in
|
||||||
|
`live_receipts_invite_test.dart`.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Bernd testet 0.1.40 + Testinstanz (übernimmt
|
||||||
|
`app-neu` beim nächsten Start). PC→Handy-Ton: Transport ist belegt (Bytes
|
||||||
|
fließen), offen ist, ob das PC-Mikrofon Ton liefert (Messung: alle Geräte
|
||||||
|
still; Voicemeeter) → Sprech-Ring / Mikrofontest prüfen.
|
||||||
|
|
||||||
|
**Stolperfallen:** (1) `what:`-Texte in Warte-Helfern werden beim Aufruf
|
||||||
|
ausgewertet, nicht beim Timeout – Zustände per Callback ausgeben.
|
||||||
|
(2) SDK-Mitglieder-Cache: `getParticipants()`/`getState(RoomMember)` sind
|
||||||
|
ohne Nachladen leer – nicht darauf warten. (3) Dart: Fehler überqueren
|
||||||
|
Error-Zone-Grenzen nicht – Completer verwenden. (4) Analyzer-Warnungen
|
||||||
|
beginnen ohne Einrückung („warning - …“) – beim grep beachten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-10-08 – Bernds Anruf-/Chat-Test mit PC-Testinstanz → Test-Version 0.1.39
|
## 2026-10-08 – Bernds Anruf-/Chat-Test mit PC-Testinstanz → Test-Version 0.1.39
|
||||||
|
|
||||||
**Erledigt (je ein Commit, alles gepusht):**
|
**Erledigt (je ein Commit, alles gepusht):**
|
||||||
|
|||||||
@@ -95,6 +95,53 @@ void main() {
|
|||||||
|
|
||||||
await _checkAudio(tester, voip, partner);
|
await _checkAudio(tester, voip, partner);
|
||||||
|
|
||||||
|
// Stumm über den echten Knopf → Gegenseite sieht „stumm“, Spur ist aus.
|
||||||
|
bool partnerSeesMuted() =>
|
||||||
|
partner.delegate.call?.remoteUserMediaStream?.audioMuted == true;
|
||||||
|
await tester.tap(find.byIcon(Icons.mic_rounded).first);
|
||||||
|
await _pumpUntil(tester, partnerSeesMuted,
|
||||||
|
timeout: const Duration(seconds: 15),
|
||||||
|
what: 'Gegenseite sieht: App ist stumm');
|
||||||
|
expect(voip.isMicMuted, isTrue);
|
||||||
|
expect(
|
||||||
|
voip.currentCall!.localUserMediaStream!.stream!.getAudioTracks().first.enabled,
|
||||||
|
isFalse,
|
||||||
|
reason: 'Mikrofonspur ist trotz Stumm noch an',
|
||||||
|
);
|
||||||
|
await tester.tap(find.byIcon(Icons.mic_off_rounded).first);
|
||||||
|
await _pumpUntil(tester, () => !partnerSeesMuted() && !voip.isMicMuted,
|
||||||
|
timeout: const Duration(seconds: 15),
|
||||||
|
what: 'Stumm wieder aufgehoben');
|
||||||
|
_log('Stumm/Entstummen über den Knopf: ok');
|
||||||
|
|
||||||
|
// Taub → Gegenseite nicht mehr hören UND selbst stumm; danach zurück.
|
||||||
|
await tester.tap(find.byIcon(Icons.headphones_rounded).first);
|
||||||
|
await _pumpUntil(
|
||||||
|
tester,
|
||||||
|
() => voip.isDeafened && partnerSeesMuted(),
|
||||||
|
timeout: const Duration(seconds: 15),
|
||||||
|
what: 'Taub: selbst stumm',
|
||||||
|
);
|
||||||
|
final remoteAudio = voip.currentCall!.getRemoteStreams
|
||||||
|
.expand((w) => w.stream?.getAudioTracks() ?? const <MediaStreamTrack>[]);
|
||||||
|
expect(remoteAudio, isNotEmpty);
|
||||||
|
expect(remoteAudio.every((t) => !t.enabled), isTrue,
|
||||||
|
reason: 'Ton der Gegenseite ist trotz Taub noch an');
|
||||||
|
await tester.tap(find.byIcon(Icons.headset_off_rounded).first);
|
||||||
|
await _pumpUntil(
|
||||||
|
tester,
|
||||||
|
() => !voip.isDeafened && !partnerSeesMuted() && !voip.isMicMuted,
|
||||||
|
timeout: const Duration(seconds: 15),
|
||||||
|
what: 'Taub aufgehoben, Mikro wieder an',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
voip.currentCall!.getRemoteStreams
|
||||||
|
.expand((w) => w.stream?.getAudioTracks() ?? const <MediaStreamTrack>[])
|
||||||
|
.every((t) => t.enabled),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
_log('Taub/Hören über den Knopf: ok');
|
||||||
|
|
||||||
// Gegenseite teilt Bildschirm → App zeigt ihn
|
// Gegenseite teilt Bildschirm → App zeigt ihn
|
||||||
await tester.runAsync(
|
await tester.runAsync(
|
||||||
() => partner.run(() => partner.delegate.call!.setScreensharingEnabled(true)),
|
() => partner.run(() => partner.delegate.call!.setScreensharingEnabled(true)),
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ class PyramidVoipManager extends CallSignalingService
|
|||||||
currentCall = call;
|
currentCall = call;
|
||||||
_isCameraMuted = call.type == CallType.kVoice;
|
_isCameraMuted = call.type == CallType.kVoice;
|
||||||
_speakerOn = true;
|
_speakerOn = true;
|
||||||
|
_isDeafened = false;
|
||||||
|
_isMicMuted = false;
|
||||||
_setupCallListeners(call);
|
_setupCallListeners(call);
|
||||||
_updateInternalStates();
|
_updateInternalStates();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -341,6 +343,8 @@ class PyramidVoipManager extends CallSignalingService
|
|||||||
_isRemoteScreensharing = remoteSharing;
|
_isRemoteScreensharing = remoteSharing;
|
||||||
_isScreensharing = localSharing;
|
_isScreensharing = localSharing;
|
||||||
|
|
||||||
|
if (_isDeafened) _applyDeafen(call);
|
||||||
|
|
||||||
// Ausgabelautstärke (Settings-Slider) auf den Ton der Gegenseite –
|
// Ausgabelautstärke (Settings-Slider) auf den Ton der Gegenseite –
|
||||||
// nur einmal pro Stream, _updateRenderers läuft auch per Timer.
|
// nur einmal pro Stream, _updateRenderers läuft auch per Timer.
|
||||||
if (remoteUser != null && remoteUser.id != _volumeAppliedStreamId) {
|
if (remoteUser != null && remoteUser.id != _volumeAppliedStreamId) {
|
||||||
@@ -506,17 +510,24 @@ class PyramidVoipManager extends CallSignalingService
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> toggleMic() async {
|
Future<void> toggleMic() => _setMicMuted(!_isMicMuted);
|
||||||
if (currentCall == null) return;
|
|
||||||
_isMicMuted = !_isMicMuted;
|
Future<void> _setMicMuted(bool muted) async {
|
||||||
|
final c = currentCall;
|
||||||
|
if (c == null) return;
|
||||||
|
_isMicMuted = muted;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final c = currentCall!;
|
await _inSdkZone(() => c.setMicrophoneMuted(muted));
|
||||||
await _inSdkZone(() => c.setMicrophoneMuted(_isMicMuted));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_isMicMuted = !_isMicMuted;
|
// Das SDK schaltet die Spur VOR dem Senden der Status-Info um. Schlägt
|
||||||
notifyListeners();
|
// nur das Senden fehl, ist das Mikro trotzdem (ent)stummt – früher
|
||||||
|
// wurde hier blind zurückgeschaltet, und der Knopf zeigte danach das
|
||||||
|
// Gegenteil des echten Zustands (Bernd 2026-10-08).
|
||||||
|
debugPrint('[VOIP] Stummschalten: $e');
|
||||||
}
|
}
|
||||||
|
_isMicMuted = c.isMicrophoneMuted;
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -566,8 +577,31 @@ class PyramidVoipManager extends CallSignalingService
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> toggleDeafen() async {
|
Future<void> toggleDeafen() async {
|
||||||
|
// Wie Discord: taub = Gegenseite nicht mehr hören UND selbst stumm;
|
||||||
|
// beim Aufheben den Mikro-Zustand von vorher wiederherstellen. (Früher
|
||||||
|
// setzte der Knopf nur ein Häkchen ohne Wirkung.)
|
||||||
_isDeafened = !_isDeafened;
|
_isDeafened = !_isDeafened;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
final c = currentCall;
|
||||||
|
if (c == null) return;
|
||||||
|
_applyDeafen(c);
|
||||||
|
if (_isDeafened) {
|
||||||
|
_micMutedBeforeDeafen = _isMicMuted;
|
||||||
|
if (!_isMicMuted) await _setMicMuted(true);
|
||||||
|
} else if (!_micMutedBeforeDeafen && _isMicMuted) {
|
||||||
|
await _setMicMuted(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _micMutedBeforeDeafen = false;
|
||||||
|
|
||||||
|
/// Ton der Gegenseite an/aus (deaktivierte Empfangsspur = stumm).
|
||||||
|
void _applyDeafen(CallSession call) {
|
||||||
|
for (final w in call.getRemoteStreams) {
|
||||||
|
for (final t in w.stream?.getAudioTracks() ?? const <MediaStreamTrack>[]) {
|
||||||
|
if (t.enabled == _isDeafened) t.enabled = !_isDeafened;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -131,8 +131,11 @@ class _RoomsPanelState extends ConsumerState<RoomsPanel> {
|
|||||||
// Im Mobil-Querformat (Höhe knapp) liegen Call-Balken und
|
// Im Mobil-Querformat (Höhe knapp) liegen Call-Balken und
|
||||||
// Konto-Balken nebeneinander statt übereinander.
|
// Konto-Balken nebeneinander statt übereinander.
|
||||||
Builder(builder: (context) {
|
Builder(builder: (context) {
|
||||||
|
// Klingeln (noch nicht angenommen) zählt nicht – dafür gibt es
|
||||||
|
// das Annehmen-Fenster (Bernd 2026-10-08).
|
||||||
final hasVoipCall = voip.currentCall != null &&
|
final hasVoipCall = voip.currentCall != null &&
|
||||||
voip.currentCall!.state != CallState.kEnded;
|
voip.currentCall!.state != CallState.kEnded &&
|
||||||
|
voip.currentCall!.state != CallState.kRinging;
|
||||||
final hasCallDock = call.isActive || hasVoipCall;
|
final hasCallDock = call.isActive || hasVoipCall;
|
||||||
final isLandscapeCompact =
|
final isLandscapeCompact =
|
||||||
MediaQuery.sizeOf(context).height < 500;
|
MediaQuery.sizeOf(context).height < 500;
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class _ScreenSharePickerState extends State<ScreenSharePicker> with SingleTicker
|
|||||||
padding: const EdgeInsets.fromLTRB(24, 20, 16, 8),
|
padding: const EdgeInsets.fromLTRB(24, 20, 16, 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text('Share your screen',
|
Text('Bildschirm teilen',
|
||||||
style: TextStyle(color: pt.fg, fontSize: 20, fontWeight: FontWeight.w700)),
|
style: TextStyle(color: pt.fg, fontSize: 20, fontWeight: FontWeight.w700)),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -97,8 +97,8 @@ class _ScreenSharePickerState extends State<ScreenSharePicker> with SingleTicker
|
|||||||
unselectedLabelColor: pt.fgMuted,
|
unselectedLabelColor: pt.fgMuted,
|
||||||
dividerColor: pt.border,
|
dividerColor: pt.border,
|
||||||
tabs: const [
|
tabs: const [
|
||||||
Tab(text: 'Screens'),
|
Tab(text: 'Bildschirme'),
|
||||||
Tab(text: 'Windows'),
|
Tab(text: 'Fenster'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -124,11 +124,34 @@ class _ScreenSharePickerState extends State<ScreenSharePicker> with SingleTicker
|
|||||||
border: Border(top: BorderSide(color: pt.border)),
|
border: Border(top: BorderSide(color: pt.border)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
children: [
|
||||||
|
// Bekannte WebRTC-Grenze unter Windows: Bei eingeschaltetem
|
||||||
|
// HDR liefert die Bildschirmaufnahme (DXGI) ein blasses,
|
||||||
|
// zu helles Bild – Zoom/Discord haben dasselbe. Abhilfe:
|
||||||
|
// Fenster teilen oder HDR kurz umschalten (Bernd 2026-10-08).
|
||||||
|
if (Theme.of(context).platform == TargetPlatform.windows)
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.hdr_on_rounded, size: 18, color: pt.fgDim),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'HDR an? Dann wirkt ein geteilter Bildschirm blass. '
|
||||||
|
'Besser ein Fenster teilen oder HDR kurz umschalten '
|
||||||
|
'(Win + Alt + B).',
|
||||||
|
style: TextStyle(color: pt.fgDim, fontSize: 11.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Spacer(),
|
||||||
|
const SizedBox(width: 12),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text('Cancel', style: TextStyle(color: pt.fgMuted)),
|
child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -149,7 +172,7 @@ class _SourceGrid extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (sources.isEmpty) {
|
if (sources.isEmpty) {
|
||||||
return Center(child: Text('No sources found', style: TextStyle(color: pt.fgDim)));
|
return Center(child: Text('Keine Quellen gefunden', style: TextStyle(color: pt.fgDim)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: pyramid
|
name: pyramid
|
||||||
description: A flexible Matrix client with resizable panel layout.
|
description: A flexible Matrix client with resizable panel layout.
|
||||||
publish_to: none
|
publish_to: none
|
||||||
version: 0.1.40+41
|
version: 0.1.41+42
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.11.1 <4.0.0"
|
sdk: ">=3.11.1 <4.0.0"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user