You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
PatientApp-KKUMC/lib/pages/conference/conference_room.dart

531 lines
21 KiB
Dart

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<bool> _onAudioEnabledStreamController = StreamController<bool>.broadcast();
Stream<bool> onAudioEnabled;
final StreamController<bool> _onVideoEnabledStreamController = StreamController<bool>.broadcast();
Stream<bool> onVideoEnabled;
final StreamController<Exception> _onExceptionStreamController = StreamController<Exception>.broadcast();
Stream<Exception> onException;
final Completer<Room> _completer = Completer<Room>();
final List<ParticipantWidget> _participants = [];
final List<ParticipantBuffer> _participantBuffer = [];
final List<StreamSubscription> _streamSubscriptions = [];
final List<RemoteDataTrack> _dataTracks = [];
final List<String> _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<ParticipantWidget> get participants {
return [..._participants];
}
Future<Room> 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<void> disconnect() async {
print('ConferenceRoom.disconnect()');
if (_timer != null) {
_timer.cancel();
}
await _room.disconnect();
}
@override
void dispose() {
print('ConferenceRoom.dispose()');
_disposeStreamsAndSubscriptions();
super.dispose();
}
Future<void> _disposeStreamsAndSubscriptions() async {
await _onAudioEnabledStreamController.close();
await _onVideoEnabledStreamController.close();
await _onExceptionStreamController.close();
for (var streamSubscription in _streamSubscriptions) {
await streamSubscription.cancel();
}
}
Future<void> 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<void> 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<void> 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<void> 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<void> 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();
}
}
}