DEV Community

Cover image for How to Build a Live Video Streaming iOS App with Agora 4.x Preview
Max Cobb
Max Cobb

Posted on • Edited on • Originally published at agora.io

1

How to Build a Live Video Streaming iOS App with Agora 4.x Preview

Recording and streaming a live feed from your app can be difficult, especially if you want to reach a global audience with little to no latency. The Agora Voice and Video SDK is perfect for reliably sending low-latency messages to a global audience, where you can have one or more people streaming their feeds.

Introduction

In this tutorial, you learn how to create an application that enables users to be either a streamer or an audience member in a session using Agora’s newest SDK version, 4.0.0. The setup is very similar to creating a video call with Agora, with a slight difference of roles: audience and broadcaster.

Prerequisites

  • An Agora developer account (see How To Get Started with Agora)
  • Xcode 12.0 or later
  • iOS device with iOS 13.0 or later (as this project uses SF Symbols)
  • A basic understanding of iOS development

Note: If you need to target an earlier version of iOS, you can change how the buttons are created in the provided example project.

Setup

Let’s start with a new, single-view iOS project. Create the project in Xcode, and then add Agora’s 4.0.0 Preview Swift Package.

Add the package by opening selecting File > Swift Packages > Add Package Dependency, then paste in the link to this Swift Package:

https://github.com/agorabuilder/AgoraRtcEngine_iOS_Preview.git
Enter fullscreen mode Exit fullscreen mode

At the time post is written, the latest release is 4.0.0.

If you want to jump ahead, you can find the full example project here on branch ng-sdk-update:

https://github.com/AgoraIO-Community/Agora-iOS-Swift-Example/tree/ng-sdk-update

Create the App

In this initial UIViewController, we can just have a button in the center to join the channel. This button will tell the application to open a view on top, enabling us to join as an audience member by default.

import UIKit
import AgoraRtcKit
class ViewController: UIViewController {
var joinButton: UIButton?
override func viewDidLoad() {
super.viewDidLoad()
addJoinButton()
}
@objc func showChannelView() {
self.present(ChannelViewController(), animated: true)
}
/// Adds a button which says "Join" to the view
/// This button takes you to the `ChannelViewController`
func addJoinButton() {
if self.joinButton != nil {
return
}
let button = UIButton(type: .custom)
button.setTitle("Join", for: .normal)
button.setTitleColor(.label, for: .normal)
button.setTitleColor(.secondaryLabel, for: .focused)
button.backgroundColor = .systemGray
button.addTarget(self, action: #selector(showChannelView), for: .touchUpInside)
self.view.addSubview(button)
button.frame = CGRect(
origin: CGPoint(x: self.view.bounds.width / 2 - 75, y: self.view.bounds.height / 2 - 25),
size: CGSize(width: 150, height: 50)
)
button.autoresizingMask = [
.flexibleTopMargin, .flexibleBottomMargin, .flexibleLeftMargin, .flexibleRightMargin
]
button.backgroundColor = .systemGreen
button.layer.cornerRadius = 25
self.joinButton = button
}
}

The view in this example is very basic, as you can see:

img

Next, create the ChannelViewController.swift file. This file will contain our UIViewController subclass called ChannelViewController, which displays the Agora pieces. In it, we will store values such as App ID, token, and channel name for connecting to the service, and add a button to change the user role between audience (default) and broadcaster. Also, initialise the Agora Engine with the correct client role. I have also preemptively added remoteUserIDs and userVideoLookup, which will keep track of the broadcasters/streamers.

import UIKit
import AgoraRtcKit
class ChannelViewController: UIViewController {
static let channelName: String = <#Static Channel Name#>
static let appId: String = <#Agora App ID#>
/// Using an app without a token requirement in this example.
static var channelToken: String? = nil
/// Setting to zero will tell Agora to assign one for you
lazy var userID: UInt = 0
/// Role of the local user, `.audience` by default here.
var userRole: AgoraClientRole = .audience
/// Creates the AgoraRtcEngineKit, with default role as `.audience` above.
lazy var agkit: AgoraRtcEngineKit = {
let engine = AgoraRtcEngineKit.sharedEngine(
withAppId: ChannelViewController.appId,
delegate: self
)
engine.setChannelProfile(.liveBroadcasting)
engine.setClientRole(self.userRole)
return engine
}()
var hostButton: UIButton?
var closeButton: UIButton?
/// UIView for holding all the camera feeds from broadcasters.
var agoraVideoHolder = UIView()
/// IDs of the broadcaster(s)
var remoteUserIDs: Set<UInt> = []
/// Dictionary to find the Video Canvas for a user by ID.
var userVideoLookup: [UInt: AgoraRtcVideoCanvas] = [:] {
didSet {
reorganiseVideos()
}
}
/// Local video UIView, used only when broadcasting
lazy var localVideoView: UIView = {
let vview = UIView()
return vview
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.agoraVideoHolder)
self.joinChannel()
}
/// Join the pre-configured Agora channel
func joinChannel() {
self.agkit.enableVideo()
self.agkit.joinChannel(
byToken: ChannelViewController.channelToken,
channelId: ChannelViewController.channelName,
uid: self.userID,
mediaOptions: AgoraRtcChannelMediaOptions()
) { [weak self] _, uid, _ in
self?.userID = uid
// Add button to toggle broadcaster/audience
self?.getHostButton().isHidden = false
// Add button to close the view
self?.getCloseButton().isHidden = false
}
}
required init() {
super.init(nibName: nil, bundle: nil)
self.modalPresentationStyle = .fullScreen
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Add Close and Host buttons to the scene
@discardableResult
func getCloseButton() -> UIButton {
if let closeButton = self.closeButton {
return closeButton
}
guard let chevronSymbol = UIImage(systemName: "chevron.left") else {
fatalError("Could not create chevron.left symbol")
}
let button = UIButton.systemButton(with: chevronSymbol, target: self, action: #selector(leaveChannel))
self.view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
[
button.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 20),
button.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20)
].forEach { $0.isActive = true}
self.closeButton = button
return button
}
@discardableResult
func getHostButton() -> UIButton {
if let hostButton = self.hostButton {
return hostButton
}
let button = UIButton(type: .custom)
button.setTitle("Host", for: .normal)
button.setTitleColor(.label, for: .normal)
button.setTitleColor(.secondaryLabel, for: .focused)
button.addTarget(self, action: #selector(toggleBroadcast), for: .touchUpInside)
self.view.addSubview(button)
button.frame = CGRect(
origin: CGPoint(x: self.view.bounds.midX - 75, y: 50),
size: CGSize(width: 150, height: 50)
)
button.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleBottomMargin]
button.backgroundColor = .systemRed
button.layer.cornerRadius = 25
self.hostButton = button
return button
}
/// Toggle between being a host or a member of the audience.
/// On changing to being a broadcaster, the app first checks
/// that it has access to both the microphone and camera on the device.
@objc func toggleBroadcast() {
// Swap the userRole
self.userRole = self.userRole == .audience ? .broadcaster : .audience
self.agkit.setClientRole(self.userRole)
}
}

When userVideoLookup is set or updated, reorganiseVideos is called to organise all of the streamed video feeds into a grid formation. Of course, a grid formation is not required, but if you want to implement the same thing, the reorganiseVideos method is provided in the example project. Here’s a link to it:

https://github.com/AgoraIO-Community/Agora-iOS-Swift-Example/blob/aebfd5c6fd6159c85906952acc9e1b2e9aaec117/Agora-iOS-Example/ChannelViewController%2BVideoControl.swift#L109

The hostButton has a target set as toggleBroadcast. This method is found right at the bottom of the gist. As you can see, it toggles the value of self.userRole between .broadcaster and .audience, and then sets the client role using setClientRole. When the local client starts streaming, additional buttons should appear (for audio and video settings). But those buttons are displayed only after the client role has been changed, which is signaled by the delegate callback.

In the gist above, the ChannelViewController is set as the AgoraRtcEngineDelegate, so we should add that protocol to our class, along with some callback methods.

