LocalParticipant¶
LocalParticipant is the participant that represents this device in a Room. Get it from
Room.localParticipant on Room.shared. You use it to publish your tracks to the remote participants, to
see what you have published, and to send them messages.
What you have published¶
You publish a track to one participant at a time (see Publishing tracks), so each
recipient has a LocalTrackPublication of its own.
LocalParticipant.localVideoTracks and LocalParticipant.localAudioTracks return those publications as
[LocalTrackPublication]: one for every participant you send the track to. If you publish your camera to
three participants, localVideoTracks holds three publications of the camera track. Video and audio are
counted separately, and a screen share counts as video.
To find the publication you send to one participant, call LocalParticipant.videoPublication(source:to:)
or LocalParticipant.audioPublication(to:) with their friend ID. Each returns nil when nothing of that
kind goes to them.
Stop publishing¶
LocalParticipant.unpublish(publication:) stops one publication: one track, to one participant.
LocalParticipant.unpublishAll() stops every publication at once. Every participant still in the room is
told to remove your tracks, so your tiles disappear on their side. The tracks themselves are not stopped:
the camera and microphone keep their state, and you can publish them again later.
import RevueSDK
/// Stop sending every track to everyone. Each remote participant is told to remove your tracks,
/// and receives didUnsubscribeTrack. The tracks themselves stay, so you can publish them again later.
@MainActor
func stopPublishing() {
Room.shared.localParticipant.unpublishAll()
}
You do not need to call unpublishAll() when a participant leaves. Their publications are removed for you.
Send a message¶
LocalParticipant.publish(message:to:timeout:) sends a text message to one participant in the room. They
receive it in didReceiveMessage. The call returns once their device confirms delivery, and throws a
RoomMessageError when the message is too long or cannot be delivered.
timeout is how long to wait for that confirmation, in seconds:
timeout |
Seconds waited |
|---|---|
| Not given | 10 |
| 5 to 30 | The value you give |
| More than 30 | 30 |
| Less than 5, including 0 and negative values | 5 |
| NaN | 10 (the default) |
A value outside 5 to 30 is not an error: the call is clamped to that range and goes ahead.
import RevueSDK
/// Send a text message to one participant in the room and wait up to 15 seconds for it to arrive.
/// The timeout can be 5 to 30 seconds; a value outside that range is clamped, not rejected.
@MainActor
func sendRoomMessage(_ text: String, to participant: RemoteParticipant) async -> Bool {
do {
try await Room.shared.localParticipant.publish(message: text, to: participant, timeout: 15)
return true
} catch RoomMessageError.messageTooLong(let size, let maxSize) {
print("Message is \(size) bytes; the limit is \(maxSize).")
return false
} catch {
print("Message was not delivered: \(error)")
return false
}
}
FriendManager.sendMessage(friendId:message:timeout:), which sends a message to a friend outside the room,
takes timeout in the same range and with the same default.
Mute: the track and each publication¶
Mute works at two levels, because a track you send to three participants has three publications.
| The track | One publication | |
|---|---|---|
| Call | mute() and unmute() on the track; for the microphone and screen share also set(source:enabled:) on LocalParticipant |
LocalTrackPublication.mute() and LocalTrackPublication.unmute() |
| Who is affected | Every participant you send the track to | Only the participant that publication goes to |
| Hardware | Not touched: the camera keeps capturing and the microphone is only silenced | Not touched: the camera and microphone keep running for everyone else |
| Reported by | Track.isMuted |
TrackPublication.isMuted on that publication |
| Event | track(_:didUpdateIsMuted:) on your TrackDelegate, added to the track's delegates |
participant(_:trackPublication:didUpdateIsMuted:) on your ParticipantDelegate, for that publication only |
For your app's camera button, switch the hardware: set(source: .camera, enabled:) does the same as
setCamera(enabled:). Turning it off stops the camera and shows every recipient the placeholder, and turning
it on again sends the camera to all of them. Neither changes Track.isMuted, and neither undoes a mute: if you
muted the camera track, unmute it yourself. Use set(source: .microphone, enabled:) for your microphone button.
The camera track stays the same from one room to the next. When you leave the room with
Room.disconnectAll() while the camera track is muted, the SDK unmutes it for you: the camera does not
turn on, and your TrackDelegate gets track(_:didUpdateIsMuted:) once. So you join the next room with the camera track
unmuted, and turning the camera on with setCamera(enabled: true) sends the camera.
import RevueSDK
/// Turn the camera off or on for everyone you send it to. This switches the hardware: when it is off every
/// recipient gets the placeholder. Track.isMuted and each publication's isMuted stay as they were.
@MainActor
func setCameraForEveryone(on: Bool) async throws {
try await Room.shared.localParticipant.set(source: .camera, enabled: on)
}
Use a publication to stop sending to one participant while the others keep receiving. Find it with
LocalParticipant.videoPublication(source:to:) or LocalParticipant.audioPublication(to:). A muted video
publication sends that participant the placeholder; a muted audio publication keeps sending, with silent
audio.
import RevueSDK
/// Stop sending your camera to one participant only. That participant gets the placeholder; everyone
/// else keeps seeing you, and the camera keeps running. Unmute the same publication to send it again.
@MainActor
func hideCamera(from participant: RemoteParticipant) async throws {
let local = Room.shared.localParticipant!
guard let publication = local.videoPublication(source: .camera, to: participant.identity) else {
return // the camera does not go to this participant
}
try await publication.mute()
}
The two levels do not change each other. Muting the track leaves every publication's isMuted as it was,
and muting a publication leaves Track.isMuted as it was. What a participant receives combines both: they
get the placeholder or silence when the track is muted, or their publication is muted, or both. So after
you mute the camera track, unmuting one participant's publication does not let them see you, and a
publication you muted stays muted when you turn the camera off and on again.
The same rules apply to every track you publish: the camera, the microphone, a screen share, and tracks you fill with your own video or audio buffers.
Screen sharing¶
Turn a screen share on and off with LocalParticipant.setScreenShare(enabled:). It controls capturing
only; it does not publish. After turning it on, publish the track it returns to each participant with
LocalParticipant.publish(videoTrack:to:options:). Each participant gets one video publication, so
unpublish the camera to that participant first.
import RevueSDK
/// Start a screen share and send it to everyone in the room in place of the camera.
/// setScreenShare only starts capturing; publishing the track to each participant is up to you.
/// Each participant gets one video publication, so the camera publication to them is removed first.
@MainActor
func startScreenShare() async throws {
let local = Room.shared.localParticipant!
guard let screen = try await local.setScreenShare(enabled: true) else { return }
for participant in Room.shared.remoteParticipants.values {
if let camera = local.videoPublication(source: .camera, to: participant.identity) {
local.unpublish(publication: camera)
}
if local.videoPublication(source: .screenShareVideo, to: participant.identity) == nil {
_ = try await local.publish(videoTrack: screen, to: participant)
}
}
}
When setScreenShare(enabled: true) returns depends on the capture mode:
- In-app: it returns once ReplayKit has started capturing. It throws when the user does not allow screen recording.
- Broadcast: it returns the track right away and shows the system broadcast picker. Capturing starts only when the user taps Start there, which may be later or never.
So the return of setScreenShare(enabled: true) does not tell you capturing has started. In both modes,
watch LocalParticipant.isScreenSharingPublisher: it sends true once capturing has started and false once
it has stopped, including when the user stops a broadcast from the system status bar.
import RevueSDK
import Combine
import Foundation
/// Update the screen share button from isScreenSharingPublisher. It sends true once capturing has
/// really started (in Broadcast mode, after the user taps Start), and false once it has stopped.
@MainActor
final class ScreenShareButtonModel: ObservableObject {
@Published private(set) var isSharing = false
private var subscription: AnyCancellable?
init() {
subscription = Room.shared.localParticipant.isScreenSharingPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] sharing in self?.isSharing = sharing }
}
}
setScreenShare(enabled: false) stops capturing and sends every recipient the placeholder. The
publications stay, so the participants keep your screen share tile until you unpublish it. Turning the
screen share on again sends it to the same publications.
import RevueSDK
/// Stop the screen share. Capturing stops and every recipient gets the placeholder, but the
/// publications stay: their screen share tiles remain until you unpublish them.
/// Calling setScreenShare(enabled: true) again resumes sending to the same publications.
@MainActor
func stopScreenShare() async throws {
try await Room.shared.localParticipant.setScreenShare(enabled: false)
}