Build a Video Chat App with Multiple Members 👥 in StackView ⎚

Eric Giannini
Agora.io
Published in
7 min readFeb 22, 2019

Agora’s Video Chat SDK is one of the easiest ways to enable multiple member video calling. In contrast with competitors, Agora’s rich, low-latency group chat is built on top of Agora’s Software Defined Real-Time Network, a technology that leverages algorithms to find the best way to route your video streaming calls!

In this basic tutorial, you will get up and running in a multiple member video chat in iOS 12 with Swift 5, Apple’s latest version of its brand new programming language. In your app you will implement the following basic features for 1) setting up, 2) joining a channel, 3) muting / unmuting a user, and 4) scaling a layout for multiple members on the screen with UIStackView.

Key to our implementation is the desire to arrange the views for multiple members in a single video call so that the layout scales whenever the number of members increases or decreases! In your implementation, you will use an instance of UIStackView.

Setting up Agora’s SDK

Create an Agora account

1. Navigate to Agora’s sign in website.

2. Pass through the onboarding process. Navigate to the dashboard, moving from **Projects > Project List**.

3. Copy your app ID.

4. Create a root directory for your project.

If you couldn’t find the dashboard or app ID, check out Agora Tutorial Video :

Cocoapods

Navigate to the root directory. If you haven’t already brewed, right now is a great time to start. Click [here](http://brew.sh/index.html). If you do not have Cocoapods installed, run: brew install cocoapods. If you have, run:

pod init

Add the following to your Podfile:

platform :ios, ‘9.0’
use_frameworks!
target ‘Your App’ do
pod ‘AgoraRtcEngine_iOS’
end

Update your local Cocoapods library:

pod update

With the pods updated, install:

pod install

Open up your app:

open YourApp.xcworkspace

Permissions

The next step is to enable permissions for you to access the devices video calling uses to create calls: camera and microphone.

Navigate to the Info.plist file in .xcworkspace file. Add the following keys.

- Privacy — Camera Usage Description

- Privacy — Microphone Usage Description

For Camera write: “Please let us use your camera.”

For Microphone write: “Please let us use your microphone.”

Integrating SDK

App Delegate

Add a constant at the top of your AppDelegate.swift:

let AppID = “”

Add your AppID there.

View Setup

In Main.storyboard add a new view controller with two subviews: one for localVideo, another for remoteVideo. Add buttons for muting local video, muting local audio, and hangup. You set your constraints.

Controller Setup

The iOS framework for Agora operates as a singleton to provide communication functionality so you will set up your controller to provide instances of the AgoraRtcEngineKit. Find your .xcworkspace and make sure that a ViewController.swift file is there. At the top of the file, add the following lines, importing the framework:

import AgoraRtcEngineKit

Agora’s Singleton design pattern for invoking an instance of the AgoraRtcEngine is triggered by by calling the sharedEngine:withAppID:delegate method where agoraKit.enableWebSdkInteroperability(true).

func initializeAgoraEngine() {
agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: AppID, delegate: self)
agoraKit.enableWebSdkInteroperability(true)
}

In order to trigger the function, however, you need both a property of type AgoraRTCEngineKit called agoraKit, as well as a constant of type String assigned to your “Your-App-ID” to be declared:

var agoraKit: AgoraRtcEngineKit!
let AppID: String = “Your-App-ID”

It is a foregone conclusion that the forced unwrapping is safe, since the app’s functionality depends upon the availability of the AgoraRtcEngineKit.

Now trigger the function in viewDidLoad() so that an instance is instantiated as the controller draws the view:

override func viewDidLoad() {
super.viewDidLoad(true)
initializeAgoraEngine()
}

With the view and controller in place, the next step is to set up video calling.

Setting up Video Mode

After initialization, you set up a mode for toggling video. Create a helper method in the ViewController.swift in the following way:

func setupVideo() { 
}

Agora’s SDK disables video by default so you need to enable it in the following way:

func setupVideo() {
agoraKit.enableVideo()
}

Agora’s iOS framework, however, is highly configurable with settings for encoding, frame rate, bit rate, or orientation. Set the values for each one of these respectively in the following way:

func setupVideo() {
agoraKit.enableVideo()
agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: AgoraVideoDimension640x360, frameRate: .fps15, bitrate: AgoraVideoBitrateStandard, orientationMode: .adaptative) ) // Default video profile is 360P }

Note: Make sure to autocomplete within Xcode because copy/paste may cause problems!

Do not forget to call setupVideo in viewDidLoad():

override func viewDidLoad() {
super.viewDidLoad()
initializeAgoraEngine()
setupVideo()
}

Joining Channel

You join a user to a channel with .joinChannel, a single method in the Agora iOS SDK that takes a value for a parameter called byToken, a value for a channelId, a value for info, as well as an uid:

func joinChannel() {
agoraKit.setDefaultAudioRouteToSpeakerphone(true)
agoraKit.joinChannel(byToken: nil, channelId: “demoChannel1”, info:nil, uid:0){[weak self] (sid, uid, elapsed) -> Void in
// Join channel “demoChannel1”
}
UIApplication.shared.isIdleTimerDisabled = true
}

The breakdown for these paraeters are:

1. byToken ~ is a token for controlling the user’s role & privileges

2. channelId ~ is an integer for identifying a channel

3. info ~ info for identifying user

4. uid ~ is the user’s unique ID, which may or may not persist across channels but remains random when 0 is passed.

Finally, call joinChannel() in viewDidLoad().

override func viewDidAppear(_ animated: Bool) {           
super.viewDidAppear(animated)
showJoinAlert()
}
func showJoinAlert() {
let alertController = UIAlertController(title: nil, message: “Ready to join channel.”, preferredStyle: .alert)
let action = UIAlertAction(title: “Join”, style: .destructive) { (action:UIAlertAction) in
self.joinChannel()
}
alertController.addAction(action)
present(alertController, animated: true, completion: nil)
}
joinChannel