The main callback methods we need for a basic streaming session are didJoinedOfUid, didOfflineOfUid, didClientRoleChanged, and firstRemoteVideoDecodedOfUid.

import AgoraRtcKit
extension ChannelViewController: AgoraRtcEngineDelegate {
/// Called when we get a new video feed from a remote user
/// - Parameters:
/// - engine: Agora Engine.
/// - uid: ID of the remote user.
/// - size: Size of the video feed.
/// - elapsed: Time elapsed (ms) from the remote user sharing their video until this callback fired.
func rtcEngine(_ engine: AgoraRtcEngineKit, firstRemoteVideoDecodedOfUid uid: UInt, size: CGSize, elapsed: Int) {
// New Remote video feed = new remote broadcaster.
// Create the video canvas, create a view for the user
// then add it to the agoraVideoHolder
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.uid = uid
let hostingView = UIView()
self.agoraVideoHolder.addSubview(hostingView)
videoCanvas.view = hostingView
videoCanvas.renderMode = .hidden
self.agkit.setupRemoteVideo(videoCanvas)
userVideoLookup[uid] = videoCanvas
}
/// Called when the user role successfully changes
/// - Parameters:
/// - engine: AgoraRtcEngine of this session.
/// - oldRole: Previous role of the user.
/// - newRole: New role of the user.
func rtcEngine(
_ engine: AgoraRtcEngineKit,
didClientRoleChanged oldRole: AgoraClientRole,
newRole: AgoraClientRole
) {
let hostButton = self.getHostButton()
let isHost = newRole == .broadcaster
hostButton.backgroundColor = isHost ? .systemGreen : .systemRed
hostButton.setTitle("Host" + (isHost ? "ing" : ""), for: .normal)
if isHost {
self.setupLocalAgoraVideo()
} else {
userVideoLookup.removeValue(forKey: self.userID)
}
// Only show the camera options when we are broadcasting
self.getControlContainer().isHidden = !isHost
}
/// Setup the canvas and rendering for the device's local video
func setupLocalAgoraVideo() {
self.agoraVideoHolder.addSubview(self.localVideoView)
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.uid = self.userID
videoCanvas.view = localVideoView
videoCanvas.renderMode = .hidden
self.agkit.setupLocalVideo(videoCanvas)
userVideoLookup[self.userID] = videoCanvas
}
func rtcEngine(
_ engine: AgoraRtcEngineKit,
didJoinedOfUid uid: UInt,
elapsed: Int
) {
// Keeping track of all broadcasters in the session
remoteUserIDs.insert(uid)
}
func rtcEngine(
_ engine: AgoraRtcEngineKit,
didOfflineOfUid uid: UInt,
reason: AgoraUserOfflineReason
) {
// Removing on quit and dropped only
// the other option is `.becomeAudience`,
// which means it's still relevant.
if reason == .quit || reason == .dropped {
remoteUserIDs.remove(uid)
} else {
// User is no longer hosting, need to change the lookups
// and remove this view from the list
userVideoLookup.removeValue(forKey: uid)
}
}
}

As mentioned in the gist, the didClientRoleChanged is the callback for when we have changed the local user role to broadcaster or audience member.

If a remote host is streaming to us, the nonhosting view should now look like this:

img

When the user is streaming, they should have additional options, including the ability to switch between the device’s front- and back-facing cameras and turning the camera or microphone on and off. In this case, a beautification toggle is added. To show and hide these options, the isHidden property of the button container is set to false or true, respectively.

img

In your application you may not want anyone to be able to start streaming. You could easily achieve this by requiring a password in the app (a static password or one authorised by a network request). Or you could offer separate apps for hosts and audience members.

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.io Developer Slack community.

Sentry growth stunted Image

If you are wasting time trying to track down the cause of a crash, it’s time for a better solution. Get your crash rates to zero (or close to zero as possible) with less time and effort.

Try Sentry for more visibility into crashes, better workflow tools, and customizable alerts and reporting.

Switch Tools 🔁

Top comments (0)

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay