Simulcast, Screen sharing & Various improvements (#4)

* Respect `RTCIceTransportPolicy` enum and organize

* Simplify syntax where possible etc.

* Combine `VideoPreset` and `VideoPresets`

* Default values for `ConnectOptions`

* Build URI instead of String manipulation

* Slight modifications to Exception

* `LiveKitTheme` for example

* `VideoEncoding` class

* Organize imports

* First simulcast implementation

* Remove unnecessary try-catches

* Update Android settings

* Remember uri and token

* example improvements

* `fit` parameter for VideoTrackRenderer

* Simulcast option for example

* Pass `defaultPublishOptions`

* Show only `VideoQuality`

* Pass tests

* Better buildUri logic

* Named parameter to positional

* Explicit imports

* `VideoParameter` instead of `VideoPreset`

* Use `mediaTrack.getSettings` when possible

* Safer dispose logic

* Safer `PCTransport`

Update transport.dart

* Synchronized events for `SignalClient`

* Use logger instead of print

* Make example compile for iOS

* First screen share implementation

* Make example work with screen share

* Example improvement

* Code optimization

* Don't depend on web_socket_channel

* Fix: Unpublish track bug

* Show participant mute state & identity

* Update protos

* Remote mute/unmute

* iOS Background mode

* Separate `createCameraTrack` and `createScreenTrack`

* Clean up

* PB fix

* format

* Fix analyzer warning

* Android clean up

* Update README.md

* Clean up
This commit is contained in:
Hiroshi Horie
2021-09-11 03:14:14 +09:00
committed by GitHub
parent 8e66e324f6
commit 50f56c5e1e
69 changed files with 2653 additions and 2351 deletions
+210 -153
View File
@@ -3,134 +3,191 @@ 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 'package:livekit_client/src/ws/interface.dart';
import 'package:synchronized/synchronized.dart' as sync;
import 'errors.dart';
import 'logger.dart';
import 'options.dart';
import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
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);
Future<void> onConnected(lk_rtc.JoinResponse response);
// websocket has closed
void onClose([String? reason]);
Future<void> onClose([String? reason]);
// when a server offer is received
void onOffer(RTCSessionDescription sd);
Future<void> onOffer(RTCSessionDescription sd);
// when an answer from server is received
void onAnswer(RTCSessionDescription sd);
Future<void> onAnswer(RTCSessionDescription sd);
// when server has a new ICE candidate
void onTrickle(RTCIceCandidate candidate, SignalTarget target);
Future<void> onTrickle(RTCIceCandidate candidate, lk_rtc.SignalTarget target);
// participant has changed
void onParticipantUpdate(List<ParticipantInfo> updates);
Future<void> onParticipantUpdate(List<lk_models.ParticipantInfo> updates);
// when a track has been added successfully
void onLocalTrackPublished(TrackPublishedResponse response);
Future<void> onLocalTrackPublished(lk_rtc.TrackPublishedResponse response);
// active speaker has changed
void onActiveSpeakersChanged(List<SpeakerInfo> speakers);
Future<void> onActiveSpeakersChanged(List<lk_models.SpeakerInfo> speakers);
// when server sends this client a leave message
void onLeave(LeaveRequest req);
Future<void> onLeave(lk_rtc.LeaveRequest req);
// explicit mute track
Future<void> onMuteTrack(lk_rtc.MuteTrackRequest req);
}
extension LKUriExt on Uri {
bool get isSecureScheme => ['https', 'wss'].contains(scheme);
}
class SignalClient {
SignalClientDelegate? delegate;
static const protocolVersion = 2;
final _lock = sync.Lock();
SignalClientDelegate? delegate;
bool _connected = false;
WebSocketChannel? _ws;
LKWebSocket? _ws;
SignalClient();
bool get connected => _connected;
Future<void> join(String url, String token, JoinOptions? options) async {
final rtcUrl = '$url/rtc';
var params = _joinParams(token);
if (options != null && options.autoSubscribe != null) {
params += '&auto_subscribe=${options.autoSubscribe! ? '1' : '0'}';
}
Uri _buildUri(
String uriOrString, {
required String token,
ConnectOptions? options,
bool reconnect = false,
bool validate = false,
bool forceSecure = false,
}) {
final Uri uri = Uri.parse(uriOrString);
final useSecure = uri.isSecureScheme || forceSecure;
final httpScheme = useSecure ? 'https' : 'http';
final wsScheme = useSecure ? 'wss' : 'ws';
return uri.replace(
scheme: validate ? httpScheme : wsScheme,
path: validate ? 'validate' : 'rtc',
queryParameters: <String, String>{
'access_token': token,
if (options != null) 'auto_subscribe': options.autoSubscribe ? '1' : '0',
if (reconnect) 'reconnect': '1',
'protocol': protocolVersion.toString(),
},
);
}
Future<void> join(
String uriString,
String token, {
ConnectOptions? options,
}) async {
// Create default options if null
options ??= const ConnectOptions();
final rtcUri = _buildUri(
uriString,
token: token,
options: options,
);
try {
final ws = await platform.connectToWebSocket(Uri.parse(rtcUrl + params));
ws.stream.listen(_handleMessage, onError: _handleError, onDone: _handleDone);
_ws = ws;
} catch (e) {
final completer = Completer<void>();
final 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((dynamic e) {
completer.completeError(ConnectError());
});
_ws = await LKWebSocket.connect(
rtcUri,
LKWebSocketOptions(
onData: _onSocketData,
onDispose: _onSocketDone,
onError: _handleError,
),
);
} catch (socketError) {
// Re-build same uri for validate mode
final validateUri = _buildUri(
uriString,
token: token,
options: options,
validate: true,
forceSecure: rtcUri.isSecureScheme,
);
return completer.future;
// Attempt Validation
try {
final validateResponse = await http.get(validateUri);
if (validateResponse.statusCode != 200) throw ConnectError(validateResponse.body);
throw ConnectError();
} catch (error) {
// Pass it up if it's already a `ConnectError`
if (error is ConnectError) rethrow;
// HTTP doesn't work either
throw ConnectError();
}
}
}
Future<void> reconnect(String url, String token) async {
Future<void> reconnect(
String uriString,
String token,
) async {
_connected = false;
_ws?.sink.close();
_ws?.dispose();
_ws = null;
url += '/rtc';
var params = _joinParams(token);
params += '&reconnect=1';
final uri = Uri.parse(url + params);
final rtcUri = _buildUri(
uriString,
token: token,
reconnect: true,
);
_ws = await LKWebSocket.connect(
rtcUri,
LKWebSocketOptions(
onData: _onSocketData,
onDispose: _onSocketDone,
onError: _handleError,
),
);
final ws = await platform.connectToWebSocket(uri);
_ws = ws;
_connected = true;
}
void close() {
_connected = false;
_ws?.sink.close();
_ws?.dispose();
}
void sendOffer(RTCSessionDescription offer) {
_sendRequest(SignalRequest(
offer: fromRTCSessionDescription(offer),
));
}
void sendOffer(RTCSessionDescription offer) => _sendRequest(lk_rtc.SignalRequest(
offer: fromRTCSessionDescription(offer),
));
void sendAnswer(RTCSessionDescription answer) {
_sendRequest(SignalRequest(
answer: fromRTCSessionDescription(answer),
));
}
void sendAnswer(RTCSessionDescription answer) => _sendRequest(lk_rtc.SignalRequest(
answer: fromRTCSessionDescription(answer),
));
void sendIceCandidate(RTCIceCandidate candidate, SignalTarget target) {
_sendRequest(SignalRequest(
trickle: TrickleRequest(
candidateInit: fromRTCIceCandidate(candidate),
target: target,
)));
}
void sendIceCandidate(RTCIceCandidate candidate, lk_rtc.SignalTarget target) => _sendRequest(
lk_rtc.SignalRequest(
trickle: lk_rtc.TrickleRequest(
candidateInit: fromRTCIceCandidate(candidate),
target: target,
),
),
);
void sendMuteTrack(String trackSid, bool muted) {
_sendRequest(SignalRequest(
mute: MuteTrackRequest(
sid: trackSid,
muted: muted,
),
));
}
void sendMuteTrack(String trackSid, bool muted) => _sendRequest(lk_rtc.SignalRequest(
mute: lk_rtc.MuteTrackRequest(
sid: trackSid,
muted: muted,
),
));
void sendAddTrack(
{required String cid,
required String name,
required TrackType type,
TrackDimension? dimension}) {
final req = AddTrackRequest(
void sendAddTrack({
required String cid,
required String name,
required lk_models.TrackType type,
TrackDimension? dimension,
}) {
final req = lk_rtc.AddTrackRequest(
cid: cid,
name: name,
type: type,
@@ -139,109 +196,109 @@ class SignalClient {
req.width = dimension.width;
req.height = dimension.height;
}
_sendRequest(SignalRequest(
_sendRequest(lk_rtc.SignalRequest(
addTrack: req,
));
}
void sendUpdateTrackSettings(UpdateTrackSettings settings) {
_sendRequest(SignalRequest(
trackSetting: settings,
));
}
void sendUpdateTrackSettings(lk_rtc.UpdateTrackSettings settings) =>
_sendRequest(lk_rtc.SignalRequest(
trackSetting: settings,
));
void sendUpdateSubscription(UpdateSubscription subscription) {
_sendRequest(SignalRequest(
subscription: subscription,
));
}
void sendUpdateSubscription(lk_rtc.UpdateSubscription subscription) =>
_sendRequest(lk_rtc.SignalRequest(
subscription: subscription,
));
void sendSetSimulcastLayers(String trackSid, List<VideoQuality> layers) {
_sendRequest(SignalRequest(
simulcast: SetSimulcastLayers(
trackSid: trackSid,
layers: layers,
)));
}
void sendSetSimulcastLayers(String trackSid, List<lk_rtc.VideoQuality> layers) =>
_sendRequest(lk_rtc.SignalRequest(
simulcast: lk_rtc.SetSimulcastLayers(
trackSid: trackSid,
layers: layers,
),
));
void sendLeave() {
_sendRequest(SignalRequest(
leave: LeaveRequest(),
));
}
void sendLeave() => _sendRequest(lk_rtc.SignalRequest(
leave: lk_rtc.LeaveRequest(),
));
void _sendRequest(SignalRequest req) {
void _sendRequest(lk_rtc.SignalRequest req) {
if (_ws == null) {
log('could not send message, not connected');
return;
}
final buf = req.writeToBuffer();
_ws?.sink.add(buf);
_ws?.send(buf);
}
void _handleMessage(dynamic message) {
if (message is! List<int>) {
return;
}
final 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: ' + json.encode(msg));
}
Future<void> _onSocketData(dynamic message) async {
if (message is! List<int>) return;
final msg = lk_rtc.SignalResponse.fromBuffer(message);
// Ensure previous delegate method's future is completed
// before calling another method
await _lock.synchronized(() async {
//
switch (msg.whichMessage()) {
case lk_rtc.SignalResponse_Message.join:
if (!_connected) {
_connected = true;
await delegate?.onConnected(msg.join);
}
break;
case lk_rtc.SignalResponse_Message.answer:
await delegate?.onAnswer(toRTCSessionDescription(msg.answer));
break;
case lk_rtc.SignalResponse_Message.offer:
await delegate?.onOffer(toRTCSessionDescription(msg.offer));
break;
case lk_rtc.SignalResponse_Message.trickle:
await delegate?.onTrickle(
toRTCIceCandidate(msg.trickle.candidateInit),
msg.trickle.target,
);
break;
case lk_rtc.SignalResponse_Message.update:
await delegate?.onParticipantUpdate(msg.update.participants);
break;
case lk_rtc.SignalResponse_Message.trackPublished:
await delegate?.onLocalTrackPublished(msg.trackPublished);
break;
case lk_rtc.SignalResponse_Message.speaker:
await delegate?.onActiveSpeakersChanged(msg.speaker.speakers);
break;
case lk_rtc.SignalResponse_Message.leave:
await delegate?.onLeave(msg.leave);
break;
case lk_rtc.SignalResponse_Message.mute:
await delegate?.onMuteTrack(msg.mute);
break;
default:
log('unsupported message: ' + json.encode(msg));
}
});
}
void _handleError(Object error) {
void _handleError(dynamic error) {
logger.warning('received websocket error $error');
}
void _handleDone() {
if (!_connected) {
return;
}
void _onSocketDone() {
if (!_connected) return;
_ws = null;
_connected = false;
delegate?.onClose();
}
}
String _joinParams(String token) {
return '?access_token=$token&protocol=$protocolVersion';
}
RTCSessionDescription toRTCSessionDescription(SessionDescription sd) {
RTCSessionDescription toRTCSessionDescription(lk_rtc.SessionDescription sd) {
return RTCSessionDescription(sd.sdp, sd.type);
}
SessionDescription fromRTCSessionDescription(RTCSessionDescription rsd) {
return SessionDescription(type: rsd.type, sdp: rsd.sdp);
lk_rtc.SessionDescription fromRTCSessionDescription(RTCSessionDescription rsd) {
return lk_rtc.SessionDescription(type: rsd.type, sdp: rsd.sdp);
}
RTCIceCandidate toRTCIceCandidate(String candidateInit) {