diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index aa52f2a6..64f6a2e2 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1269,25 +1269,12 @@ class ChatController extends State ); if (callType == null) return; - final success = await showFutureLoadingDialog( - context: context, - future: () => - Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials(), - ); - if (success.result != null) { - final voipPlugin = Matrix.of(context).voipPlugin; - try { - await voipPlugin!.voip.inviteToCall(room.id, callType); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toLocalizedString(context))), - ); - } - } else { - await showOkAlertDialog( - context: context, - title: L10n.of(context)!.unavailable, - okLabel: L10n.of(context)!.next, + final voipPlugin = Matrix.of(context).voipPlugin; + try { + await voipPlugin!.voip.inviteToCall(room, callType); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toLocalizedString(context))), ); } } diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 53a681d3..0db6c5fb 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -145,7 +145,7 @@ class _SpaceViewState extends State { icon: Icons.send_outlined, ), if (spaceChild != null && - (activeSpace?.canChangeStateEvent(EventTypes.spaceChild) ?? false)) + (activeSpace?.canChangeStateEvent(EventTypes.SpaceChild) ?? false)) SheetAction( key: SpaceChildContextAction.removeFromSpace, label: L10n.of(context)!.removeFromSpace, @@ -474,7 +474,7 @@ class _SpaceViewState extends State { onTap: () => _onJoinSpaceChild(spaceChild), ), if (activeSpace?.canChangeStateEvent( - EventTypes.spaceChild, + EventTypes.SpaceChild, ) == true) Material( diff --git a/lib/pages/dialer/dialer.dart b/lib/pages/dialer/dialer.dart index 9aead4b9..84e6fe92 100644 --- a/lib/pages/dialer/dialer.dart +++ b/lib/pages/dialer/dialer.dart @@ -25,13 +25,14 @@ import 'package:flutter/services.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart' hide VideoRenderer; import 'package:just_audio/just_audio.dart'; import 'package:matrix/matrix.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/voip/video_renderer.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'pip/pip_view.dart'; @@ -75,19 +76,13 @@ class _StreamView extends StatelessWidget { child: Stack( alignment: Alignment.center, children: [ - if (videoMuted) - Container( - color: Colors.transparent, - ), - if (!videoMuted) - RTCVideoView( - // yes, it must explicitly be casted even though I do not feel - // comfortable with it... - wrappedStream.renderer as RTCVideoRenderer, - mirror: mirrored, - objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, - ), - if (videoMuted) + VideoRenderer( + wrappedStream, + mirror: mirrored, + fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, + ), + if (videoMuted) ...[ + Container(color: Colors.black54), Positioned( child: Avatar( mxContent: avatarUrl, @@ -98,6 +93,7 @@ class _StreamView extends StatelessWidget { // matrixClient: matrixClient, ), ), + ], if (!isScreenSharing) Positioned( left: 4.0, @@ -159,8 +155,6 @@ class MyCallingPage extends State { return null; } - bool get speakerOn => call.speakerOn; - bool get isMicrophoneMuted => call.isMicrophoneMuted; bool get isLocalVideoMuted => call.isLocalVideoMuted; @@ -175,9 +169,6 @@ class MyCallingPage extends State { bool get connected => call.state == CallState.kConnected; - bool get mirrored => call.facingMode == 'user'; - - List get streams => call.streams; double? _localVideoHeight; double? _localVideoWidth; EdgeInsetsGeometry? _localVideoMargin; @@ -205,12 +196,12 @@ class MyCallingPage extends State { final call = this.call; call.onCallStateChanged.stream.listen(_handleCallState); call.onCallEventChanged.stream.listen((event) { - if (event == CallEvent.kFeedsChanged) { + if (event == CallStateChange.kFeedsChanged) { setState(() { call.tryRemoveStopedStreams(); }); - } else if (event == CallEvent.kLocalHoldUnhold || - event == CallEvent.kRemoteHoldUnhold) { + } else if (event == CallStateChange.kLocalHoldUnhold || + event == CallStateChange.kRemoteHoldUnhold) { setState(() {}); Logs().i( 'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}', @@ -286,7 +277,7 @@ class MyCallingPage extends State { if (call.isRinging) { call.reject(); } else { - call.hangup(); + call.hangup(reason: CallErrorCode.userHangup); } }); } @@ -341,11 +332,6 @@ class MyCallingPage extends State { await Helper.switchCamera( call.localUserMediaStream!.stream!.getVideoTracks()[0], ); - if (PlatformInfos.isMobile) { - call.facingMode == 'user' - ? call.facingMode = 'environment' - : call.facingMode = 'user'; - } } setState(() {}); } @@ -450,16 +436,10 @@ class MyCallingPage extends State { hangupButton, ]; case CallState.kFledgling: - // TODO: Handle this case. - break; case CallState.kWaitLocalMedia: - // TODO: Handle this case. - break; case CallState.kCreateOffer: - // TODO: Handle this case. - break; + case CallState.kEnding: case null: - // TODO: Handle this case. break; } return []; diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart index 73a2e9ff..eb3ce54d 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart @@ -299,7 +299,7 @@ class UserBottomSheetView extends StatelessWidget { BorderRadius.circular(AppConfig.borderRadius / 2), color: Theme.of(context).colorScheme.onInverseSurface, child: DropdownButton( - onChanged: user.canChangePowerLevel || + onChanged: user.canChangeUserPowerLevel || // Workaround until https://github.com/famedly/matrix-dart-sdk/pull/1765 (user.room.canChangePowerLevel && user.id == user.room.client.userID) diff --git a/lib/utils/voip/callkeep_manager.dart b/lib/utils/voip/callkeep_manager.dart index 6de7cb6e..a3db3eba 100644 --- a/lib/utils/voip/callkeep_manager.dart +++ b/lib/utils/voip/callkeep_manager.dart @@ -8,8 +8,6 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:fluffychat/utils/voip_plugin.dart'; - class CallKeeper { CallKeeper(this.callKeepManager, this.call) { call.onCallStateChanged.stream.listen(_handleCallState); @@ -40,31 +38,14 @@ class CallKeeper { case CallState.kEnded: callKeepManager.hangup(call.callId); break; - /* TODO: - case CallState.kMuted: - callKeepManager.setMutedCall(uuid, true); - break; - case CallState.kHeld: - callKeepManager.setOnHold(uuid, true); - break; - */ + case CallState.kFledgling: - // TODO: Handle this case. - break; case CallState.kInviteSent: - // TODO: Handle this case. - break; case CallState.kWaitLocalMedia: - // TODO: Handle this case. - break; case CallState.kCreateOffer: - // TODO: Handle this case. - break; case CallState.kCreateAnswer: - // TODO: Handle this case. - break; case CallState.kRinging: - // TODO: Handle this case. + case CallState.kEnding: break; } } @@ -84,7 +65,6 @@ class CallKeepManager { static final CallKeepManager _instance = CallKeepManager._internal(); late FlutterCallkeep _callKeep; - VoipPlugin? _voipPlugin; String get appName => 'FluffyChat'; @@ -130,7 +110,7 @@ class CallKeepManager { }); call.onCallEventChanged.stream.listen( (event) { - if (event == CallEvent.kLocalHoldUnhold) { + if (event == CallStateChange.kLocalHoldUnhold) { Logs().i( 'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}', ); @@ -170,10 +150,7 @@ class CallKeepManager { Future initialize() async { _callKeep.on(CallKeepPerformAnswerCallAction(), answerCall); _callKeep.on(CallKeepDidPerformDTMFAction(), didPerformDTMFAction); - _callKeep.on( - CallKeepDidReceiveStartCallAction(), - didReceiveStartCallAction, - ); + _callKeep.on(CallKeepDidToggleHoldAction(), didToggleHoldCallAction); _callKeep.on( CallKeepDidPerformSetMutedCallAction(), @@ -313,7 +290,7 @@ class CallKeepManager { Future endCall(CallKeepPerformEndCallAction event) async { final keeper = calls[event.callUUID]; - keeper?.call.hangup(); + keeper?.call.hangup(reason: CallErrorCode.userHangup); removeCall(event.callUUID); } @@ -322,25 +299,6 @@ class CallKeepManager { keeper.call.sendDTMF(event.digits!); } - Future didReceiveStartCallAction( - CallKeepDidReceiveStartCallAction event, - ) async { - if (event.handle == null) { - // @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined` - return; - } - final callUUID = event.callUUID!; - if (event.callUUID == null) { - final call = - await _voipPlugin!.voip.inviteToCall(event.handle!, CallType.kVideo); - addCall(callUUID, CallKeeper(this, call)); - } - await _callKeep.startCall(callUUID, event.handle!, event.handle!); - Timer(const Duration(seconds: 1), () { - _callKeep.setCurrentCallActive(callUUID); - }); - } - Future didPerformSetMutedCallAction( CallKeepDidPerformSetMutedCallAction event, ) async { diff --git a/lib/utils/voip/video_renderer.dart b/lib/utils/voip/video_renderer.dart new file mode 100644 index 00000000..46171fdb --- /dev/null +++ b/lib/utils/voip/video_renderer.dart @@ -0,0 +1,86 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:matrix/matrix.dart'; + +class VideoRenderer extends StatefulWidget { + final WrappedMediaStream? stream; + final bool mirror; + final RTCVideoViewObjectFit fit; + + const VideoRenderer( + this.stream, { + this.mirror = false, + this.fit = RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, + super.key, + }); + + @override + State createState() => _VideoRendererState(); +} + +class _VideoRendererState extends State { + RTCVideoRenderer? _renderer; + bool _rendererReady = false; + MediaStream? get mediaStream => widget.stream?.stream; + StreamSubscription? _streamChangeSubscription; + + Future _initializeRenderer() async { + _renderer ??= RTCVideoRenderer(); + await _renderer!.initialize(); + _renderer!.srcObject = mediaStream; + return _renderer!; + } + + void disposeRenderer() { + try { + _renderer?.srcObject = null; + _renderer?.dispose(); + _renderer = null; + // ignore: empty_catches + } catch (e) {} + } + + @override + void initState() { + _streamChangeSubscription = + widget.stream?.onStreamChanged.stream.listen((stream) { + setState(() { + _renderer?.srcObject = stream; + }); + }); + setupRenderer(); + super.initState(); + } + + Future setupRenderer() async { + await _initializeRenderer(); + setState(() => _rendererReady = true); + } + + @override + void dispose() { + _streamChangeSubscription?.cancel(); + disposeRenderer(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => !_rendererReady + ? Container() + : Builder( + key: widget.key, + builder: (ctx) { + return RTCVideoView( + _renderer!, + mirror: widget.mirror, + filterQuality: FilterQuality.medium, + objectFit: widget.fit, + placeholderBuilder: (_) => + Container(color: Colors.white.withOpacity(0.18)), + ); + }, + ); +} diff --git a/lib/utils/voip_plugin.dart b/lib/utils/voip_plugin.dart index bf36385a..8a71390e 100644 --- a/lib/utils/voip_plugin.dart +++ b/lib/utils/voip_plugin.dart @@ -90,11 +90,6 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate { ]) => webrtc_impl.createPeerConnection(configuration, constraints); - @override - VideoRenderer createRenderer() { - return webrtc_impl.RTCVideoRenderer(); - } - Future get hasCallingAccount async => kIsWeb ? false : await CallKeepManager().hasPhoneAccountEnabled; @@ -179,12 +174,12 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate { } @override - Future handleGroupCallEnded(GroupCall groupCall) async { + Future handleGroupCallEnded(GroupCallSession groupCall) async { // TODO: implement handleGroupCallEnded } @override - Future handleNewGroupCall(GroupCall groupCall) async { + Future handleNewGroupCall(GroupCallSession groupCall) async { // TODO: implement handleNewGroupCall } @@ -197,4 +192,8 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate { Future handleMissedCall(CallSession session) async { // TODO: implement handleMissedCall } + + @override + // TODO: implement keyProvider + EncryptionKeyProvider? get keyProvider => throw UnimplementedError(); } diff --git a/pubspec.lock b/pubspec.lock index 212580e9..bb8cc24c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1210,10 +1210,10 @@ packages: dependency: "direct main" description: name: matrix - sha256: "36c7e13d5d7420898f2597d6f5f0611a9da8114a0fde11f41b9e54cd1140b05f" + sha256: "32c21a2ac2c221ce887b00a87f965bd8df1a3a4ba8794bbe86be8b56214051fb" url: "https://pub.dev" source: hosted - version: "0.27.0" + version: "0.28.1" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 65291a4a..4befc500 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: keyboard_shortcuts: ^0.1.4 latlong2: ^0.9.1 linkify: ^5.0.0 - matrix: ^0.27.0 + matrix: ^0.28.1 native_imaging: ^0.1.0 package_info_plus: ^6.0.0 pasteboard: ^0.2.0 diff --git a/test/utils/test_client.dart b/test/utils/test_client.dart index 7a60ec94..9a4d879d 100644 --- a/test/utils/test_client.dart +++ b/test/utils/test_client.dart @@ -1,7 +1,6 @@ // ignore_for_file: depend_on_referenced_packages import 'package:matrix/encryption/utils/key_verification.dart'; -import 'package:matrix/fake_matrix_api.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart';