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