Setting up Local Video

It can be confusing to configure video, especially with concepts such as mode. Agora’s SDK, however, makes it really easy. The canvas, like a piece of artwork, is the for UI.

func setupLocalVideo() {
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.uid = 0
videoCanvas.view = localVideo
videoCanvas.renderMode = .hidden
agoraKit.setupLocalVideo(videoCanvas)
}

The constant videoCanvas instantiates an instance of AgoraRtcVideoCanvas() whose properties you set subsequently. Assigning 0 to .uid randomizes the a user’s ID. Assigning .renderMode to .hidden scales the video’s view. If you recall our setup from Main.storyboard, the view property of the videoCanvas is assigned to the localVideo property.

Finally, call setupLocalVideo() in viewDidLoad().

Delegate Extension

You extend the functionality of the delegate, implementing methods for decoding, setting up the remote video, monitoring offline / online, or muting:

extension VideoCallViewController: AgoraRtcEngineDelegate {
func rtcEngine(_ engine: AgoraRtcEngineKit,
firstRemoteVideoDecodedOfUid uid:UInt, size:CGSize, elapsed:Int) {
if (remoteVideo.isHidden) {
remoteVideo.isHidden = false
}
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.uid = uid
videoCanvas.view = remoteVideo
videoCanvas.renderMode = .hidden
agoraKit.setupRemoteVideo(videoCanvas)
}
internal func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid:UInt, reason:AgoraUserOfflineReason) {self.remoteVideo.isHidden = true
}
func rtcEngine(_ engine: AgoraRtcEngineKit, didVideoMuted muted:Bool, byUid:UInt) { remoteVideo.isHidden = muted
remoteVideoMutedIndicator.isHidden = !muted
}
}

With the delegate’s functionality extended, there leaves the button’s functionality.

Buttons

Leave

Program the leave button to leave the channel. It is really easy, as leaveChannel() method is native.

@IBAction func didClickHangUpButton(_ sender: UIButton) {
leaveChannel()
}
func leaveChannel() {
agoraKit.leaveChannel(nil)
hideControlButtons()
UIApplication.shared.isIdleTimerDisabled = false
remoteVideo.removeFromSuperview()
localVideo.removeFromSuperview()
}

Make the buttons clickable with selectors by dropping an instance of UITapGestureRecognizer on top of the view.

func setupButtons() {perform(#selector(hideControlButtons), with:nil, afterDelay:8)let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(VideoCallViewController.ViewTapped))
view.addGestureRecognizer(tapGestureRecognizer)
view.isUserInteractionEnabled = true
}
func hideControlButtons() {
controlButtons.isHidden = true
}

Mute/unmute

Muting / unmuting audio is done by calling muteLocalAudioStream() on our shared instance of the AgoraRtcEngineKit.

@IBAction func didClickMuteButton(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
agoraKit.muteLocalAudioStream(sender.isSelected)
resetHideButtonsTimer()
}

Multiple Talking Heads

Inside of your view controller drop an IBOutlet of type UIStackView called stackView:

@IBOutlet weak var stackView: UIStackView!

You leverage the tags property on view for loading uid that identify instances of UIView() in your extension:

let videoView = UIView()
videoView.tag = Int(uid)
videoView.backgroundColor = UIColor.purple
stackView.addArrangedSubview(videoView)

These are the talking heads. To remove a talking head after a user signs off, implement the didOfflineOfUid method in the following way.

internal func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid:UInt, reason:AgoraUserOfflineReason) {
guard let view = stackView.arrangedSubviews.first(where: { (view) -> Bool in
return view.tag == Int(uid)
}) else { return }
stackView.removeArrangedSubview(view)
}

Modify setupLocalVideo(uid: UInt) with addArrangedSubview:

let videoView = UIView()
videoView.tag = Int(uid)
videoView.backgroundColor = UIColor.orange stackView.addArrangedSubview(videoView)

Modify joinChannel() to invoke setupLocalVideo(uid:uid):

guard let _self = self else { return }DispatchQueue.main.async {
_self.setupLocalVideo(uid: uid)
}

In the extension add the following code to the firstRemoteVideoDecodedOfUid method:

let videoView = UIView()videoView.tag = Int(uid)videoView.backgroundColor = UIColor.purplelet videoCanvas = AgoraRtcVideoCanvas()videoCanvas.uid = uidvideoCanvas.view = videoViewvideoCanvas.renderMode = .hiddenagoraKit.setupRemoteVideo(videoCanvas)stackView.addArrangedSubview(videoView)

Finally, do not forget to connect your IBOutlet to your storyboard.

⌘B and ⌘R!

Now all you have to do is build and run: ⌘B and ⌘R! Voila! You might see yourself as talking head upside down or sideways!

There’s Lincoln utilizing Agora’s iOS Video Chat SDK for Multiple Members !

How to test?

If you would like to check your code with the iOS app’s own code, please feel free to check out the code on GitHub. Search for talkingHeads otherwise feel free to copy the view controller code from paste bin into your app: https://pastebin.com/uXRDM7LD. If you would like to check out how your app works with the web, try out our handy dandy web app here: sidsharma27.github.io.

Where to go next?

There you go! Run the app on the iPhone. If you want to add more granular features, checkout our developer documentation where you can cover: security keys, statistics, adjust volume, adjust pitch and tone, rotate video, set up camera focus, share screen, or many, many, many other features native to the Agora iOS Video SDK.

--

--

Eric Giannini
Agora.io

🙌 Working with Swift, Kotlin, the world wide web