Tracks¶
A Track is one stream of audio or video. Your camera, microphone and screen share are local tracks
(LocalVideoTrack and LocalAudioTrack), and what a remote participant sends you arrives as remote tracks
(RemoteVideoTrack and RemoteAudioTrack). A track tells you whether its data is flowing (Track.isMuted),
whether it is switched on (Track.trackState), what it is (Track.kind, Track.source, Track.name)
and, for video, its size (Track.dimensions).
A track reaches a participant through a publication, one per recipient. What you publish to each participant is on Track publications; the numbers behind a track are on Statistics.
Where tracks come from¶
| Local track | Where to get it |
|---|---|
| Camera | LocalParticipant.cameraTrack; the camera runs after setCamera(enabled: true) |
| Microphone | LocalParticipant.microphoneTrack; the microphone runs after setMicrophone(enabled: true) |
| Screen share | LocalParticipant.screenShareTrack, also returned by setScreenShare(enabled: true) |
| Your own audio or video buffers | LocalAudioTrack.createBufferTrack(name:) and LocalVideoTrack.createBufferTrack(name:source:) |
A remote participant's tracks are on the RemoteParticipant: Participant.firstCameraVideoTrack,
Participant.firstScreenShareVideoTrack and Participant.firstAudioTrack are the shortcuts, and
Participant.videoTracks and Participant.audioTracks list the publications, each with its
TrackPublication.track. Each participant sends you at most one video track and one audio track at a time.
Track.sid identifies a track on this device only. The two sides of a call do not share sids: the sid of your
camera track and the sid the receiver sees for it are different values, and the sid of a remote track repeats
from one participant to the next. Tell tracks apart by Track.source and Track.name, and do not send a sid
to the other side.
Mute: is the data flowing¶
Track.isMuted says whether the track's data is flowing. A muted video track sends the placeholder and a muted
audio track sends silence to every participant you send the track to, including the ones you publish it to
later: when you publish a muted track to a new participant, they get the placeholder or silence from the start.
So isMuted always matches what the track sends, whoever receives it.
When isMuted flips, the track calls track(_:didUpdateIsMuted:) on every TrackDelegate in its
Track.delegates, once per flip, however many participants you send it to. Like every delegate in RevueSDK,
it is called on the main thread.
import RevueSDK
/// Follow the camera track's own mute state. track(_:didUpdateIsMuted:) fires once per flip,
/// however many participants you send the track to, and arrives on the main thread.
final class CameraMuteObserver: TrackDelegate {
private(set) var isCameraMuted = false
init() {
Room.shared.localParticipant.cameraTrack.delegates.add(delegate: self)
}
func track(_ track: Track, didUpdateIsMuted isMuted: Bool) {
isCameraMuted = isMuted
}
}
On a remote track, isMuted is true while the sender is not sending you real data, whether they muted the
whole track or only your publication of it: the receiving side cannot tell the two apart. Its
TrackDelegate gets track(_:didUpdateIsMuted:) the same way.
Mute has a second level: a publication, which is one participant's copy of the track. Muting a publication
affects that participant only and does not change Track.isMuted. The two levels, and what a participant
receives when both apply, are on the
LocalParticipant page.
Mute a local track¶
Call mute() and unmute() on a LocalAudioTrack or LocalVideoTrack. What they do depends on the track:
- For the microphone and screen share tracks,
mute()is the same asset(source:enabled:)withfalseonLocalParticipant: every recipient gets silence or the placeholder. The microphone keeps running and is silenced. - For the camera track,
mute()stops sending its frames: every recipient gets the placeholder, and the camera keeps running. It is separate fromsetCamera(enabled:)andset(source: .camera, enabled:), which switch the hardware and do not changeisMuted. Unmute the track yourself; unmuting while the camera is off still sends the placeholder. - For a track you fill with your own buffers,
mute()touches only that track. A muted buffer audio track sends silence to everyone it goes to and does not touch the microphone, so you can mute your background music while you keep talking. A muted buffer video track sends the placeholder.
You can mute before you publish: when the track is already muted and you publish it to someone later, they receive silence or the placeholder from the start.
Both calls fire track(_:didUpdateIsMuted:) once and leave every publication's isMuted as it was.
Hardware is switched on LocalParticipant¶
The camera, microphone and screen share have one switch each, and it is on LocalParticipant:
LocalParticipant.setCamera(enabled:options:), LocalParticipant.setMicrophone(enabled:) and
LocalParticipant.setScreenShare(enabled:). Track.trackState reflects that switch: .started once you turn
the hardware on, .stopped once you turn it off, and it changes as soon as you call the switch. The camera
hardware follows after a short delay.
LocalParticipant.isCameraActive and LocalParticipant.isMicrophoneActive read it for you.
After setMicrophone(enabled: true), isMicrophoneActive is true until
you call setMicrophone(enabled: false), whatever you mute, publish or unpublish in between. The camera works the same way:
muting the camera track leaves the camera running, and only setCamera(enabled:) or set(source: .camera, enabled:) turns it on or off.
import RevueSDK
/// Turn the microphone hardware on or off. isMicrophoneActive follows setMicrophone only: after
/// setMicrophone(enabled: true) it stays true until you call setMicrophone(enabled: false),
/// whatever you mute, publish or unpublish in between.
@MainActor
func setMicrophoneHardware(on: Bool) -> Bool {
let local = Room.shared.localParticipant!
_ = local.setMicrophone(enabled: on)
return local.isMicrophoneActive
}
There is no start() or stop() on a track. On a remote track, trackState means whether data is being
received; to stop handling what a participant sends you, call RemoteTrackPublication.set(enabled:) on their
publication instead.
The name of a remote track¶
Track.name on a remote track tells you what the sender published:
| What the sender published | name on your side |
|---|---|
| Their camera | camera |
| Their microphone | microphone |
| Their screen share | screen_share |
| A track they fill with their own buffers | The name the sender gave it with createBufferTrack, for example bgm |
When the sender replaces a buffer track with another one, say they unpublish bgm and publish tts, you get a
new track whose name is the new one, with didSubscribeTrack as for any new track. Use Track.source and
Track.name together to tell a participant's background music from their voice.
import RevueSDK
/// Tell a participant's background music from their voice. A track the sender fills with their own
/// buffers carries the name they gave it ("bgm" here); their microphone track is named "microphone".
func isPlayingBackgroundMusic(friendId: String) -> Bool {
Room.shared.remoteParticipants[friendId]?.firstAudioTrack?.name == "bgm"
}
Volume of a remote audio track¶
RemoteAudioTrack.volume sets how loud you hear that participant. The range is 0 to 10: 1.0 leaves the audio
unchanged (the default), 0 silences it, and values above 1.0 amplify it. A value outside the range is clamped.
Once you have the track, setting volume takes effect at once on the audio already flowing.
At 0 the audio keeps arriving; only its level is zero. Statistics and speaking detection are unaffected, and a recording of the track stays continuous, with silence in that stretch. The gain is applied once, before the audio reaches anything that listens to the track on your side, so renderers and recordings hear the same volume.
import RevueSDK
/// Set how loud you hear one participant. 0 silences them while their audio keeps arriving,
/// 1 leaves it unchanged, and values up to 10 amplify. The change takes effect at once.
func setListeningVolume(of friendId: String, to volume: Double) {
let track = Room.shared.remoteParticipants[friendId]?.firstAudioTrack as? RemoteAudioTrack
track?.volume = volume
}
The value lives on the track. When the call is interrupted but not ended, for example while the connection
drops and comes back, the track and its volume stay. A new call is a new track, and its volume starts at 1.0.
If the user chose a volume for a participant, keep it yourself and set it again when their track arrives in
didSubscribeTrack.
import RevueSDK
/// A new call gives you a new RemoteAudioTrack, and its volume starts at 1.0. Keep the volume the
/// user chose for each participant yourself, and set it again when their track arrives.
final class ListeningVolumeStore: RoomDelegate {
private var volumes: [String: Double] = [:] // friend ID → the volume the user chose
init() {
Room.shared.delegates.add(delegate: self)
}
func remember(_ volume: Double, for friendId: String) {
volumes[friendId] = volume
let track = Room.shared.remoteParticipants[friendId]?.firstAudioTrack as? RemoteAudioTrack
track?.volume = volume
}
func room(_ room: Room, participant: RemoteParticipant, didSubscribeTrack publication: RemoteTrackPublication) {
guard let track = publication.track as? RemoteAudioTrack,
let volume = volumes[participant.identity] else { return }
track.volume = volume
}
}
Dimensions¶
Track.dimensions is the width and height of a video track, and nil on an audio track. Track.aspectRatio
is derived from it, and 1.0 while the size is unknown. On a remote video track dimensions is updated from the
frames you receive, so it follows the size the sender is sending you. On a local video track it is not filled yet:
Not supported yet
dimensions is nil on a local video track. It is filled on remote video tracks only.