import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:http/http.dart' as http; import 'errors.dart'; import 'logger.dart'; import 'options.dart'; import 'track/track.dart'; import 'version.dart'; import 'proto/livekit_models.pb.dart'; import 'proto/livekit_rtc.pb.dart'; import '_websocket_api.dart' if (dart.library.io) '_websocket_io.dart' if (dart.library.html) '_websocket_html.dart' as platform; mixin SignalClientDelegate { // initial connection established void onConnected(JoinResponse response); // websocket has closed void onClose([String? reason]); // when a server offer is received void onOffer(RTCSessionDescription sd); // when an answer from server is received void onAnswer(RTCSessionDescription sd); // when server has a new ICE candidate void onTrickle(RTCIceCandidate candidate, SignalTarget target); // participant has changed void onParticipantUpdate(List updates); // when a track has been added successfully void onLocalTrackPublished(TrackPublishedResponse response); // active speaker has changed void onActiveSpeakersChanged(List speakers); // when server sends this client a leave message void onLeave(LeaveRequest req); } class SignalClient { SignalClientDelegate? delegate; bool _connected = false; WebSocketChannel? _ws; SignalClient(); bool get connected => this._connected; Future join(String url, String token, JoinOptions? options) async { var rtcUrl = '$url/rtc'; var params = _joinParams(token); if (options != null && options.autoSubscribe != null) { params += '&auto_subscribe=${options.autoSubscribe! ? '1' : '0'}'; } try { var ws = await platform.connectToWebSocket(Uri.parse(rtcUrl + params)); ws.stream .listen(_handleMessage, onError: _handleError, onDone: _handleDone); _ws = ws; } catch (e) { var completer = Completer(); var validateUri = Uri.parse('http${rtcUrl.substring(2)}/validate$params'); http.get(validateUri).then((response) { if (response.statusCode != 200) { completer.completeError(ConnectError(response.body)); } else { completer.completeError(ConnectError()); } }).catchError((e) { completer.completeError(ConnectError()); }); return completer.future; } } Future reconnect(String url, String token) async { _connected = false; _ws?.sink.close(); _ws = null; url += '/rtc'; var params = _joinParams(token); params += '&reconnect=1'; var uri = Uri.parse(url + params); var ws = await platform.connectToWebSocket(uri); _ws = ws; _connected = true; } close() { this._connected = false; this._ws?.sink.close(); } sendOffer(RTCSessionDescription offer) { this._sendRequest(new SignalRequest( offer: fromRTCSessionDescription(offer), )); } sendAnswer(RTCSessionDescription answer) { this._sendRequest(new SignalRequest( answer: fromRTCSessionDescription(answer), )); } sendIceCandidate(RTCIceCandidate candidate, SignalTarget target) { this._sendRequest(new SignalRequest( trickle: new TrickleRequest( candidateInit: fromRTCIceCandidate(candidate), target: target, ))); } sendMuteTrack(String trackSid, bool muted) { this._sendRequest(new SignalRequest( mute: new MuteTrackRequest( sid: trackSid, muted: muted, ), )); } sendAddTrack( {required String cid, required String name, required TrackType type, TrackDimension? dimension}) { var req = new AddTrackRequest( cid: cid, name: name, type: type, ); if (dimension != null) { req.width = dimension.width; req.height = dimension.height; } this._sendRequest(new SignalRequest( addTrack: req, )); } sendUpdateTrackSettings(UpdateTrackSettings settings) { this._sendRequest(new SignalRequest( trackSetting: settings, )); } sendUpdateSubscription(UpdateSubscription subscription) { this._sendRequest(new SignalRequest( subscription: subscription, )); } sendSetSimulcastLayers(String trackSid, List layers) { this._sendRequest(new SignalRequest( simulcast: new SetSimulcastLayers( trackSid: trackSid, layers: layers, ))); } sendLeave() { this._sendRequest(new SignalRequest( leave: new LeaveRequest(), )); } _sendRequest(SignalRequest req) { if (this._ws == null) { log('could not send message, not connected'); return; } var buf = req.writeToBuffer(); this._ws?.sink.add(buf); } _handleMessage(dynamic message) { if (!(message is List)) { return; } var msg = SignalResponse.fromBuffer(message); switch (msg.whichMessage()) { case SignalResponse_Message.join: if (!_connected) { _connected = true; delegate?.onConnected(msg.join); } break; case SignalResponse_Message.answer: delegate?.onAnswer(toRTCSessionDescription(msg.answer)); break; case SignalResponse_Message.offer: delegate?.onOffer(toRTCSessionDescription(msg.offer)); break; case SignalResponse_Message.trickle: delegate?.onTrickle( toRTCIceCandidate(msg.trickle.candidateInit), msg.trickle.target); break; case SignalResponse_Message.update: delegate?.onParticipantUpdate(msg.update.participants); break; case SignalResponse_Message.trackPublished: delegate?.onLocalTrackPublished(msg.trackPublished); break; case SignalResponse_Message.speaker: delegate?.onActiveSpeakersChanged(msg.speaker.speakers); break; case SignalResponse_Message.leave: delegate?.onLeave(msg.leave); break; default: log('unsupported message: ' + jsonEncode(msg)); } } _handleError(Object error) { logger.warning('received websocket error $error'); } _handleDone() { if (!_connected) { return; } _ws = null; _connected = false; delegate?.onClose(); } } String _joinParams(String token) { return '?access_token=$token&protocol=$protocolVersion'; } RTCSessionDescription toRTCSessionDescription(SessionDescription sd) { return new RTCSessionDescription(sd.sdp, sd.type); } SessionDescription fromRTCSessionDescription(RTCSessionDescription rsd) { return new SessionDescription(type: rsd.type, sdp: rsd.sdp); } RTCIceCandidate toRTCIceCandidate(String candidateInit) { var candInit = jsonDecode(candidateInit); return new RTCIceCandidate( candInit['candidate'], candInit['sdpMid'], candInit['sdpMLineIndex']); } String fromRTCIceCandidate(RTCIceCandidate candidate) { return jsonEncode(candidate.toMap()); }