Skip to content

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.