Skip to content
Connecting to Multiple Channels with Agora on iOS Featured

Connecting to Multiple Channels with Agora on iOS

By Author: Max Cobb In Developer

Since Agora’s SDK 3.0.0 users are now able to join an unlimited number of channels. The only limit is that you can only publish your own camera feed to one channel at a time.

This may be useful in one instance if you have a classroom channel where a teacher would stream to, and want the students to do smaller group work, while keeping an eye on them!

Prerequisites

  • An Agora developer account (see How To Get Started with Agora)
  • Xcode 11.0 or later
  • iOS device with minimum iOS 13.0
  • A basic understanding of iOS development
  • CocoaPods

Setup

Create an iOS project in Xcode, then install the CocoaPod AgoraUIKit_iOS. This pod contains some useful classes that make the setup of our project and utilisation of the Agora SDK much easier, although it is by no means a requirement of streaming multiple channels.

To see how to set up the video canvases yourself instead of with AgoraUIKit, check out the Quickstart Guide for iOS.

To install the CocoaPod, your Podfile should look like this:

target 'My-Agora-Project' do
 pod 'AgoraUIKit_iOS', '1.3.2'
end

The latest AgoraUIKit release at the time of writing this post is v1.3.2.

Run pod init, and open the .xcworkspace file to get started.

Set Up the UI

In this example, we will just have the option to join two different channels, one with a “broadcaster” role and the other as “audience”.

If we make a class for this view called ViewForm which accepts one parameter, stating whether it is for an audience view or a streamer view. We make two of these views, each to fill up either the left or right side of the screen.

At this stage, the ViewForm object looks like this:

Now that the views are in place, we need to add the buttons and other parts to our views. Each ViewForm has a method placeFields() to do this. This could be called directly from the initialiser, but the steps have been broken up for this guide:

This is what those views look like so far in both light and dark mode on iOS:

The grey box showing where the video streams will be has only been added to make it clear where those views are, it is an empty view in the actual project so far.

Connect to Both Channels

To know what channels we need to connect to, we must fetch the values from left and right view’s channelField UITextFields, and ensure they are not nil or empty:

guard let leftChannel = self.leftView.channelField.text,
     let rightChannel = self.rightView.channelField.text,
     !leftChannel.isEmpty, !rightChannel.isEmpty else {
   return
}

When connecting to a video channel with Agora you would typically call the method joinChannel, on your instance of the AgoraRtcEngineKit, explained in the iOS quickstart guide here. This is the method we will use to connect to the channel we intend to stream to – in the leftView.

As we are using AgoraUIKit_iOS, we can just call the join method on the AgoraVideoViewer class being used, this joins the channel using the AgoraRtcEngineKit.joinChannel method.

// Join channel as broadcaster with AgoraVideoViewer
self.agoraVideoView.join(channel: leftChannel, as: .broadcaster)

Note: This project is meant for reference purposes and development environments, it is not intended for production environments. Token authentication is recommended for all RTE apps running in production environments. For more information about token based authentication within the Agora platform please refer to this guide: https://bit.ly/3sNiFRs

In order to connect to the second channel we need to create an AgoraRtcChannel object and assign the AgoraRtcChannelDelegate.

// Join second channel as audience directly
self.rightView.agChannel = self.agkit.createRtcChannel(rightChannel)
self.rightView.agChannel?.setRtcChannelDelegate(self)
self.rightView.agChannel?.join(
    byToken: nil, info: nil, uid: 0,
    options: AgoraRtcChannelMediaOptions()
)

To assign the delegate we must have the ViewController inherit AgoraRtcChannelDelegate.

At this point, when the user clicks “Join” we need to change the “Join” button to a “Leave” button, and toggle the value of joinedChannels:

self.submitButton.setTitle("Leave", for: .normal)
self.joinedChannels = true

Now the user will join the channel on the left by streaming their local camera feed, and join the channel on the right as an audience member.

Functionality for leaving the channels will be explained later in this blog.

Display Video Feeds

The video feeds in the example on the left is completely handled by AgoraUIKit_iOS, but if you want to see how to do this manually, it is fully explained in this section in the iOS Quickstart Guide.

What typically happens is an AgoraRtcVideoCanvas is created, its uid set to the user ID, a UIView is assigned to display the video, and setupRemoteVideo or setupLocalVideo is called on the engine object:

extension ViewController: AgoraRtcEngineDelegate {
   // Monitors the remoteVideoStateChangedOfUid callback
    func rtcEngine(
        _ engine: AgoraRtcEngineKit,
        remoteVideoStateChangedOfUid uid: UInt,
        state: AgoraVideoRemoteState,
        reason: AgoraVideoRemoteStateReason,
        elapsed: Int
    ) {
        switch state {
            case .starting:
                let videoCanvas = AgoraRtcVideoCanvas()
                videoCanvas.uid = uid
                videoCanvas.view = remoteView
                // Set the remote video view
                engine.setupRemoteVideo(videoCanvas)
            default: break
        }
    }
}

