Skip to main content
RemoteVideoTrack represents a video track received from a remote participant. It provides methods to attach video renderers for displaying remote participant video.

Overview

Remote video tracks are automatically created and managed by the SDK when a remote participant publishes a video track. You typically interact with them through delegate callbacks rather than creating them directly.

Receiving Remote Video

Subscribe to remote video tracks through delegate methods:
extension MyClass: RoomDelegate {
    func room(
        _ room: Room,
        participant: RemoteParticipant,
        didSubscribe track: RemoteTrack,
        publication: RemoteTrackPublication
    ) {
        if let videoTrack = track as? RemoteVideoTrack {
            // Video track subscribed and ready
            let videoView = VideoView()
            videoTrack.add(videoRenderer: videoView)
            
            // Add view to your UI
            addVideoView(videoView, for: participant)
        }
    }
}

Properties

Inherited Properties

From the base Track class:
  • name: The track name
  • sid: The server-assigned track ID
  • kind: Always .video for video tracks
  • source: Track source (.camera, .screenShareVideo, etc.)
  • isMuted: Whether the remote participant has muted this track
  • trackState: Current state (.started or .stopped)
  • dimensions: Current video dimensions (resolution)
  • videoFrame: The last received video frame
  • statistics: Real-time statistics if enabled

Methods

add(videoRenderer:)

Adds a VideoRenderer to receive video frames from this track.
public func add(videoRenderer: VideoRenderer)
videoRenderer
VideoRenderer
An object conforming to the VideoRenderer protocol that will receive video frames for rendering
Typically, you’ll use the SDK’s VideoView class which already implements VideoRenderer. Example:
let videoView = VideoView(frame: CGRect(x: 0, y: 0, width: 400, height: 300))
videoTrack.add(videoRenderer: videoView)

// Add to your view hierarchy
self.view.addSubview(videoView)
Multiple renderers can be added to the same track. This is useful for displaying the same video in multiple locations (e.g., thumbnail and full-screen views).

remove(videoRenderer:)

Removes a previously added VideoRenderer.
public func remove(videoRenderer: VideoRenderer)
videoRenderer
VideoRenderer
The renderer to remove
Always remove renderers when they’re no longer needed to prevent memory leaks and unnecessary processing. Example:
videoTrack.remove(videoRenderer: videoView)
videoView.removeFromSuperview()

start()

Starts rendering the video track. Inherited from Track.
public func start() async throws
Remote tracks are typically started automatically when subscribed. Manual control is available for advanced use cases.

stop()

Stops rendering the video track. Inherited from Track.
public func stop() async throws

set(reportStatistics:)

Enables or disables statistics reporting at runtime.
public func set(reportStatistics: Bool) async
reportStatistics
Bool
true to enable statistics collection, false to disable

Track Lifecycle

  1. Publication: Remote participant publishes video track
  2. Discovery: Local participant receives track publication in delegate callback
  3. Subscription: SDK automatically subscribes (or manual subscription if auto-subscribe is disabled)
  4. Rendering: Attach VideoRenderer to display video
  5. Unsubscription: Remove renderer when participant unpublishes or leaves

Video Rendering

Using VideoView

The SDK provides VideoView for easy video rendering:
func room(
    _ room: Room,
    participant: RemoteParticipant,
    didSubscribe track: RemoteTrack,
    publication: RemoteTrackPublication
) {
    guard let videoTrack = track as? RemoteVideoTrack else { return }
    
    let videoView = VideoView()
    videoView.layoutMode = .fill // or .fit
    videoView.isDebugMode = false
    
    videoTrack.add(videoRenderer: videoView)
    
    // Add to your view hierarchy
    participantView.addSubview(videoView)
    
    // Set up constraints or frame
    videoView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        videoView.topAnchor.constraint(equalTo: participantView.topAnchor),
        videoView.leadingAnchor.constraint(equalTo: participantView.leadingAnchor),
        videoView.trailingAnchor.constraint(equalTo: participantView.trailingAnchor),
        videoView.bottomAnchor.constraint(equalTo: participantView.bottomAnchor)
    ])
}

Custom Video Renderer

Implement VideoRenderer protocol for custom video processing:
class CustomVideoRenderer: VideoRenderer {
    // Required for adaptive stream
    @MainActor var isAdaptiveStreamEnabled: Bool { true }
    @MainActor var adaptiveStreamSize: CGSize { 
        CGSize(width: 640, height: 480)
    }
    
    func set(size: CGSize) {
        print("Video size: \(size)")
    }
    
    func render(frame: VideoFrame) {
        // Access frame data
        let dimensions = frame.dimensions
        let rotation = frame.rotation
        
        // Convert to image, save, process, etc.
        if let image = frame.toUIImage() {
            processFrame(image)
        }
    }
    
    func render(
        frame: VideoFrame,
        captureDevice: AVCaptureDevice?,
        captureOptions: VideoCaptureOptions?
    ) {
        // Optional method with capture info
        render(frame: frame)
    }
    
    private func processFrame(_ image: UIImage) {
        // Custom processing
    }
}

let customRenderer = CustomVideoRenderer()
videoTrack.add(videoRenderer: customRenderer)

