import 'dart:async'; import 'dart:typed_data'; import 'package:diplomaticquarterapp/pages/conference/participant_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:twilio_programmable_video/twilio_programmable_video.dart'; class ConferenceRoom with ChangeNotifier { final String name; final String token; final String identity; final StreamController _onAudioEnabledStreamController = StreamController.broadcast(); Stream onAudioEnabled; final StreamController _onVideoEnabledStreamController = StreamController.broadcast(); Stream onVideoEnabled; final StreamController _onExceptionStreamController = StreamController.broadcast(); Stream onException; final Completer _completer = Completer(); final List _participants = []; final List _participantBuffer = []; final List _streamSubscriptions = []; final List _dataTracks = []; final List _messages = []; CameraCapturer _cameraCapturer; Room _room; Timer _timer; ConferenceRoom({ @required this.name, @required this.token, @required this.identity, }) { onAudioEnabled = _onAudioEnabledStreamController.stream; onVideoEnabled = _onVideoEnabledStreamController.stream; onException = _onExceptionStreamController.stream; } List get participants { return [..._participants]; } Future connect() async { print('ConferenceRoom.connect()'); try { await TwilioProgrammableVideo.debug(dart: true, native: true); await TwilioProgrammableVideo.setSpeakerphoneOn(true); _cameraCapturer = CameraCapturer(CameraSource.FRONT_CAMERA); var connectOptions = ConnectOptions( token, roomName: name, preferredAudioCodecs: [OpusCodec()], audioTracks: [LocalAudioTrack(true)], dataTracks: [LocalDataTrack()], videoTracks: [LocalVideoTrack(true, _cameraCapturer)], enableDominantSpeaker: true, ); _room = await TwilioProgrammableVideo.connect(connectOptions); _streamSubscriptions.add(_room.onConnected.listen(_onConnected)); _streamSubscriptions.add(_room.onConnectFailure.listen(_onConnectFailure)); return _completer.future; } catch (err) { print(err); rethrow; } } Future disconnect() async { print('ConferenceRoom.disconnect()'); if (_timer != null) { _timer.cancel(); } await _room.disconnect(); } @override void dispose() { print('ConferenceRoom.dispose()'); _disposeStreamsAndSubscriptions(); super.dispose(); } Future _disposeStreamsAndSubscriptions() async { await _onAudioEnabledStreamController.close(); await _onVideoEnabledStreamController.close(); await _onExceptionStreamController.close(); for (var streamSubscription in _streamSubscriptions) { await streamSubscription.cancel(); } } Future sendMessage(String message) async { final tracks = _room.localParticipant.localDataTracks; final localDataTrack = tracks.isEmpty ? null : tracks[0].localDataTrack; if (localDataTrack == null || _messages.isNotEmpty) { print('ConferenceRoom.sendMessage => Track is not available yet, buffering message.'); _messages.add(message); return; } await localDataTrack.send(message); } Future sendBufferMessage(ByteBuffer message) async { final tracks = _room.localParticipant.localDataTracks; final localDataTrack = tracks.isEmpty ? null : tracks[0].localDataTrack; if (localDataTrack == null) { return; } await localDataTrack.sendBuffer(message); } Future toggleVideoEnabled() async { final tracks = _room.localParticipant.localVideoTracks; final localVideoTrack = tracks.isEmpty ? null : tracks[0].localVideoTrack; if (localVideoTrack == null) { print('ConferenceRoom.toggleVideoEnabled() => Track is not available yet!'); return; } await localVideoTrack.enable(!localVideoTrack.isEnabled); var index = _participants.indexWhere((ParticipantWidget participant) => !participant.isRemote); if (index < 0) { return; } var participant = _participants[index]; _participants.replaceRange( index, index + 1, [ participant.copyWith(videoEnabled: localVideoTrack.isEnabled), ], ); print('ConferenceRoom.toggleVideoEnabled() => ${localVideoTrack.isEnabled}'); _onVideoEnabledStreamController.add(localVideoTrack.isEnabled); notifyListeners(); } Future toggleAudioEnabled() async { final tracks = _room.localParticipant.localAudioTracks; final localAudioTrack = tracks.isEmpty ? null : tracks[0].localAudioTrack; if (localAudioTrack == null) { print('ConferenceRoom.toggleAudioEnabled() => Track is not available yet!'); return; } await localAudioTrack.enable(!localAudioTrack.isEnabled); var index = _participants.indexWhere((ParticipantWidget participant) => !participant.isRemote); if (index < 0) { return; } var participant = _participants[index]; _participants.replaceRange( index, index + 1, [ participant.copyWith(audioEnabled: localAudioTrack.isEnabled), ], ); print('ConferenceRoom.toggleAudioEnabled() => ${localAudioTrack.isEnabled}'); _onAudioEnabledStreamController.add(localAudioTrack.isEnabled); notifyListeners(); } Future switchCamera() async { print('ConferenceRoom.switchCamera()'); try { await _cameraCapturer.switchCamera(); } on FormatException catch (e) { print( 'ConferenceRoom.switchCamera() failed because of FormatException with message: ${e.message}', ); } } void addDummy({Widget child}) { print('ConferenceRoom.addDummy()'); if (_participants.length >= 18) { throw PlatformException( code: 'ConferenceRoom.maximumReached', message: 'Maximum reached', details: 'Currently the lay-out can only render a maximum of 18 participants', ); } _participants.insert( 0, ParticipantWidget( id: (_participants.length + 1).toString(), child: child, isRemote: true, audioEnabled: true, videoEnabled: true, isDummy: true, ), ); notifyListeners(); } void removeDummy() { print('ConferenceRoom.removeDummy()'); var dummy = _participants.firstWhere((participant) => participant.isDummy, orElse: () => null); if (dummy != null) { _participants.remove(dummy); notifyListeners(); } } void _onConnected(Room room) { print('ConferenceRoom._onConnected => state: ${room.state}'); // When connected for the first time, add remote participant listeners _streamSubscriptions.add(_room.onParticipantConnected.listen(_onParticipantConnected)); _streamSubscriptions.add(_room.onParticipantDisconnected.listen(_onParticipantDisconnected)); _streamSubscriptions.add(_room.onDominantSpeakerChange.listen(_onDominantSpeakerChanged)); // Only add ourselves when connected for the first time too. _participants.add( _buildParticipant( child: room.localParticipant.localVideoTracks[0].localVideoTrack.widget(), id: identity, audioEnabled: true, videoEnabled: true, ), ); for (final remoteParticipant in room.remoteParticipants) { var participant = _participants.firstWhere((participant) => participant.id == remoteParticipant.sid, orElse: () => null); if (participant == null) { print('Adding participant that was already present in the room ${remoteParticipant.sid}, before I connected'); _addRemoteParticipantListeners(remoteParticipant); } } // We have to listen for the [onDataTrackPublished] event on the [LocalParticipant] in // order to be able to use the [send] method. _streamSubscriptions.add(room.localParticipant.onDataTrackPublished.listen(_onLocalDataTrackPublished)); notifyListeners(); _completer.complete(room); _timer = Timer.periodic(const Duration(minutes: 1), (_) { // Let's see if we can send some data over the DataTrack API sendMessage('And another minute has passed since I connected...'); // Also try the ByteBuffer way of sending data final list = 'This data has been sent over the ByteBuffer channel of the DataTrack API'.codeUnits; var bytes = Uint8List.fromList(list); sendBufferMessage(bytes.buffer); }); } void _onLocalDataTrackPublished(LocalDataTrackPublishedEvent event) { // Send buffered messages, if any... while (_messages.isNotEmpty) { var message = _messages.removeAt(0); print('Sending buffered message: $message'); event.localDataTrackPublication.localDataTrack.send(message); } } void _onConnectFailure(RoomConnectFailureEvent event) { print('ConferenceRoom._onConnectFailure: ${event.exception}'); _completer.completeError(event.exception); } void _onDominantSpeakerChanged(DominantSpeakerChangedEvent event) { print('ConferenceRoom._onDominantSpeakerChanged: ${event.remoteParticipant.identity}'); var oldDominantParticipant = _participants.firstWhere((p) => p.isDominant, orElse: () => null); if (oldDominantParticipant != null) { var oldDominantParticipantIndex = _participants.indexOf(oldDominantParticipant); _participants.replaceRange(oldDominantParticipantIndex, oldDominantParticipantIndex + 1, [oldDominantParticipant.copyWith(isDominant: false)]); } var newDominantParticipant = _participants.firstWhere((p) => p.id == event.remoteParticipant.sid); var newDominantParticipantIndex = _participants.indexOf(newDominantParticipant); _participants.replaceRange(newDominantParticipantIndex, newDominantParticipantIndex + 1, [newDominantParticipant.copyWith(isDominant: true)]); notifyListeners(); } void _onParticipantConnected(RoomParticipantConnectedEvent event) { print('ConferenceRoom._onParticipantConnected, ${event.remoteParticipant.sid}'); _addRemoteParticipantListeners(event.remoteParticipant); } void _onParticipantDisconnected(RoomParticipantDisconnectedEvent event) { print('ConferenceRoom._onParticipantDisconnected: ${event.remoteParticipant.sid}'); _participants.removeWhere((ParticipantWidget p) => p.id == event.remoteParticipant.sid); notifyListeners(); } ParticipantWidget _buildParticipant({ @required Widget child, @required String id, @required bool audioEnabled, @required bool videoEnabled, RemoteParticipant remoteParticipant, }) { return ParticipantWidget( id: remoteParticipant?.sid, isRemote: remoteParticipant != null, child: child, audioEnabled: audioEnabled, videoEnabled: videoEnabled, ); } void _addRemoteParticipantListeners(RemoteParticipant remoteParticipant) { print('ConferenceRoom._addRemoteParticipantListeners() => Adding listeners to remoteParticipant ${remoteParticipant.sid}'); _streamSubscriptions.add(remoteParticipant.onAudioTrackDisabled.listen(_onAudioTrackDisabled)); _streamSubscriptions.add(remoteParticipant.onAudioTrackEnabled.listen(_onAudioTrackEnabled)); _streamSubscriptions.add(remoteParticipant.onAudioTrackPublished.listen(_onAudioTrackPublished)); _streamSubscriptions.add(remoteParticipant.onAudioTrackSubscribed.listen(_onAudioTrackSubscribed)); _streamSubscriptions.add(remoteParticipant.onAudioTrackSubscriptionFailed.listen(_onAudioTrackSubscriptionFailed)); _streamSubscriptions.add(remoteParticipant.onAudioTrackUnpublished.listen(_onAudioTrackUnpublished)); _streamSubscriptions.add(remoteParticipant.onAudioTrackUnsubscribed.listen(_onAudioTrackUnsubscribed)); _streamSubscriptions.add(remoteParticipant.onDataTrackPublished.listen(_onDataTrackPublished)); _streamSubscriptions.add(remoteParticipant.onDataTrackSubscribed.listen(_onDataTrackSubscribed)); _streamSubscriptions.add(remoteParticipant.onDataTrackSubscriptionFailed.listen(_onDataTrackSubscriptionFailed)); _streamSubscriptions.add(remoteParticipant.onDataTrackUnpublished.listen(_onDataTrackUnpublished)); _streamSubscriptions.add(remoteParticipant.onDataTrackUnsubscribed.listen(_onDataTrackUnsubscribed)); _streamSubscriptions.add(remoteParticipant.onVideoTrackDisabled.listen(_onVideoTrackDisabled)); _streamSubscriptions.add(remoteParticipant.onVideoTrackEnabled.listen(_onVideoTrackEnabled)); _streamSubscriptions.add(remoteParticipant.onVideoTrackPublished.listen(_onVideoTrackPublished)); _streamSubscriptions.add(remoteParticipant.onVideoTrackSubscribed.listen(_onVideoTrackSubscribed)); _streamSubscriptions.add(remoteParticipant.onVideoTrackSubscriptionFailed.listen(_onVideoTrackSubscriptionFailed)); _streamSubscriptions.add(remoteParticipant.onVideoTrackUnpublished.listen(_onVideoTrackUnpublished)); _streamSubscriptions.add(remoteParticipant.onVideoTrackUnsubscribed.listen(_onVideoTrackUnsubscribed)); } void _onAudioTrackDisabled(RemoteAudioTrackEvent event) { print('ConferenceRoom._onAudioTrackDisabled(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}, isEnabled: ${event.remoteAudioTrackPublication.isTrackEnabled}'); _setRemoteAudioEnabled(event); } void _onAudioTrackEnabled(RemoteAudioTrackEvent event) { print('ConferenceRoom._onAudioTrackEnabled(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}, isEnabled: ${event.remoteAudioTrackPublication.isTrackEnabled}'); _setRemoteAudioEnabled(event); } void _onAudioTrackPublished(RemoteAudioTrackEvent event) { print('ConferenceRoom._onAudioTrackPublished(), ${event.remoteParticipant.sid}}'); } void _onAudioTrackSubscribed(RemoteAudioTrackSubscriptionEvent event) { print('ConferenceRoom._onAudioTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}'); _addOrUpdateParticipant(event); } void _onAudioTrackSubscriptionFailed(RemoteAudioTrackSubscriptionFailedEvent event) { print('ConferenceRoom._onAudioTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}'); _onExceptionStreamController.add( PlatformException( code: 'ConferenceRoom.audioTrackSubscriptionFailed', message: 'AudioTrack Subscription Failed', details: event.exception.toString(), ), ); } void _onAudioTrackUnpublished(RemoteAudioTrackEvent event) { print('ConferenceRoom._onAudioTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}'); } void _onAudioTrackUnsubscribed(RemoteAudioTrackSubscriptionEvent event) { print('ConferenceRoom._onAudioTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrack.sid}'); } void _onDataTrackPublished(RemoteDataTrackEvent event) { print('ConferenceRoom._onDataTrackPublished(), ${event.remoteParticipant.sid}}'); } void _onDataTrackSubscribed(RemoteDataTrackSubscriptionEvent event) { print('ConferenceRoom._onDataTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}'); final dataTrack = event.remoteDataTrackPublication.remoteDataTrack; _dataTracks.add(dataTrack); _streamSubscriptions.add(dataTrack.onMessage.listen(_onMessage)); _streamSubscriptions.add(dataTrack.onBufferMessage.listen(_onBufferMessage)); } void _onDataTrackSubscriptionFailed(RemoteDataTrackSubscriptionFailedEvent event) { print('ConferenceRoom._onDataTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}'); _onExceptionStreamController.add( PlatformException( code: 'ConferenceRoom.dataTrackSubscriptionFailed', message: 'DataTrack Subscription Failed', details: event.exception.toString(), ), ); } void _onDataTrackUnpublished(RemoteDataTrackEvent event) { print('ConferenceRoom._onDataTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}'); } void _onDataTrackUnsubscribed(RemoteDataTrackSubscriptionEvent event) { print('ConferenceRoom._onDataTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrack.sid}'); } void _onVideoTrackDisabled(RemoteVideoTrackEvent event) { print('ConferenceRoom._onVideoTrackDisabled(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}, isEnabled: ${event.remoteVideoTrackPublication.isTrackEnabled}'); _setRemoteVideoEnabled(event); } void _onVideoTrackEnabled(RemoteVideoTrackEvent event) { print('ConferenceRoom._onVideoTrackEnabled(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}, isEnabled: ${event.remoteVideoTrackPublication.isTrackEnabled}'); _setRemoteVideoEnabled(event); } void _onVideoTrackPublished(RemoteVideoTrackEvent event) { print('ConferenceRoom._onVideoTrackPublished(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}'); } void _onVideoTrackSubscribed(RemoteVideoTrackSubscriptionEvent event) { print('ConferenceRoom._onVideoTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrack.sid}'); _addOrUpdateParticipant(event); } void _onVideoTrackSubscriptionFailed(RemoteVideoTrackSubscriptionFailedEvent event) { print('ConferenceRoom._onVideoTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}'); _onExceptionStreamController.add( PlatformException( code: 'ConferenceRoom.videoTrackSubscriptionFailed', message: 'VideoTrack Subscription Failed', details: event.exception.toString(), ), ); } void _onVideoTrackUnpublished(RemoteVideoTrackEvent event) { print('ConferenceRoom._onVideoTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}'); } void _onVideoTrackUnsubscribed(RemoteVideoTrackSubscriptionEvent event) { print('ConferenceRoom._onVideoTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrack.sid}'); } void _onMessage(RemoteDataTrackStringMessageEvent event) { print('onMessage => ${event.remoteDataTrack.sid}, ${event.message}'); } void _onBufferMessage(RemoteDataTrackBufferMessageEvent event) { print('onBufferMessage => ${event.remoteDataTrack.sid}, ${String.fromCharCodes(event.message.asUint8List())}'); } void _setRemoteAudioEnabled(RemoteAudioTrackEvent event) { if (event.remoteAudioTrackPublication == null) { return; } var index = _participants.indexWhere((ParticipantWidget participant) => participant.id == event.remoteParticipant.sid); if (index < 0) { return; } var participant = _participants[index]; _participants.replaceRange( index, index + 1, [ participant.copyWith(audioEnabled: event.remoteAudioTrackPublication.isTrackEnabled), ], ); notifyListeners(); } void _setRemoteVideoEnabled(RemoteVideoTrackEvent event) { if (event.remoteVideoTrackPublication == null) { return; } var index = _participants.indexWhere((ParticipantWidget participant) => participant.id == event.remoteParticipant.sid); if (index < 0) { return; } var participant = _participants[index]; _participants.replaceRange( index, index + 1, [ participant.copyWith(videoEnabled: event.remoteVideoTrackPublication.isTrackEnabled), ], ); notifyListeners(); } void _addOrUpdateParticipant(RemoteParticipantEvent event) { print('ConferenceRoom._addOrUpdateParticipant(), ${event.remoteParticipant.sid}'); final participant = _participants.firstWhere( (ParticipantWidget participant) => participant.id == event.remoteParticipant.sid, orElse: () => null, ); if (participant != null) { print('Participant found: ${participant.id}, updating A/V enabled values'); _setRemoteVideoEnabled(event); _setRemoteAudioEnabled(event); } else { final bufferedParticipant = _participantBuffer.firstWhere( (ParticipantBuffer participant) => participant.id == event.remoteParticipant.sid, orElse: () => null, ); if (bufferedParticipant != null) { _participantBuffer.remove(bufferedParticipant); } else if (event is RemoteAudioTrackEvent) { print('Audio subscription came first, waiting for the video subscription...'); _participantBuffer.add( ParticipantBuffer( id: event.remoteParticipant.sid, audioEnabled: event.remoteAudioTrackPublication?.remoteAudioTrack?.isEnabled ?? true, ), ); return; } if (event is RemoteVideoTrackSubscriptionEvent) { print('New participant, adding: ${event.remoteParticipant.sid}'); _participants.insert( 0, _buildParticipant( child: event.remoteVideoTrack.widget(), id: event.remoteParticipant.sid, remoteParticipant: event.remoteParticipant, audioEnabled: bufferedParticipant?.audioEnabled ?? true, videoEnabled: event.remoteVideoTrackPublication?.remoteVideoTrack?.isEnabled ?? true, ), ); } notifyListeners(); } } }