Files
pyramid/lib/features/call_ui/view/avatars_controls.dart
T
Bernd SteckmeisterandClaude Opus 5.5 441cc6ef54 fix(calls): Bildschirmübertragung ohne Absturz, Lautsprecher, Mikrofon im Hintergrund, zurück in den Anruf
Bernds Anruftest Handy↔PC (2026-10-07):
- Teilen am Handy: App "verkleinerte sich", Anruf danach weg. Ursache:
  Android 14+ verlangt vor MediaProjection einen Vordergrunddienst vom Typ
  mediaProjection; flutter_webrtc startet keinen, der Manifest-Eintrag
  verwies auf eine nicht mehr existierende Klasse. Neu: CallService
  (microphone|mediaProjection) + CallForeground: Zustimmung → Dienst
  (wartet auf echten startForeground) → Mitschnitt. Emulator-Test (Android
  16): mit Fix 1080x2400@30 läuft; Gegenprobe ohne Dienst wirft genau
  'Media projections require a foreground service ...'.
- Dienst läuft während jedes Anrufs: Mikrofon bleibt im Hintergrund aktiv,
  Benachrichtigung "Anruf läuft" führt zurück. Mikrofon-Typ nur mit
  Berechtigung, sonst schmalere Variante statt Dienst-Abbruch.
- Handy hörte nichts: SDK schaltet Sprachanrufe auf die Hörmuschel. Jetzt
  Lautsprecher als Standard (wie Discord) + Umschalter in der Anrufleiste.
- LiveKit-Teilen am Handy öffnete den PC-Quellen-Auswähler → System-Dialog.
- Im offenen Chat (Handy) Leiste "Anruf läuft · Zurück".
- Einstellungen → Sprache & Video: Mikrofontest mit Pegel (PC-Diagnose:
  das Standardgerät lieferte nur Stille – Voicemeeter/virtuelle Geräte).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 00:01:29 +02:00

190 lines
6.6 KiB
Dart

part of '../voice_channel.dart';
class _BigAvatar extends StatelessWidget {
final _ParticipantData data;
final PyramidTheme pt;
const _BigAvatar({required this.data, required this.pt});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 100, height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(colors: [data.avatarColor, data.avatarColor.withAlpha(150)], begin: Alignment.topLeft, end: Alignment.bottomRight),
shape: BoxShape.circle,
),
child: Center(child: Text(data.name.isNotEmpty ? data.name[0].toUpperCase() : '?', style: const TextStyle(color: Colors.white, fontSize: 40, fontWeight: FontWeight.bold))),
),
);
}
}
class _SmallAvatar extends StatelessWidget {
final _ParticipantData data;
final PyramidTheme pt;
const _SmallAvatar({required this.data, required this.pt});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 44, height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(colors: [data.avatarColor, data.avatarColor.withAlpha(150)], begin: Alignment.topLeft, end: Alignment.bottomRight),
shape: BoxShape.circle,
),
child: Center(child: Text(data.name.isNotEmpty ? data.name[0].toUpperCase() : '?', style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold))),
),
);
}
}
class _CallControls extends StatelessWidget {
final bool isMuted;
final bool isDeafened;
final bool isCameraOff;
final bool isScreenSharing;
final VoidCallback onToggleMic;
final VoidCallback onToggleDeafen;
final VoidCallback onToggleCam;
final VoidCallback onToggleScreen;
final VoidCallback onHangUp;
final PyramidTheme pt;
// Handy: Lautsprecher/Hörmuschel statt „Ton aus“ (Platz in der Leiste).
final bool isSpeakerOn;
final VoidCallback? onToggleSpeaker;
const _CallControls({
required this.isMuted,
required this.isDeafened,
required this.isCameraOff,
required this.isScreenSharing,
required this.onToggleMic,
required this.onToggleDeafen,
required this.onToggleCam,
required this.onToggleScreen,
required this.onHangUp,
required this.pt,
this.isSpeakerOn = true,
this.onToggleSpeaker,
});
@override
Widget build(BuildContext context) {
final platform = Theme.of(context).platform;
final showSpeaker = onToggleSpeaker != null &&
(platform == TargetPlatform.android || platform == TargetPlatform.iOS);
return SafeArea(
top: false,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
decoration: BoxDecoration(color: pt.bg1, border: Border(top: BorderSide(color: pt.border))),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_VoiceCtrlBtn(icon: isMuted ? Icons.mic_off_rounded : Icons.mic_rounded, danger: isMuted, pt: pt, onTap: onToggleMic),
const SizedBox(width: 16),
if (showSpeaker)
_VoiceCtrlBtn(
icon: isSpeakerOn ? Icons.volume_up_rounded : Icons.phone_in_talk_rounded,
active: isSpeakerOn,
pt: pt,
onTap: onToggleSpeaker!,
)
else
_VoiceCtrlBtn(icon: isDeafened ? Icons.headset_off_rounded : Icons.headphones_rounded, danger: isDeafened, pt: pt, onTap: onToggleDeafen),
const SizedBox(width: 16),
_VoiceCtrlBtn(icon: isCameraOff ? Icons.videocam_off_rounded : Icons.videocam_rounded, active: !isCameraOff, pt: pt, onTap: onToggleCam),
const SizedBox(width: 16),
_VoiceCtrlBtn(icon: Icons.screen_share_rounded, active: isScreenSharing, pt: pt, onTap: onToggleScreen),
const SizedBox(width: 24),
Container(width: 1, height: 32, color: pt.border),
const SizedBox(width: 24),
_VoiceCtrlBtn(icon: Icons.call_end_rounded, isDanger: true, pt: pt, onTap: onHangUp),
],
),
),
);
}
}
class _NoCall extends StatelessWidget {
final PyramidTheme pt;
final WidgetRef ref;
const _NoCall({required this.pt, required this.ref});
@override
Widget build(BuildContext context) {
return Container(
color: pt.bg1,
child: Column(
children: [
Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: pt.border))),
child: Row(
children: [
PyrIconBtn(
icon: const Icon(Icons.menu_rounded),
tooltip: 'Toggle sidebar',
onPressed: () => ref.read(railCollapsedProvider.notifier).update((s) => !s),
),
const SizedBox(width: 8),
Icon(Icons.headphones_rounded, size: 16, color: pt.fgMuted),
const SizedBox(width: 8),
Text('No active call', style: TextStyle(color: pt.fg, fontSize: 15, fontWeight: FontWeight.w600)),
],
),
),
Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('𐃏', style: TextStyle(fontSize: 48, color: pt.fgDim.withAlpha(100))),
const SizedBox(height: 12),
Text('Join a voice room to start a call', style: TextStyle(color: pt.fgMuted, fontSize: 14)),
],
),
),
),
],
),
);
}
}
class _FullscreenVideoPage extends StatelessWidget {
final _ParticipantData data;
const _FullscreenVideoPage({required this.data});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(children: [
Positioned.fill(
child: data.videoWidget != null
? _MirrorWrapper(
isLocal: data.id == 'local' || (data.originalParticipant is LocalParticipant),
isScreenShare: data.isScreenShare,
child: data.videoWidget!,
)
: _BigAvatar(data: data, pt: PyramidTheme.of(context)),
),
Positioned(
top: 0, right: 0,
child: SafeArea(
child: IconButton(
icon: const Icon(Icons.fullscreen_exit_rounded, color: Colors.white, size: 28),
onPressed: () => Navigator.of(context).pop(),
),
),
),
]),
);
}
}