Skip to main content
Version: 2.0.x

Rendering Host and Audience Views - iOS

In a live stream setup, only hosts (participants in SEND_AND_RECV mode) can broadcast their audio and video. Audience members (in RECV_ONLY mode) are passive viewers who do not share their audio/video.

To ensure optimal performance and a clean user experience, your app should:

  • Render video only for hosts (i.e., participants in SEND_AND_RECV mode).
  • Display the total audience count to give context on viewership without rendering individual audience tiles.

Filtering and Rendering Hosts

The steps involved in rendering the audio and video of hosts are as follows.

  1. Filtering Hosts and Checking their Mic/Webcam Status
  2. Rendering Video Streams of Hosts
  3. Handling Audience Views

1. Filtering Hosts and Checking their Mic/Webcam Status

In a live stream, only participants in SEND_AND_RECV mode (i.e., hosts) actively share their audio and video. To render their streams, begin by accessing all participants using Meeting classs. Then, filter out only those in SEND_AND_RECV mode.

For each of these participants, use the Participant class, which provides real-time information like displayName, micOn, and webcamOn. Display their name along with the current status of their microphone and webcam. If the webcam is off, show a simple placeholder with their name. If it's on, render their video feed. This ensures only hosts are visible to the audience, keeping the experience clean and intentional.

In a live stream, only participants in SEND_AND_RECV mode (i.e., hosts) actively share their audio and video. To render their streams, begin by filtering all participants to show only those in SEND_AND_RECV mode.

// Function to get visible participants (hosts only)
private func getVisibleParticipants() -> [Participant] {
// Only show participants who are in SEND_AND_RECV mode
return liveStreamViewController.participants.filter { participant in
participant.mode == .SEND_AND_RECV
}
}

In the LiveStreamViewController class, we track the microphone status of each participant:

// Inside LiveStreamViewController's ParticipantEventListener implementation
func onStreamEnabled(_ stream: MediaStream, forParticipant participant: Participant) {
DispatchQueue.main.async {
// Only handle streams for SEND_AND_RECV participants
if participant.mode == .SEND_AND_RECV {
if let track = stream.track as? RTCVideoTrack {
if case .state(let mediaKind) = stream.kind, mediaKind == .video {
self.participantVideoTracks[participant.id] = track
self.participantCameraStatus[participant.id] = true
}
}

if case .state(let mediaKind) = stream.kind, mediaKind == .audio {
self.participantMicStatus[participant.id] = true
}
} else {
// For RECV_ONLY participants, ensure their tracks are removed
self.participantVideoTracks.removeValue(forKey: participant.id)
self.participantCameraStatus[participant.id] = false
self.participantMicStatus[participant.id] = false
}
}
}

func onStreamDisabled(_ stream: MediaStream, forParticipant participant: Participant) {
DispatchQueue.main.async {
if case .state(let mediaKind) = stream.kind {
switch mediaKind {
case .video:
self.participantVideoTracks.removeValue(forKey: participant.id)
self.participantCameraStatus[participant.id] = false
case .audio:
self.participantMicStatus[participant.id] = false
}
}
}
}

Then create a view for displaying each participant's video along with their name and microphone status:

struct ParticipantContainerView: View {
let participant: Participant
@ObservedObject var liveStreamViewController: LiveStreamViewController

// Name and mic status overlay
private var nameAndMicOverlay: some View {
VStack {
Spacer()
HStack {
Text(participant.displayName)
.foregroundColor(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.black.opacity(0.5))
.cornerRadius(4)

Image(systemName: liveStreamViewController.participantMicStatus[participant.id] ?? false ? "mic.fill" : "mic.slash.fill")
.foregroundColor(liveStreamViewController.participantMicStatus[participant.id] ?? false ? .green : .red)
.padding(4)
.background(Color.black.opacity(0.5))
.clipShape(Circle())

Spacer()
}
.padding(8)
}
}

var body: some View {
// Only render if participant is in SEND_AND_RECV mode
if participant.mode == .SEND_AND_RECV {
ZStack {
participantView(participant: participant, liveStreamViewController: liveStreamViewController)
nameAndMicOverlay
}
.background(Color.black.opacity(0.9))
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.7), radius: 10, x: 0, y: 5)
}
}

private func participantView(participant: Participant, liveStreamViewController: LiveStreamViewController) -> some View {
ZStack {
ParticipantView(participant: participant, liveStreamViewController: liveStreamViewController)
}
}
}

2. Rendering Video Streams of Hosts

Once you've filtered for participants in SEND_AND_RECV mode (i.e., hosts), you can use the Participant class to access their real-time data, including their webcamStream, webcamOn, and whether they are the local participant.

To render the video stream of a participant, we need to create a view that handles the WebRTC video track.

First, we define a VideoView class:

class VideoView: UIView {
var videoView: RTCMTLVideoView = {
let view = RTCMTLVideoView()
view.videoContentMode = .scaleAspectFill
view.backgroundColor = UIColor.black
view.clipsToBounds = true
view.transform = CGAffineTransform(scaleX: 1, y: 1)
return view
}()

init(track: RTCVideoTrack?, frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear

// Set videoView frame to match parent view
videoView.frame = bounds

DispatchQueue.main.async {
self.addSubview(self.videoView)
self.bringSubviewToFront(self.videoView)
track?.add(self.videoView)
}
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func layoutSubviews() {
super.layoutSubviews()
// Update videoView frame when parent view size changes
videoView.frame = bounds
}
}

Then create a SwiftUI wrapper to use this UIKit view:

struct VideoStreamView: UIViewRepresentable {
let track: RTCVideoTrack

func makeUIView(context: Context) -> VideoView {
let view = VideoView(track: track, frame: .zero)
return view
}

func updateUIView(_ uiView: VideoView, context: Context) {
track.add(uiView.videoView)
}
}

Now create a participant view to display either the video or a placeholder:

struct ParticipantView: View {
let participant: Participant
@ObservedObject var liveStreamViewController: LiveStreamViewController

var body: some View {
ZStack {
if participant.mode == .SEND_AND_RECV,
let track = liveStreamViewController.participantVideoTracks[participant.id] {
VideoStreamView(track: track)
} else {
Color.black.opacity(1.0)
VStack {
if participant.mode == .RECV_ONLY {
Text("Viewer")
.foregroundColor(.white)
Text(participant.displayName)
.foregroundColor(.gray)
.font(.caption)
} else {
Text("No media")
.foregroundColor(.white)
}
}
}
}
}
}

3. Handling Audience View

For audience members, we provide a different set of controls and manage their view state. The main distinction is determining whether the current participant is in audience mode:

private var isAudienceMode: Bool {
// Derive audience mode from the current participant's mode
if let localParticipant = liveStreamViewController.participants.first(where: { $0.isLocal }) {
return localParticipant.mode == .RECV_ONLY
}
return currentMode == .RECV_ONLY
}
note

Certainly! You can refer to the videosdk-ils-iOS-sdk-example directory in the official VideoSDK iOS example repository for a comprehensive implementation of interactive live streaming features, including participant management and UI controls.

API Reference

The API references for all the methods and events utilized in this guide are provided below.

Got a Question? Ask us on discord