💄 Optimized voice chat
This commit is contained in:
@ -294,7 +294,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
value: null,
|
||||
onTap: disableVideo,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.videocam_off, color: Colors.white),
|
||||
leading: const Icon(Icons.videocam_off),
|
||||
title: Text(AppLocalizations.of(context)!.chatCallVideoOff),
|
||||
),
|
||||
),
|
||||
|
75
lib/widgets/chat/call/no_content.dart
Normal file
75
lib/widgets/chat/call/no_content.dart
Normal file
@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/widgets/account/avatar.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class NoContentWidget extends StatefulWidget {
|
||||
final Account? userinfo;
|
||||
final bool isSpeaking;
|
||||
|
||||
const NoContentWidget({super.key, this.userinfo, required this.isSpeaking});
|
||||
|
||||
@override
|
||||
State<NoContentWidget> createState() => _NoContentWidgetState();
|
||||
}
|
||||
|
||||
class _NoContentWidgetState extends State<NoContentWidget> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _animationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(NoContentWidget old) {
|
||||
super.didUpdateWidget(old);
|
||||
if (widget.isSpeaking) {
|
||||
_animationController.repeat(reverse: true);
|
||||
} else {
|
||||
_animationController.animateTo(0, duration: 300.ms).then((_) => _animationController.reset());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final radius = math.min(MediaQuery.of(context).size.width, MediaQuery.of(context).size.height) * 0.1;
|
||||
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: Center(
|
||||
child: Animate(
|
||||
autoPlay: false,
|
||||
controller: _animationController,
|
||||
effects: [
|
||||
CustomEffect(
|
||||
begin: widget.isSpeaking ? 2 : 0,
|
||||
end: 8,
|
||||
curve: Curves.easeInOut,
|
||||
duration: 1250.ms,
|
||||
builder: (context, value, child) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(radius + 8)),
|
||||
border: value > 0 ? Border.all(color: Colors.green, width: value) : null,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
)
|
||||
],
|
||||
child: AccountAvatar(
|
||||
source: widget.userinfo!.avatar,
|
||||
radius: radius,
|
||||
direct: true,
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class NoVideoWidget extends StatelessWidget {
|
||||
const NoVideoWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
alignment: Alignment.center,
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) => Icon(
|
||||
Icons.videocam_off_outlined,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: math.min(constraints.maxHeight, constraints.maxWidth) * 0.3,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/call.dart';
|
||||
import 'package:solian/widgets/chat/call/no_video.dart';
|
||||
import 'package:solian/widgets/chat/call/no_content.dart';
|
||||
import 'package:solian/widgets/chat/call/participant_info.dart';
|
||||
import 'package:solian/widgets/chat/call/participant_stats.dart';
|
||||
|
||||
@ -83,6 +86,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
||||
|
||||
TrackPublication? get _firstAudioPublication;
|
||||
|
||||
Account? _userinfoMetadata;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -104,65 +109,65 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
void onParticipantChanged() => setState(() {});
|
||||
void onParticipantChanged() {
|
||||
setState(() {
|
||||
if (widget.participant.metadata != null) {
|
||||
_userinfoMetadata = Account.fromJson(jsonDecode(widget.participant.metadata!));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> extraWidgets(bool isScreenShare) => [];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext ctx) => Container(
|
||||
foregroundDecoration: BoxDecoration(
|
||||
border: widget.participant.isSpeaking && !widget.isScreenShare
|
||||
? Border.all(
|
||||
width: 5,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(ctx).cardColor,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Video
|
||||
InkWell(
|
||||
onTap: () => setState(() => _visible = !_visible),
|
||||
child: _activeVideoTrack != null && !_activeVideoTrack!.muted
|
||||
? VideoTrackRenderer(
|
||||
_activeVideoTrack!,
|
||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
)
|
||||
: const NoVideoWidget(),
|
||||
),
|
||||
if (widget.showStatsLayer)
|
||||
Positioned(
|
||||
top: 30,
|
||||
right: 30,
|
||||
child: ParticipantStatsWidget(
|
||||
participant: widget.participant,
|
||||
)),
|
||||
// Bottom bar
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...extraWidgets(widget.isScreenShare),
|
||||
ParticipantInfoWidget(
|
||||
title: widget.participant.name.isNotEmpty
|
||||
? '${widget.participant.name} (${widget.participant.identity})'
|
||||
: widget.participant.identity,
|
||||
audioAvailable:
|
||||
_firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
|
||||
connectionQuality: widget.participant.connectionQuality,
|
||||
isScreenShare: widget.isScreenShare,
|
||||
Widget build(BuildContext ctx) {
|
||||
return Container(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Video
|
||||
InkWell(
|
||||
onTap: () => setState(() => _visible = !_visible),
|
||||
child: _activeVideoTrack != null && !_activeVideoTrack!.muted
|
||||
? VideoTrackRenderer(
|
||||
_activeVideoTrack!,
|
||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
)
|
||||
: NoContentWidget(
|
||||
userinfo: _userinfoMetadata,
|
||||
isSpeaking: widget.participant.isSpeaking,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.showStatsLayer)
|
||||
Positioned(
|
||||
top: 30,
|
||||
right: 30,
|
||||
child: ParticipantStatsWidget(
|
||||
participant: widget.participant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
// Bottom bar
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...extraWidgets(widget.isScreenShare),
|
||||
ParticipantInfoWidget(
|
||||
title: widget.participant.name.isNotEmpty
|
||||
? '${widget.participant.name} (${widget.participant.identity})'
|
||||
: widget.participant.identity,
|
||||
audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
|
||||
connectionQuality: widget.participant.connectionQuality,
|
||||
isScreenShare: widget.isScreenShare,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> {
|
||||
@ -202,7 +207,6 @@ class _RemoteParticipantWidgetState extends _ParticipantWidgetState<RemotePartic
|
||||
pub: _firstAudioPublication!,
|
||||
icon: Icons.volume_up,
|
||||
),
|
||||
// Menu for RemoteTrackPublication<RemoteVideoTrack>
|
||||
if (_videoPublication != null)
|
||||
RemoteTrackPublicationMenuWidget(
|
||||
pub: _videoPublication!,
|
||||
|
@ -17,7 +17,7 @@ class ParticipantInfoWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 7,
|
||||
horizontal: 10,
|
||||
@ -31,6 +31,7 @@ class ParticipantInfoWidget extends StatelessWidget {
|
||||
child: Text(
|
||||
title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
isScreenShare
|
||||
|
Reference in New Issue
Block a user