diff --git a/lib/livekit_client.dart b/lib/livekit_client.dart index 3e77ad8..e32f142 100644 --- a/lib/livekit_client.dart +++ b/lib/livekit_client.dart @@ -1,8 +1,8 @@ library livekit_client; +export 'src/livekit.dart'; export 'src/errors.dart'; export 'src/room.dart'; -export 'src/rtc_engine.dart'; export 'src/participant/participant.dart'; export 'src/participant/local_participant.dart'; export 'src/participant/remote_participant.dart'; diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index 1ea5136..12a0d69 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -9,9 +9,11 @@ class RTCConfiguration { iceServersMap.add(element.toMap()); }); return { + // only supports unified plan + 'sdpSemantics': 'unified-plan', if (iceCandidatePoolSize != null) "iceCandidatePoolSize": iceCandidatePoolSize, - if (iceServersMap.isNotEmpty) "iceServers": iceServersMap, + "iceServers": iceServersMap, if (iceTransportPolicy != null) "iceTransportPolicy": iceTransportPolicy, }; } @@ -27,10 +29,8 @@ class RTCIceServer { Map toMap() { return { "urls": urls, - if (username != null) - "username": username, - if (credential != null) - "credential": credential, + if (username != null) "username": username, + if (credential != null) "credential": credential, }; } } diff --git a/lib/src/livekit.dart b/lib/src/livekit.dart new file mode 100644 index 0000000..5d19f8d --- /dev/null +++ b/lib/src/livekit.dart @@ -0,0 +1,9 @@ +import 'room.dart'; + +class LiveKitClient { + // TODO: take in connect options + static Future connect(String url, String token) { + var room = Room(); + return room.connect(url, token); + } +} diff --git a/lib/src/participant/local_participant.dart b/lib/src/participant/local_participant.dart index f77bcce..4b52e97 100644 --- a/lib/src/participant/local_participant.dart +++ b/lib/src/participant/local_participant.dart @@ -34,6 +34,7 @@ class LocalParticipant extends Participant { return stream; } + /// publish an audio track to the room Future publishAudioTrack(LocalAudioTrack track) async { if (audioTracks.values.any( (element) => element.track?.mediaTrack.id == track.mediaTrack.id)) { @@ -59,6 +60,7 @@ class LocalParticipant extends Participant { return pub; } + /// publish a video track to the room Future publishVideoTrack(LocalVideoTrack track) async { if (audioTracks.values.any( (element) => element.track?.mediaTrack.id == track.mediaTrack.id)) { diff --git a/lib/src/room.dart b/lib/src/room.dart index a5b3dd1..dfe380e 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -73,8 +73,8 @@ class Room with ParticipantDelegate { Completer? _connectCompleter; - Room(SignalClient client, RTCConfiguration? rtcConfig) - : _engine = new RTCEngine(client, rtcConfig) { + Room([RTCConfiguration? rtcConfig]) + : _engine = new RTCEngine(SignalClient(), rtcConfig) { _engine.onTrack = _onTrackAdded; _engine.onICEConnected = _handleICEConnected; _engine.onDisconnected = _handleDisconnect; @@ -85,7 +85,7 @@ class Room with ParticipantDelegate { // TODO: handle reconnecting & reconnected events } - Future connect(String url, String token, JoinOptions? opts) async { + Future connect(String url, String token, [JoinOptions? opts]) async { var completer = new Completer(); _connectCompleter = completer; @@ -109,10 +109,10 @@ class Room with ParticipantDelegate { // room is not ready until ICE is connected. so we would return a completer for now // if it times out, we'll fail the completer - Timer(Duration(seconds: 5), () { - _connectCompleter?.completeError(ConnectError()); - _connectCompleter = null; - }); + // Timer(Duration(seconds: 5), () { + // _connectCompleter?.completeError(ConnectError()); + // _connectCompleter = null; + // }); return completer.future; } diff --git a/lib/src/rtc_engine.dart b/lib/src/rtc_engine.dart index 95b60ae..e053435 100644 --- a/lib/src/rtc_engine.dart +++ b/lib/src/rtc_engine.dart @@ -64,6 +64,8 @@ class RTCEngine with SignalClientDelegate { var completer = new Completer(); joinCompleter = completer; + client.join(url, token, opts); + // if it's not complete after 5 seconds, fail new Timer(connectionTimeout, () { joinCompleter?.completeError(new ConnectError()); @@ -116,7 +118,12 @@ class RTCEngine with SignalClientDelegate { return; } - var remoteDesc = await pub.pc.getRemoteDescription(); + RTCSessionDescription? remoteDesc; + if (pub.pc.iceConnectionState != null) { + // when not initially connected, this crashes on iOS + remoteDesc = await pub.pc.getRemoteDescription(); + } + // handle cases that we couldn't create a new offer due to a pending answer // that's lost in transit if (remoteDesc != null && @@ -151,8 +158,9 @@ class RTCEngine with SignalClientDelegate { }; pubPC.onRenegotiationNeeded = () { - if (pubPC.iceConnectionState == - RTCIceConnectionState.RTCIceConnectionStateNew) { + if (pubPC.iceConnectionState == null || + pubPC.iceConnectionState == + RTCIceConnectionState.RTCIceConnectionStateNew) { return; } negotiate(); @@ -185,13 +193,16 @@ class RTCEngine with SignalClientDelegate { }; // create data channels - var lossyInit = new RTCDataChannelInit(); - lossyInit.ordered = true; - lossyInit.maxRetransmits = 1; + var lossyInit = new RTCDataChannelInit() + ..maxRetransmits = 1 + ..ordered = true + ..binaryType = 'binary'; lossyDC = await pubPC.createDataChannel(lossyDataChannel, lossyInit); - var reliableInit = new RTCDataChannelInit(); - reliableInit.ordered = true; + var reliableInit = new RTCDataChannelInit() + ..ordered = true + ..maxRetransmits = 50 + ..binaryType = 'binary'; reliableDC = await pubPC.createDataChannel(reliableDataChannel, reliableInit); @@ -224,7 +235,7 @@ class RTCEngine with SignalClientDelegate { //------------------ SignalClient Delegate methods -------------------------// - void onConnected(JoinResponse response) { + void onConnected(JoinResponse response) async { // create peer connections this.isClosed = false; @@ -243,7 +254,7 @@ class RTCEngine with SignalClientDelegate { rtcConfig.iceServers = iceServers; } - _configurePeerConnections(); + await _configurePeerConnections(); negotiate(); @@ -251,7 +262,9 @@ class RTCEngine with SignalClientDelegate { joinCompleter = null; } - void onClose(String? reason) {} + void onClose([String? reason]) { + // TODO: handle reconnect when signal interrupted + } void onOffer(RTCSessionDescription sd) async { var sub = subscriber; diff --git a/lib/src/signal_client.dart b/lib/src/signal_client.dart index 030c439..256403e 100644 --- a/lib/src/signal_client.dart +++ b/lib/src/signal_client.dart @@ -19,7 +19,7 @@ mixin SignalClientDelegate { // initial connection established void onConnected(JoinResponse response); // websocket has closed - void onClose(String? reason); + void onClose([String? reason]); // when a server offer is received void onOffer(RTCSessionDescription sd); // when an answer from server is received @@ -37,19 +37,19 @@ mixin SignalClientDelegate { } class SignalClient { - SignalClientDelegate delegate; + SignalClientDelegate? delegate; bool _connected = false; WebSocketChannel? _ws; - SignalClient(this.delegate); + SignalClient(); bool get connected => this._connected; - join(String url, String token, JoinOptions options) { + join(String url, String token, JoinOptions? options) { url += '/rtc'; var params = _paramsForToken(token); - if (options.autoSubscribe != null) { + if (options != null && options.autoSubscribe != null) { params += '&auto_subscribe=${options.autoSubscribe! ? '1' : '0'}'; } var uri = Uri.parse(url + params); @@ -61,6 +61,7 @@ class SignalClient { _ws = ws; } catch (e) { // failed before error handler is installed, fail immediately + _handleError(e); } } @@ -164,30 +165,30 @@ class SignalClient { case SignalResponse_Message.join: if (!_connected) { _connected = true; - delegate.onConnected(msg.join); + delegate?.onConnected(msg.join); } break; case SignalResponse_Message.answer: - delegate.onAnswer(toRTCSessionDescription(msg.answer)); + delegate?.onAnswer(toRTCSessionDescription(msg.answer)); break; case SignalResponse_Message.offer: - delegate.onOffer(toRTCSessionDescription(msg.offer)); + delegate?.onOffer(toRTCSessionDescription(msg.offer)); break; case SignalResponse_Message.trickle: - delegate.onTrickle( + delegate?.onTrickle( toRTCIceCandidate(msg.trickle.candidateInit), msg.trickle.target); break; case SignalResponse_Message.update: - delegate.onParticipantUpdate(msg.update.participants); + delegate?.onParticipantUpdate(msg.update.participants); break; case SignalResponse_Message.trackPublished: - delegate.onLocalTrackPublished(msg.trackPublished); + delegate?.onLocalTrackPublished(msg.trackPublished); break; case SignalResponse_Message.speaker: - delegate.onActiveSpeakersChanged(msg.speaker.speakers); + delegate?.onActiveSpeakersChanged(msg.speaker.speakers); break; case SignalResponse_Message.leave: - delegate.onLeave(msg.leave); + delegate?.onLeave(msg.leave); break; default: log('unsupported message: ' + jsonEncode(msg)); @@ -199,7 +200,12 @@ class SignalClient { } _handleDone() { + if (!_connected) { + return; + } _ws = null; + _connected = false; + delegate?.onClose(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 247bef1..cecdb2f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,4 +1,4 @@ -name: livekit_client_flutter +name: livekit_client description: Flutter client for LiveKit version: 0.0.1 homepage: https://livekit.io