import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:universal_io/io.dart'; import 'package:vmeeting/service/routes/routes_name.dart'; import 'package:vmeeting/src/utils/configs.dart'; import 'package:web_browser_detect/web_browser_detect.dart'; import 'package:connectycube_sdk/connectycube_sdk.dart'; import 'login_screen.dart'; import 'managers/call_manager.dart'; import 'utils/platform_utils.dart'; class ConversationCallScreen extends StatefulWidget { final P2PSession _callSession; final bool _isIncoming; @override State<StatefulWidget> createState() { return _ConversationCallScreenState(_callSession, _isIncoming); } ConversationCallScreen(this._callSession, this._isIncoming); } class _ConversationCallScreenState extends State<ConversationCallScreen> implements RTCSessionStateCallback<P2PSession> { static const String TAG = "_ConversationCallScreenState"; final P2PSession _callSession; final bool _isIncoming; final CubeStatsReportsManager _statsReportsManager = CubeStatsReportsManager(); bool _isCameraEnabled = true; bool _isSpeakerEnabled = Platform.isIOS ? false : true; bool _isMicMute = false; bool _isFrontCameraUsed = true; final int _currentUserId = CubeChatConnection.instance.currentUser!.id!; MapEntry<int, RTCVideoRenderer>? primaryRenderer; Map<int, RTCVideoRenderer> minorRenderers = {}; RTCVideoViewObjectFit primaryVideoFit = RTCVideoViewObjectFit.RTCVideoViewObjectFitCover; bool _enableScreenSharing; _ConversationCallScreenState(this._callSession, this._isIncoming) : _enableScreenSharing = !_callSession.startScreenSharing; @override void initState() { super.initState(); _initAlreadyReceivedStreams(); _callSession.onLocalStreamReceived = _addLocalMediaStream; _callSession.onRemoteStreamReceived = _addRemoteMediaStream; _callSession.onSessionClosed = _onSessionClosed; _statsReportsManager.init(_callSession); _callSession.setSessionCallbacksListener(this); if (_isIncoming) { if (_callSession.state == RTCSessionState.RTC_SESSION_NEW) { _callSession.acceptCall(); } } else { _callSession.startCall(); } CallManager.instance.onMicMuted = (muted, sessionId) { setState(() { _isMicMute = muted; _callSession.setMicrophoneMute(_isMicMute); }); }; } @override void dispose() { super.dispose(); stopBackgroundExecution(); primaryRenderer?.value.srcObject = null; primaryRenderer?.value.dispose(); minorRenderers.forEach((opponentId, renderer) { log("[dispose] dispose renderer for $opponentId", TAG); try { renderer.srcObject?.dispose(); renderer.srcObject = null; renderer.dispose(); } catch (e) { log('Error $e'); } }); } Future<void> _addLocalMediaStream(MediaStream stream) async { log("_addLocalMediaStream, stream Id: ${stream.id}", TAG); _addMediaStream(_currentUserId, stream); } void _addRemoteMediaStream(session, int userId, MediaStream stream) { log("_addRemoteMediaStream for user $userId", TAG); _addMediaStream(userId, stream); } Future<void> _removeMediaStream(callSession, int userId) async { log("_removeMediaStream for user $userId", TAG); var videoRenderer = minorRenderers[userId]; if (videoRenderer == null) return; videoRenderer.srcObject = null; videoRenderer.dispose(); setState(() { minorRenderers.remove(userId); }); } Future<void> _addMediaStream(int userId, MediaStream stream) async { if (primaryRenderer == null) { primaryRenderer = MapEntry(userId, RTCVideoRenderer()); await primaryRenderer!.value.initialize(); setState(() { primaryRenderer?.value.srcObject = stream; }); return; } if (minorRenderers[userId] == null) { minorRenderers[userId] = RTCVideoRenderer(); await minorRenderers[userId]?.initialize(); } setState(() { minorRenderers[userId]?.srcObject = stream; if (primaryRenderer?.key == _currentUserId || primaryRenderer?.key == userId) { _replacePrimaryRenderer(userId); } }); } void _replacePrimaryRenderer(int newPrimaryUser) { if (primaryRenderer?.key != newPrimaryUser) { minorRenderers.addEntries([primaryRenderer!]); } primaryRenderer = MapEntry(newPrimaryUser, minorRenderers.remove(newPrimaryUser)!); } void _onSessionClosed(session) { log("_onSessionClosed", TAG); _callSession.removeSessionCallbacksListener(); _statsReportsManager.dispose(); Navigator.pushReplacementNamed(context, MainRoutes.main_page); } Widget buildMinorVideoItem(int opponentId, RTCVideoRenderer renderer) { return Expanded( child: Stack( children: [ RTCVideoView( renderer, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: false, ), Align( alignment: Alignment.centerLeft, child: Container( margin: EdgeInsets.all(8), child: Padding( padding: EdgeInsets.symmetric( vertical: 10, ), child: RotatedBox( quarterTurns: -1, child: StreamBuilder<CubeMicLevelEvent>( stream: _statsReportsManager.micLevelStream .where((event) => event.userId == opponentId), builder: (context, snapshot) { if (!snapshot.hasData) { return LinearProgressIndicator(value: 0); } else { var micLevelForUser = snapshot.data!; return LinearProgressIndicator( value: micLevelForUser.micLevel); } }, ), ), ), )), Align( alignment: Alignment.topCenter, child: Container( margin: EdgeInsets.only(top: 8), child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(12)), child: Container( padding: EdgeInsets.all(8), color: Colors.black26, child: StreamBuilder<CubeVideoBitrateEvent>( stream: _statsReportsManager.videoBitrateStream .where((event) => event.userId == opponentId), builder: (context, snapshot) { if (!snapshot.hasData) { return Text( '0 kbits/sec', style: TextStyle(color: Colors.white), ); } else { var videoBitrateForUser = snapshot.data!; return Text( '${videoBitrateForUser.bitRate} kbits/sec', style: TextStyle(color: Colors.white), ); } }, ), ), ), )) ], ), ); } List<Widget> renderStreamsGrid(Orientation orientation) { List<Widget> streamsExpanded = []; if (primaryRenderer != null) { streamsExpanded.add(Expanded( child: RTCVideoView( primaryRenderer!.value, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: true, ))); } if (CallManager.instance.remoteStreams.isNotEmpty) { minorRenderers.addEntries([ ...CallManager.instance.remoteStreams.entries.map((mediaStreamEntry) { var videoRenderer = RTCVideoRenderer(); videoRenderer.initialize().then((value) { videoRenderer.srcObject = mediaStreamEntry.value; }); return MapEntry(mediaStreamEntry.key, videoRenderer); }) ]); CallManager.instance.remoteStreams.clear(); } streamsExpanded.addAll(minorRenderers.entries .map( (entry) => buildMinorVideoItem(entry.key, entry.value), ) .toList()); if (streamsExpanded.length > 2) { List<Widget> rows = []; for (var i = 0; i < streamsExpanded.length; i += 2) { var chunkEndIndex = i + 2; if (streamsExpanded.length < chunkEndIndex) { chunkEndIndex = streamsExpanded.length; } var chunk = streamsExpanded.sublist(i, chunkEndIndex); rows.add( Expanded( child: orientation == Orientation.portrait ? Row(children: chunk) : Column(children: chunk), ), ); } return rows; } return streamsExpanded; } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () => _onBackPressed(context), child: Scaffold( backgroundColor: Colors.grey, body: Stack(fit: StackFit.loose, clipBehavior: Clip.none, children: [ _isVideoCall() ? OrientationBuilder( builder: (context, orientation) { return _callSession.opponentsIds.length > 1 ? _buildGroupCallLayout(orientation) : _buildPrivateCallLayout(orientation); }, ) : Center( child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ Padding( padding: EdgeInsets.only(bottom: 24), child: Text( "Audio call", style: TextStyle(fontSize: 28), ), ), Padding( padding: EdgeInsets.only(bottom: 12), child: Text( "Members:", style: TextStyle( fontSize: 20, fontStyle: FontStyle.italic), ), ), Text( _callSession.opponentsIds.join(", "), style: TextStyle(fontSize: 20), ), ], ), ), Align( alignment: Alignment.bottomCenter, child: _getActionsPanel(), ), ]), ), ); } Widget _buildGroupCallLayout(Orientation orientation) { return Center( child: Container( child: orientation == Orientation.portrait ? Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: renderGroupCallViews(orientation)) : Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: renderGroupCallViews(orientation)), ), ); } Widget _buildPrivateCallLayout(Orientation orientation) { return Container( child: Stack(children: [ if (primaryRenderer != null) _buildPrimaryVideoView(orientation), if (minorRenderers.isNotEmpty) Align( alignment: Alignment.topRight, child: Padding( padding: orientation == Orientation.portrait ? EdgeInsets.only( top: MediaQuery.of(context).padding.top + 10, right: MediaQuery.of(context).padding.right + 10) : EdgeInsets.only( right: MediaQuery.of(context).padding.right + 10, top: MediaQuery.of(context).padding.top + 10), child: buildItems( minorRenderers, orientation == Orientation.portrait ? MediaQuery.of(context).size.width / 3 : MediaQuery.of(context).size.width / 4, orientation == Orientation.portrait ? MediaQuery.of(context).size.height / 4 : MediaQuery.of(context).size.height / 2.5) .first, )) ]), ); } List<Widget> renderGroupCallViews(Orientation orientation) { List<Widget> streamsExpanded = []; if (primaryRenderer != null) { streamsExpanded.add( Expanded(flex: 3, child: _buildPrimaryVideoView(orientation)), ); } var itemHeight; var itemWidth; if (orientation == Orientation.portrait) { itemHeight = MediaQuery.of(context).size.height / 3 * 0.8; itemWidth = itemHeight / 3 * 4; } else { itemWidth = MediaQuery.of(context).size.width / 3 * 0.8; itemHeight = itemWidth / 4 * 3; } var minorItems = buildItems(minorRenderers, itemWidth, itemHeight); if (minorRenderers.isNotEmpty) { var membersList = Expanded( flex: 1, child: ListView( scrollDirection: orientation == Orientation.landscape ? Axis.vertical : Axis.horizontal, children: minorItems, ), ); streamsExpanded.add(membersList); } return streamsExpanded; } List<Widget> buildItems(Map<int, RTCVideoRenderer> renderers, double itemWidth, double itemHeight) { return renderers.entries .map( (entry) => GestureDetector( onTap: () { setState(() { _replacePrimaryRenderer(entry.key); }); }, child: AbsorbPointer( child: Container( width: itemWidth, height: itemHeight, padding: EdgeInsets.all(4), child: Stack( children: [ StreamBuilder<CubeMicLevelEvent>( stream: _statsReportsManager.micLevelStream .where((event) => event.userId == entry.key), builder: (context, snapshot) { var width = !snapshot.hasData ? 0 : snapshot.data!.micLevel * 4; return Container( decoration: ShapeDecoration( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6.0), side: BorderSide( width: width.toDouble(), color: Colors.green, strokeAlign: 1.0), ), ), ); }, ), ClipRRect( borderRadius: BorderRadius.circular(6.0), child: RTCVideoView( entry.value, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: entry.key == _currentUserId && _isFrontCameraUsed && _enableScreenSharing, ), ), if (entry.key != _currentUserId) Align( alignment: Alignment.topCenter, child: Container( margin: EdgeInsets.only(top: 8), child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(12)), child: Container( padding: EdgeInsets.all(6), color: Colors.black26, child: StreamBuilder<CubeVideoBitrateEvent>( stream: _statsReportsManager .videoBitrateStream .where( (event) => event.userId == entry.key), builder: (context, snapshot) { if (!snapshot.hasData) { return Text( '0 kbits/sec', style: TextStyle(color: Colors.white), ); } else { var videoBitrateForUser = snapshot.data!; return Text( '${videoBitrateForUser.bitRate} kbits/sec', style: TextStyle(color: Colors.white), ); } }, ), ), ), )), Align( alignment: Alignment.bottomCenter, child: Container( margin: EdgeInsets.only(bottom: 8), child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(12)), child: Container( padding: EdgeInsets.all(6), color: Colors.black26, child: Text( entry.key == CubeChatConnection .instance.currentUser?.id ? 'Me' : usersForCall .where((user) => user.id == entry.key) .first .fullName ?? 'Unknown', style: TextStyle(color: Colors.white), ), ), ), ), ), ], ), ), ), ), ) .toList(); } Widget _buildPrimaryVideoView(Orientation orientation) { return Stack( children: [ RTCVideoView( primaryRenderer!.value, objectFit: primaryVideoFit, mirror: primaryRenderer!.key == _currentUserId && _isFrontCameraUsed && _enableScreenSharing, ), Align( alignment: Alignment.centerLeft, child: StreamBuilder<CubeMicLevelEvent>( stream: _statsReportsManager.micLevelStream .where((event) => event.userId == primaryRenderer!.key), builder: (context, snapshot) { return Padding( padding: EdgeInsets.only( top: MediaQuery.of(context).padding.top + 50, left: MediaQuery.of(context).padding.left + 15, bottom: MediaQuery.of(context).padding.bottom + 100), child: RotatedBox( quarterTurns: -1, child: ConstrainedBox( constraints: BoxConstraints(maxWidth: 200), child: LinearProgressIndicator( value: !snapshot.hasData ? 0 : snapshot.data!.micLevel, ), ), )); }, ), ), Align( alignment: Alignment.topLeft, child: Padding( padding: orientation == Orientation.portrait ? EdgeInsets.only( top: MediaQuery.of(context).padding.top + 10, left: MediaQuery.of(context).padding.left + 10) : EdgeInsets.only( left: MediaQuery.of(context).padding.left + 10, top: MediaQuery.of(context).padding.top + 10), child: FloatingActionButton( elevation: 0, heroTag: "ToggleScreenFit", child: Icon( primaryVideoFit == RTCVideoViewObjectFit.RTCVideoViewObjectFitCover ? Icons.zoom_in_map : Icons.zoom_out_map, color: Colors.white, ), onPressed: () => _switchPrimaryVideoFit(), backgroundColor: Colors.black38, ), ), ), if (primaryRenderer!.key != _currentUserId) Align( alignment: Alignment.topCenter, child: Container( margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top + 10), child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(12)), child: Container( padding: EdgeInsets.all(6), color: Colors.black26, child: StreamBuilder<CubeVideoBitrateEvent>( stream: _statsReportsManager.videoBitrateStream .where((event) => event.userId == primaryRenderer!.key), builder: (context, snapshot) { if (!snapshot.hasData) { return Text( '0 kbits/sec', style: TextStyle(color: Colors.white), ); } else { var videoBitrateForUser = snapshot.data!; return Text( '${videoBitrateForUser.bitRate} kbits/sec', style: TextStyle(color: Colors.white), ); } }, ), ), ), ), ), ], ); } _switchPrimaryVideoFit() async { setState(() { primaryVideoFit = primaryVideoFit == RTCVideoViewObjectFit.RTCVideoViewObjectFitCover ? RTCVideoViewObjectFit.RTCVideoViewObjectFitContain : RTCVideoViewObjectFit.RTCVideoViewObjectFitCover; }); } Widget _getActionsPanel() { return Container( margin: EdgeInsets.only( bottom: MediaQuery.of(context).padding.bottom + 8, left: MediaQuery.of(context).padding.left + 8, right: MediaQuery.of(context).padding.right + 8), child: ClipRRect( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(32), bottomRight: Radius.circular(32), topLeft: Radius.circular(32), topRight: Radius.circular(32)), child: Container( padding: EdgeInsets.all(4), color: Colors.black26, child: Row( mainAxisSize: MainAxisSize.min, children: <Widget>[ Padding( padding: EdgeInsets.only(right: 4), child: FloatingActionButton( elevation: 0, heroTag: "Mute", child: Icon( _isMicMute ? Icons.mic_off : Icons.mic, color: _isMicMute ? Colors.grey : Colors.white, ), onPressed: () => _muteMic(), backgroundColor: Colors.black38, ), ), Visibility( visible: _enableScreenSharing, child: Padding( padding: EdgeInsets.only(right: 4), child: FloatingActionButton( elevation: 0, heroTag: "ToggleCamera", child: Icon( _isVideoEnabled() ? Icons.videocam : Icons.videocam_off, color: _isVideoEnabled() ? Colors.white : Colors.grey, ), onPressed: () => _toggleCamera(), backgroundColor: Colors.black38, ), ), ), SpeedDial( heroTag: "Options", icon: Icons.more_vert, activeIcon: Icons.close, backgroundColor: Colors.black38, switchLabelPosition: true, overlayColor: Colors.black, elevation: 0, overlayOpacity: 0.5, children: [ SpeedDialChild( elevation: 0, child: Icon( _enableScreenSharing ? Icons.screen_share : Icons.stop_screen_share, color: Colors.white, ), backgroundColor: Colors.black38, foregroundColor: Colors.white, label: '${_enableScreenSharing ? 'Start' : 'Stop'} Screen Sharing', onTap: () => _toggleScreenSharing(), ), SpeedDialChild( elevation: 0, visible: !(kIsWeb && (Browser().browserAgent == BrowserAgent.Safari || Browser().browserAgent == BrowserAgent.Firefox)), child: Icon( kIsWeb || WebRTC.platformIsDesktop ? Icons.surround_sound : _isSpeakerEnabled ? Icons.volume_up : Icons.volume_off, color: _isSpeakerEnabled ? Colors.white : Colors.grey, ), backgroundColor: Colors.black38, foregroundColor: Colors.white, label: 'Switch ${kIsWeb || WebRTC.platformIsDesktop ? 'Audio output' : 'Speakerphone'}', onTap: () => _switchSpeaker(), ), SpeedDialChild( elevation: 0, visible: kIsWeb || WebRTC.platformIsDesktop, child: Icon( Icons.record_voice_over, color: Colors.white, ), backgroundColor: Colors.black38, foregroundColor: Colors.white, label: 'Switch Audio Input device', onTap: () => _switchAudioInput(), ), SpeedDialChild( elevation: 0, visible: _enableScreenSharing, child: Icon( Icons.cameraswitch, color: _isVideoEnabled() ? Colors.white : Colors.grey, ), backgroundColor: Colors.black38, foregroundColor: Colors.white, label: 'Switch Camera', onTap: () => _switchCamera(), ), ], ), Expanded( child: SizedBox(), flex: 1, ), Padding( padding: EdgeInsets.only(left: 0), child: FloatingActionButton( child: Icon( Icons.call_end, color: Colors.white, ), backgroundColor: Colors.red, onPressed: () => _endCall(), ), ), ], ), ), ), ); } _endCall() { CallManager.instance.hungUp(); } Future<bool> _onBackPressed(BuildContext context) { return Future.value(false); } _muteMic() { setState(() { _isMicMute = !_isMicMute; _callSession.setMicrophoneMute(_isMicMute); CallManager.instance.muteCall(_callSession.sessionId, _isMicMute); }); } _switchCamera() { if (!_isVideoEnabled()) return; if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { _callSession.switchCamera().then((isFrontCameraUsed) { setState(() { _isFrontCameraUsed = isFrontCameraUsed; }); }); } else { showDialog( context: context, builder: (BuildContext context) { return FutureBuilder<List<MediaDeviceInfo>>( future: _callSession.getCameras(), builder: (context, snapshot) { if (!snapshot.hasData || snapshot.data!.isEmpty) { return AlertDialog( content: const Text('No cameras found'), actions: <Widget>[ TextButton( style: TextButton.styleFrom( textStyle: Theme.of(context).textTheme.labelLarge, ), child: const Text('Ok'), onPressed: () { Navigator.of(context).pop(); }, ) ], ); } else { return SimpleDialog( title: const Text('Select camera'), children: snapshot.data?.map( (mediaDeviceInfo) { return SimpleDialogOption( onPressed: () { Navigator.pop(context, mediaDeviceInfo.deviceId); }, child: Text(mediaDeviceInfo.label), ); }, ).toList(), ); } }, ); }, ).then((deviceId) { log("onCameraSelected deviceId: $deviceId", TAG); if (deviceId != null) _callSession.switchCamera(deviceId: deviceId); }); } } _toggleCamera() { if (!_isVideoCall()) return; setState(() { _isCameraEnabled = !_isCameraEnabled; _callSession.setVideoEnabled(_isCameraEnabled); }); } _toggleScreenSharing() async { var foregroundServiceFuture = _enableScreenSharing ? startBackgroundExecution() : stopBackgroundExecution(); var hasPermissions = await hasBackgroundExecutionPermissions(); if (!hasPermissions) { await initForegroundService(); } var desktopCapturerSource = _enableScreenSharing && isDesktop ? await showDialog<DesktopCapturerSource>( context: context, builder: (context) => ScreenSelectDialog(), ) : null; foregroundServiceFuture.then((_) { _callSession .enableScreenSharing(_enableScreenSharing, desktopCapturerSource: desktopCapturerSource, useIOSBroadcasting: true, requestAudioForScreenSharing: true) .then((voidResult) { setState(() { _enableScreenSharing = !_enableScreenSharing; _isFrontCameraUsed = _enableScreenSharing; }); }); }); } bool _isVideoEnabled() { return _isVideoCall() && _isCameraEnabled; } bool _isVideoCall() { return CallType.VIDEO_CALL == _callSession.callType; } _switchSpeaker() { if (kIsWeb || WebRTC.platformIsDesktop) { showDialog( context: context, builder: (BuildContext context) { return FutureBuilder<List<MediaDeviceInfo>>( future: _callSession.getAudioOutputs(), builder: (context, snapshot) { if (!snapshot.hasData || snapshot.data!.isEmpty) { return AlertDialog( content: const Text('No Audio output devices found'), actions: <Widget>[ TextButton( style: TextButton.styleFrom( textStyle: Theme.of(context).textTheme.labelLarge, ), child: const Text('Ok'), onPressed: () { Navigator.of(context).pop(); }, ) ], ); } else { return SimpleDialog( title: const Text('Select Audio output device'), children: snapshot.data?.map( (mediaDeviceInfo) { return SimpleDialogOption( onPressed: () { Navigator.pop(context, mediaDeviceInfo.deviceId); }, child: Text(mediaDeviceInfo.label), ); }, ).toList(), ); } }, ); }, ).then((deviceId) { log("onAudioOutputSelected deviceId: $deviceId", TAG); if (deviceId != null) { setState(() { if (kIsWeb) { primaryRenderer?.value.audioOutput(deviceId); minorRenderers.forEach((userId, renderer) { renderer.audioOutput(deviceId); }); } else { _callSession.selectAudioOutput(deviceId); } }); } }); } else { setState(() { _isSpeakerEnabled = !_isSpeakerEnabled; _callSession.enableSpeakerphone(_isSpeakerEnabled); }); } } _switchAudioInput() { if (kIsWeb || WebRTC.platformIsDesktop) { showDialog( context: context, builder: (BuildContext context) { return FutureBuilder<List<MediaDeviceInfo>>( future: _callSession.getAudioInputs(), builder: (context, snapshot) { if (!snapshot.hasData || snapshot.data!.isEmpty) { return AlertDialog( content: const Text('No Audio input devices found'), actions: <Widget>[ TextButton( style: TextButton.styleFrom( textStyle: Theme.of(context).textTheme.labelLarge, ), child: const Text('Ok'), onPressed: () { Navigator.of(context).pop(); }, ) ], ); } else { return SimpleDialog( title: const Text('Select Audio input device'), children: snapshot.data?.map( (mediaDeviceInfo) { return SimpleDialogOption( onPressed: () { Navigator.pop(context, mediaDeviceInfo.deviceId); }, child: Text(mediaDeviceInfo.label), ); }, ).toList(), ); } }, ); }, ).then((deviceId) { log("onAudioOutputSelected deviceId: $deviceId", TAG); if (deviceId != null) { setState(() { _callSession.selectAudioInput(deviceId); }); } }); } } @override void onConnectedToUser(P2PSession session, int userId) { log("onConnectedToUser userId= $userId"); } @override void onConnectionClosedForUser(P2PSession session, int userId) { log("onConnectionClosedForUser userId= $userId"); _removeMediaStream(session, userId); } @override void onDisconnectedFromUser(P2PSession session, int userId) { log("onDisconnectedFromUser userId= $userId"); } void _initAlreadyReceivedStreams() { if (CallManager.instance.remoteStreams.isNotEmpty) { minorRenderers.addEntries([ ...CallManager.instance.remoteStreams.entries.map((mediaStreamEntry) { var videoRenderer = RTCVideoRenderer(); videoRenderer.initialize().then((value) { videoRenderer.srcObject = mediaStreamEntry.value; }); return MapEntry(mediaStreamEntry.key, videoRenderer); }) ]); // CallManager.instance.remoteStreams // .clear(); //TODO VT check concurrency issue } createLocalRenderer() { var renderer = MapEntry(_currentUserId, RTCVideoRenderer()); renderer.value.initialize().then((value) { renderer.value.srcObject = CallManager.instance.localMediaStream; }); return renderer; } if (CallManager.instance.localMediaStream != null) { if (minorRenderers.isNotEmpty) { var tempPrimaryRenderer = minorRenderers.entries.first; primaryRenderer = tempPrimaryRenderer; minorRenderers.remove(tempPrimaryRenderer.key); minorRenderers.addEntries([createLocalRenderer()]); } else { primaryRenderer = createLocalRenderer(); // CallManager.instance.localMediaStream = null; } } } @override void onConnectingToUser(P2PSession session, int userId) { // TODO: implement onConnectingToUser } @override void onConnectionFailedWithUser(P2PSession session, int userId) { // TODO: implement onConnectionFailedWithUser } }