跳轉到

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 as set(source:enabled:) with false on LocalParticipant: 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 from setCamera(enabled:) and set(source: .camera, enabled:), which switch the hardware and do not change isMuted. 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:

尚未支援

本地視訊軌道的 dimensions 是 nil,目前只有遠端視訊軌道有值。