💄 Optimized voice chat
This commit is contained in:
parent
34dee3773d
commit
541df5c3bc
@ -24,8 +24,8 @@
|
||||
android:name="de.julianassmann.flutter_background.IsolateHolderService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
@ -147,12 +147,12 @@ class _ChatCallState extends State<ChatCall> {
|
||||
|
||||
void autoPublish() async {
|
||||
try {
|
||||
if (_enableVideo) await _callRoom.localParticipant?.setCameraEnabled(true);
|
||||
if(_enableVideo) await _callRoom.localParticipant?.setCameraEnabled(true);
|
||||
} catch (error) {
|
||||
await context.showErrorDialog(error);
|
||||
}
|
||||
try {
|
||||
if (_enableAudio) await _callRoom.localParticipant?.setMicrophoneEnabled(true);
|
||||
if(_enableAudio) await _callRoom.localParticipant?.setMicrophoneEnabled(true);
|
||||
} catch (error) {
|
||||
await context.showErrorDialog(error);
|
||||
}
|
||||
@ -204,17 +204,23 @@ class _ChatCallState extends State<ChatCall> {
|
||||
List<ParticipantTrack> userMediaTracks = [];
|
||||
List<ParticipantTrack> screenTracks = [];
|
||||
for (var participant in _callRoom.remoteParticipants.values) {
|
||||
for (var t in participant.videoTrackPublications) {
|
||||
for (var t in participant.trackPublications.values) {
|
||||
if (t.isScreenShare) {
|
||||
screenTracks.add(ParticipantTrack(
|
||||
participant: participant,
|
||||
videoTrack: t.track,
|
||||
videoTrack: t.track as VideoTrack,
|
||||
isScreenShare: true,
|
||||
));
|
||||
} else if (t.track is VideoTrack) {
|
||||
userMediaTracks.add(ParticipantTrack(
|
||||
participant: participant,
|
||||
videoTrack: t.track as VideoTrack,
|
||||
isScreenShare: false,
|
||||
));
|
||||
} else {
|
||||
userMediaTracks.add(ParticipantTrack(
|
||||
participant: participant,
|
||||
videoTrack: t.track,
|
||||
videoTrack: null,
|
||||
isScreenShare: false,
|
||||
));
|
||||
}
|
||||
@ -247,19 +253,25 @@ class _ChatCallState extends State<ChatCall> {
|
||||
return a.participant.joinedAt.millisecondsSinceEpoch - b.participant.joinedAt.millisecondsSinceEpoch;
|
||||
});
|
||||
|
||||
final localParticipantTracks = _callRoom.localParticipant?.videoTrackPublications;
|
||||
final localParticipantTracks = _callRoom.localParticipant?.trackPublications.values;
|
||||
if (localParticipantTracks != null) {
|
||||
for (var t in localParticipantTracks) {
|
||||
if (t.isScreenShare) {
|
||||
screenTracks.add(ParticipantTrack(
|
||||
participant: _callRoom.localParticipant!,
|
||||
videoTrack: t.track,
|
||||
videoTrack: t.track as VideoTrack,
|
||||
isScreenShare: true,
|
||||
));
|
||||
} else if (t.track is VideoTrack) {
|
||||
userMediaTracks.add(ParticipantTrack(
|
||||
participant: _callRoom.localParticipant!,
|
||||
videoTrack: t.track as VideoTrack,
|
||||
isScreenShare: false,
|
||||
));
|
||||
} else {
|
||||
userMediaTracks.add(ParticipantTrack(
|
||||
participant: _callRoom.localParticipant!,
|
||||
videoTrack: t.track,
|
||||
videoTrack: null,
|
||||
isScreenShare: false,
|
||||
));
|
||||
}
|
||||
@ -363,7 +375,6 @@ class _ChatCallState extends State<ChatCall> {
|
||||
Widget build(BuildContext context) {
|
||||
return IndentWrapper(
|
||||
title: AppLocalizations.of(context)!.chatCall,
|
||||
noSafeArea: true,
|
||||
hideDrawer: true,
|
||||
child: FutureBuilder(
|
||||
future: exchangeToken(),
|
||||
@ -377,15 +388,14 @@ class _ChatCallState extends State<ChatCall> {
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
child: _participantTracks.isNotEmpty
|
||||
? ParticipantWidget.widgetFor(_participantTracks.first)
|
||||
: Container(),
|
||||
),
|
||||
if (_callRoom.localParticipant != null)
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: ControlsWidget(_callRoom, _callRoom.localParticipant!),
|
||||
)
|
||||
),
|
||||
if (_callRoom.localParticipant != null) ControlsWidget(_callRoom, _callRoom.localParticipant!),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
@ -398,7 +408,7 @@ class _ChatCallState extends State<ChatCall> {
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: math.max(0, _participantTracks.length - 1),
|
||||
itemBuilder: (BuildContext context, int index) => SizedBox(
|
||||
width: 180,
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: ParticipantWidget.widgetFor(_participantTracks[index + 1]),
|
||||
),
|
||||
@ -423,8 +433,8 @@ class _ChatCallState extends State<ChatCall> {
|
||||
WakelockPlus.disable();
|
||||
(() async {
|
||||
_callRoom.removeListener(onRoomDidUpdate);
|
||||
await _callRoom.disconnect();
|
||||
await _callListener.dispose();
|
||||
await _callRoom.disconnect();
|
||||
await _callRoom.dispose();
|
||||
})();
|
||||
super.dispose();
|
||||
|
@ -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,23 +109,19 @@ 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,
|
||||
),
|
||||
Widget build(BuildContext ctx) {
|
||||
return Container(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Video
|
||||
@ -131,7 +132,10 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
||||
_activeVideoTrack!,
|
||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
)
|
||||
: const NoVideoWidget(),
|
||||
: NoContentWidget(
|
||||
userinfo: _userinfoMetadata,
|
||||
isSpeaking: widget.participant.isSpeaking,
|
||||
),
|
||||
),
|
||||
if (widget.showStatsLayer)
|
||||
Positioned(
|
||||
@ -139,7 +143,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
||||
right: 30,
|
||||
child: ParticipantStatsWidget(
|
||||
participant: widget.participant,
|
||||
)),
|
||||
),
|
||||
),
|
||||
// Bottom bar
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
@ -152,8 +157,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
||||
title: widget.participant.name.isNotEmpty
|
||||
? '${widget.participant.name} (${widget.participant.identity})'
|
||||
: widget.participant.identity,
|
||||
audioAvailable:
|
||||
_firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
|
||||
audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
|
||||
connectionQuality: widget.participant.connectionQuality,
|
||||
isScreenShare: widget.isScreenShare,
|
||||
),
|
||||
@ -163,6 +167,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -30,5 +30,11 @@
|
||||
<string>public.app-category.social-networking</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Allow you add photo to your message or post</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Allow you take photo/video for your message or post</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Allow you record audio for your message or post</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -14,6 +14,8 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array/>
|
||||
</dict>
|
||||
|
Loading…
Reference in New Issue
Block a user