Documentation
This commit is contained in:
@@ -1,3 +1,254 @@
|
|||||||
# LiveKit Flutter SDK
|
# LiveKit Flutter SDK
|
||||||
|
|
||||||
Docs coming soon. package available on pub.dev as [livekit_client](https://pub.dev/packages/livekit_client)
|
Official Flutter SDK for [LiveKit](https://livekit.io). Easily add real-time video and audio to your Flutter apps.
|
||||||
|
|
||||||
|
This package is published to pub.dev as [livekit_client](https://pub.dev/packages/livekit_client).
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
Docs and guides at [https://docs.livekit.io](https://docs.livekit.io)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Include this package to your `pubspec.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
...
|
||||||
|
dependencies:
|
||||||
|
livekit_client: <version>
|
||||||
|
```
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
Camera and microphone usage need to be declared in your `Info.plist` file.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
...
|
||||||
|
<dict>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>$(PRODUCT_NAME) uses your camera</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>$(PRODUCT_NAME) uses your microphone</string>
|
||||||
|
</dict>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
We require a set of permissions that need to be declared in your `AppManifest.xml`. These are required by Flutter WebRTC, which we depend on.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.your.package">
|
||||||
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
|
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
...
|
||||||
|
</manifest>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example app
|
||||||
|
|
||||||
|
We built a multi-user conferencing app as an example in the [example/](example/) folder. You can join the same room from any supported LiveKit clients.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Connecting to a room, publish video & audio
|
||||||
|
|
||||||
|
```dart
|
||||||
|
var room = await LiveKitClient.connect(this.url, this.token);
|
||||||
|
try {
|
||||||
|
// video will fail when running in ios simulator
|
||||||
|
var localVideo = await LocalVideoTrack.createCameraTrack();
|
||||||
|
await room.localParticipant.publishVideoTrack(localVideo);
|
||||||
|
} catch (e) {
|
||||||
|
print('could not publish video: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
var localAudio = await LocalAudioTrack.createTrack();
|
||||||
|
await room.localParticipant.publishAudioTrack(localAudio);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rendering video
|
||||||
|
|
||||||
|
Each track can be rendered separately with the provided `VideoTrackRenderer` widget.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
VideoTrack? track;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (track != null) {
|
||||||
|
return VideoTrackRenderer(track);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audio handling
|
||||||
|
|
||||||
|
Audio tracks are rendered automatically as long as you are subscribed to them.
|
||||||
|
|
||||||
|
### Handling changes
|
||||||
|
|
||||||
|
LiveKit client makes it simple to build declarative UI that reacts to state changes. It notifies changes in two ways
|
||||||
|
|
||||||
|
* `ChangeNotifier` - generic notification of changes
|
||||||
|
* `RoomDelegate` and `ParticipantDelegate` - notification of specific events.
|
||||||
|
|
||||||
|
This example will show you how to use both to react to room events.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class RoomWidget extends StatefulWidget {
|
||||||
|
final Room room;
|
||||||
|
|
||||||
|
RoomWidget(this.room);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _RoomState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RoomState extends State<RoomWidget> with RoomDelegate {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.room.delegate = this;
|
||||||
|
widget.room.addListener(_onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.room.delegate = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChange() {
|
||||||
|
// perform computations and then call setState
|
||||||
|
// setState will trigger a build
|
||||||
|
setState(() {
|
||||||
|
// your updates here
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onDisconnected() {
|
||||||
|
// onDisconnected is a RoomDelegate method, handle when disconnected from room
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// your build function
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly, you could do the same when rendering participants. Reacting to changes makes it possible to handle tracks published/unpublished or re-ordering participants in your UI.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class VideoView extends StatefulWidget {
|
||||||
|
final Participant participant;
|
||||||
|
|
||||||
|
VideoView(this.participant);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _VideoViewState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoViewState extends State<VideoView> with ParticipantDelegate {
|
||||||
|
TrackPublication? videoPub;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.participant.addListener(this._onParticipantChanged);
|
||||||
|
// trigger initial change
|
||||||
|
_onParticipantChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.participant.removeListener(this._onParticipantChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant VideoView oldWidget) {
|
||||||
|
oldWidget.participant.removeListener(_onParticipantChanged);
|
||||||
|
widget.participant.addListener(_onParticipantChanged);
|
||||||
|
_onParticipantChanged();
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onParticipantChanged() {
|
||||||
|
var subscribedVideos = widget.participant.videoTracks.values.where((pub) {
|
||||||
|
return pub.kind == TrackType.VIDEO &&
|
||||||
|
!pub.isScreenShare &&
|
||||||
|
pub.subscribed;
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (subscribedVideos.length > 0) {
|
||||||
|
var videoPub = subscribedVideos.first;
|
||||||
|
if (videoPub is RemoteTrackPublication) {
|
||||||
|
videoPub.videoQuality = widget.quality;
|
||||||
|
}
|
||||||
|
// when muted, show placeholder
|
||||||
|
if (!videoPub.muted) {
|
||||||
|
this.videoPub = videoPub;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.videoPub = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var videoPub = this.videoPub;
|
||||||
|
if (videoPub != null) {
|
||||||
|
return VideoTrackRenderer(videoPub.track as VideoTrack);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mute, unmute local tracks
|
||||||
|
|
||||||
|
On `LocalTrackPublication`s, you could control if the track is muted by setting its `muted` property. Changing the mute status will generate an `onTrackMuted` or `onTrack Unmuted` delegate call for the local participant. Other participant will receive the status change as well.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// mute track
|
||||||
|
trackPub.muted = true;
|
||||||
|
|
||||||
|
// unmute track
|
||||||
|
trackPub.muted = false;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subscriber controls
|
||||||
|
|
||||||
|
When subscribing to remote tracks, the client has precise control over status of its subscriptions. You could subscribe or unsubscribe to a track, change its quality, or disabling the track temporarily.
|
||||||
|
|
||||||
|
These controls are accessible on the `RemoteTrackPublication` object.
|
||||||
|
|
||||||
|
For more info, see [Subscriber controls](https://docs.livekit.io/guides/room/receive#subscriber-controls).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache License 2.0
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
A huge thank you to [flutter-webrtc](https://github.com/flutter-webrtc/flutter-webrtc) for making it possible to use WebRTC in Flutter.
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
/// Flutter Client SDK to LiveKit.
|
||||||
library livekit_client;
|
library livekit_client;
|
||||||
|
|
||||||
export 'src/livekit.dart';
|
export 'src/livekit.dart';
|
||||||
export 'src/errors.dart';
|
export 'src/errors.dart';
|
||||||
export 'src/room.dart';
|
export 'src/room.dart';
|
||||||
|
export 'src/options.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';
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'room.dart';
|
import 'room.dart';
|
||||||
import 'signal_client.dart';
|
import 'options.dart';
|
||||||
|
|
||||||
|
/// Main entry point to connect to a room.
|
||||||
|
/// {@category Room}
|
||||||
class LiveKitClient {
|
class LiveKitClient {
|
||||||
|
/// Connects to a LiveKit room
|
||||||
static Future<Room> connect(String url, String token,
|
static Future<Room> connect(String url, String token,
|
||||||
[JoinOptions? options]) {
|
[JoinOptions? options]) {
|
||||||
var room = Room();
|
var room = Room();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../track/track.dart';
|
|||||||
import '../track/track_publication.dart';
|
import '../track/track_publication.dart';
|
||||||
import 'participant.dart';
|
import 'participant.dart';
|
||||||
|
|
||||||
|
/// Represents the current participant in the room.
|
||||||
class LocalParticipant extends Participant {
|
class LocalParticipant extends Participant {
|
||||||
RTCEngine _engine;
|
RTCEngine _engine;
|
||||||
|
|
||||||
@@ -22,6 +23,8 @@ class LocalParticipant extends Participant {
|
|||||||
updateFromInfo(info);
|
updateFromInfo(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// for internal use
|
||||||
|
/// {@nodoc}
|
||||||
RTCEngine get engine => _engine;
|
RTCEngine get engine => _engine;
|
||||||
|
|
||||||
/// publish an audio track to the room
|
/// publish an audio track to the room
|
||||||
@@ -53,7 +56,7 @@ class LocalParticipant extends Participant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// publish a video track to the room
|
/// Publish a video track to the room
|
||||||
Future<TrackPublication> publishVideoTrack(LocalVideoTrack track) async {
|
Future<TrackPublication> publishVideoTrack(LocalVideoTrack track) async {
|
||||||
if (videoTracks.values.any(
|
if (videoTracks.values.any(
|
||||||
(element) => element.track?.mediaTrack.id == track.mediaTrack.id)) {
|
(element) => element.track?.mediaTrack.id == track.mediaTrack.id)) {
|
||||||
@@ -83,6 +86,7 @@ class LocalParticipant extends Participant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unpublish a track that's already published
|
||||||
unpublishTrack(Track track) {
|
unpublishTrack(Track track) {
|
||||||
var existing = tracks.values.where((element) => element.track == track);
|
var existing = tracks.values.where((element) => element.track == track);
|
||||||
if (existing.isEmpty) {
|
if (existing.isEmpty) {
|
||||||
@@ -107,6 +111,8 @@ class LocalParticipant extends Participant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Publish a new data payload to the room.
|
||||||
|
/// @param destinationSids When empty, data will be forwarded to each participant in the room.
|
||||||
publishData(List<int> data, DataPacket_Kind reliability,
|
publishData(List<int> data, DataPacket_Kind reliability,
|
||||||
{List<String>? destinationSids}) {
|
{List<String>? destinationSids}) {
|
||||||
RTCDataChannel? channel;
|
RTCDataChannel? channel;
|
||||||
@@ -135,6 +141,9 @@ class LocalParticipant extends Participant {
|
|||||||
channel.send(RTCDataChannelMessage.fromBinary(buffer));
|
channel.send(RTCDataChannelMessage.fromBinary(buffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// for internal use
|
||||||
|
/// {@nodoc}
|
||||||
|
@override
|
||||||
updateFromInfo(ParticipantInfo info) {
|
updateFromInfo(ParticipantInfo info) {
|
||||||
super.updateFromInfo(info);
|
super.updateFromInfo(info);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,42 @@ import '../track/remote_track_publication.dart';
|
|||||||
import '../track/track.dart';
|
import '../track/track.dart';
|
||||||
import '../track/track_publication.dart';
|
import '../track/track_publication.dart';
|
||||||
|
|
||||||
|
/// Callbacks for participant changes
|
||||||
mixin ParticipantDelegate {
|
mixin ParticipantDelegate {
|
||||||
|
/// The participant's metadata has changed
|
||||||
void onMetadataChanged(Participant participant) {}
|
void onMetadataChanged(Participant participant) {}
|
||||||
|
|
||||||
|
/// The participant's isSpeaking property has changed
|
||||||
void onSpeakingChanged(Participant participant, bool speaking) {}
|
void onSpeakingChanged(Participant participant, bool speaking) {}
|
||||||
|
|
||||||
|
/// This participant has muted one of their tracks
|
||||||
void onTrackMuted(Participant participant, TrackPublication publication) {}
|
void onTrackMuted(Participant participant, TrackPublication publication) {}
|
||||||
|
|
||||||
|
/// This participant has unmuted one of their tracks
|
||||||
void onTrackUnmuted(Participant participant, TrackPublication publication) {}
|
void onTrackUnmuted(Participant participant, TrackPublication publication) {}
|
||||||
|
|
||||||
|
/// This participant has published a new [Track] to the [Room].
|
||||||
void onTrackPublished(
|
void onTrackPublished(
|
||||||
RemoteParticipant participant, RemoteTrackPublication publication) {}
|
RemoteParticipant participant, RemoteTrackPublication publication) {}
|
||||||
|
|
||||||
|
/// This participant has unpublished one of their [Track].
|
||||||
void onTrackUnpublished(
|
void onTrackUnpublished(
|
||||||
RemoteParticipant participant, RemoteTrackPublication publication) {}
|
RemoteParticipant participant, RemoteTrackPublication publication) {}
|
||||||
|
|
||||||
|
/// The [LocalParticipant] has subscribed to a new track published by this
|
||||||
|
/// [RemoteParticipant]
|
||||||
void onTrackSubscribed(RemoteParticipant participant, Track track,
|
void onTrackSubscribed(RemoteParticipant participant, Track track,
|
||||||
RemoteTrackPublication publication) {}
|
RemoteTrackPublication publication) {}
|
||||||
|
|
||||||
|
/// The [LocalParticipant] has unsubscribed from a track published by this
|
||||||
|
/// [RemoteParticipant]. This event is fired when the track was unpublished
|
||||||
void onTrackUnsubscribed(RemoteParticipant participant, Track track,
|
void onTrackUnsubscribed(RemoteParticipant participant, Track track,
|
||||||
RemoteTrackPublication publication) {}
|
RemoteTrackPublication publication) {}
|
||||||
|
|
||||||
|
/// Data received from this [RemoteParticipant].
|
||||||
void onDataReceived(RemoteParticipant participant, List<int> data) {}
|
void onDataReceived(RemoteParticipant participant, List<int> data) {}
|
||||||
|
|
||||||
|
/// An error has occured during track subscription.
|
||||||
void onTrackSubscriptionFailed(
|
void onTrackSubscriptionFailed(
|
||||||
RemoteParticipant participant, String sid, String? message) {}
|
RemoteParticipant participant, String sid, String? message) {}
|
||||||
}
|
}
|
||||||
@@ -97,11 +119,14 @@ class Participant extends ChangeNotifier {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// internal use
|
/// for internal use
|
||||||
|
/// {@nodoc}
|
||||||
bool get hasInfo => _participantInfo != null;
|
bool get hasInfo => _participantInfo != null;
|
||||||
|
|
||||||
Participant(this.sid, this.identity);
|
Participant(this.sid, this.identity);
|
||||||
|
|
||||||
|
/// for internal use
|
||||||
|
/// {@nodoc}
|
||||||
set isSpeaking(bool speaking) {
|
set isSpeaking(bool speaking) {
|
||||||
if (_isSpeaking == speaking) {
|
if (_isSpeaking == speaking) {
|
||||||
return;
|
return;
|
||||||
@@ -125,6 +150,8 @@ class Participant extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// for internal use
|
||||||
|
/// {@nodoc}
|
||||||
updateFromInfo(ParticipantInfo info) {
|
updateFromInfo(ParticipantInfo info) {
|
||||||
this.identity = info.identity;
|
this.identity = info.identity;
|
||||||
this.sid = info.sid;
|
this.sid = info.sid;
|
||||||
@@ -134,10 +161,14 @@ class Participant extends ChangeNotifier {
|
|||||||
this._participantInfo = info;
|
this._participantInfo = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// for internal use
|
||||||
|
/// {@nodoc}
|
||||||
muteChanged() {
|
muteChanged() {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// for internal use
|
||||||
|
/// {@nodoc}
|
||||||
addTrackPublication(TrackPublication pub) {
|
addTrackPublication(TrackPublication pub) {
|
||||||
pub.track?.sid = pub.sid;
|
pub.track?.sid = pub.sid;
|
||||||
tracks[pub.sid] = pub;
|
tracks[pub.sid] = pub;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../track/track.dart';
|
|||||||
import '../track/video_track.dart';
|
import '../track/video_track.dart';
|
||||||
import 'participant.dart';
|
import 'participant.dart';
|
||||||
|
|
||||||
|
/// Represents other participant in the [Room].
|
||||||
class RemoteParticipant extends Participant {
|
class RemoteParticipant extends Participant {
|
||||||
SignalClient _client;
|
SignalClient _client;
|
||||||
|
|
||||||
@@ -27,6 +28,8 @@ class RemoteParticipant extends Participant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// for internal use
|
||||||
|
/// {@nodoc}
|
||||||
addSubscribedMediaTrack(
|
addSubscribedMediaTrack(
|
||||||
MediaStreamTrack mediaTrack, MediaStream stream, String? sid) async {
|
MediaStreamTrack mediaTrack, MediaStream stream, String? sid) async {
|
||||||
if (sid == null) {
|
if (sid == null) {
|
||||||
@@ -70,6 +73,8 @@ class RemoteParticipant extends Participant {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// for internal use
|
||||||
|
/// {@nodoc}
|
||||||
@override
|
@override
|
||||||
void updateFromInfo(ParticipantInfo info) {
|
void updateFromInfo(ParticipantInfo info) {
|
||||||
var hadInfo = hasInfo;
|
var hadInfo = hasInfo;
|
||||||
|
|||||||
+53
-5
@@ -8,6 +8,7 @@ import 'package:tuple/tuple.dart';
|
|||||||
import 'errors.dart';
|
import 'errors.dart';
|
||||||
import 'extensions.dart';
|
import 'extensions.dart';
|
||||||
import 'logger.dart';
|
import 'logger.dart';
|
||||||
|
import 'options.dart';
|
||||||
import 'participant/local_participant.dart';
|
import 'participant/local_participant.dart';
|
||||||
import 'participant/participant.dart';
|
import 'participant/participant.dart';
|
||||||
import 'participant/remote_participant.dart';
|
import 'participant/remote_participant.dart';
|
||||||
@@ -25,39 +26,83 @@ enum RoomState {
|
|||||||
Reconnecting,
|
Reconnecting,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delegate for [Room] callbacks
|
||||||
mixin RoomDelegate {
|
mixin RoomDelegate {
|
||||||
// room level callbacks
|
// room level callbacks
|
||||||
|
/// When the connection to the server has been interrupted and it's attempting
|
||||||
|
/// to reconnect.
|
||||||
void onReconnecting() {}
|
void onReconnecting() {}
|
||||||
|
|
||||||
|
/// Connection to room is re-established. All existing state is preserved.
|
||||||
void onReconnected() {}
|
void onReconnected() {}
|
||||||
|
|
||||||
|
/// Disconnected from the room
|
||||||
void onDisconnected() {}
|
void onDisconnected() {}
|
||||||
|
|
||||||
|
/// When a new [RemoteParticipant] joins *after* the current participant has connected
|
||||||
|
/// It will not fire for participants that are already in the room
|
||||||
void onParticipantConnected(Participant participant) {}
|
void onParticipantConnected(Participant participant) {}
|
||||||
|
|
||||||
|
/// When a [RemoteParticipant] leaves the room
|
||||||
void onParticipantDisconnected(Participant participant) {}
|
void onParticipantDisconnected(Participant participant) {}
|
||||||
|
|
||||||
|
/// Active speakers changed. List of speakers are ordered by their audio level.
|
||||||
|
/// loudest speakers first. This will include the [LocalParticipant] too.
|
||||||
void onActiveSpeakersChanged(List<Participant> participants) {}
|
void onActiveSpeakersChanged(List<Participant> participants) {}
|
||||||
|
|
||||||
// callbacks about participant events
|
// callbacks about participant events
|
||||||
|
|
||||||
|
/// Participant metadata is a simple way for app-specific state to be pushed to
|
||||||
|
/// all users.
|
||||||
|
/// When RoomService.UpdateParticipantMetadata is called to change a
|
||||||
|
/// participant's state, *all* participants in the room will fire this event.
|
||||||
void onMetadataChanged(Participant participant) {}
|
void onMetadataChanged(Participant participant) {}
|
||||||
|
|
||||||
|
/// A track that was muted, fires on both [RemoteParticipant]s and
|
||||||
|
/// [LocalParticipant]
|
||||||
void onTrackMuted(Participant participant, TrackPublication publication) {}
|
void onTrackMuted(Participant participant, TrackPublication publication) {}
|
||||||
|
|
||||||
|
/// A track that was unmuted, fires on both [RemoteParticipant]s and
|
||||||
|
/// [LocalParticipant]
|
||||||
void onTrackUnmuted(Participant participant, TrackPublication publication) {}
|
void onTrackUnmuted(Participant participant, TrackPublication publication) {}
|
||||||
|
|
||||||
|
/// When a new track is published to room *after* the current participant has
|
||||||
|
/// joined. It will not fire for tracks that are already published
|
||||||
void onTrackPublished(
|
void onTrackPublished(
|
||||||
RemoteParticipant participant, RemoteTrackPublication publication) {}
|
RemoteParticipant participant, RemoteTrackPublication publication) {}
|
||||||
|
|
||||||
|
/// A [RemoteParticipant] has unpublished a track
|
||||||
void onTrackUnpublished(
|
void onTrackUnpublished(
|
||||||
RemoteParticipant participant, RemoteTrackPublication publication) {}
|
RemoteParticipant participant, RemoteTrackPublication publication) {}
|
||||||
|
|
||||||
|
/// The [LocalParticipant] has subscribed to a new track. This event will **always**
|
||||||
|
/// fire as long as new tracks are ready for use.
|
||||||
void onTrackSubscribed(RemoteParticipant participant, Track track,
|
void onTrackSubscribed(RemoteParticipant participant, Track track,
|
||||||
RemoteTrackPublication publication) {}
|
RemoteTrackPublication publication) {}
|
||||||
|
|
||||||
|
/// A subscribed track is no longer available.
|
||||||
void onTrackUnsubscribed(RemoteParticipant participant, Track track,
|
void onTrackUnsubscribed(RemoteParticipant participant, Track track,
|
||||||
RemoteTrackPublication publication) {}
|
RemoteTrackPublication publication) {}
|
||||||
|
|
||||||
|
/// Data received from another [RemoteParticipant].
|
||||||
|
/// Data packets provides the ability to use LiveKit to send/receive arbitrary
|
||||||
|
/// payloads.
|
||||||
void onDataReceived(RemoteParticipant participant, List<int> data) {}
|
void onDataReceived(RemoteParticipant participant, List<int> data) {}
|
||||||
|
|
||||||
|
/// Encountered failure attempting to subscribe to track.
|
||||||
void onTrackSubscriptionFailed(
|
void onTrackSubscriptionFailed(
|
||||||
RemoteParticipant participant, String sid, String? message) {}
|
RemoteParticipant participant, String sid, String? message) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Room is the main entrypoint to working with LiveKit. It provides
|
/// Room is the primary construct for LiveKit conferences. It contains a
|
||||||
/// updates to its state via two ways, by assigning a delegate, or using
|
/// group of [Participant]s, each publishing and subscribing to [Track]s.
|
||||||
|
/// Notifies changes to its state via two ways, by assigning a delegate, or using
|
||||||
/// it as a provider.
|
/// it as a provider.
|
||||||
/// Room will trigger a change update when
|
/// Room will trigger a change notification update when
|
||||||
/// * state changes
|
/// * state changes
|
||||||
/// * participant membership changes
|
/// * participant membership changes
|
||||||
/// * active speakers are different
|
/// * active speakers are different
|
||||||
|
/// {@category Room}
|
||||||
class Room extends ChangeNotifier with ParticipantDelegate {
|
class Room extends ChangeNotifier with ParticipantDelegate {
|
||||||
RoomState _state = RoomState.Disconnected;
|
RoomState _state = RoomState.Disconnected;
|
||||||
|
|
||||||
@@ -92,6 +137,8 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
|
|
||||||
Completer<Room>? _connectCompleter;
|
Completer<Room>? _connectCompleter;
|
||||||
|
|
||||||
|
/// internal use
|
||||||
|
/// {@nodoc}
|
||||||
Room([RTCConfiguration? rtcConfig])
|
Room([RTCConfiguration? rtcConfig])
|
||||||
: _engine = new RTCEngine(SignalClient(), rtcConfig) {
|
: _engine = new RTCEngine(SignalClient(), rtcConfig) {
|
||||||
_engine.onTrack = _onTrackAdded;
|
_engine.onTrack = _onTrackAdded;
|
||||||
@@ -148,6 +195,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disconnects from the room, notifying server of disconnection.
|
||||||
disconnect() {
|
disconnect() {
|
||||||
_engine.client.sendLeave();
|
_engine.client.sendLeave();
|
||||||
_handleDisconnect();
|
_handleDisconnect();
|
||||||
@@ -288,7 +336,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsed = unpackStreamId(stream.id);
|
var parsed = _unpackStreamId(stream.id);
|
||||||
var trackSid = parsed.item2;
|
var trackSid = parsed.item2;
|
||||||
if (trackSid == null) {
|
if (trackSid == null) {
|
||||||
trackSid = track.id;
|
trackSid = track.id;
|
||||||
@@ -354,7 +402,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Tuple2<String, String?> unpackStreamId(String streamId) {
|
Tuple2<String, String?> _unpackStreamId(String streamId) {
|
||||||
var parts = streamId.split('|');
|
var parts = streamId.split('|');
|
||||||
if (parts.length != 2) {
|
if (parts.length != 2) {
|
||||||
return Tuple2(parts[0], null);
|
return Tuple2(parts[0], null);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart';
|
|||||||
import 'errors.dart';
|
import 'errors.dart';
|
||||||
import 'extensions.dart';
|
import 'extensions.dart';
|
||||||
import 'logger.dart';
|
import 'logger.dart';
|
||||||
|
import 'options.dart';
|
||||||
import 'proto/livekit_rtc.pb.dart';
|
import 'proto/livekit_rtc.pb.dart';
|
||||||
import 'proto/livekit_models.pb.dart';
|
import 'proto/livekit_models.pb.dart';
|
||||||
import 'signal_client.dart';
|
import 'signal_client.dart';
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import 'dart:convert';
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import 'errors.dart';
|
||||||
import 'logger.dart';
|
import 'logger.dart';
|
||||||
|
import 'options.dart';
|
||||||
import 'track/track.dart';
|
import 'track/track.dart';
|
||||||
import 'version.dart';
|
import 'version.dart';
|
||||||
import 'proto/livekit_models.pb.dart';
|
import 'proto/livekit_models.pb.dart';
|
||||||
@@ -16,12 +17,6 @@ import '_websocket_api.dart'
|
|||||||
if (dart.library.io) '_websocket_io.dart'
|
if (dart.library.io) '_websocket_io.dart'
|
||||||
if (dart.library.html) '_websocket_html.dart' as platform;
|
if (dart.library.html) '_websocket_html.dart' as platform;
|
||||||
|
|
||||||
class JoinOptions {
|
|
||||||
final bool? autoSubscribe;
|
|
||||||
|
|
||||||
const JoinOptions({this.autoSubscribe});
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin SignalClientDelegate {
|
mixin SignalClientDelegate {
|
||||||
// initial connection established
|
// initial connection established
|
||||||
void onConnected(JoinResponse response);
|
void onConnected(JoinResponse response);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class AudioTrack extends Track {
|
|||||||
AudioTrack(String name, MediaStreamTrack track, this.mediaStream)
|
AudioTrack(String name, MediaStreamTrack track, this.mediaStream)
|
||||||
: super(TrackType.AUDIO, name, track);
|
: super(TrackType.AUDIO, name, track);
|
||||||
|
|
||||||
|
/// Start playing audio track. On web platform, create an audio element and
|
||||||
|
/// start playback
|
||||||
start() {
|
start() {
|
||||||
if (!(this is LocalAudioTrack)) {
|
if (!(this is LocalAudioTrack)) {
|
||||||
audio.startAudio(getCid(), mediaTrack);
|
audio.startAudio(getCid(), mediaTrack);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class LocalAudioTrack extends AudioTrack {
|
|||||||
LocalAudioTrack(String name, MediaStreamTrack track, MediaStream stream)
|
LocalAudioTrack(String name, MediaStreamTrack track, MediaStream stream)
|
||||||
: super(name, track, stream);
|
: super(name, track, stream);
|
||||||
|
|
||||||
|
/// Creates a new audio track from the default audio input device.
|
||||||
static Future<LocalAudioTrack> createTrack(
|
static Future<LocalAudioTrack> createTrack(
|
||||||
[LocalAudioTrackOptions? options]) async {
|
[LocalAudioTrackOptions? options]) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class LocalTrackPublication extends TrackPublication {
|
|||||||
this.track = track;
|
this.track = track;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mute or unmute the current track. When muted, track will stop sending data
|
||||||
set muted(bool val) {
|
set muted(bool val) {
|
||||||
if (val == muted) {
|
if (val == muted) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import '../errors.dart';
|
|||||||
import 'options.dart';
|
import 'options.dart';
|
||||||
import 'video_track.dart';
|
import 'video_track.dart';
|
||||||
|
|
||||||
|
/// A video track from the local device. Use static methods in this class to create
|
||||||
|
/// video tracks.
|
||||||
class LocalVideoTrack extends VideoTrack {
|
class LocalVideoTrack extends VideoTrack {
|
||||||
RTCRtpSender? get sender => transceiver?.sender;
|
RTCRtpSender? get sender => transceiver?.sender;
|
||||||
|
|
||||||
LocalVideoTrack(String name, MediaStreamTrack mediaTrack, MediaStream stream)
|
LocalVideoTrack(String name, MediaStreamTrack mediaTrack, MediaStream stream)
|
||||||
: super(name, mediaTrack, stream);
|
: super(name, mediaTrack, stream);
|
||||||
|
|
||||||
|
/// Creates a LocalVideoTrack from camera input.
|
||||||
static Future<LocalVideoTrack> createCameraTrack(
|
static Future<LocalVideoTrack> createCameraTrack(
|
||||||
[LocalVideoTrackOptions? options]) async {
|
[LocalVideoTrackOptions? options]) async {
|
||||||
if (options == null) {
|
if (options == null) {
|
||||||
@@ -24,6 +27,8 @@ class LocalVideoTrack extends VideoTrack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restarts the track with new options. This is useful when switching between
|
||||||
|
/// front and back cameras.
|
||||||
Future<void> restartTrack([LocalVideoTrackOptions? options]) async {
|
Future<void> restartTrack([LocalVideoTrackOptions? options]) async {
|
||||||
if (sender == null) {
|
if (sender == null) {
|
||||||
return Future.error(TrackCreateError('could not restart track'));
|
return Future.error(TrackCreateError('could not restart track'));
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/// Options when creating a LocalVideoTrack.
|
||||||
class LocalVideoTrackOptions {
|
class LocalVideoTrackOptions {
|
||||||
CameraPosition position = CameraPosition.FRONT;
|
CameraPosition position = CameraPosition.FRONT;
|
||||||
VideoParameter params;
|
VideoParameter params;
|
||||||
@@ -65,4 +66,5 @@ class VideoPresets {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Options when creating an LocalAudioTrack. Placeholder for now.
|
||||||
class LocalAudioTrackOptions {}
|
class LocalAudioTrackOptions {}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import '../participant/remote_participant.dart';
|
|||||||
import 'track.dart';
|
import 'track.dart';
|
||||||
import 'track_publication.dart';
|
import 'track_publication.dart';
|
||||||
|
|
||||||
|
/// Represents a track publication from a RemoteParticipant. Provides methods to
|
||||||
|
/// control if we should subscribe to the track, and its quality (for video).
|
||||||
class RemoteTrackPublication extends TrackPublication {
|
class RemoteTrackPublication extends TrackPublication {
|
||||||
RemoteParticipant _participant;
|
RemoteParticipant _participant;
|
||||||
bool _unsubscribed = false;
|
bool _unsubscribed = false;
|
||||||
@@ -37,6 +39,8 @@ class RemoteTrackPublication extends TrackPublication {
|
|||||||
_sendUpdateTrackSettings();
|
_sendUpdateTrackSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// for internal use
|
||||||
|
/// {@nodoc}
|
||||||
set muted(bool val) {
|
set muted(bool val) {
|
||||||
if (val == muted) {
|
if (val == muted) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class TrackDimension {
|
|||||||
TrackDimension(this.width, this.height);
|
TrackDimension(this.width, this.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrapper around a MediaStreamTrack with additional metadata.
|
||||||
class Track {
|
class Track {
|
||||||
static const ScreenShareName = "screen";
|
static const ScreenShareName = "screen";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import '../proto/livekit_models.pb.dart';
|
import '../proto/livekit_models.pb.dart';
|
||||||
import 'track.dart';
|
import 'track.dart';
|
||||||
|
|
||||||
|
/// Represents a track that's published to the server. This class contains
|
||||||
|
/// metadata associated with tracks.
|
||||||
class TrackPublication {
|
class TrackPublication {
|
||||||
Track? track;
|
Track? track;
|
||||||
String name;
|
String name;
|
||||||
@@ -19,6 +21,7 @@ class TrackPublication {
|
|||||||
updateFromInfo(info);
|
updateFromInfo(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True when the track is published with name [Track.ScreenShareName].
|
||||||
bool get isScreenShare =>
|
bool get isScreenShare =>
|
||||||
kind == TrackType.VIDEO && name == Track.ScreenShareName;
|
kind == TrackType.VIDEO && name == Track.ScreenShareName;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class VideoTrack extends Track with ChangeNotifier {
|
|||||||
|
|
||||||
MediaStream? get mediaStream => _mediaStream;
|
MediaStream? get mediaStream => _mediaStream;
|
||||||
|
|
||||||
|
/// internal use
|
||||||
|
/// {@nodoc}
|
||||||
set mediaStream(MediaStream? stream) {
|
set mediaStream(MediaStream? stream) {
|
||||||
_mediaStream = stream;
|
_mediaStream = stream;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart';
|
|||||||
import '../track/video_track.dart';
|
import '../track/video_track.dart';
|
||||||
import '../track/local_video_track.dart';
|
import '../track/local_video_track.dart';
|
||||||
|
|
||||||
|
/// Widget that renders a [VideoTrack].
|
||||||
class VideoTrackRenderer extends StatefulWidget {
|
class VideoTrackRenderer extends StatefulWidget {
|
||||||
final VideoTrack track;
|
final VideoTrack track;
|
||||||
final RTCVideoRenderer renderer;
|
final RTCVideoRenderer renderer;
|
||||||
|
|||||||
Reference in New Issue
Block a user