The difference with our second channel, is that we are instead using the AgoraRtcChannelDelegate. The methods in AgoraRtcChannelDelegate are very similar to those in AgoraRtcEngineDelegate. The main visual difference is that the first parameter is the AgoraRtcChannel object, rather than the engine:

extension ViewController: AgoraRtcChannelDelegate {
    // Monitors the remoteVideoStateChangedOfUid callback
    func rtcEngine(
        _ rtcChannel: AgoraRtcChannel,
        remoteVideoStateChangedOfUid uid: UInt,
        state: AgoraVideoRemoteState,
        reason: AgoraVideoRemoteStateReason,
        elapsed: Int
    ) {
        switch state {
            case .starting:
                let videoCanvas = AgoraRtcVideoCanvas()
                videoCanvas.uid = uid
                // remoteView is any UIView for the video feed
                videoCanvas.view = remoteView
                // Specify the channelId of the remote user
                videoCanvas.channelId = rtcChannel.getId()
                // Set the remote video view
                self.agkit.setupRemoteVideo(videoCanvas)
            default: break
        }
    }
}

The main difference in the above snippet is that we are using an extra property of AgoraRtcVideoCanvas, channelId. At the point of telling the engine to setup the remote video, the engine is aware of what channel it needs to do so for – rather than the channel that has been joined directly, without first creating a channel object.

In our example, we are going to utilise another class from AgoraUIKit_iOS to display the video feeds; this class is AgoraSingleVideoView. AgoraSingleVideoView can be useful for quickly displaying a remote user’s state with a basic design, see the documentation here.

To create AgoraSingleVideoView we will add a method to our ViewController called getOrCreateUserVideo, which also keeps a record of the created video views in a dictionary remoteUsersRHS:

extension ViewController: AgoraRtcChannelDelegate {
    func getOrCreateUserVideo(
        _ rtcChannel: AgoraRtcChannel, with userId: UInt
    ) -> AgoraSingleVideoView {
        if let remoteView = self.remoteUsersRHS[userId] {
            return remoteView
        }
        let remoteVideoView = AgoraSingleVideoView(
            uid: userId, micColor: .systemBlue
        )
        remoteVideoView.canvas.channelId = rtcChannel.getId()
​
        self.agkit.setupRemoteVideo(remoteVideoView.canvas)
        self.remoteUsersRHS[userId] = remoteVideoView
        return remoteVideoView
   }
}

remoteUsersRHS here is declared in ViewController with the type: [UInt: AgoraSingleVideoView].

We now only need to add three methods from the AgoraRtcChannelDelegateremoteVideoStateChangedOfUid for checking when the video state changes, remoteAudioStateChangedOfUid for audio state changes, and didOfflineOfUid for removing a view when a remote user leave:

Now that we are creating AgoraSingleVideoView objects for each streaming member of the channel, we need to display them within the ViewForm videosHolder.

The positioning will need to be updated each time a new member is added or removed from remoteUsersRHS, one way to do that is to add a didSet to that variable. The declaration of remoteUsersRHS inside the ViewController will now look like this:

/// Dictionary of User ID to AgoraSingleVideo Views
var remoteUsersRHS: [UInt: AgoraSingleVideoView] = [:] {
   didSet { self.setVideoPositions() }
}

And the functionality of setVideoPositions adds all the remote users in a grid formation, similar to AgoraVideoViewer—except this time we must write out the functionality ourselves.

After joining both channels and laying out our view, this could be the layout if both channels have active members in them:

Connecting to Multiple Channels with Agora on iOS - Screenshot #2

Leaving the Channels

As well as joining, we must also add functionality to leave the channels.

Whenever leaving channels, we should do several things, including removing all the remote video streams from the view, otherwise they may get stuck displaying the last frame before leaving.

In this case, AgoraVideoViewer handles the views to gracefully leave a channel, but with the manually connected to secondary channel we will do those things ourselves.

We call .leave() and .destroy() on the AgoraRtcChannel instance. Then set each canvas’ view to nil, remove the views from their parents, and clear the remoteUsersRHS dictionary.

extension ViewController {
    func leaveChannels() {
        // Leave the one we are streaming to
        self.agoraVideoView.leaveChannel()
        // Leave the channel we are the audience in
        self.rightView.agChannel?.leave()
        self.remoteUsersRHS.forEach { users in
            users.value.canvas.view = nil
            users.value.removeFromSuperview()
        }
        // IMPORTANT: The channel must be destroyed when leaving
        rightView.agChannel?.destroy()
        rightView.agChannel = nil
        self.remoteUsersRHS.removeAll()
        // Change button back to "Join"
        self.joinedChannels = false
        self.submitButton.setTitle("Join", for: .normal)
    }
}

Summary

Now you have a working example that connects to two channels: both of which receive remote video streams, and one that your camera feed is also streaming into. You could repeat the receiving only channel connection as many times as you wish, to view many channel feeds at the same time.

See the following link for an advanced guide for connecting to multiple channels:

Connecting to Multiple Channels with Agora on iOS - Screenshot #3

Other Resources

For more information about building applications using Agora.io SDKs, take a look at the Agora Video Call Quickstart Guide and Agora API Reference.

I also invite you to join the Agora Developer Slack community.