From 9ab10eebce24f25306b72f854b1a7c4f2b215a12 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 28 Apr 2024 16:58:23 +0300 Subject: [PATCH] Zoom Video SDK integrated --- lib/main.dart | 9 + lib/pages/conference/zoom/call_screen.dart | 1614 ++++++++++++++++++++ lib/pages/conference/zoom/jwt.dart | 39 + lib/pages/conference/zoom/video_view.dart | 258 ++++ lib/pages/conference/zoom/zoom_config.dart | 4 + lib/routes.dart | 7 + pubspec.yaml | 3 + 7 files changed, 1934 insertions(+) create mode 100644 lib/pages/conference/zoom/call_screen.dart create mode 100644 lib/pages/conference/zoom/jwt.dart create mode 100644 lib/pages/conference/zoom/video_view.dart create mode 100644 lib/pages/conference/zoom/zoom_config.dart diff --git a/lib/main.dart b/lib/main.dart index cb064e0e..ed62d9c2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,7 @@ import 'package:diplomaticquarterapp/uitl/translations_delegate_base.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:logger/logger.dart'; import 'package:provider/provider.dart'; @@ -97,6 +98,14 @@ class _MyApp extends State { // ]); // final themeNotifier = Provider.of(context); precacheImage(AssetImage('assets/images/powerd-by.jpg'), context); + + var zoom = ZoomVideoSdk(); + InitConfig initConfig = InitConfig( + domain: "zoom.us", + enableLog: true, + ); + zoom.initSdk(initConfig); + return LayoutBuilder( builder: (context, constraints) { return OrientationBuilder( diff --git a/lib/pages/conference/zoom/call_screen.dart b/lib/pages/conference/zoom/call_screen.dart new file mode 100644 index 00000000..c7a640e6 --- /dev/null +++ b/lib/pages/conference/zoom/call_screen.dart @@ -0,0 +1,1614 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:diplomaticquarterapp/pages/conference/zoom/jwt.dart'; +import 'package:diplomaticquarterapp/pages/conference/zoom/video_view.dart'; +import 'package:events_emitter/events_emitter.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_cmd_channel.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_event_listener.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_live_transcription_message_info.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_user.dart'; +import 'package:google_fonts/google_fonts.dart'; + +// import '../components/video_view.dart'; +// import '../components/comment_list.dart'; +// import 'intro_screen.dart'; +// import 'join_screen.dart'; + +class CallScreen extends StatefulHookWidget { + const CallScreen({Key? key}) : super(key: key); + + @override + State createState() => _CallScreenState(); +} + +class _CallScreenState extends State { + static TextEditingController changeNameController = TextEditingController(); + double opacityLevel = 1.0; + + String currentCameraDeviceId = "1"; + + void _changeOpacity() { + setState(() => opacityLevel = opacityLevel == 0 ? 1.0 : 0.0); + } + + @override + Widget build(BuildContext context) { + var zoom = ZoomVideoSdk(); + var eventListener = ZoomVideoSdkEventListener(); + var isInSession = useState(false); + var sessionName = useState(''); + var sessionPassword = useState(''); + var users = useState([]); + var fullScreenUser = useState(null); + var sharingUser = useState(null); + var videoInfo = useState(''); + var isSharing = useState(false); + var isMuted = useState(true); + var isVideoOn = useState(false); + var isSpeakerOn = useState(false); + var isRenameModalVisible = useState(false); + var isRecordingStarted = useState(false); + var isMicOriginalOn = useState(false); + var isMounted = useIsMounted(); + var audioStatusFlag = useState(false); + var videoStatusFlag = useState(false); + var userNameFlag = useState(false); + var userShareStatusFlag = useState(false); + var isReceiveSpokenLanguageContentEnabled = useState(false); + var isVideoMirrored = useState(false); + var isOriginalAspectRatio = useState(false); + + //hide status bar + SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack); + var circleButtonSize = 65.0; + Color backgroundColor = const Color(0xFF232323); + Color buttonBackgroundColor = const Color.fromRGBO(0, 0, 0, 0.6); + Color chatTextColor = const Color(0xFFAAAAAA); + Widget moreOptions; + final args = ModalRoute.of(context)!.settings.arguments as CallArguments; + + useEffect(() { + Future.microtask(() async { + var token = generateJwt(args.sessionName, args.role); + try { + Map SDKaudioOptions = {"connect": true, "mute": true, "autoAdjustSpeakerVolume": false}; + Map SDKvideoOptions = { + "localVideoOn": true, + }; + JoinSessionConfig joinSession = JoinSessionConfig( + sessionName: args.sessionName, + sessionPassword: args.sessionPwd, + token: token, + userName: args.displayName, + audioOptions: SDKaudioOptions, + videoOptions: SDKvideoOptions, + sessionIdleTimeoutMins: int.parse(args.sessionIdleTimeoutMins), + ); + await zoom.joinSession(joinSession); + } catch (e) { + print(e); + const AlertDialog( + title: Text("Error"), + content: Text("Failed to join the session"), + ); + Future.delayed(const Duration(milliseconds: 1000)).asStream().listen((event) { + Navigator.pop(context); + // Navigator.popAndPushNamed( + // context, + // "Join", + // arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), + // ); + }); + } + }); + return null; + }, []); + + useEffect(() { + updateVideoInfo() { + Timer timer = Timer.periodic(const Duration(milliseconds: 1000), (time) async { + if (!isMounted()) return; + + bool? videoOn = false; + videoOn = await fullScreenUser.value?.videoStatus?.isOn(); + + // Video statistic info doesn't update when there's no remote users + if (fullScreenUser.value == null || (videoOn != null && videoOn == false) || users.value.length < 2) { + time.cancel(); + videoInfo.value = ""; + return; + } + + var fps = isSharing.value ? await fullScreenUser.value?.shareStatisticInfo?.getFps() : await fullScreenUser.value?.videoStatisticInfo?.getFps(); + + var height = isSharing.value ? await fullScreenUser.value?.shareStatisticInfo?.getHeight() : await fullScreenUser.value?.videoStatisticInfo?.getHeight(); + + var width = isSharing.value ? await fullScreenUser.value?.shareStatisticInfo?.getWidth() : await fullScreenUser.value?.videoStatisticInfo?.getWidth(); + + videoInfo.value = ("${width}x$height ${fps}FPS"); + }); + } + + updateVideoInfo(); + return null; + }); + + useEffect(() { + eventListener.addEventListener(); + EventEmitter emitter = eventListener.eventEmitter; + + final sessionJoinListener = emitter.on(EventType.onSessionJoin, (sessionUser) async { + isInSession.value = true; + zoom.session.getSessionName().then((value) => sessionName.value = value!); + sessionPassword.value = await zoom.session.getSessionPassword(); + ZoomVideoSdkUser mySelf = ZoomVideoSdkUser.fromJson(jsonDecode(sessionUser.toString())); + List? remoteUsers = await zoom.session.getRemoteUsers(); + var muted = await mySelf.audioStatus?.isMuted(); + var videoOn = await mySelf.videoStatus?.isOn(); + var speakerOn = await zoom.audioHelper.getSpeakerStatus(); + fullScreenUser.value = mySelf; + remoteUsers?.insert(0, mySelf); + users.value = remoteUsers!; + isMuted.value = muted!; + isSpeakerOn.value = speakerOn; + isVideoOn.value = videoOn!; + users.value = remoteUsers; + isReceiveSpokenLanguageContentEnabled.value = await zoom.liveTranscriptionHelper.isReceiveSpokenLanguageContentEnabled(); + }); + + final sessionLeaveListener = emitter.on(EventType.onSessionLeave, (data) async { + isInSession.value = false; + users.value = []; + fullScreenUser.value = null; + }); + + final sessionNeedPasswordListener = emitter.on(EventType.onSessionNeedPassword, (data) async { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Session Need Password'), + content: const Text('Password is required'), + actions: [ + TextButton( + onPressed: () async => { + // Navigator.popAndPushNamed(context, 'Join', arguments: JoinArguments(args.isJoin, args.sessionName, "", args.displayName, args.sessionIdleTimeoutMins, args.role)), + Navigator.pop(context), + await zoom.leaveSession(false), + }, + child: const Text('OK'), + ), + ], + ), + ); + }); + + final sessionPasswordWrongListener = emitter.on(EventType.onSessionPasswordWrong, (data) async { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Session Password Incorrect'), + content: const Text('Password is wrong'), + actions: [ + TextButton( + onPressed: () async => { + // Navigator.popAndPushNamed(context, 'Join', arguments: JoinArguments(args.isJoin, args.sessionName, "", args.displayName, args.sessionIdleTimeoutMins, args.role)), + Navigator.pop(context), + await zoom.leaveSession(false), + }, + child: const Text('OK'), + ), + ], + ), + ); + }); + + final userVideoStatusChangedListener = emitter.on(EventType.onUserVideoStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + var userListJson = jsonDecode(data['changedUsers']) as List; + List userList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + for (var user in userList) { + { + if (user.userId == mySelf?.userId) { + mySelf?.videoStatus?.isOn().then((on) => isVideoOn.value = on); + } + } + } + videoStatusFlag.value = !videoStatusFlag.value; + }); + + final userAudioStatusChangedListener = emitter.on(EventType.onUserAudioStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + var userListJson = jsonDecode(data['changedUsers']) as List; + List userList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + for (var user in userList) { + { + if (user.userId == mySelf?.userId) { + mySelf?.audioStatus?.isMuted().then((muted) => isMuted.value = muted); + } + } + } + audioStatusFlag.value = !audioStatusFlag.value; + }); + + final userShareStatusChangeListener = emitter.on(EventType.onUserShareStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + ZoomVideoSdkUser shareUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'].toString())); + + if (data['status'] == ShareStatus.Start) { + sharingUser.value = shareUser; + fullScreenUser.value = shareUser; + isSharing.value = (shareUser.userId == mySelf?.userId); + } else { + sharingUser.value = null; + isSharing.value = false; + fullScreenUser.value = mySelf; + } + userShareStatusFlag.value = !userShareStatusFlag.value; + }); + + final userJoinListener = emitter.on(EventType.onUserJoin, (data) async { + if (!isMounted()) return; + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + var userListJson = jsonDecode(data['remoteUsers']) as List; + List remoteUserList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + remoteUserList.insert(0, mySelf!); + users.value = remoteUserList; + }); + + final userLeaveListener = emitter.on(EventType.onUserLeave, (data) async { + if (!isMounted()) return; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + data = data as Map; + var remoteUserListJson = jsonDecode(data['remoteUsers']) as List; + List remoteUserList = remoteUserListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + var leftUserListJson = jsonDecode(data['leftUsers']) as List; + List leftUserLis = leftUserListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + if (fullScreenUser.value != null) { + for (var user in leftUserLis) { + { + if (fullScreenUser.value?.userId == user.userId) { + fullScreenUser.value = mySelf; + } + } + } + } else { + fullScreenUser.value = mySelf; + } + remoteUserList.add(mySelf!); + users.value = remoteUserList; + }); + + final userNameChangedListener = emitter.on(EventType.onUserNameChanged, (data) async { + if (!isMounted()) return; + data = data as Map; + ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser'])); + int index; + for (var user in users.value) { + if (user.userId == changedUser.userId) { + index = users.value.indexOf(user); + users.value[index] = changedUser; + } + } + userNameFlag.value = !userNameFlag.value; + }); + + final commandReceived = emitter.on(EventType.onCommandReceived, (data) async { + data = data as Map; + debugPrint("sender: ${ZoomVideoSdkUser.fromJson(jsonDecode(data['sender']))}, command: ${data['command']}"); + }); + + final chatDeleteMessageNotify = emitter.on(EventType.onChatDeleteMessageNotify, (data) async { + data = data as Map; + debugPrint("onChatDeleteMessageNotify: messageID: ${data['msgID']}, deleteBy: ${data['type']}"); + }); + + final liveStreamStatusChangeListener = emitter.on(EventType.onLiveStreamStatusChanged, (data) async { + data = data as Map; + debugPrint("onLiveStreamStatusChanged: status: ${data['status']}"); + }); + + final liveTranscriptionStatusChangeListener = emitter.on(EventType.onLiveTranscriptionStatus, (data) async { + data = data as Map; + debugPrint("onLiveTranscriptionStatus: status: ${data['status']}"); + }); + + final cloudRecordingStatusListener = emitter.on(EventType.onCloudRecordingStatus, (data) async { + data = data as Map; + debugPrint("onCloudRecordingStatus: status: ${data['status']}"); + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + if (data['status'] == RecordingStatus.Start) { + if (mySelf != null && !mySelf.isHost!) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + content: const Text('The session is being recorded.'), + actions: [ + TextButton( + onPressed: () async { + await zoom.acceptRecordingConsent(); + // if (context.mounted) { + Navigator.pop(context); + // }; + }, + child: const Text('accept'), + ), + TextButton( + onPressed: () async { + String currentConsentType = await zoom.getRecordingConsentType(); + if (currentConsentType == ConsentType.ConsentType_Individual) { + await zoom.declineRecordingConsent(); + Navigator.pop(context); + } else { + await zoom.declineRecordingConsent(); + zoom.leaveSession(false); + // if (!context.mounted) return; + // Navigator.popAndPushNamed( + // context, + // "Join", + // arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), + // ); + Navigator.pop(context); + } + }, + child: const Text('decline'), + ), + ], + ), + ); + } + isRecordingStarted.value = true; + } else { + isRecordingStarted.value = false; + } + }); + + final liveTranscriptionMsgInfoReceivedListener = emitter.on(EventType.onLiveTranscriptionMsgInfoReceived, (data) async { + data = data as Map; + ZoomVideoSdkLiveTranscriptionMessageInfo? messageInfo = ZoomVideoSdkLiveTranscriptionMessageInfo.fromJson(jsonDecode(data['messageInfo'])); + debugPrint("onLiveTranscriptionMsgInfoReceived: content: ${messageInfo.messageContent}"); + }); + + final inviteByPhoneStatusListener = emitter.on(EventType.onInviteByPhoneStatus, (data) async { + data = data as Map; + debugPrint("onInviteByPhoneStatus: status: ${data['status']}, reason: ${data['reason']}"); + }); + + final multiCameraStreamStatusChangedListener = emitter.on(EventType.onMultiCameraStreamStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser'])); + var status = data['status']; + for (var user in users.value) { + { + if (changedUser.userId == user.userId) { + if (status == MultiCameraStreamStatus.Joined) { + user.hasMultiCamera = true; + } else if (status == MultiCameraStreamStatus.Left) { + user.hasMultiCamera = false; + } + } + } + } + }); + + final requireSystemPermission = emitter.on(EventType.onRequireSystemPermission, (data) async { + data = data as Map; + ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser'])); + var permissionType = data['permissionType']; + switch (permissionType) { + case SystemPermissionType.Camera: + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Can't Access Camera"), + content: const Text("please turn on the toggle in system settings to grant permission"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + break; + case SystemPermissionType.Microphone: + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Can't Access Microphone"), + content: const Text("please turn on the toggle in system settings to grant permission"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + break; + } + }); + + final networkStatusChangeListener = emitter.on(EventType.onUserVideoNetworkStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? networkUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'])); + + if (data['status'] == NetworkStatus.Bad) { + debugPrint("onUserVideoNetworkStatusChanged: status: ${data['status']}, user: ${networkUser.userName}"); + } + }); + + final eventErrorListener = emitter.on( + EventType.onError, + (data) async { + data = data as Map; + String errorType = data['errorType']; + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Error"), + content: Text(errorType), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + if (errorType == Errors.SessionJoinFailed || errorType == Errors.SessionDisconncting) { + Timer( + const Duration(milliseconds: 1000), + () => Navigator.pop(context), + // Navigator.popAndPushNamed( + // context, + // "Join", + // arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), + // ), + ); + } + }, + ); + + final userRecordingConsentListener = emitter.on(EventType.onUserRecordingConsent, (data) async { + data = data as Map; + ZoomVideoSdkUser? user = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'])); + debugPrint('userRecordingConsentListener: user= ${user.userName}'); + }); + + final callCRCDeviceStatusListener = emitter.on(EventType.onCallCRCDeviceStatusChanged, (data) async { + data = data as Map; + debugPrint('onCallCRCDeviceStatusChanged: status = ${data['status']}'); + }); + + final originalLanguageMsgReceivedListener = emitter.on(EventType.onOriginalLanguageMsgReceived, (data) async { + data = data as Map; + ZoomVideoSdkLiveTranscriptionMessageInfo? messageInfo = ZoomVideoSdkLiveTranscriptionMessageInfo.fromJson(jsonDecode(data['messageInfo'])); + debugPrint("onOriginalLanguageMsgReceived: content: ${messageInfo.messageContent}"); + }); + + final chatPrivilegeChangedListener = emitter.on(EventType.onChatPrivilegeChanged, (data) async { + data = data as Map; + String type = data['privilege']; + debugPrint('chatPrivilegeChangedListener: type= $type'); + }); + + return () => { + sessionJoinListener.cancel(), + sessionLeaveListener.cancel(), + sessionPasswordWrongListener.cancel(), + sessionNeedPasswordListener.cancel(), + userVideoStatusChangedListener.cancel(), + userAudioStatusChangedListener.cancel(), + userJoinListener.cancel(), + userLeaveListener.cancel(), + userNameChangedListener.cancel(), + userShareStatusChangeListener.cancel(), + liveStreamStatusChangeListener.cancel(), + cloudRecordingStatusListener.cancel(), + inviteByPhoneStatusListener.cancel(), + eventErrorListener.cancel(), + commandReceived.cancel(), + chatDeleteMessageNotify.cancel(), + liveTranscriptionStatusChangeListener.cancel(), + liveTranscriptionMsgInfoReceivedListener.cancel(), + multiCameraStreamStatusChangedListener.cancel(), + requireSystemPermission.cancel(), + userRecordingConsentListener.cancel(), + networkStatusChangeListener.cancel(), + callCRCDeviceStatusListener.cancel(), + originalLanguageMsgReceivedListener.cancel(), + chatPrivilegeChangedListener.cancel(), + }; + }, [zoom, users.value, isMounted]); + + void onPressAudio() async { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + if (mySelf != null) { + final audioStatus = mySelf.audioStatus; + if (audioStatus != null) { + var muted = await audioStatus.isMuted(); + if (muted) { + await zoom.audioHelper.unMuteAudio(mySelf.userId); + } else { + await zoom.audioHelper.muteAudio(mySelf.userId); + } + } + } + } + + void onPressCameraChange() async { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + if (mySelf != null) { + final videoStatus = mySelf.videoStatus; + if (videoStatus != null) { + if (currentCameraDeviceId == "1") { + currentCameraDeviceId = "2"; + await zoom.videoHelper.switchCamera("2"); + } else { + currentCameraDeviceId = "1"; + await zoom.videoHelper.switchCamera("1"); + } + } + } + } + + void onPressVideo() async { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + if (mySelf != null) { + final videoStatus = mySelf.videoStatus; + if (videoStatus != null) { + var videoOn = await videoStatus.isOn(); + if (videoOn) { + await zoom.videoHelper.stopVideo(); + } else { + await zoom.videoHelper.startVideo(); + } + } + } + } + + void onPressShare() async { + var isOtherSharing = await zoom.shareHelper.isOtherSharing(); + var isShareLocked = await zoom.shareHelper.isShareLocked(); + + if (isOtherSharing) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Error"), + content: const Text('Other is sharing'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + } else if (isShareLocked) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Error"), + content: const Text('Share is locked by host'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + } else if (isSharing.value) { + zoom.shareHelper.stopShare(); + } else { + zoom.shareHelper.shareScreen(); + } + } + + moreOptions = Center( + child: Stack( + children: [ + Visibility( + visible: isRenameModalVisible.value, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.bottomLeft, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + color: Colors.white, + ), + width: MediaQuery.of(context).size.width - 130, + height: 130, + child: Center( + child: (Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 20, left: 20), + child: Text( + 'Change Name', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10, left: 20), + child: SizedBox( + width: MediaQuery.of(context).size.width - 230, + child: TextField( + onEditingComplete: () {}, + autofocus: true, + cursorColor: Colors.black, + controller: changeNameController, + decoration: InputDecoration( + isDense: true, + hintText: 'New name', + hintStyle: TextStyle( + fontSize: 14.0, + color: chatTextColor, + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 15), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 40), + child: InkWell( + child: Text( + 'Apply', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 16, + ), + ), + ), + onTap: () async { + if (fullScreenUser.value != null) { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + await zoom.userHelper.changeName((mySelf?.userId)!, changeNameController.text); + changeNameController.clear(); + } + isRenameModalVisible.value = false; + }, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 40), + child: InkWell( + child: Text( + 'Cancel', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 16, + ), + ), + ), + onTap: () async { + isRenameModalVisible.value = false; + }, + ), + ) + ], + ), + ) + ], + )), + ), + ), + ], + )), + ], + ), + ); + + void onSelectedUserVolume(ZoomVideoSdkUser user) async { + var isShareAudio = user.isSharing; + bool canSetVolume = await user.canSetUserVolume(user.userId, isShareAudio); + num userVolume; + + List options = [ + ListTile( + title: Text( + 'Adjust Volume', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + ), + ), + ListTile( + title: Text( + 'Current volume', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + debugPrint('user volume'), + userVolume = await user.getUserVolume(user.userId, isShareAudio), + debugPrint('user ${user.userName}\'s volume is ${userVolume!}'), + }, + ), + ]; + if (canSetVolume) { + options.add( + ListTile( + title: Text( + 'Volume up', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + userVolume = await user.getUserVolume(user.userId, isShareAudio), + if (userVolume < 10) + { + await user.setUserVolume(user.userId, userVolume + 1, isShareAudio), + } + else + { + debugPrint("Cannot volume up."), + } + }, + ), + ); + options.add( + ListTile( + title: Text( + 'Volume down', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + userVolume = await user.getUserVolume(user.userId, isShareAudio), + if (userVolume > 0) + { + await user.setUserVolume(user.userId, userVolume - 1, isShareAudio), + } + else + { + debugPrint("Cannot volume down."), + } + }, + ), + ); + } + showDialog( + context: context, + builder: (context) { + return Dialog( + elevation: 0.0, + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: SizedBox( + height: options.length * 58, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListView( + shrinkWrap: true, + children: ListTile.divideTiles( + context: context, + tiles: options, + ).toList(), + ), + ], + ), + )); + }); + } + + Future onPressMore() async { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + bool isShareLocked = await zoom.shareHelper.isShareLocked(); + bool canSwitchSpeaker = await zoom.audioHelper.canSwitchSpeaker(); + bool canStartRecording = (await zoom.recordingHelper.canStartRecording()) == Errors.Success; + var startLiveTranscription = (await zoom.liveTranscriptionHelper.getLiveTranscriptionStatus()) == LiveTranscriptionStatus.Start; + bool canStartLiveTranscription = await zoom.liveTranscriptionHelper.canStartLiveTranscription(); + bool canStartLiveStream = await zoom.liveStreamHelper.canStartLiveStream() == Errors.Success; + debugPrint(await zoom.liveStreamHelper.canStartLiveStream()); + bool startLiveStream = false; + bool isHost = (mySelf != null) ? (await mySelf.getIsHost()) : false; + isOriginalAspectRatio.value = await zoom.videoHelper.isOriginalAspectRatioEnabled(); + bool canCallOutToCRC = await zoom.CRCHelper.isCRCEnabled(); + + List options = [ + ListTile( + title: Text( + 'More', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + ), + ), + ListTile( + title: Text( + 'is Support Virtual Background', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + debugPrint("${await zoom.virtualBackgroundHelper.isSupportVirtualBackground()}"), + Navigator.of(context).pop(), + }, + ), + ListTile( + title: Text( + 'Get Chat Privilege', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + debugPrint("Chat Privilege = ${await zoom.chatHelper.getChatPrivilege()}"), + Navigator.of(context).pop(), + }, + ), + ListTile( + title: Text( + 'Get Session Dial-in Number infos', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + debugPrint("session number = ${await zoom.session.getSessionNumber()}"), + Navigator.of(context).pop(), + }, + ), + ListTile( + title: Text( + '${isMicOriginalOn.value ? 'Disable' : 'Enable'} Original Sound', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + debugPrint("${isMicOriginalOn.value}"), + await zoom.audioSettingHelper.enableMicOriginalInput(!isMicOriginalOn.value), + isMicOriginalOn.value = await zoom.audioSettingHelper.isMicOriginalInputEnable(), + debugPrint("Original sound ${isMicOriginalOn.value ? 'Enabled' : 'Disabled'}"), + Navigator.of(context).pop(), + }, + ) + ]; + + if (canCallOutToCRC) { + options.add(ListTile( + title: Text( + 'Call-out to CRC devices', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + debugPrint('CRC result = ${await zoom.CRCHelper.callCRCDevice("bjn.vc", ZoomVideoSdkCRCProtocolType.SIP)}'), + Navigator.of(context).pop(), + }, + )); + options.add(ListTile( + title: Text( + 'Cancel call-out to CRC devices', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + debugPrint('cancel result= ${await zoom.CRCHelper.cancelCallCRCDevice()}'), + Navigator.of(context).pop(), + }, + )); + } + + if (canSwitchSpeaker) { + options.add(ListTile( + title: Text( + 'Turn ${isSpeakerOn.value ? 'off' : 'on'} Speaker', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + await zoom.audioHelper.setSpeaker(!isSpeakerOn.value), + isSpeakerOn.value = await zoom.audioHelper.getSpeakerStatus(), + debugPrint('Turned ${isSpeakerOn.value ? 'on' : 'off'} Speaker'), + Navigator.of(context).pop(), + }, + )); + } + + if (isHost) { + options.add(ListTile( + title: Text( + '${isShareLocked ? 'Unlock' : 'Lock'} Share', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + debugPrint("isShareLocked = ${await zoom.shareHelper.lockShare(!isShareLocked)}"), + Navigator.of(context).pop(), + })); + options.add(ListTile( + title: Text( + 'Change Name', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () => { + isRenameModalVisible.value = true, + Navigator.of(context).pop(), + }, + )); + } + + if (canStartLiveTranscription) { + options.add(ListTile( + title: Text( + "${startLiveTranscription ? 'Stop' : 'Start'} Live Transcription", + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + if (startLiveTranscription) + { + debugPrint('stopLiveTranscription= ${await zoom.liveTranscriptionHelper.stopLiveTranscription()}'), + } + else + { + debugPrint('startLiveTranscription= ${await zoom.liveTranscriptionHelper.startLiveTranscription()}'), + }, + Navigator.of(context).pop(), + }, + )); + options.add(ListTile( + title: Text( + '${isReceiveSpokenLanguageContentEnabled.value ? 'Disable' : 'Enable'} receiving original caption', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + await zoom.liveTranscriptionHelper.enableReceiveSpokenLanguageContent(!isReceiveSpokenLanguageContentEnabled.value), + isReceiveSpokenLanguageContentEnabled.value = await zoom.liveTranscriptionHelper.isReceiveSpokenLanguageContentEnabled(), + debugPrint("isReceiveSpokenLanguageContentEnabled = ${isReceiveSpokenLanguageContentEnabled.value}"), + Navigator.of(context).pop(), + })); + } + + if (canStartRecording) { + options.add(ListTile( + title: Text( + '${isRecordingStarted.value ? 'Stop' : 'Start'} Recording', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + if (!isRecordingStarted.value) + { + debugPrint('isRecordingStarted = ${await zoom.recordingHelper.startCloudRecording()}'), + } + else + { + debugPrint('isRecordingStarted = ${await zoom.recordingHelper.stopCloudRecording()}'), + }, + Navigator.of(context).pop(), + })); + } + + if (isVideoOn.value) { + options.add(ListTile( + title: Text( + 'Mirror the video', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + await zoom.videoHelper.mirrorMyVideo(!isVideoMirrored.value), + isVideoMirrored.value = await zoom.videoHelper.isMyVideoMirrored(), + Navigator.of(context).pop(), + })); + options.add(ListTile( + title: Text( + '${isOriginalAspectRatio.value ? 'Enable' : 'Disable'} original aspect ratio', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + ), + onTap: () async => { + await zoom.videoHelper.enableOriginalAspectRatio(!isOriginalAspectRatio.value), + isOriginalAspectRatio.value = await zoom.videoHelper.isOriginalAspectRatioEnabled(), + debugPrint("isOriginalAspectRatio= ${isOriginalAspectRatio.value}"), + Navigator.of(context).pop(), + })); + } + showDialog( + context: context, + builder: (context) { + return Dialog( + elevation: 0.0, + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: SizedBox( + height: 500, + child: Scrollbar( + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: ListTile.divideTiles( + context: context, + tiles: options, + ).toList(), + ), + ), + ), + ); + }); + } + + void onLeaveSession(bool isEndSession) async { + await zoom.leaveSession(isEndSession); + Navigator.pop(context); + // if (context.mounted) { + // await Navigator.popAndPushNamed( + // context, + // "Join", + // arguments: JoinArguments(!isEndSession, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), + // ); + // } + } + + void showLeaveOptions() async { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + bool isHost = await mySelf!.getIsHost(); + + Widget endSession; + Widget leaveSession; + Widget cancel = TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.pop(context); //close Dialog + }, + ); + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + endSession = TextButton( + child: const Text('End Session'), + onPressed: () => onLeaveSession(true), + ); + leaveSession = TextButton( + child: const Text('Leave Session'), + onPressed: () => onLeaveSession(false), + ); + break; + default: + endSession = CupertinoActionSheetAction( + isDestructiveAction: true, + child: const Text('End Session'), + onPressed: () => onLeaveSession(true), + ); + leaveSession = CupertinoActionSheetAction( + child: const Text('Leave Session'), + onPressed: () => onLeaveSession(false), + ); + break; + } + + List options = [ + leaveSession, + cancel, + ]; + + if (Platform.isAndroid) { + if (isHost) { + options.removeAt(1); + options.insert(0, endSession); + } + showDialog( + context: context, + builder: (context) { + return AlertDialog( + content: const Text("Do you want to leave this session?"), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), + actions: options, + ); + }); + } else { + options.removeAt(1); + if (isHost) { + options.insert(1, endSession); + } + showCupertinoModalPopup( + context: context, + builder: (context) => CupertinoActionSheet( + message: const Text('Are you sure that you want to leave the session?'), + actions: options, + cancelButton: cancel, + ), + ); + } + } + + final chatMessageController = TextEditingController(); + + void sendChatMessage(String message) async { + await zoom.chatHelper.sendChatToAll(message); + ZoomVideoSdkUser? self = await zoom.session.getMySelf(); + ZoomVideoSdkCmdChannel cmdChannel = zoom.cmdChannel; + for (var user in users.value) { + if (user.userId != self?.userId) { + await zoom.cmdChannel.sendCommand(user.userId, message); + } + } + chatMessageController.clear(); + // send the chat as a command + } + + void onSelectedUser(ZoomVideoSdkUser user) async { + setState(() { + fullScreenUser.value = user; + }); + } + + Widget fullScreenView; + Widget smallView; + + if (isInSession.value && fullScreenUser.value != null && users.value.isNotEmpty) { + fullScreenView = AnimatedOpacity( + opacity: opacityLevel, + duration: const Duration(seconds: 3), + child: VideoView( + user: fullScreenUser.value, + hasMultiCamera: false, + sharing: sharingUser.value == null ? false : (sharingUser.value?.userId == fullScreenUser.value?.userId), + preview: false, + focused: false, + multiCameraIndex: "0", + videoAspect: VideoAspect.Original, + fullScreen: true, + resolution: VideoResolution.Resolution360, + isPiPView: false, + ), + ); + + smallView = Container( + height: 110, + margin: const EdgeInsets.only(left: 20, right: 20), + alignment: Alignment.center, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: users.value.length, + itemBuilder: (BuildContext context, int index) { + return InkWell( + onTap: () async { + onSelectedUser(users.value[index]); + }, + onDoubleTap: () async { + onSelectedUserVolume(users.value[index]); + }, + child: Center( + child: VideoView( + user: users.value[index], + hasMultiCamera: false, + sharing: sharingUser.value == null ? false : sharingUser.value?.userId == users.value[index].userId, + preview: false, + focused: false, + multiCameraIndex: "0", + videoAspect: VideoAspect.Original, + fullScreen: false, + resolution: VideoResolution.Resolution180, + isPiPView: false, + ), + ), + ); + }, + separatorBuilder: (BuildContext context, int index) => const Divider(), + ), + ); + } else { + fullScreenView = Container( + color: Colors.black, + child: const Center( + child: Text( + "Connecting...", + style: TextStyle( + fontSize: 20, + color: Colors.white, + ), + ), + )); + smallView = Container( + height: 110, + color: Colors.transparent, + ); + } + + _changeOpacity; + return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: backgroundColor, + body: Stack( + children: [ + fullScreenView, + Container( + padding: const EdgeInsets.only(top: 35), + child: Stack( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 70, + width: 150, + margin: const EdgeInsets.only(top: 16, left: 8), + padding: const EdgeInsets.all(8), + alignment: Alignment.topLeft, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + color: buttonBackgroundColor, + ), + child: InkWell( + onTap: () async { + showDialog( + context: context, + builder: (context) { + return Dialog( + elevation: 0.0, + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: SizedBox( + height: 280, + width: 200, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListView( + shrinkWrap: true, + children: ListTile.divideTiles( + context: context, + tiles: [ + ListTile( + title: Text( + 'Session Information', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ListTile( + title: Text( + 'Session Name', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ), + subtitle: Text( + sessionName.value, + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 12, + ), + ), + ), + ), + ListTile( + title: Text( + 'Session Password', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ), + subtitle: Text( + sessionPassword.value, + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 12, + ), + ), + ), + ), + ListTile( + title: Text( + 'Participants', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ), + subtitle: Text( + '${users.value.length}', + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 12, + ), + ), + ), + ), + ], + ).toList(), + ), + ], + ), + )); + }); + }, + child: Stack( + children: [ + Column( + children: [ + const Padding(padding: EdgeInsets.symmetric(vertical: 3)), + Align( + alignment: Alignment.centerLeft, + child: Text( + sessionName.value, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 5)), + Align( + alignment: Alignment.centerLeft, + child: Text( + "Participants: ${users.value.length}", + style: GoogleFonts.lato( + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ) + ], + ), + Container( + alignment: Alignment.centerRight, + child: Image.asset( + "assets/icons/unlocked@2x.png", + height: 22, + )), + ], + ), + ), + ), + TextButton( + onPressed: (showLeaveOptions), + child: Container( + alignment: Alignment.topRight, + margin: const EdgeInsets.only(top: 16, right: 8), + padding: const EdgeInsets.only(top: 5, bottom: 5, left: 16, right: 16), + height: 28, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20.0)), + color: buttonBackgroundColor, + ), + child: const Text( + "LEAVE", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFFE02828), + ), + ), + )), + ], + ), + Align( + alignment: Alignment.centerRight, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: onPressAudio, + icon: isMuted.value ? Image.asset("assets/icons/unmute@2x.png") : Image.asset("assets/icons/mute@2x.png"), + iconSize: circleButtonSize, + tooltip: isMuted.value == true ? "Unmute" : "Mute", + ), + IconButton( + onPressed: onPressShare, + icon: isSharing.value ? Image.asset("assets/icons/share-off@2x.png") : Image.asset("assets/icons/share-on@2x.png"), + iconSize: circleButtonSize, + ), + IconButton( + onPressed: onPressVideo, + iconSize: circleButtonSize, + icon: isVideoOn.value ? Image.asset("assets/icons/video-off@2x.png") : Image.asset("assets/icons/video-on@2x.png"), + ), + IconButton( + onPressed: onPressCameraChange, + iconSize: circleButtonSize, + icon: const Icon(Icons.cameraswitch_sharp, size: 65.0), + ), + IconButton( + onPressed: onPressMore, + icon: Image.asset("assets/icons/more@2x.png"), + iconSize: circleButtonSize, + ), + ], + ), + ), + // Container( + // margin: const EdgeInsets.only(left: 16, right: 16, bottom: 40, top: 10), + // alignment: Alignment.bottomCenter, + // child: SizedBox( + // height: MediaQuery.of(context).viewInsets.bottom == 0 ? 65 : MediaQuery.of(context).viewInsets.bottom + 18, + // child: TextField( + // maxLines: 1, + // textAlign: TextAlign.left, + // style: TextStyle(color: chatTextColor), + // cursorColor: chatTextColor, + // textAlignVertical: TextAlignVertical.center, + // controller: chatMessageController, + // decoration: InputDecoration( + // contentPadding: const EdgeInsets.only(left: 16, top: 10, bottom: 10, right: 16), + // enabledBorder: OutlineInputBorder( + // borderSide: BorderSide(width: 1, color: chatTextColor), //<-- SEE HERE + // ), + // hintText: 'Type comment', + // hintStyle: TextStyle( + // fontSize: 14.0, + // color: chatTextColor, + // ), + // ), + // onSubmitted: (String str) { + // sendChatMessage(str); + // }, + // ), + // ), + // ), + Container( + alignment: Alignment.bottomLeft, + margin: const EdgeInsets.only(bottom: 120), + child: smallView, + ), + // CommentList(zoom: zoom, eventListener: eventListener), + moreOptions, + ], + )), + ], + ) + // drawer: const MenuBar() + ); + } +} + +class CallArguments { + final bool isJoin; + final String sessionName; + final String sessionPwd; + final String displayName; + final String sessionIdleTimeoutMins; + final String role; + + CallArguments(this.sessionName, this.sessionPwd, this.displayName, this.sessionIdleTimeoutMins, this.role, this.isJoin); +} + +class JoinArguments { + final bool isJoin; + final String sessionName; + final String sessionPwd; + final String displayName; + final String sessionTimeout; + final String roleType; + + JoinArguments(this.isJoin, this.sessionName, this.sessionPwd, this.displayName, this.sessionTimeout, this.roleType); +} diff --git a/lib/pages/conference/zoom/jwt.dart b/lib/pages/conference/zoom/jwt.dart new file mode 100644 index 00000000..3047fe5f --- /dev/null +++ b/lib/pages/conference/zoom/jwt.dart @@ -0,0 +1,39 @@ +import 'dart:math'; + +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:diplomaticquarterapp/pages/conference/zoom/zoom_config.dart'; + +String makeId(int length) { + String result = ""; + String characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + int charactersLength = characters.length; + for (var i = 0; i < length; i++) { + result += characters[Random().nextInt(charactersLength)]; + } + return result; +} + +String generateJwt(String sessionName, String roleType) { + try { + var iat = DateTime.now(); + var exp = DateTime.now().add(Duration(days: 2)); + final jwt = JWT( + { + 'app_key': configs["ZOOM_SDK_KEY"], + 'version': 1, + 'user_identity': makeId(10), + 'iat': (iat.millisecondsSinceEpoch / 1000).round(), + 'exp': (exp.millisecondsSinceEpoch / 1000).round(), + 'tpc': sessionName, + 'role_type': int.parse(roleType), + 'cloud_recording_option': 1, + }, + ); + var token = jwt.sign(SecretKey(configs["ZOOM_SDK_SECRET"])); + return token; + } catch (e) { + print(e); + return ''; + } +} diff --git a/lib/pages/conference/zoom/video_view.dart b/lib/pages/conference/zoom/video_view.dart new file mode 100644 index 00000000..088379cd --- /dev/null +++ b/lib/pages/conference/zoom/video_view.dart @@ -0,0 +1,258 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_zoom_videosdk/flutter_zoom_view.dart' as FlutterZoomView; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; + +const SHOW_TALKING_ICON_DURATION = 2000; + +class VideoView extends FlutterZoomView.ZoomView { + const VideoView({ + super.key, + required super.user, + required super.sharing, + required super.preview, + required super.focused, + required super.hasMultiCamera, + required super.multiCameraIndex, + required super.videoAspect, + required super.fullScreen, + required super.resolution, + required super.isPiPView, + }); + + @override + Widget build(BuildContext context) { + var isVideoOn = useState(false); + var isTalking = useState(false); + var isMuted = useState(false); + var isMounted = useIsMounted(); + var zoom = ZoomVideoSdk(); + var isSharing = useState(false); + user?.audioStatus?.isMuted().then((muted) => isMuted.value = muted); + + useEffect(() { + updateVideoStatus() { + if (user == null) return; + Future.microtask(() async { + if (isMounted()) { + isVideoOn.value = (await user!.videoStatus!.isOn()); + isSharing.value = sharing; + } + }); + } + + resetAudioStatus() { + isTalking.value = false; + isMuted.value = false; + } + + updateAudioStatus() async { + if (!isMounted()) return; + var talking = await user?.audioStatus?.isTalking(); + var muted = await user?.audioStatus?.isMuted(); + isMuted.value = muted!; + isTalking.value = talking!; + if (talking) { + Timer( + const Duration(milliseconds: SHOW_TALKING_ICON_DURATION), + () => { + if (isMounted()) + { + isTalking.value = false, + } + }); + } + } + + updateVideoStatus(); + return null; + }, [zoom, user]); + + ImageIcon audioStatusIcon; + if (isTalking.value) { + audioStatusIcon = const ImageIcon( + AssetImage("assets/icons/talking@2x.png"), + ); + } else if (isMuted.value) { + audioStatusIcon = const ImageIcon( + AssetImage("assets/icons/muted@2x.png"), + ); + } + // Pass parameters to the platform side. + final Map creationParams = {}; + creationParams.putIfAbsent("userId", () => user?.userId); + creationParams.putIfAbsent("sharing", () => sharing); + creationParams.putIfAbsent("preview", () => preview); + creationParams.putIfAbsent("focused", () => focused); + creationParams.putIfAbsent("hasMultiCamera", () => hasMultiCamera); + if (videoAspect.isEmpty) { + creationParams.putIfAbsent("videoAspect", () => VideoAspect.PanAndScan); + } else { + creationParams.putIfAbsent("videoAspect", () => videoAspect); + } + creationParams.putIfAbsent("fullScreen", () => fullScreen); + if (resolution.isNotEmpty) { + creationParams.putIfAbsent("videoAspect", () => videoAspect); + } + + if (fullScreen) { + if (sharing) { + return Container( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + alignment: Alignment.center, + child: FlutterZoomView.View(creationParams: creationParams), + ); + } else if (isVideoOn.value) { + return Container( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + alignment: Alignment.center, + child: FlutterZoomView.View(creationParams: creationParams), + ); + } else { + return Container( + margin: const EdgeInsets.symmetric(vertical: 0), + child: Container( + alignment: Alignment.center, + child: const Image( + image: AssetImage("assets/icons/default-avatar.png"), + )), + ); + } + } else { + if (isVideoOn.value || sharing) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + height: 110, + width: 110, + child: Stack( + children: [ + Container( + height: 110, + width: 110, + decoration: BoxDecoration( + color: const Color(0xff232323), + border: Border.all( + color: const Color(0xff666666), + width: 1, + ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + alignment: Alignment.center, + child: FlutterZoomView.View(creationParams: creationParams), + ), + Container( + height: 110, + width: 110, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + Container( + alignment: Alignment.bottomCenter, + child: Container( + height: 20, + width: 110, + decoration: const BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + Container( + alignment: Alignment.bottomLeft, + margin: const EdgeInsets.only(left: 5), + child: Text( + user!.userName, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + Container( + alignment: Alignment.bottomRight, + margin: const EdgeInsets.only(right: 5, bottom: 5), + // height: 110, + // width: 110, + child: Image( + height: 12, + width: 12, + image: isMuted.value ? const AssetImage("assets/icons/muted@2x.png") : const AssetImage("assets/icons/talking@2x.png"), + fit: BoxFit.cover, + ), + ) + ], + ), + ); + } else { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + height: 110, + width: 110, + child: Stack( + children: [ + Container( + height: 110, + width: 110, + decoration: BoxDecoration( + color: const Color(0xff232323), + border: Border.all( + color: const Color(0xff666666), + width: 1, + ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + alignment: Alignment.center, + child: Container( + alignment: Alignment.center, + child: const Image( + height: 60, + width: 60, + image: AssetImage("assets/icons/default-avatar.png"), + )), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 20, + width: 110, + decoration: const BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + Container( + alignment: Alignment.bottomLeft, + margin: const EdgeInsets.only(left: 5), + child: Text( + user!.userName, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + Container( + alignment: Alignment.bottomRight, + margin: const EdgeInsets.only(right: 5, bottom: 5), + child: Image( + height: 12, + width: 12, + image: isMuted.value ? const AssetImage("assets/icons/muted@2x.png") : const AssetImage("assets/icons/talking@2x.png"), + fit: BoxFit.cover, + ), + ), + ], + ), + ); + } + } + } +} diff --git a/lib/pages/conference/zoom/zoom_config.dart b/lib/pages/conference/zoom/zoom_config.dart new file mode 100644 index 00000000..09370c00 --- /dev/null +++ b/lib/pages/conference/zoom/zoom_config.dart @@ -0,0 +1,4 @@ +const Map configs = { + 'ZOOM_SDK_KEY': 'b9T74nhfTg-ioP9urm970A', + 'ZOOM_SDK_SECRET': 'KOzmjBNXQ1f4IPHpnngfL29uZvJMufSy2Fk8', +}; diff --git a/lib/routes.dart b/lib/routes.dart index 7e0bd7ee..6e8aca9e 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -5,6 +5,7 @@ import 'package:diplomaticquarterapp/pages/DrawerPages/family/add-family_type.da import 'package:diplomaticquarterapp/pages/DrawerPages/family/my-family.dart'; import 'package:diplomaticquarterapp/pages/TestPage.dart'; import 'package:diplomaticquarterapp/pages/appUpdatePage/app_update_page.dart'; +import 'package:diplomaticquarterapp/pages/conference/zoom/call_screen.dart'; import 'package:diplomaticquarterapp/pages/landing/landing_page.dart'; import 'package:diplomaticquarterapp/pages/livecare/incoming_call.dart'; import 'package:diplomaticquarterapp/pages/livecare/livecare_home.dart'; @@ -57,6 +58,9 @@ const String INCOMING_CALL_PAGE = 'INCOMING_CALL_PAGE'; const String HEALTH_WEATHER = 'health-weather'; const APP_UPDATE = 'app-update'; + +const ZOOM_CALL_PAGE = 'zoom_call_page'; + var routes = { SPLASH: (_) => SplashScreen(), HOME: (_) => LandingPage(), @@ -84,6 +88,9 @@ var routes = { CART_ORDER_PAGE: (_) => CartOrderPage(), // CALL_PAGE: (_) => CallPage(), INCOMING_CALL_PAGE: (_) => IncomingCall(), + + ZOOM_CALL_PAGE: (_) => CallScreen(), + OPENTOK_CALL_PAGE: (_) => OpenTokConnectCallPage( apiKey: OPENTOK_API_KEY, sessionId: '1_MX40NjIwOTk2Mn5-MTY0NzE3MzcwNjA0MH55RUJoZnd0ZGh2U3BPc01ENVZBelQvT1Z-fg', diff --git a/pubspec.yaml b/pubspec.yaml index 8bd88bf3..851f41d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -171,6 +171,9 @@ dependencies: logger: ^2.0.2+1 network_info_plus: any + flutter_zoom_videosdk: ^1.10.11 + dart_jsonwebtoken: ^2.14.0 + dependency_overrides: dev_dependencies: