Skip to main content

Handling events

Observe and respond to events in the LiveKit SDK.

Overview

The LiveKit SDKs use events to communicate with the application changes that are taking place in the room.

There are two kinds of events, room events and participant events. Room events are emitted from the main Room object, reflecting any change in the room. Participant events are emitted from each Participant, when that specific participant has changed.

Room events are generally a superset of participant events. As you can see, some events are fired on both Room and Participant; this is intentional. This duplication is designed to make it easier to componentize your application. For example, if you have a UI component that renders a participant, it should only listen to events scoped to that participant.

Declarative UI

Event handling can be quite complicated in a realtime, multi-user system. Participants could be joining and leaving, each publishing tracks or muting them. To simplify this, LiveKit offers built-in support for declarative UI for most platforms.

With declarative UI you specify the how the UI should look given a particular state, without having to worry about the sequence of transformations to apply. Modern frameworks are highly efficient at detecting changes and rendering only what's changed.

We offer a few hooks and components that makes working with React much simpler.

  • useParticipant - maps participant events to state
  • useTracks - returns the current state of the specified audio or video track
  • VideoTrack - React component that renders a video track
  • RoomAudioRenderer - React component that renders the sound of all audio tracks
const Stage = () => {
const tracks = useTracks([Track.Source.Camera, Track.Source.ScreenShare]);
return (
<LiveKitRoom
{/* ... */}
>
// Render all video
{tracks.map((track) => {
<VideoTrack trackRef={track} />;
})}
// ...and all audio tracks.
<RoomAudioRenderer />
</LiveKitRoom>
);
};
function ParticipantList() {
// Render a list of all participants in the room.
const participants = useParticipants();
<ParticipantLoop participants={participants}>
<ParticipantName />
</ParticipantLoop>;
}

Most core objects in the Swift SDK, including Room, Participant, and TrackReference, implement the ObservableObject protocol so they are ready-made for use with SwiftUI.

For the simplest integration, the Swift Components SDK contains ready-made utilities for modern SwiftUI apps, built on .environmentObject:

  • RoomScope - creates and (optionally) connects to a Room, leaving upon dismissal
  • ForEachParticipant - iterates each Participant in the current room, automatically updating
  • ForEachTrack - iterates each TrackReference on the current participant, automatically updating
struct MyChatView: View {
var body: some View {
RoomScope(url: /* URL */,
token: /* Token */,
connect: true,
enableCamera: true,
enableMicrophone: true) {
VStack {
ForEachParticipant { _ in
VStack {
ForEachTrack(filter: .video) { _ in
MyVideoView()
.frame(width: 100, height: 100)
}
}
}
}
}
}
}
struct MyVideoView: View {
@EnvironmentObject private var trackReference: TrackReference
var body: some View {
VideoTrackView(trackReference: trackReference)
.frame(width: 100, height: 100)
}
}

The Room and Participant objects have built-in Flow support. Any property marked with a @FlowObservable annotation can be observed with the flow utility method. It can be used like this:

@Composable
fun Content(
room: Room
) {
val remoteParticipants by room::remoteParticipants.flow.collectAsState(emptyMap())
val remoteParticipantsList = remoteParticipants.values.toList()
LazyRow {
items(
count = remoteParticipantsList.size,
key = { index -> remoteParticipantsList[index].sid }
) { index ->
ParticipantItem(room = room, participant = remoteParticipantsList[index])
}
}
}
@Composable
fun ParticipantItem(
room: Room,
participant: Participant,
) {
val videoTracks by participant::videoTracks.flow.collectAsState(emptyList())
val subscribedTrack = videoTracks.firstOrNull { (pub) -> pub.subscribed } ?: return
val videoTrack = subscribedTrack.second as? VideoTrack ?: return
VideoTrackView(
room = room,
videoTrack = videoTrack,
)
}

Flutter supports declarative UI by default. The LiveKit SDK notifies changes in two ways:

  • ChangeNotifier - generic notification of changes. This is useful when you are building reactive UI and only care about changes that may impact rendering
  • EventsListener<Event> - listener pattern to listen to specific events (see events.dart)
class RoomWidget extends StatefulWidget {
final Room room;
RoomWidget(this.room);
State<StatefulWidget> createState() {
return _RoomState();
}
}
class _RoomState extends State<RoomWidget> {
late final EventsListener<RoomEvent> _listener = widget.room.createListener();
void initState() {
super.initState();
// used for generic change updates
widget.room.addListener(_onChange);
// Used for specific events
_listener
..on<RoomDisconnectedEvent>((_) {
// handle disconnect
})
..on<ParticipantConnectedEvent>((e) {
print("participant joined: ${e.participant.identity}");
})
}
void dispose() {
// Be sure to dispose listener to stop listening to further updates
_listener.dispose();
widget.room.removeListener(_onChange);
super.dispose();
}
void _onChange() {
// Perform computations and then call setState
// setState will trigger a build
setState(() {
// your updates here
});
}
Widget build(BuildContext context) => Scaffold(
// Builds a room layout with a main participant in the center, and a row of
// participants at the bottom.
// ParticipantWidget is located here: https://github.com/livekit/client-sdk-flutter/blob/main/example/lib/widgets/participant.dart
body: Column(
children: [
Expanded(
child: participants.isNotEmpty
? ParticipantWidget.widgetFor(participants.first)
: Container()),
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: math.max(0, participants.length - 1),
itemBuilder: (BuildContext context, int index) => SizedBox(
width: 100,
height: 100,
child: ParticipantWidget.widgetFor(participants[index + 1]),
),
),
),
],
),
);
}

Events

This table captures a consistent set of events that are available across platform SDKs. In addition to what's listed here, there may be platform-specific events on certain platforms.

EventDescriptionRoom EventParticipant Event
ParticipantConnectedA RemoteParticipant joins after the local participant.✔️
ParticipantDisconnectedA RemoteParticipant leaves✔️
ReconnectingThe connection to the server has been interrupted and it's attempting to reconnect.✔️
ReconnectedReconnection has been successful✔️
DisconnectedDisconnected from room due to the room closing or unrecoverable failure✔️
TrackPublishedA new track is published to room after the local participant has joined✔️✔️
TrackUnpublishedA RemoteParticipant has unpublished a track✔️✔️
TrackSubscribedThe LocalParticipant has subscribed to a track✔️✔️
TrackUnsubscribedA previously subscribed track has been unsubscribed✔️✔️
TrackMutedA track was muted, fires for both local tracks and remote tracks✔️✔️
TrackUnmutedA track was unmuted, fires for both local tracks and remote tracks✔️✔️
LocalTrackPublishedA local track was published successfully✔️✔️
LocalTrackUnpublishedA local track was unpublished✔️✔️
ActiveSpeakersChangedCurrent active speakers has changed✔️
IsSpeakingChangedThe current participant has changed speaking status✔️
ConnectionQualityChangedConnection quality was changed for a Participant✔️✔️
ParticipantAttributesChangedA participant's attributes were updated✔️✔️
ParticipantMetadataChangedA participant's metadata was updated✔️✔️
RoomMetadataChangedMetadata associated with the room has changed✔️
DataReceivedData received from another participant or server✔️✔️
TrackStreamStateChangedIndicates if a subscribed track has been paused due to bandwidth✔️✔️
TrackSubscriptionPermissionChangedOne of subscribed tracks have changed track-level permissions for the current participant✔️✔️
ParticipantPermissionsChangedWhen the current participant's permissions have changed✔️✔️