Documentation

This commit is contained in:
David Zhao
2021-08-30 15:31:12 -07:00
parent 583e08c5b1
commit d011bc7d3d
19 changed files with 383 additions and 16 deletions
+252 -1
View File
@@ -1,3 +1,254 @@
# 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.
+2
View File
@@ -1,8 +1,10 @@
/// Flutter Client SDK to LiveKit.
library livekit_client;
export 'src/livekit.dart';
export 'src/errors.dart';
export 'src/room.dart';
export 'src/options.dart';
export 'src/participant/participant.dart';
export 'src/participant/local_participant.dart';
export 'src/participant/remote_participant.dart';
+4 -1
View File
@@ -1,7 +1,10 @@
import 'room.dart';
import 'signal_client.dart';
import 'options.dart';
/// Main entry point to connect to a room.
/// {@category Room}
class LiveKitClient {
/// Connects to a LiveKit room
static Future<Room> connect(String url, String token,
[JoinOptions? options]) {
var room = Room();
+10 -1
View File
@@ -11,6 +11,7 @@ import '../track/track.dart';
import '../track/track_publication.dart';
import 'participant.dart';
/// Represents the current participant in the room.
class LocalParticipant extends Participant {
RTCEngine _engine;
@@ -22,6 +23,8 @@ class LocalParticipant extends Participant {
updateFromInfo(info);
}
/// for internal use
/// {@nodoc}
RTCEngine get engine => _engine;
/// 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 {
if (videoTracks.values.any(
(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) {
var existing = tracks.values.where((element) => element.track == track);
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,
{List<String>? destinationSids}) {
RTCDataChannel? channel;
@@ -135,6 +141,9 @@ class LocalParticipant extends Participant {
channel.send(RTCDataChannelMessage.fromBinary(buffer));
}
/// for internal use
/// {@nodoc}
@override
updateFromInfo(ParticipantInfo info) {
super.updateFromInfo(info);
}
+32 -1
View File
@@ -6,20 +6,42 @@ import '../track/remote_track_publication.dart';
import '../track/track.dart';
import '../track/track_publication.dart';
/// Callbacks for participant changes
mixin ParticipantDelegate {
/// The participant's metadata has changed
void onMetadataChanged(Participant participant) {}
/// The participant's isSpeaking property has changed
void onSpeakingChanged(Participant participant, bool speaking) {}
/// This participant has muted one of their tracks
void onTrackMuted(Participant participant, TrackPublication publication) {}
/// This participant has unmuted one of their tracks
void onTrackUnmuted(Participant participant, TrackPublication publication) {}
/// This participant has published a new [Track] to the [Room].
void onTrackPublished(
RemoteParticipant participant, RemoteTrackPublication publication) {}
/// This participant has unpublished one of their [Track].
void onTrackUnpublished(
RemoteParticipant participant, RemoteTrackPublication publication) {}
/// The [LocalParticipant] has subscribed to a new track published by this
/// [RemoteParticipant]
void onTrackSubscribed(RemoteParticipant participant, Track track,
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,
RemoteTrackPublication publication) {}
/// Data received from this [RemoteParticipant].
void onDataReceived(RemoteParticipant participant, List<int> data) {}
/// An error has occured during track subscription.
void onTrackSubscriptionFailed(
RemoteParticipant participant, String sid, String? message) {}
}
@@ -97,11 +119,14 @@ class Participant extends ChangeNotifier {
return result;
}
/// internal use
/// for internal use
/// {@nodoc}
bool get hasInfo => _participantInfo != null;
Participant(this.sid, this.identity);
/// for internal use
/// {@nodoc}
set isSpeaking(bool speaking) {
if (_isSpeaking == speaking) {
return;
@@ -125,6 +150,8 @@ class Participant extends ChangeNotifier {
}
}
/// for internal use
/// {@nodoc}
updateFromInfo(ParticipantInfo info) {
this.identity = info.identity;
this.sid = info.sid;
@@ -134,10 +161,14 @@ class Participant extends ChangeNotifier {
this._participantInfo = info;
}
/// for internal use
/// {@nodoc}
muteChanged() {
notifyListeners();
}
/// for internal use
/// {@nodoc}
addTrackPublication(TrackPublication pub) {
pub.track?.sid = pub.sid;
tracks[pub.sid] = pub;
@@ -7,6 +7,7 @@ import '../track/track.dart';
import '../track/video_track.dart';
import 'participant.dart';
/// Represents other participant in the [Room].
class RemoteParticipant extends Participant {
SignalClient _client;
@@ -27,6 +28,8 @@ class RemoteParticipant extends Participant {
}
}
/// for internal use
/// {@nodoc}
addSubscribedMediaTrack(
MediaStreamTrack mediaTrack, MediaStream stream, String? sid) async {
if (sid == null) {
@@ -70,6 +73,8 @@ class RemoteParticipant extends Participant {
notifyListeners();
}
/// for internal use
/// {@nodoc}
@override
void updateFromInfo(ParticipantInfo info) {
var hadInfo = hasInfo;
+53 -5
View File
@@ -8,6 +8,7 @@ import 'package:tuple/tuple.dart';
import 'errors.dart';
import 'extensions.dart';
import 'logger.dart';
import 'options.dart';
import 'participant/local_participant.dart';
import 'participant/participant.dart';
import 'participant/remote_participant.dart';
@@ -25,39 +26,83 @@ enum RoomState {
Reconnecting,
}
/// Delegate for [Room] callbacks
mixin RoomDelegate {
// room level callbacks
/// When the connection to the server has been interrupted and it's attempting
/// to reconnect.
void onReconnecting() {}
/// Connection to room is re-established. All existing state is preserved.
void onReconnected() {}
/// Disconnected from the room
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) {}
/// When a [RemoteParticipant] leaves the room
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) {}
// 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) {}
/// A track that was muted, fires on both [RemoteParticipant]s and
/// [LocalParticipant]
void onTrackMuted(Participant participant, TrackPublication publication) {}
/// A track that was unmuted, fires on both [RemoteParticipant]s and
/// [LocalParticipant]
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(
RemoteParticipant participant, RemoteTrackPublication publication) {}
/// A [RemoteParticipant] has unpublished a track
void onTrackUnpublished(
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,
RemoteTrackPublication publication) {}
/// A subscribed track is no longer available.
void onTrackUnsubscribed(RemoteParticipant participant, Track track,
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) {}
/// Encountered failure attempting to subscribe to track.
void onTrackSubscriptionFailed(
RemoteParticipant participant, String sid, String? message) {}
}
/// Room is the main entrypoint to working with LiveKit. It provides
/// updates to its state via two ways, by assigning a delegate, or using
/// Room is the primary construct for LiveKit conferences. It contains a
/// 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.
/// Room will trigger a change update when
/// Room will trigger a change notification update when
/// * state changes
/// * participant membership changes
/// * active speakers are different
/// {@category Room}
class Room extends ChangeNotifier with ParticipantDelegate {
RoomState _state = RoomState.Disconnected;
@@ -92,6 +137,8 @@ class Room extends ChangeNotifier with ParticipantDelegate {
Completer<Room>? _connectCompleter;
/// internal use
/// {@nodoc}
Room([RTCConfiguration? rtcConfig])
: _engine = new RTCEngine(SignalClient(), rtcConfig) {
_engine.onTrack = _onTrackAdded;
@@ -148,6 +195,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
return completer.future;
}
/// Disconnects from the room, notifying server of disconnection.
disconnect() {
_engine.client.sendLeave();
_handleDisconnect();
@@ -288,7 +336,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
return;
}
var parsed = unpackStreamId(stream.id);
var parsed = _unpackStreamId(stream.id);
var trackSid = parsed.item2;
if (trackSid == null) {
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('|');
if (parts.length != 2) {
return Tuple2(parts[0], null);
+1
View File
@@ -4,6 +4,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'errors.dart';
import 'extensions.dart';
import 'logger.dart';
import 'options.dart';
import 'proto/livekit_rtc.pb.dart';
import 'proto/livekit_models.pb.dart';
import 'signal_client.dart';
+2 -7
View File
@@ -3,11 +3,12 @@ import 'dart:convert';
import 'dart:developer';
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: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';
@@ -16,12 +17,6 @@ import '_websocket_api.dart'
if (dart.library.io) '_websocket_io.dart'
if (dart.library.html) '_websocket_html.dart' as platform;
class JoinOptions {
final bool? autoSubscribe;
const JoinOptions({this.autoSubscribe});
}
mixin SignalClientDelegate {
// initial connection established
void onConnected(JoinResponse response);
+2
View File
@@ -11,6 +11,8 @@ class AudioTrack extends Track {
AudioTrack(String name, MediaStreamTrack track, this.mediaStream)
: super(TrackType.AUDIO, name, track);
/// Start playing audio track. On web platform, create an audio element and
/// start playback
start() {
if (!(this is LocalAudioTrack)) {
audio.startAudio(getCid(), mediaTrack);
+1
View File
@@ -8,6 +8,7 @@ class LocalAudioTrack extends AudioTrack {
LocalAudioTrack(String name, MediaStreamTrack track, MediaStream stream)
: super(name, track, stream);
/// Creates a new audio track from the default audio input device.
static Future<LocalAudioTrack> createTrack(
[LocalAudioTrackOptions? options]) async {
try {
@@ -11,6 +11,7 @@ class LocalTrackPublication extends TrackPublication {
this.track = track;
}
/// Mute or unmute the current track. When muted, track will stop sending data
set muted(bool val) {
if (val == muted) {
return;
+5
View File
@@ -4,12 +4,15 @@ import '../errors.dart';
import 'options.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 {
RTCRtpSender? get sender => transceiver?.sender;
LocalVideoTrack(String name, MediaStreamTrack mediaTrack, MediaStream stream)
: super(name, mediaTrack, stream);
/// Creates a LocalVideoTrack from camera input.
static Future<LocalVideoTrack> createCameraTrack(
[LocalVideoTrackOptions? options]) async {
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 {
if (sender == null) {
return Future.error(TrackCreateError('could not restart track'));
+2
View File
@@ -1,3 +1,4 @@
/// Options when creating a LocalVideoTrack.
class LocalVideoTrackOptions {
CameraPosition position = CameraPosition.FRONT;
VideoParameter params;
@@ -65,4 +66,5 @@ class VideoPresets {
];
}
/// Options when creating an LocalAudioTrack. Placeholder for now.
class LocalAudioTrackOptions {}
@@ -4,6 +4,8 @@ import '../participant/remote_participant.dart';
import 'track.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 {
RemoteParticipant _participant;
bool _unsubscribed = false;
@@ -37,6 +39,8 @@ class RemoteTrackPublication extends TrackPublication {
_sendUpdateTrackSettings();
}
/// for internal use
/// {@nodoc}
set muted(bool val) {
if (val == muted) {
return;
+1
View File
@@ -10,6 +10,7 @@ class TrackDimension {
TrackDimension(this.width, this.height);
}
/// Wrapper around a MediaStreamTrack with additional metadata.
class Track {
static const ScreenShareName = "screen";
+3
View File
@@ -1,6 +1,8 @@
import '../proto/livekit_models.pb.dart';
import 'track.dart';
/// Represents a track that's published to the server. This class contains
/// metadata associated with tracks.
class TrackPublication {
Track? track;
String name;
@@ -19,6 +21,7 @@ class TrackPublication {
updateFromInfo(info);
}
/// True when the track is published with name [Track.ScreenShareName].
bool get isScreenShare =>
kind == TrackType.VIDEO && name == Track.ScreenShareName;
+2
View File
@@ -13,6 +13,8 @@ class VideoTrack extends Track with ChangeNotifier {
MediaStream? get mediaStream => _mediaStream;
/// internal use
/// {@nodoc}
set mediaStream(MediaStream? stream) {
_mediaStream = stream;
notifyListeners();
+1
View File
@@ -4,6 +4,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../track/video_track.dart';
import '../track/local_video_track.dart';
/// Widget that renders a [VideoTrack].
class VideoTrackRenderer extends StatefulWidget {
final VideoTrack track;
final RTCVideoRenderer renderer;