Tracks represent media streams in LiveKit. They can be audio or video, local or remote, and have different sources like camera, microphone, or screen share.
Track Types
The SDK provides several track classes:
Track - Base class for all tracks
AudioTrack - Audio track protocol
VideoTrack - Video track protocol
LocalAudioTrack - Local audio track (e.g., microphone)
LocalVideoTrack - Local video track (e.g., camera)
RemoteAudioTrack - Remote participant’s audio
RemoteVideoTrack - Remote participant’s video
Track Hierarchy
Track (base class)
├── LocalTrack
│ ├── LocalAudioTrack
│ └── LocalVideoTrack
└── RemoteTrack
├── RemoteAudioTrack
└── RemoteVideoTrack
Track Properties
All tracks share common properties:
track.sid // Server-assigned track ID (Sid?)
track.name // Track name (String)
track.kind // Track kind (.audio or .video)
track.source // Track source (.camera, .microphone, .screenShareVideo, etc.)
track.isMuted // Whether track is muted
track.trackState // Track state (.stopped or .started)
track.dimensions // Video dimensions (Dimensions?, video tracks only)
track.statistics // Track statistics (TrackStatistics?)
Track Kind
Tracks can be audio or video:
public enum Kind: Int, Codable {
case audio
case video
case none
}
if track.kind == .video {
let videoTrack = track as? VideoTrack
videoTrack?.add(videoRenderer: myVideoView)
}
Track Source
The source indicates where the track originates:
public enum Source: Int, Codable {
case unknown
case camera
case microphone
case screenShareVideo
case screenShareAudio
}
switch track.source {
case .camera:
print("Camera video track")
case .microphone:
print("Microphone audio track")
case .screenShareVideo:
print("Screen share video track")
default:
break
}
Standard Track Names
Common track names are defined as constants:
Track.cameraName // "camera"
Track.microphoneName // "microphone"
Track.screenShareVideoName // "screen_share"
Track.screenShareAudioName // "screen_share_audio"
Track State
Tracks have two states:
public enum TrackState: Int {
case stopped // Track is not capturing/receiving
case started // Track is actively capturing/receiving
}
Starting and Stopping Tracks
// Start the track
try await track.start()
// Stop the track
try await track.stop()
// Check state
if track.trackState == .started {
print("Track is active")
}
Local tracks start automatically when published. Remote tracks start automatically when subscribed.
Local Tracks
Local tracks represent media from the local participant.
Creating Local Video Tracks
Create camera tracks:
// Create with default options
let cameraTrack = LocalVideoTrack.createCameraTrack()
// Create with custom options
let options = CameraCaptureOptions(
position: .front,
preferredFormat: .preset1920x1080,
preferredFPS: 30
)
let cameraTrack = LocalVideoTrack.createCameraTrack(options: options)
Create screen share tracks:
// iOS: In-app screen share
let screenTrack = LocalVideoTrack.createInAppScreenShareTrack()
// iOS: System-wide screen share (requires Broadcast Upload Extension)
let screenTrack = LocalVideoTrack.createBroadcastScreenCapturerTrack()
// macOS: Main display screen share
let mainDisplay = try await MacOSScreenCapturer.mainDisplaySource()
let screenTrack = LocalVideoTrack.createMacOSScreenShareTrack(source: mainDisplay)
Creating Local Audio Tracks
// Create with default options
let audioTrack = LocalAudioTrack.createTrack()
// Create with custom options
let options = AudioCaptureOptions(
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
)
let audioTrack = LocalAudioTrack.createTrack(options: options)
Muting and Unmuting
Local tracks can be muted and unmuted:
// Mute the track
try await localVideoTrack.mute()
// Unmute the track
try await localVideoTrack.unmute()
// Check mute state
if localVideoTrack.isMuted {
print("Track is muted")
}
Muting a local track stops capturing but keeps the track published. The track continues to exist but sends blank frames.
Video Processors
Apply processors to local video tracks:
let blurProcessor = BackgroundBlurVideoProcessor()
localVideoTrack.processor = blurProcessor
// Remove processor
localVideoTrack.processor = nil
Remote Tracks
Remote tracks represent media from other participants.
Accessing Remote Tracks
Get remote tracks from track publications:
func room(_ room: Room, participant: RemoteParticipant, didSubscribeTrack publication: RemoteTrackPublication) {
guard let track = publication.track else { return }
if let videoTrack = track as? VideoTrack {
// Handle video track
videoTrack.add(videoRenderer: videoView)
} else if let audioTrack = track as? AudioTrack {
// Audio tracks play automatically
}
}
Video Tracks
Render video tracks:
let videoView = VideoView()
if let videoTrack = track as? VideoTrack {
videoTrack.add(videoRenderer: videoView)
}
// Remove when done
videoTrack.remove(videoRenderer: videoView)
Audio Tracks
Audio tracks are played automatically when subscribed:
if let audioTrack = track as? AudioTrack {
// Audio plays automatically
// Optionally add custom audio renderers:
audioTrack.add(audioRenderer: myCustomRenderer)
}
Track Dimensions
Video tracks have dimensions that may change:
if let dimensions = videoTrack.dimensions {
print("Video size: \(dimensions.width) x \(dimensions.height)")
print("Aspect ratio: \(videoTrack.aspectRatio)")
}
Monitor dimension changes:
class MyTrackDelegate: TrackDelegate {
func track(_ track: VideoTrack, didUpdateDimensions dimensions: Dimensions?) {
guard let dimensions = dimensions else { return }
print("Video dimensions changed: \(dimensions.width) x \(dimensions.height)")
}
}
videoTrack.delegates.add(delegate: myDelegate)
Track Statistics
Enable statistics reporting:
// Enable when creating room
let roomOptions = RoomOptions(
reportRemoteTrackStatistics: true
)
// Or enable on individual tracks
await track.set(reportStatistics: true)
Access statistics:
if let stats = track.statistics {
if let inbound = stats.inboundRtp?.first {
print("Received bytes: \(inbound.bytesReceived ?? 0)")
print("Packets lost: \(inbound.packetsLost ?? 0)")
print("Bitrate: \(inbound.formattedBps())")
}
if let outbound = stats.outboundRtp?.first {
print("Sent bytes: \(outbound.bytesSent ?? 0)")
print("Bitrate: \(outbound.formattedBps())")
}
}
Monitor statistics updates:
func track(_ track: Track, didUpdateStatistics statistics: TrackStatistics, simulcastStatistics: [VideoCodec: TrackStatistics]) {
print("Track stats updated")
}
Track Events
Implement TrackDelegate to receive track events:
@objc protocol TrackDelegate {
optional func track(_ track: VideoTrack, didUpdateDimensions dimensions: Dimensions?)
optional func track(_ track: Track, didUpdateStatistics statistics: TrackStatistics, simulcastStatistics: [VideoCodec: TrackStatistics])
}
Example Usage
class TrackObserver: TrackDelegate {
func track(_ track: VideoTrack, didUpdateDimensions dimensions: Dimensions?) {
guard let dimensions = dimensions else { return }
updateVideoLayout(for: dimensions)
}
func track(_ track: Track, didUpdateStatistics statistics: TrackStatistics, simulcastStatistics: [VideoCodec: TrackStatistics]) {
if let inbound = statistics.inboundRtp?.first {
updateNetworkIndicator(packetsLost: inbound.packetsLost ?? 0)
}
}
}
track.delegates.add(delegate: observer)
Video Frame Access
Access the latest video frame:
if let videoFrame = videoTrack.videoFrame {
// videoFrame is a VideoFrame object
let buffer = videoFrame.buffer
let rotation = videoFrame.rotation
let timeStamp = videoFrame.timeStamp
// Process the frame
}
Video frames are only cached when the track has at least one renderer attached. Otherwise videoFrame will be nil.
Track Lifecycle
Understanding the track lifecycle:
Local Track Lifecycle
- Created - Track is instantiated
- Started - Capturer begins capturing (camera/microphone)
- Published - Track is published to the room
- Muted/Unmuted - Track can be muted/unmuted while published
- Unpublished - Track is unpublished from the room
- Stopped - Capturer stops capturing
Remote Track Lifecycle
- Published - Remote participant publishes the track
- Subscribed - Local participant subscribes to the track
- Started - Track begins receiving data
- Muted/Unmuted - Remote participant mutes/unmutes the track
- Unsubscribed - Local participant unsubscribes
- Unpublished - Remote participant unpublishes the track
- Stopped - Track stops receiving data
Best Practices
- Remove renderers: Always remove video renderers when done to prevent memory leaks
- Handle dimensions: Video dimensions may be nil initially or change during the session
- Check track type: Use
as? casting to safely access audio/video specific features
- Monitor mute state: Track both local mute state and remote participant mute states
- Enable statistics carefully: Statistics reporting has performance overhead, enable only when needed
- Stop tracks: Stop local tracks when unpublishing to release camera/microphone resources