💄 Optimized voice chat
This commit is contained in:
parent
34dee3773d
commit
541df5c3bc
@ -24,8 +24,8 @@
|
|||||||
android:name="de.julianassmann.flutter_background.IsolateHolderService"
|
android:name="de.julianassmann.flutter_background.IsolateHolderService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|
||||||
android:foregroundServiceType="mediaProjection" />
|
android:foregroundServiceType="mediaProjection" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
@ -204,17 +204,23 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
List<ParticipantTrack> userMediaTracks = [];
|
List<ParticipantTrack> userMediaTracks = [];
|
||||||
List<ParticipantTrack> screenTracks = [];
|
List<ParticipantTrack> screenTracks = [];
|
||||||
for (var participant in _callRoom.remoteParticipants.values) {
|
for (var participant in _callRoom.remoteParticipants.values) {
|
||||||
for (var t in participant.videoTrackPublications) {
|
for (var t in participant.trackPublications.values) {
|
||||||
if (t.isScreenShare) {
|
if (t.isScreenShare) {
|
||||||
screenTracks.add(ParticipantTrack(
|
screenTracks.add(ParticipantTrack(
|
||||||
participant: participant,
|
participant: participant,
|
||||||
videoTrack: t.track,
|
videoTrack: t.track as VideoTrack,
|
||||||
isScreenShare: true,
|
isScreenShare: true,
|
||||||
));
|
));
|
||||||
|
} else if (t.track is VideoTrack) {
|
||||||
|
userMediaTracks.add(ParticipantTrack(
|
||||||
|
participant: participant,
|
||||||
|
videoTrack: t.track as VideoTrack,
|
||||||
|
isScreenShare: false,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
userMediaTracks.add(ParticipantTrack(
|
userMediaTracks.add(ParticipantTrack(
|
||||||
participant: participant,
|
participant: participant,
|
||||||
videoTrack: t.track,
|
videoTrack: null,
|
||||||
isScreenShare: false,
|
isScreenShare: false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -247,19 +253,25 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
return a.participant.joinedAt.millisecondsSinceEpoch - b.participant.joinedAt.millisecondsSinceEpoch;
|
return a.participant.joinedAt.millisecondsSinceEpoch - b.participant.joinedAt.millisecondsSinceEpoch;
|
||||||
});
|
});
|
||||||
|
|
||||||
final localParticipantTracks = _callRoom.localParticipant?.videoTrackPublications;
|
final localParticipantTracks = _callRoom.localParticipant?.trackPublications.values;
|
||||||
if (localParticipantTracks != null) {
|
if (localParticipantTracks != null) {
|
||||||
for (var t in localParticipantTracks) {
|
for (var t in localParticipantTracks) {
|
||||||
if (t.isScreenShare) {
|
if (t.isScreenShare) {
|
||||||
screenTracks.add(ParticipantTrack(
|
screenTracks.add(ParticipantTrack(
|
||||||
participant: _callRoom.localParticipant!,
|
participant: _callRoom.localParticipant!,
|
||||||
videoTrack: t.track,
|
videoTrack: t.track as VideoTrack,
|
||||||
isScreenShare: true,
|
isScreenShare: true,
|
||||||
));
|
));
|
||||||
|
} else if (t.track is VideoTrack) {
|
||||||
|
userMediaTracks.add(ParticipantTrack(
|
||||||
|
participant: _callRoom.localParticipant!,
|
||||||
|
videoTrack: t.track as VideoTrack,
|
||||||
|
isScreenShare: false,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
userMediaTracks.add(ParticipantTrack(
|
userMediaTracks.add(ParticipantTrack(
|
||||||
participant: _callRoom.localParticipant!,
|
participant: _callRoom.localParticipant!,
|
||||||
videoTrack: t.track,
|
videoTrack: null,
|
||||||
isScreenShare: false,
|
isScreenShare: false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -363,7 +375,6 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IndentWrapper(
|
return IndentWrapper(
|
||||||
title: AppLocalizations.of(context)!.chatCall,
|
title: AppLocalizations.of(context)!.chatCall,
|
||||||
noSafeArea: true,
|
|
||||||
hideDrawer: true,
|
hideDrawer: true,
|
||||||
child: FutureBuilder(
|
child: FutureBuilder(
|
||||||
future: exchangeToken(),
|
future: exchangeToken(),
|
||||||
@ -377,15 +388,14 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
child: _participantTracks.isNotEmpty
|
child: _participantTracks.isNotEmpty
|
||||||
? ParticipantWidget.widgetFor(_participantTracks.first)
|
? ParticipantWidget.widgetFor(_participantTracks.first)
|
||||||
: Container(),
|
: Container(),
|
||||||
),
|
),
|
||||||
if (_callRoom.localParticipant != null)
|
),
|
||||||
SafeArea(
|
if (_callRoom.localParticipant != null) ControlsWidget(_callRoom, _callRoom.localParticipant!),
|
||||||
top: false,
|
|
||||||
child: ControlsWidget(_callRoom, _callRoom.localParticipant!),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@ -398,7 +408,7 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: math.max(0, _participantTracks.length - 1),
|
itemCount: math.max(0, _participantTracks.length - 1),
|
||||||
itemBuilder: (BuildContext context, int index) => SizedBox(
|
itemBuilder: (BuildContext context, int index) => SizedBox(
|
||||||
width: 180,
|
width: 120,
|
||||||
height: 120,
|
height: 120,
|
||||||
child: ParticipantWidget.widgetFor(_participantTracks[index + 1]),
|
child: ParticipantWidget.widgetFor(_participantTracks[index + 1]),
|
||||||
),
|
),
|
||||||
@ -423,8 +433,8 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
WakelockPlus.disable();
|
WakelockPlus.disable();
|
||||||
(() async {
|
(() async {
|
||||||
_callRoom.removeListener(onRoomDidUpdate);
|
_callRoom.removeListener(onRoomDidUpdate);
|
||||||
await _callRoom.disconnect();
|
|
||||||
await _callListener.dispose();
|
await _callListener.dispose();
|
||||||
|
await _callRoom.disconnect();
|
||||||
await _callRoom.dispose();
|
await _callRoom.dispose();
|
||||||
})();
|
})();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
@ -294,7 +294,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
value: null,
|
value: null,
|
||||||
onTap: disableVideo,
|
onTap: disableVideo,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(Icons.videocam_off, color: Colors.white),
|
leading: const Icon(Icons.videocam_off),
|
||||||
title: Text(AppLocalizations.of(context)!.chatCallVideoOff),
|
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/material.dart';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:solian/models/account.dart';
|
||||||
import 'package:solian/models/call.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_info.dart';
|
||||||
import 'package:solian/widgets/chat/call/participant_stats.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;
|
TrackPublication? get _firstAudioPublication;
|
||||||
|
|
||||||
|
Account? _userinfoMetadata;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -104,23 +109,19 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
|||||||
super.didUpdateWidget(oldWidget);
|
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) => [];
|
List<Widget> extraWidgets(bool isScreenShare) => [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext ctx) => Container(
|
Widget build(BuildContext ctx) {
|
||||||
foregroundDecoration: BoxDecoration(
|
return Container(
|
||||||
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(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Video
|
// Video
|
||||||
@ -131,7 +132,10 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
|||||||
_activeVideoTrack!,
|
_activeVideoTrack!,
|
||||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||||
)
|
)
|
||||||
: const NoVideoWidget(),
|
: NoContentWidget(
|
||||||
|
userinfo: _userinfoMetadata,
|
||||||
|
isSpeaking: widget.participant.isSpeaking,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (widget.showStatsLayer)
|
if (widget.showStatsLayer)
|
||||||
Positioned(
|
Positioned(
|
||||||
@ -139,7 +143,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
|||||||
right: 30,
|
right: 30,
|
||||||
child: ParticipantStatsWidget(
|
child: ParticipantStatsWidget(
|
||||||
participant: widget.participant,
|
participant: widget.participant,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
// Bottom bar
|
// Bottom bar
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
@ -152,8 +157,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
|||||||
title: widget.participant.name.isNotEmpty
|
title: widget.participant.name.isNotEmpty
|
||||||
? '${widget.participant.name} (${widget.participant.identity})'
|
? '${widget.participant.name} (${widget.participant.identity})'
|
||||||
: widget.participant.identity,
|
: widget.participant.identity,
|
||||||
audioAvailable:
|
audioAvailable: _firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
|
||||||
_firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
|
|
||||||
connectionQuality: widget.participant.connectionQuality,
|
connectionQuality: widget.participant.connectionQuality,
|
||||||
isScreenShare: widget.isScreenShare,
|
isScreenShare: widget.isScreenShare,
|
||||||
),
|
),
|
||||||
@ -164,6 +168,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends Stat
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> {
|
class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> {
|
||||||
@override
|
@override
|
||||||
@ -202,7 +207,6 @@ class _RemoteParticipantWidgetState extends _ParticipantWidgetState<RemotePartic
|
|||||||
pub: _firstAudioPublication!,
|
pub: _firstAudioPublication!,
|
||||||
icon: Icons.volume_up,
|
icon: Icons.volume_up,
|
||||||
),
|
),
|
||||||
// Menu for RemoteTrackPublication<RemoteVideoTrack>
|
|
||||||
if (_videoPublication != null)
|
if (_videoPublication != null)
|
||||||
RemoteTrackPublicationMenuWidget(
|
RemoteTrackPublicationMenuWidget(
|
||||||
pub: _videoPublication!,
|
pub: _videoPublication!,
|
||||||
|
@ -17,7 +17,7 @@ class ParticipantInfoWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Container(
|
Widget build(BuildContext context) => Container(
|
||||||
color: Colors.black.withOpacity(0.3),
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 7,
|
vertical: 7,
|
||||||
horizontal: 10,
|
horizontal: 10,
|
||||||
@ -31,6 +31,7 @@ class ParticipantInfoWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
title!,
|
title!,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
isScreenShare
|
isScreenShare
|
||||||
|
@ -30,5 +30,11 @@
|
|||||||
<string>public.app-category.social-networking</string>
|
<string>public.app-category.social-networking</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array/>
|
<array/>
|
||||||
</dict>
|
</dict>
|
||||||
|
Loading…
Reference in New Issue
Block a user