ffd70caffd
Nach docs/STREAMING_60FPS.md: neue Presets 720p/1080p mit 60 FPS und hoher Bitrate (5/8 Mbit/s), Preset-Aufloesung+FPS werden jetzt bis in die Capture-Constraints durchgereicht (vorher blieb der LiveKit-Default 1080p/15 aktiv), 60-FPS-Presets publishen ohne Simulcast mit DegradationPreference.maintainFramerate (unter Last Aufloesung statt Frames opfern). contentHint 'motion' ist in flutter_webrtc 1.4.1 aus Dart nicht setzbar (geprueft) - im Code dokumentiert. 4 neue Vertragstests fuer die Preset-Tabellen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
134 lines
5.2 KiB
Dart
134 lines
5.2 KiB
Dart
part of '../voice_channel.dart';
|
|
|
|
void _handleLiveKitScreenShare(BuildContext context, VoiceChannelService call) async {
|
|
if (call.isScreenSharing) {
|
|
await call.stopScreenShare();
|
|
} else {
|
|
final source = await ScreenSharePicker.show(context);
|
|
if (source != null) await call.startScreenShare(source.id);
|
|
}
|
|
}
|
|
|
|
void _showLiveKitQualityMenu(BuildContext context, VoiceChannelService call, Offset pos) {
|
|
final pt = PyramidTheme.of(context);
|
|
showMenu(
|
|
context: context,
|
|
position: RelativeRect.fromLTRB(pos.dx - 210, pos.dy - 350, pos.dx, pos.dy),
|
|
color: pt.bg2,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(pt.rBase)),
|
|
items: <PopupMenuEntry>[
|
|
const PopupMenuItem(enabled: false, height: 28, child: Text('SENDEN', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Colors.grey))),
|
|
for (final q in [
|
|
('sd', 'SD', '360p'),
|
|
('hd', 'HD', '720p'),
|
|
('hd60', 'HD 60fps', '720p·60'),
|
|
('fhd', 'Full HD', '1080p'),
|
|
('fhd60', 'Full HD 60fps', '1080p·60'),
|
|
('4k', '4K', '2160p'),
|
|
])
|
|
PopupMenuItem(onTap: () => call.changeQuality(q.$1), child: _QualityItem(label: q.$2, hint: q.$3, active: call.currentQualityKey == q.$1, pt: pt)),
|
|
const PopupMenuDivider(),
|
|
const PopupMenuItem(enabled: false, height: 28, child: Text('EMPFANGEN', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Colors.grey))),
|
|
for (final q in [('auto', 'Auto', ''), ('high', 'Hoch', 'FHD'), ('medium', 'Mittel', 'HD'), ('low', 'Niedrig', 'SD')])
|
|
PopupMenuItem(onTap: () => call.setSubscribeQuality(q.$1), child: _QualityItem(label: q.$2, hint: q.$3, active: call.subscribeQualityKey == q.$1, pt: pt)),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _showVoipQualityMenu(BuildContext context, CallSignalingService voip, Offset pos) {
|
|
final pt = PyramidTheme.of(context);
|
|
showMenu(
|
|
context: context,
|
|
position: RelativeRect.fromLTRB(pos.dx - 210, pos.dy - 200, pos.dx, pos.dy),
|
|
color: pt.bg2,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(pt.rBase)),
|
|
items: <PopupMenuEntry>[
|
|
const PopupMenuItem(enabled: false, height: 28, child: Text('SENDEN', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: Colors.grey))),
|
|
for (final q in [('sd', 'SD', '360p'), ('hd', 'HD', '720p'), ('fhd', 'Full HD', '1080p'), ('4k', '4K', '2160p')])
|
|
PopupMenuItem(onTap: () => voip.changeQuality(q.$1), child: _QualityItem(label: q.$2, hint: q.$3, active: voip.currentQualityKey == q.$1, pt: pt)),
|
|
],
|
|
);
|
|
}
|
|
|
|
class _QualityItem extends StatelessWidget {
|
|
final String label;
|
|
final String? hint;
|
|
final bool active;
|
|
final PyramidTheme pt;
|
|
const _QualityItem({required this.label, required this.active, required this.pt, this.hint});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(children: [
|
|
if (active) Icon(Icons.check_rounded, size: 16, color: pt.accent) else const SizedBox(width: 16),
|
|
const SizedBox(width: 8),
|
|
Text(label, style: TextStyle(color: active ? pt.accent : pt.fg, fontSize: 13, fontWeight: active ? FontWeight.bold : FontWeight.normal)),
|
|
if (hint != null && hint!.isNotEmpty) ...[const Spacer(), Text(hint!, style: TextStyle(color: pt.fgMuted, fontSize: 11))],
|
|
]);
|
|
}
|
|
}
|
|
|
|
class _VoiceCtrlBtn extends StatefulWidget {
|
|
final IconData icon;
|
|
final PyramidTheme pt;
|
|
final bool active;
|
|
final bool danger;
|
|
final bool isDanger;
|
|
final VoidCallback? onTap;
|
|
|
|
const _VoiceCtrlBtn({required this.icon, required this.pt, this.onTap, this.active = false, this.danger = false, this.isDanger = false});
|
|
|
|
@override
|
|
State<_VoiceCtrlBtn> createState() => _VoiceCtrlBtnState();
|
|
}
|
|
|
|
class _VoiceCtrlBtnState extends State<_VoiceCtrlBtn> {
|
|
bool _hovered = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final pt = widget.pt;
|
|
Color bg;
|
|
Color iconColor;
|
|
if (widget.isDanger) { bg = pt.danger; iconColor = Colors.white; }
|
|
else if (widget.danger) { bg = pt.danger.withAlpha(30); iconColor = pt.danger; }
|
|
else if (widget.active) { bg = pt.accent.withAlpha(30); iconColor = pt.accent; }
|
|
else { bg = _hovered ? pt.bgHover : pt.bg3; iconColor = pt.fg; }
|
|
|
|
return MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
onEnter: (_) => setState(() => _hovered = true),
|
|
onExit: (_) => setState(() => _hovered = false),
|
|
child: GestureDetector(
|
|
onTap: widget.onTap,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 150),
|
|
width: 52, height: 52,
|
|
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(26)),
|
|
child: Center(child: Icon(widget.icon, size: 20, color: iconColor)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _MirrorWrapper extends StatelessWidget {
|
|
final Widget child;
|
|
final bool isLocal;
|
|
final bool isScreenShare;
|
|
|
|
const _MirrorWrapper({required this.child, required this.isLocal, required this.isScreenShare});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (isLocal && !isScreenShare) {
|
|
return Transform(
|
|
alignment: Alignment.center,
|
|
transform: Matrix4.identity()..rotateY(3.14159),
|
|
child: child,
|
|
);
|
|
}
|
|
return child;
|
|
}
|
|
}
|