Skip to main content
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

  1. Created - Track is instantiated
  2. Started - Capturer begins capturing (camera/microphone)
  3. Published - Track is published to the room
  4. Muted/Unmuted - Track can be muted/unmuted while published
  5. Unpublished - Track is unpublished from the room
  6. Stopped - Capturer stops capturing

Remote Track Lifecycle

  1. Published - Remote participant publishes the track
  2. Subscribed - Local participant subscribes to the track
  3. Started - Track begins receiving data
  4. Muted/Unmuted - Remote participant mutes/unmutes the track
  5. Unsubscribed - Local participant unsubscribes
  6. Unpublished - Remote participant unpublishes the track
  7. Stopped - Track stops receiving data

Best Practices

  1. Remove renderers: Always remove video renderers when done to prevent memory leaks
  2. Handle dimensions: Video dimensions may be nil initially or change during the session
  3. Check track type: Use as? casting to safely access audio/video specific features
  4. Monitor mute state: Track both local mute state and remote participant mute states
  5. Enable statistics carefully: Statistics reporting has performance overhead, enable only when needed
  6. Stop tracks: Stop local tracks when unpublishing to release camera/microphone resources

Build docs developers (and LLMs) love