Managing Video Views

Track Association

Maintain a mapping between participants and video views:
class ParticipantVideoManager: RoomDelegate {
    var videoViews: [String: VideoView] = [:] // participantSid -> VideoView
    var videoTracks: [String: RemoteVideoTrack] = [:]
    
    func room(
        _ room: Room,
        participant: RemoteParticipant,
        didSubscribe track: RemoteTrack,
        publication: RemoteTrackPublication
    ) {
        guard let videoTrack = track as? RemoteVideoTrack else { return }
        
        // Create and configure view
        let videoView = createVideoView(for: participant)
        
        // Attach track to view
        videoTrack.add(videoRenderer: videoView)
        
        // Store references
        videoViews[participant.sid] = videoView
        videoTracks[participant.sid] = videoTrack
    }
    
    func room(
        _ room: Room,
        participant: RemoteParticipant,
        didUnsubscribe track: RemoteTrack,
        publication: RemoteTrackPublication
    ) {
        guard track is RemoteVideoTrack else { return }
        
        // Clean up
        if let videoView = videoViews[participant.sid],
           let videoTrack = videoTracks[participant.sid] {
            videoTrack.remove(videoRenderer: videoView)
            videoView.removeFromSuperview()
        }
        
        videoViews.removeValue(forKey: participant.sid)
        videoTracks.removeValue(forKey: participant.sid)
    }
    
    private func createVideoView(for participant: RemoteParticipant) -> VideoView {
        let view = VideoView()
        view.layoutMode = .fill
        // Add to UI...
        return view
    }
}

Adaptive Streaming

The SDK automatically adjusts video quality based on renderer visibility and size:
class AdaptiveVideoView: UIView, VideoRenderer {
    @MainActor var isAdaptiveStreamEnabled: Bool {
        // Only request high quality when visible
        return !isHidden && window != nil
    }
    
    @MainActor var adaptiveStreamSize: CGSize {
        // Return actual view size for quality calculation
        return bounds.size
    }
    
    func render(frame: VideoFrame) {
        // Render frame
    }
}

Video Statistics

Monitor video quality and performance:
func room(
    _ room: Room,
    track: RemoteVideoTrack,
    didUpdateStatistics statistics: TrackStatistics,
    simulcastStatistics: [VideoCodec: TrackStatistics]
) {
    if let inbound = statistics.inboundRtp {
        print("Received: \(inbound.bytesReceived ?? 0) bytes")
        print("FPS: \(inbound.framesPerSecond ?? 0)")
        print("Resolution: \(inbound.frameWidth ?? 0)x\(inbound.frameHeight ?? 0)")
        print("Bitrate: \(inbound.bps) bps")
    }
}

Thread Safety

RemoteVideoTrack is @unchecked Sendable and thread-safe. All public methods can be called from any thread. Video renderer callbacks may be delivered on background threads.

Example Usage

class RemoteVideoViewController: UIViewController, RoomDelegate {
    var room: Room!
    var videoViews: [String: VideoView] = [:]
    var videoTracks: [String: RemoteVideoTrack] = [:]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        room.add(delegate: self)
    }
    
    func room(
        _ room: Room,
        participant: RemoteParticipant,
        didSubscribe track: RemoteTrack,
        publication: RemoteTrackPublication
    ) {
        guard let videoTrack = track as? RemoteVideoTrack else { return }
        
        // Create video view
        let videoView = VideoView()
        videoView.layoutMode = .fill
        
        // Add to participant's container
        if let container = participantContainer(for: participant) {
            container.addSubview(videoView)
            videoView.frame = container.bounds
            videoView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        }
        
        // Attach track
        videoTrack.add(videoRenderer: videoView)
        
        // Store references
        videoViews[participant.sid] = videoView
        videoTracks[participant.sid] = videoTrack
        
        // Enable statistics
        Task {
            await videoTrack.set(reportStatistics: true)
        }
    }
    
    func room(
        _ room: Room,
        participant: RemoteParticipant,
        didUnsubscribe track: RemoteTrack,
        publication: RemoteTrackPublication
    ) {
        guard track is RemoteVideoTrack,
              let videoView = videoViews[participant.sid],
              let videoTrack = videoTracks[participant.sid] else { return }
        
        // Clean up
        videoTrack.remove(videoRenderer: videoView)
        videoView.removeFromSuperview()
        
        videoViews.removeValue(forKey: participant.sid)
        videoTracks.removeValue(forKey: participant.sid)
    }
    
    func room(
        _ room: Room,
        participant: RemoteParticipant,
        publication: RemoteTrackPublication,
        didUpdateIsMuted isMuted: Bool
    ) {
        if publication.kind == .video {
            // Handle mute state UI update
            updateMuteIndicator(for: participant, isMuted: isMuted)
        }
    }
    
    private func participantContainer(for participant: RemoteParticipant) -> UIView? {
        // Return container view for this participant
        return nil // Implement based on your UI
    }
    
    private func updateMuteIndicator(for participant: RemoteParticipant, isMuted: Bool) {
        // Update UI to show mute state
    }
}

See Also

Build docs developers (and LLMs) love