Room implementation, library exports
This commit is contained in:
+16
-5
@@ -1,7 +1,18 @@
|
|||||||
library livekit_client;
|
library livekit_client;
|
||||||
|
|
||||||
/// A Calculator.
|
export 'src/errors.dart';
|
||||||
class Calculator {
|
export 'src/room.dart';
|
||||||
/// Returns [value] plus 1.
|
export 'src/rtc_engine.dart';
|
||||||
int addOne(int value) => value + 1;
|
export 'src/participant/participant.dart';
|
||||||
}
|
export 'src/participant/local_participant.dart';
|
||||||
|
export 'src/participant/remote_participant.dart';
|
||||||
|
export 'src/proto/livekit_models.pbenum.dart';
|
||||||
|
export 'src/proto/livekit_rtc.pbenum.dart';
|
||||||
|
export 'src/participant/local_participant.dart';
|
||||||
|
export 'src/track/track.dart';
|
||||||
|
export 'src/track/video_track.dart';
|
||||||
|
export 'src/track/local_audio_track.dart';
|
||||||
|
export 'src/track/local_video_track.dart';
|
||||||
|
export 'src/track/track_publication.dart';
|
||||||
|
export 'src/track/local_track_publication.dart';
|
||||||
|
export 'src/track/remote_track_publication.dart';
|
||||||
|
|||||||
@@ -26,7 +26,14 @@ class RemoteParticipant extends Participant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addSubscribedMediaTrack(MediaStreamTrack mediaTrack, String sid) async {
|
addSubscribedMediaTrack(MediaStreamTrack mediaTrack, String? sid) async {
|
||||||
|
if (sid == null) {
|
||||||
|
var msg = 'addSubscribedMediaTrack received null sid';
|
||||||
|
delegate?.onTrackSubscriptionFailed(this, '', msg);
|
||||||
|
roomDelegate?.onTrackSubscriptionFailed(this, '', msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var pub = getTrackPublication(sid);
|
var pub = getTrackPublication(sid);
|
||||||
if (pub == null) {
|
if (pub == null) {
|
||||||
// we may have received the track prior to metadata. wait up to 3s
|
// we may have received the track prior to metadata. wait up to 3s
|
||||||
|
|||||||
+248
-9
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
import 'errors.dart';
|
||||||
import 'extensions.dart';
|
import 'extensions.dart';
|
||||||
import 'logger.dart';
|
import 'logger.dart';
|
||||||
import 'participant/local_participant.dart';
|
import 'participant/local_participant.dart';
|
||||||
@@ -10,6 +12,9 @@ import 'proto/livekit_models.pb.dart';
|
|||||||
import 'proto/livekit_rtc.pb.dart';
|
import 'proto/livekit_rtc.pb.dart';
|
||||||
import 'rtc_engine.dart';
|
import 'rtc_engine.dart';
|
||||||
import 'signal_client.dart';
|
import 'signal_client.dart';
|
||||||
|
import 'track/remote_track_publication.dart';
|
||||||
|
import 'track/track.dart';
|
||||||
|
import 'track/track_publication.dart';
|
||||||
|
|
||||||
enum RoomState {
|
enum RoomState {
|
||||||
Disconnected,
|
Disconnected,
|
||||||
@@ -17,7 +22,33 @@ enum RoomState {
|
|||||||
Reconnecting,
|
Reconnecting,
|
||||||
}
|
}
|
||||||
|
|
||||||
class Room {
|
mixin RoomDelegate {
|
||||||
|
// room level callbacks
|
||||||
|
void onReconnecting() {}
|
||||||
|
void onReconnected() {}
|
||||||
|
void onDisconnected() {}
|
||||||
|
void onParticipantConnected(Participant participant) {}
|
||||||
|
void onParticipantDisconnected(Participant participant) {}
|
||||||
|
void onActiveSpeakersChanged(List<Participant> participants) {}
|
||||||
|
|
||||||
|
// callbacks about participant events
|
||||||
|
void onMetadataChanged(Participant participant) {}
|
||||||
|
void onTrackMuted(Participant participant, TrackPublication publication) {}
|
||||||
|
void onTrackUnmuted(Participant participant, TrackPublication publication) {}
|
||||||
|
void onTrackPublished(
|
||||||
|
RemoteParticipant participant, RemoteTrackPublication publication) {}
|
||||||
|
void onTrackUnpublished(
|
||||||
|
RemoteParticipant participant, RemoteTrackPublication publication) {}
|
||||||
|
void onTrackSubscribed(RemoteParticipant participant, Track track,
|
||||||
|
RemoteTrackPublication publication) {}
|
||||||
|
void onTrackUnsubscribed(RemoteParticipant participant, Track track,
|
||||||
|
RemoteTrackPublication publication) {}
|
||||||
|
void onDataReceived(RemoteParticipant participant, List<int> data) {}
|
||||||
|
void onTrackSubscriptionFailed(
|
||||||
|
RemoteParticipant participant, String sid, String? message) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Room with ParticipantDelegate {
|
||||||
RoomState state = RoomState.Disconnected;
|
RoomState state = RoomState.Disconnected;
|
||||||
|
|
||||||
/// map of SID to RemoteParticipant
|
/// map of SID to RemoteParticipant
|
||||||
@@ -35,6 +66,9 @@ class Room {
|
|||||||
/// a list of participants that are actively speaking, including local participant.
|
/// a list of participants that are actively speaking, including local participant.
|
||||||
List<Participant> activeSpeakers = [];
|
List<Participant> activeSpeakers = [];
|
||||||
|
|
||||||
|
/// delegate for room events
|
||||||
|
RoomDelegate? delegate;
|
||||||
|
|
||||||
RTCEngine _engine;
|
RTCEngine _engine;
|
||||||
|
|
||||||
Completer<Room>? _connectCompleter;
|
Completer<Room>? _connectCompleter;
|
||||||
@@ -42,6 +76,7 @@ class Room {
|
|||||||
Room(SignalClient client, RTCConfiguration? rtcConfig)
|
Room(SignalClient client, RTCConfiguration? rtcConfig)
|
||||||
: _engine = new RTCEngine(client, rtcConfig) {
|
: _engine = new RTCEngine(client, rtcConfig) {
|
||||||
_engine.onTrack = _onTrackAdded;
|
_engine.onTrack = _onTrackAdded;
|
||||||
|
_engine.onICEConnected = _handleICEConnected;
|
||||||
_engine.onDisconnected = _handleDisconnect;
|
_engine.onDisconnected = _handleDisconnect;
|
||||||
_engine.onParticipantUpdateCallback = _handleParticipantUpdate;
|
_engine.onParticipantUpdateCallback = _handleParticipantUpdate;
|
||||||
_engine.onActiveSpeakerchangedCallback = _handleSpeakerUpdate;
|
_engine.onActiveSpeakerchangedCallback = _handleSpeakerUpdate;
|
||||||
@@ -50,7 +85,7 @@ class Room {
|
|||||||
// 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;
|
||||||
|
|
||||||
@@ -59,20 +94,224 @@ class Room {
|
|||||||
'connected to LiveKit server, version: ${joinResponse.serverVersion}');
|
'connected to LiveKit server, version: ${joinResponse.serverVersion}');
|
||||||
|
|
||||||
state = RoomState.Connected;
|
state = RoomState.Connected;
|
||||||
var pi = joinResponse.participant;
|
localParticipant = new LocalParticipant(
|
||||||
localParticipant = new LocalParticipant(pi.sid, pi.identity, _engine);
|
engine: _engine,
|
||||||
|
info: joinResponse.participant,
|
||||||
|
);
|
||||||
|
localParticipant.roomDelegate = this;
|
||||||
|
|
||||||
|
sid = joinResponse.room.sid;
|
||||||
|
name = joinResponse.room.name;
|
||||||
|
|
||||||
|
for (var info in joinResponse.otherParticipants) {
|
||||||
|
_getOrCreateRemoteParticipant(info.sid, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleDisconnect() {}
|
disconnect() {
|
||||||
|
_engine.client.sendLeave();
|
||||||
|
_handleDisconnect();
|
||||||
|
}
|
||||||
|
|
||||||
_handleParticipantUpdate(List<ParticipantInfo> participants) {}
|
RemoteParticipant _getOrCreateRemoteParticipant(
|
||||||
|
String sid, ParticipantInfo? info) {
|
||||||
|
var participant = participants[sid];
|
||||||
|
if (participant != null) {
|
||||||
|
return participant;
|
||||||
|
}
|
||||||
|
|
||||||
_handleSpeakerUpdate(List<SpeakerInfo> speakers) {}
|
if (info == null) {
|
||||||
|
participant = RemoteParticipant(_engine.client, sid, '');
|
||||||
|
} else {
|
||||||
|
participant = RemoteParticipant.fromInfo(_engine.client, info);
|
||||||
|
}
|
||||||
|
participant.roomDelegate = this;
|
||||||
|
participants[sid] = participant;
|
||||||
|
|
||||||
_handleDataPacket(UserPacket packet, DataPacket_Kind kind) {}
|
return participant;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleICEConnected() {
|
||||||
|
_connectCompleter?.complete(this);
|
||||||
|
_connectCompleter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleDisconnect() {
|
||||||
|
if (state == RoomState.Disconnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var p in participants.values) {
|
||||||
|
for (var pub in p.tracks.values) {
|
||||||
|
p.unpublishTrack(pub.sid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var pub in localParticipant.tracks.values) {
|
||||||
|
pub.track?.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
_engine.close();
|
||||||
|
participants.clear();
|
||||||
|
activeSpeakers.clear();
|
||||||
|
state = RoomState.Disconnected;
|
||||||
|
delegate?.onDisconnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleParticipantUpdate(List<ParticipantInfo> updates) {
|
||||||
|
for (var info in updates) {
|
||||||
|
if (localParticipant.sid == info.sid) {
|
||||||
|
localParticipant.updateFromInfo(info);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.state == ParticipantInfo_State.DISCONNECTED) {
|
||||||
|
_handleParticipantDisconnect(info.sid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isNew = !participants.containsKey(info.sid);
|
||||||
|
var participant = _getOrCreateRemoteParticipant(info.sid, info);
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
delegate?.onParticipantConnected(participant);
|
||||||
|
} else {
|
||||||
|
participant.updateFromInfo(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleSpeakerUpdate(List<SpeakerInfo> speakers) {
|
||||||
|
var seenSids = Set<String>();
|
||||||
|
List<Participant> newSpeakers = [];
|
||||||
|
for (var info in speakers) {
|
||||||
|
seenSids.add(info.sid);
|
||||||
|
|
||||||
|
if (info.sid == localParticipant.sid) {
|
||||||
|
localParticipant.audioLevel = info.level;
|
||||||
|
localParticipant.isSpeaking = true;
|
||||||
|
newSpeakers.add(localParticipant);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var participant = participants[info.sid];
|
||||||
|
if (participant != null) {
|
||||||
|
participant.audioLevel = info.level;
|
||||||
|
participant.isSpeaking = true;
|
||||||
|
newSpeakers.add(participant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear previous speakers
|
||||||
|
if (seenSids.contains(localParticipant.sid)) {
|
||||||
|
localParticipant.audioLevel = 0;
|
||||||
|
localParticipant.isSpeaking = false;
|
||||||
|
}
|
||||||
|
for (var participant in participants.values) {
|
||||||
|
if (!seenSids.contains(participant.sid)) {
|
||||||
|
participant.audioLevel = 0;
|
||||||
|
participant.isSpeaking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeSpeakers = newSpeakers;
|
||||||
|
delegate?.onActiveSpeakersChanged(newSpeakers);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleDataPacket(UserPacket packet, DataPacket_Kind kind) {
|
||||||
|
var participant = participants[packet.participantSid];
|
||||||
|
if (participant == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
participant.delegate?.onDataReceived(participant, packet.payload);
|
||||||
|
delegate?.onDataReceived(participant, packet.payload);
|
||||||
|
}
|
||||||
|
|
||||||
_onTrackAdded(
|
_onTrackAdded(
|
||||||
MediaStreamTrack track, MediaStream? stream, RTCRtpReceiver? receiver) {}
|
MediaStreamTrack track, MediaStream? stream, RTCRtpReceiver? receiver) {
|
||||||
|
if (stream == null) {
|
||||||
|
// we need the stream to get the track's id
|
||||||
|
logger.severe('received track without mediastream');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed = unpackStreamId(stream.id);
|
||||||
|
var trackSid = parsed.item2;
|
||||||
|
if (trackSid == null) {
|
||||||
|
trackSid = track.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var participant = _getOrCreateRemoteParticipant(parsed.item1, null);
|
||||||
|
participant.addSubscribedMediaTrack(track, trackSid);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleParticipantDisconnect(String sid) {
|
||||||
|
var participant = participants.remove(sid);
|
||||||
|
if (participant == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var track in participant.tracks.values) {
|
||||||
|
participant.unpublishTrack(track.sid, true);
|
||||||
|
}
|
||||||
|
delegate?.onParticipantDisconnected(participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------- forward participant delegate calls ---------------------//
|
||||||
|
|
||||||
|
void onMetadataChanged(Participant participant) {
|
||||||
|
delegate?.onMetadataChanged(participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTrackMuted(Participant participant, TrackPublication publication) {
|
||||||
|
delegate?.onTrackMuted(participant, publication);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTrackUnmuted(Participant participant, TrackPublication publication) {
|
||||||
|
delegate?.onTrackUnmuted(participant, publication);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTrackPublished(
|
||||||
|
RemoteParticipant participant, RemoteTrackPublication publication) {
|
||||||
|
delegate?.onTrackPublished(participant, publication);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTrackUnpublished(
|
||||||
|
RemoteParticipant participant, RemoteTrackPublication publication) {
|
||||||
|
delegate?.onTrackUnpublished(participant, publication);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTrackSubscribed(RemoteParticipant participant, Track track,
|
||||||
|
RemoteTrackPublication publication) {
|
||||||
|
delegate?.onTrackSubscribed(participant, track, publication);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTrackUnsubscribed(RemoteParticipant participant, Track track,
|
||||||
|
RemoteTrackPublication publication) {
|
||||||
|
delegate?.onTrackUnsubscribed(participant, track, publication);
|
||||||
|
}
|
||||||
|
|
||||||
|
// omitted because data dispatching is handled in _handleDataPacket
|
||||||
|
void onDataReceived(RemoteParticipant participant, List<int> data) {}
|
||||||
|
|
||||||
|
void onTrackSubscriptionFailed(
|
||||||
|
RemoteParticipant participant, String sid, String? message) {
|
||||||
|
delegate?.onTrackSubscriptionFailed(participant, sid, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Tuple2<String, String?> unpackStreamId(String streamId) {
|
||||||
|
var parts = streamId.split('|');
|
||||||
|
if (parts.length != 2) {
|
||||||
|
return Tuple2(parts[0], null);
|
||||||
|
}
|
||||||
|
return Tuple2(parts[0], parts[1]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,11 +286,8 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onLocalTrackPublished(TrackPublishedResponse response) {
|
void onLocalTrackPublished(TrackPublishedResponse response) {
|
||||||
var completer = pendingTrackResolvers[response.cid];
|
var completer = pendingTrackResolvers.remove(response.cid);
|
||||||
if (completer != null) {
|
completer?.complete(Future.value(response.track));
|
||||||
completer.complete(Future.value(response.track));
|
|
||||||
pendingTrackResolvers.remove(response.cid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onActiveSpeakersChanged(List<SpeakerInfo> speakers) {
|
void onActiveSpeakersChanged(List<SpeakerInfo> speakers) {
|
||||||
|
|||||||
@@ -186,6 +186,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
quiver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quiver
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -233,6 +240,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.0"
|
version: "0.3.0"
|
||||||
|
tuple:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: tuple
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ dependencies:
|
|||||||
flutter_webrtc: ^0.6.4
|
flutter_webrtc: ^0.6.4
|
||||||
logging: ^1.0.1
|
logging: ^1.0.1
|
||||||
protobuf: ^2.0.0
|
protobuf: ^2.0.0
|
||||||
|
tuple: ^2.0.0
|
||||||
web_socket_channel: ^2.1.0
|
web_socket_channel: ^2.1.0
|
||||||
uuid: ^3.0.4
|
uuid: ^3.0.4
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user