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)
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)
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
true to enable statistics collection, false to disable
Track Lifecycle
- Publication: Remote participant publishes video track
- Discovery: Local participant receives track publication in delegate callback
- Subscription: SDK automatically subscribes (or manual subscription if auto-subscribe is disabled)
- Rendering: Attach
VideoRenderer to display video
- 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