Events¶
RevueSDK reports what happens through three delegate protocols. Each one has an empty default for every method, so implement only the events you need.
| Delegate | Add it to | Tells you about |
|---|---|---|
RevueDelegate |
Revue.delegates on Revue.shared |
Friends: friend requests, invitations, each friend's connection, name and status, and whether you are discoverable |
RoomDelegate |
Room.delegates on Room.shared |
The call: participants joining and leaving, and every participant event below |
ParticipantDelegate |
Participant.delegates on one participant |
One participant: the same participant events, for that participant only |
The delegate lists hold weak references, so keep your delegates alive yourself. destroy() removes every
delegate: register them again after every initialize(_:)
(see Initialization and configuration).
Two delegate layers¶
Every participant event reaches both the participant's ParticipantDelegate and every RoomDelegate. Use
RoomDelegate to handle all participants in one place, and ParticipantDelegate to follow one participant,
for example to drive their video tile.
Because the same event arrives twice when you use both layers, listen on one layer only for anything that notifies the user, such as the prompt for a mute request. Otherwise the prompt appears twice.
| Event | RoomDelegate |
ParticipantDelegate |
|---|---|---|
| A participant joined | participantDidConnect |
— |
| A participant left | participantDidDisconnect |
didDisconnectWithReason |
| Friend connection changed | didUpdatePaths |
didUpdatePaths |
| Connection quality changed | didUpdateConnectionQuality |
didUpdateConnectionQuality |
| Name, status message or status changed (nil when the name or status message is cleared) | didUpdateName, didUpdateStatusMessage, didUpdateStatus |
didUpdateName, didUpdateStatusMessage, didUpdateStatus |
| You published or unpublished a track | didPublishTrack, didUnpublishTrack |
didPublishTrack, didUnpublishTrack |
| You started or stopped receiving a track | didSubscribeTrack, didUnsubscribeTrack |
didSubscribeTrack, didUnsubscribeTrack |
| A publication was muted or unmuted | didUpdateIsMuted |
didUpdateIsMuted |
| Track statistics updated | didUpdateStatistics |
didUpdateStatistics |
| A participant sent you a message | didReceiveMessage |
didReceiveMessage |
| A participant asked you to mute or unmute | didReceiveMuteRequest |
didReceiveMuteRequest |
| Speaking started or stopped | didUpdateSpeakingParticipants |
didUpdateIsSpeaking |
The name, status and mute events cover you as well as remote participants. In RoomDelegate, the publish
events label you as localParticipant: instead of participant:. Muting your own track with
mute() is not a publication event: it arrives on your TrackDelegate instead.
import RevueSDK
/// Show the mute request prompt from one layer only. The participant's ParticipantDelegate receives
/// the same didReceiveMuteRequest, so handling it there as well would show the prompt twice.
final class MuteRequestPrompter: RoomDelegate {
var onMuteRequest: ((RemoteParticipant, Bool) -> Void)?
func room(_ room: Room, participant: RemoteParticipant, didReceiveMuteRequest isMuted: Bool) {
onMuteRequest?(participant, isMuted)
}
}
To follow one participant, add a ParticipantDelegate to that participant:
import RevueSDK
/// Follow one participant, for example to drive their video tile. The delegate list holds weak
/// references, so keep the observer alive for as long as the tile is shown.
final class ParticipantTileObserver: ParticipantDelegate {
var onSpeaking: ((Bool) -> Void)?
var onMuted: ((TrackPublication, Bool) -> Void)?
func observe(friendId: String) {
Room.shared.remoteParticipants[friendId]?.delegates.add(delegate: self)
}
func participant(_ participant: Participant, didUpdateIsSpeaking isSpeaking: Bool) {
onSpeaking?(isSpeaking)
}
func participant(_ participant: Participant, trackPublication: TrackPublication, didUpdateIsMuted isMuted: Bool) {
onMuted?(trackPublication, isMuted)
}
}
Speaking¶
didUpdateIsSpeaking goes to ParticipantDelegate only, each time that participant starts or stops
speaking. RoomDelegate receives didUpdateSpeakingParticipants instead: the whole list of participants
who are speaking, loudest first, each time it changes. Room.activeSpeakers holds the same list.
import RevueSDK
/// Highlight whoever is speaking. RoomDelegate gets the whole list of speakers, loudest first,
/// each time it changes; Room.shared.activeSpeakers holds the same list.
final class ActiveSpeakerHandler: RoomDelegate {
var onSpeakersChanged: (([Participant]) -> Void)?
func room(_ room: Room, didUpdateSpeakingParticipants speakers: [Participant]) {
onSpeakersChanged?(speakers)
}
}
Threads¶
RevueDelegate, RoomDelegate and ParticipantDelegate are always called on the main thread, so you can update
your UI directly from any delegate method. So is VideoCapturerDelegate, which reports a capturer's size and
state; see Camera and capture options. Move heavy work, such as decoding a large message or writing
files, to a background thread yourself, then come back to the main thread to show the result.
import RevueSDK
import Foundation
struct ChatMessage: Decodable {
let text: String
}
/// Delegate methods are called on the main thread. Decode on a background queue,
/// then come back to the main thread to update the UI.
final class ChatMessageHandler: RoomDelegate {
var onMessage: ((RemoteParticipant, ChatMessage) -> Void)?
private let decodeQueue = DispatchQueue(label: "chat.decode", qos: .userInitiated)
func room(_ room: Room, participant: RemoteParticipant, didReceiveMessage message: String) {
decodeQueue.async { [weak self] in
guard let chat = try? JSONDecoder().decode(ChatMessage.self, from: Data(message.utf8)) else { return }
DispatchQueue.main.async {
self?.onMessage?(participant, chat)
}
}
}
}
Calling thread¶
Call RevueSDK APIs on the main thread: that is the way they are tested. Revue and the methods that end a
call, RemoteParticipant.disconnect() and Room.disconnectAll(), are marked @MainActor, so the compiler
checks those calls for you. For the rest, follow the same rule even though the compiler does not check it.
The exception is the methods that push audio or video buffers into the SDK: BufferCapturer.capture(_:)
(and BufferCapturer.capture(_:timeStampNs:rotation:)) and AudioBufferCapturer.capture(_:). You can call them
from any thread. You call them for every frame or audio buffer, so do not call them on the main thread: call
them from the thread or queue that produces the buffers, such as your capture queue, so that they do not hold
up your UI. See Your own frames and audio.