This tutorial describes how to add video chat to your MacOS applications using Swift and the Agora Video SDK.
With this sample app, you can:
- Join a channel
- Leave a channel
- Mute/unmute video and audio
- Enable/disable video
- Create input/output devices
- Send a message to a channel
- Screen share
- Set the resolution, the height, and the frame rate
- Enable encryption
- Xcode 8.0+
This section shows you how to prepare, build, and run the sample application.
To build and run the sample application, you must obtain an app ID:
- Create a developer account at agora.io. Once you finish the sign-up process, you are redirected to the dashboard.
- Navigate in the dashboard tree on the left to Projects > Project List.
- Copy the app ID that you obtained from the dashboard into a text file. You will use this when you launch the application.
-
Open
OpenVideoCall.xcodeprojand edit theKeyCenter.swiftfile. In theKeyCenterdeclaration, update<#Your App Id#>with your app ID.static let AppId: String = <#Your App Id#>
-
Download the Agora Video SDK. Unzip the downloaded SDK package and copy the
AgoraRtcEngineKit.frameworkfile from the SDKlibsfolder into the sample applicationOpenVideoCallfolder. -
Build and run the project. Ensure a valid provisioning profile is applied or your project will not run. Your application will look like this when it loads.
- Add Frameworks and Libraries
- Design the User Interface
- Create the MainViewController Class
- Create the MainViewController Class Extension
- Create the MainViewController Class Delegates
- Create the RoomViewController Class
- Create RoomViewController Agora Methods and Delegates
- Create the ChatMessageViewController Class
- Create the DevicesViewController Class
- Create the DevicesViewController Class Extensions
- Create the SettingsViewController Class
Under the Build Phases tab, add the following frameworks and libraries to your project:
SystemConfiguration.frameworklibresolv.tbdroomview-CoreWLAN.framework`CoreAudio.frameworkCoreMedia.frameworkAudioToolbox.frameworkAgoraRtcEngineKit.frameworkVideoToolbox.frameworkAVFoundation.framework
- Add Assets
- Create the MainViewController UI
- Create the RoomViewController UI and the ChatMessageViewController UI
- Create the DevicesViewController UI
- Create the SettingsViewController UI
Add the following assets to Assets.xcassets.
Note: Use Xcode to import assets to Assets.xcassets. PDF files are used for these assets, which contain images for each iOS screen resolution.
| Asset | Description |
|---|---|
btn_endcall |
An image of a red telephone for the hang up button |
btn_filter and btn_filter_blue |
Images of glasses for filtering |
btn_message and btn_message_blue |
Images of chat bubbles to initiate a call |
btn_mute and btn_mute_blue |
Images of a microphone to mute/unmute audio |
btn_screen_sharing and btn_screen_sharing_blue |
Images of an arrow to start/stop screen sharing |
btn_setting |
An image of a cog to open the settings window |
btn_video |
An image of a camera to start video |
btn_voice |
An image of an arrow with sound lines, indicating that audio chat is enabled |
icon_sharing_desktop |
Image of a monitor to share the desktop |
Create the layout for the MainViewController.
Note: This layout includes navigation segues to move from screen to screen.
When the application publishes, the MainViewController UI will look like this:
Create the layout for the RoomViewController and ChatMessageViewController. The ChatMessageViewController view is embedded in the RoomViewController view.
Note: This RoomViewController layout includes popover and embed segues to display additional UI for the view.
When the application publishes, the RoomViewController UI and ChatViewController UI combine to look like this:
Create the layout for the DevicesViewController .
When the application publishes, the DevicesViewController UI will look like this:
Create the layout for the SettingsViewController.
When the application publishes, the SettingsViewController UI will look like this:
MainViewController.swift defines and connects application functionality with the MainViewController UI.
- Define Global Variables
- Override the Base Superclass Methods
- Override the prepare() Segue Method
- Create the IBAction Methods
The MainViewController class has six IBOutlet variables. These map to the MainViewController UI elements.
| Variable | Description |
|---|---|
roomInputTextField |
Maps to the Channel name NSTextField in the MainViewController layout |
encryptionTextField |
Maps to the Encryption key NSTextField in the MainViewController layout |
encryptionPopUpButton |
Maps to the AES 128 NSPopUpButton in the MainViewController layout |
testButton |
Maps to the Test NSButton in the MainViewController layout |
joinButton |
Maps to the Join NSButton in the MainViewController layout |
settingsButton |
Maps to the Settings NSButton in the MainViewController layout |
import Cocoa
class MainViewController: NSViewController {
@IBOutlet weak var roomInputTextField: NSTextField!
@IBOutlet weak var encryptionTextField: NSTextField!
@IBOutlet weak var encryptionPopUpButton: NSPopUpButton!
@IBOutlet weak var testButton: NSButton!
@IBOutlet weak var joinButton: NSButton!
@IBOutlet weak var settingsButton: NSButton!
...
}The MainViewController class has one public variable and two private variables.
- The
videoProfilevariable is initialized with the default Agora video profile usingAgoraVideoProfile.defaultProfile(). - The
agoraKitprivate variable is declared as anAgoraRtcEngineKitobject and represents the Agora RTC engine. - The
encryptionTypeprivate variable is initialized toEncryptionType.xts128.
var videoProfile = AgoraVideoProfile.defaultProfile()
fileprivate var agoraKit: AgoraRtcEngineKit!
fileprivate var encryptionType = EncryptionType.xts128 The viewDidLoad() method initializes the MainViewController:
- Set the view's
wantsLayerproperty totrue. - Set the view layer's
backgroundColorcolor toNSColor.white.cgColor. - Load the Agora RTC engine SDK using
loadAgoraKit(). - Load the encryption settings using
loadEncryptionItems().
override func viewDidLoad() {
super.viewDidLoad()
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.white.cgColor
loadAgoraKit()
loadEncryptionItems()
}The viewDidAppear() method is triggered when the view appears on the screen.
Set the keyboard focus to the room's text input field using roomInputTextField.becomeFirstResponder().
override func viewDidAppear() {
super.viewDidAppear()
roomInputTextField.becomeFirstResponder()
}Override the prepare() segue method to manage the application navigation.
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
guard let segueId = segue.identifier , !segueId.isEmpty else {
return
}
...
} If the segueId is roomVCToSettingsVC, prepare the settings view through the segue destination SettingsViewController:
- Set
settingsVC.videoProfileto the currentvideoProfile. - Set
settingsVC.delegatetoself.
if segueId == "roomVCToSettingsVC" {
let settingsVC = segue.destinationController as! SettingsViewController
settingsVC.videoProfile = videoProfile
settingsVC.delegate = self
}If the segueId is roomNameVCToVideoVC, prepare the room view through the segue destination RoomViewController:
- Set
roomVC.roomNametosender. - Set
roomVC.encryptionSecretto the text entered in theencryptionTextField. - Set
roomVC.encryptionTypeto the currentencryptionType. - Set
roomVC.videoProfileto the currentvideoProfile. - Set
roomVC.delegatetoself.
else if segueId == "roomNameVCToVideoVC" {
let videoVC = segue.destinationController as! RoomViewController
if let sender = sender as? String {
videoVC.roomName = sender
}
videoVC.encryptionSecret = encryptionTextField.stringValue
videoVC.encryptionType = encryptionType
videoVC.videoProfile = videoProfile
videoVC.delegate = self
}If the segueId is roomVCToDevicesVC, prepare the devices view through the segue destination DevicesViewController:
- Set
devicesVC.agoraKittoagoraKit. - Set
devicesVC.couldTesttotrue.
else if segueId == "roomVCToDevicesVC" {
let devicesVC = segue.destinationController as! DevicesViewController
devicesVC.agoraKit = agoraKit
devicesVC.couldTest = true
}The Encryption dropdown menu in the MainViewController layout invokes the doEncryptionChanged() IBAction method. This method sets the encryptionType value to the selected index of EncryptionType.allValue.
@IBAction func doEncryptionChanged(_ sender: NSPopUpButton) {
encryptionType = EncryptionType.allValue[sender.indexOfSelectedItem]
}The Test UI Button in the MainViewController layout invokes the doTestClicked() IBAction method. This method opens the Devices View using performSegue().
@IBAction func doTestClicked(_ sender: NSButton) {
performSegue(withIdentifier: "roomVCToDevicesVC", sender: nil)
}The Join UI Button in the MainViewController layout invokes the doJoinClicked() IBAction method. This method enters the user into the room specified by roomInputTextField using enter().
@IBAction func doJoinClicked(_ sender: NSButton) {
enter(roomName: roomInputTextField.stringValue)
}The Settings UI Button in the MainViewController layout invokes the doSettingsClicked() IBAction method. This method opens the Settings View using performSegue().
@IBAction func doSettingsClicked(_ sender: NSButton) {
performSegue(withIdentifier: "roomVCToSettingsVC", sender: nil)
}The MainViewController extension contains methods to load the Agora RTC engine and manage UI navigation.
private extension MainViewController {
...
}The loadAgoraKit() method intializes the Agora RTC engine.
Create agoraKit with the KeyCenter.AppId using AgoraRtcEngineKit.sharedEngine() and enable video using agoraKit.enableVideo().
func loadAgoraKit() {
agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: KeyCenter.AppId, delegate: self)
agoraKit.enableVideo()
}The loadEncryptionItems method populates the encryption type UI dropdown menu using encryptionPopUpButton.addItems().
Initialize the selection with encryptionType.description() using encryptionPopUpButton.selectItem().
func loadEncryptionItems() {
encryptionPopUpButton.addItems(withTitles: EncryptionType.allValue.map { type -> String in
return type.description()
})
encryptionPopUpButton.selectItem(withTitle: encryptionType.description())
}The enter() method enters the user into the channel with the name roomName.
Ensure roomName is valid before navigating from the main view to the room view by applying the identifier roomNameVCToVideoVC to performSegue().
func enter(roomName: String?) {
guard let roomName = roomName , !roomName.isEmpty else {
return
}
performSegue(withIdentifier: "roomNameVCToVideoVC", sender: roomName)
}The MainViewController delegates implement the required and optional methods for the Agora SDK, UI components, and navigation to/from other views.
- Create the SettingsVCDelegate
- Create the RoomVCDelegate
- Create the AgoraRtcEngineDelegate
- Create the NSControlTextEditingDelegate
The settingsVC method is a delegate method for the SettingsVCDelegate.
This method is invoked when the video profile for the SettingsViewController changes. It updates the videoProfile with profile, and sets settingsVC.view.window?.contentViewController to self.
extension MainViewController: SettingsVCDelegate {
func settingsVC(_ settingsVC: SettingsViewController, closeWithProfile profile: AgoraVideoProfile) {
videoProfile = profile
settingsVC.view.window?.contentViewController = self
}
}The roomVCNeedClose method is a delegate method for the RoomVCDelegate. This method is invoked when the user leaves the room.
Do one of the following:
- If
windowis valid, apply the remaining code. - If
windowis invalid, invokereturn.
extension MainViewController: RoomVCDelegate {
func roomVCNeedClose(_ roomVC: RoomViewController) {
guard let window = roomVC.view.window else {
return
}
...
}
}- Invoke
window.toggleFullScreen()if the window's masking style has a.fullScreenoption. - Add two additional options
.fullSizeContentViewand.miniaturizableto the window's masking style array usingwindow.styleMask.insert(). - Set the window's delegate to
nil. - Initialize the window's collection behavior using
NSWindowCollectionBehavior().
if window.styleMask.contains(.fullScreen) {
window.toggleFullScreen(nil)
}
window.styleMask.insert([.fullSizeContentView, .miniaturizable])
window.delegate = nil
window.collectionBehavior = NSWindowCollectionBehavior()Set the content view controller to self and set the window to a fixed aspect ratio.
- Create a local variable
sizeusingCGSize(). - Set the
minSizeand themaxSizetosizeand set the current size by usingwindow.setContentSize(),
window.contentViewController = self
let size = CGSize(width: 720, height: 600)
window.minSize = size
window.setContentSize(size)
window.maxSize = sizeThe AgoraRtcEngineDelegate defines the required callback methods for the Agora SDK.
extension MainViewController: AgoraRtcEngineDelegate {
...
}The reportAudioVolumeIndicationOfSpeakers callback is triggered when the speaker volume indicators change.
Set a name for the VolumeChangeNotificationKey notification and the value for the totalVolume using NotificationCenter.default.post().
func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) {
NotificationCenter.default.post(name: Notification.Name(rawValue: VolumeChangeNotificationKey), object: NSNumber(value: totalVolume as Int))
}The device callback is triggered when the user's device is changed.
Set a name for the DeviceListChangeNotificationKey notification and the value for the deviceType using NotificationCenter.default.post().
func rtcEngine(_ engine: AgoraRtcEngineKit, device deviceId: String, type deviceType: AgoraMediaDeviceType, stateChanged state: Int) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: DeviceListChangeNotificationKey), object: NSNumber(value: deviceType.rawValue))
}The controlTextDidChange() method is triggered when the text input field is being edited.
Before formatting the string input, ensure that the field is valid .
- Format the field's string value using
MediaCharacter.updateToLegalMediaString(). - Set
field.stringValuetolegalString, which replaces the field's text with the newly formatted text.
extension MainViewController: NSControlTextEditingDelegate {
override func controlTextDidChange(_ obj: Notification) {
guard let field = obj.object as? NSTextField else {
return
}
let legalString = MediaCharacter.updateToLegalMediaString(from: field.stringValue)
field.stringValue = legalString
}
}RoomViewController.swift defines and connects application functionality with the RoomViewController UI.
- Define the RoomVCDelegate Protocol
- Define IBOutlet Variables
- Define Global Variables
- Define Private Class Variables
- Create Superclass Methods
- Create IBAction Methods
- Create Private and Public Methods
The roomVCNeedClose() method is used for communication between the RoomViewController class and its delegate. The method tells the delegate to close the room.
import Cocoa
import Quartz.ImageKit
protocol RoomVCDelegate: class {
func roomVCNeedClose(_ roomVC: RoomViewController)
}The RoomViewController class has IBOutlet variables to manage buttons, view containers, and handle other UI elements. The variables map to the RoomViewController UI elements.
| Variable | Description |
|---|---|
roomNameLabel |
Label for the room name in the header of the layout |
buttonContainerView |
Container for the buttons |
containerView |
Container for the videos in the room |
messageTableContainerView |
List of messages |
muteVideoButton |
Button to mute/unmute the video |
muteAudioButton |
Button to mute/unmute the audio |
screenSharingButton |
Button to share/unshare the screen |
windowListView |
List for the windows |
filterButton |
Button for filtering |
messageButton |
Button for messaging |
messageInputerView |
Container for message creation |
messageTextField |
Text field for the message creation |
class RoomViewController: NSViewController {
@IBOutlet weak var roomNameLabel: NSTextField!
@IBOutlet weak var containerView: NSView!
@IBOutlet weak var buttonContainerView: NSView!
@IBOutlet weak var messageTableContainerView: NSView!
@IBOutlet weak var muteVideoButton: NSButton!
@IBOutlet weak var muteAudioButton: NSButton!
@IBOutlet weak var screenSharingButton: NSButton!
@IBOutlet weak var windowListView: IKImageBrowserView!
@IBOutlet weak var filterButton: NSButton!
@IBOutlet weak var messageButton: NSButton!
@IBOutlet weak var messageInputerView: NSView!
@IBOutlet weak var messageTextField: NSTextField!
...
} The remaining code in this section is contained within the RoomViewController class declaration.
The RoomViewController class has five public variables. These variables manage the RoomViewController settings.
| Variable | Description |
|---|---|
roomName |
The name of the room |
encryptionSecret |
The encryption key for the room |
encryptionType |
The encryption type for the room |
videoProfile |
The video profile for the room |
delegate |
The delegate for the RoomViewController class |
AgoraRtcEngineKit |
The Agora RTC engine SDK object |
var roomName: String!
var encryptionSecret: String?
var encryptionType: EncryptionType!
var videoProfile: AgoraVideoProfile!
var delegate: RoomVCDelegate?
...
var agoraKit: AgoraRtcEngineKit!
...- UI Management Variables
- Video Session Variables
- Audio and Video Control Variables
- Screen Sharing Control Variables
- Filtering Control Variable
- Chat Message Control Variables
The shouldHideFlowViews variable defaults to false. When this variable changes:
-
Set the
isHiddenproperty of thebuttonContainerView,messageTableContainerView, androomNameLabelto the new value ofshouldHideFlowViews. -
If the screen sharing status is
.listmode, set thescreenSharingStatusto.none. -
Manage the
messageTextFieldand themessageInputerViewbased onshouldHideFlowViews.- If
shouldHideFlowViewsistrue, remove the focus from themessageTextFieldusing theresignFirstResponder()and hide themessageInputerViewby setting itsisHiddenproperty totrue. - If
shouldHideFlowViewsisfalse, set the focus to themessageTextFieldusing thebecomeFirstResponder()and show themessageInputerViewby setting itsisHiddenproperty tofalse.
- If
fileprivate var shouldHideFlowViews = false {
didSet {
buttonContainerView?.isHidden = shouldHideFlowViews
messageTableContainerView?.isHidden = shouldHideFlowViews
roomNameLabel?.isHidden = shouldHideFlowViews
if screenSharingStatus == .list {
screenSharingStatus = .none
}
if shouldHideFlowViews {
messageTextField?.resignFirstResponder()
messageInputerView?.isHidden = true
} else {
buttonContainerView?.isHidden = false
if isInputing {
messageTextField?.becomeFirstResponder()
messageInputerView?.isHidden = false
}
}
}
}The shouldCompressSelfView variable defaults to false. When this variable changes, invoke the updateSelfViewVisiable() method.
fileprivate var shouldCompressSelfView = false {
didSet {
updateSelfViewVisiable()
}
} The videoSessions and doubleClickFullSession variables handle the video sessions for the room.
Initialize videoSessions to an empty array. When videoSessions is set, update the interface with the video sessions using updateInterface().
fileprivate var videoSessions = [VideoSession]() {
didSet {
updateInterface(with: videoSessions)
}
}Initialize doubleClickEnabled to false.
When doubleClickFullSession is set, update the interface with the video sessions using updateInterface() if the number of sessions is 3 or more, and the interface has not already been updated (to avoid duplication).
Initialize the videoViewLayout using the VideoViewLayout().
The dataChannelId is set to -1 by default and manages the room channel.
fileprivate var doubleClickEnabled = false
fileprivate var doubleClickFullSession: VideoSession? {
didSet {
if videoSessions.count >= 3 && doubleClickFullSession != oldValue {
updateInterface(with: videoSessions)
}
}
}
fileprivate let videoViewLayout = VideoViewLayout()
fileprivate var dataChannelId: Int = -1 The audioMuted and videoMuted variables are set to false by default, and manage the audio and video streams, respectively.
When audioMuted is set, the muteAudioButton image is updated, and the audio stream is muted/unmuted using agoraKit.muteLocalAudioStream().
fileprivate var audioMuted = false {
didSet {
muteAudioButton?.image = NSImage(named: audioMuted ? "btn_mute_blue" : "btn_mute")
agoraKit.muteLocalAudioStream(audioMuted)
}
}When videoMuted is set:
- The
muteVideoButtonimage is updated. - The video stream is stopped/started using
agoraKit.muteLocalVideoStream()andsetVideoMuted(). - The video view of the current user is set to hidden/not hidden using
updateSelfViewVisiable().
fileprivate var videoMuted = false {
didSet {
muteVideoButton?.image = NSImage(named: videoMuted ? "btn_video" : "btn_voice")
agoraKit.muteLocalVideoStream(videoMuted)
setVideoMuted(videoMuted, forUid: 0)
updateSelfViewVisiable()
}
}The ScreenSharingStatus enumerates values for none, list, and sharing.
The nextStatus() method toggles between active and non-active status states.
- If the current value is
.none, return.list. - If the current value is
.list, return.none. - If the current value is
.sharing, return.none.
enum ScreenSharingStatus {
case none, list, sharing
func nextStatus() -> ScreenSharingStatus {
switch self {
case .none: return .list
case .list: return .none
case .sharing: return .none
}
}
}The screenSharingStatus variable is set to ScreenSharingStatus.none by default, and manages the current status of the screen share.
When the screenSharingButton is set:
- The
screenSharingButtonimage is updated. - If the old value is
.sharing, the screen share stopsstopShareWindow(). - if
screenSharingStatusis equal to.list, the window list is shown/hidden.
The windows variable is initialized using WindowList().
fileprivate var screenSharingStatus = ScreenSharingStatus.none {
didSet {
screenSharingButton?.image = NSImage(named: (screenSharingStatus == .sharing) ? "btn_screen_sharing_blue" : "btn_screen_sharing")
if oldValue == .sharing {
stopShareWindow()
}
showWindowList(screenSharingStatus == .list)
}
}
fileprivate var windows = WindowList()The isFiltering variable is set to false by default. When this variable is set:
- The creation of
agoraKitis verified. - If filtering is enabled, set the video preprocessing using
AGVideoPreProcessing.registerVideoPreprocessing()and update thefilterButtonwith the blue image. - If filtering is not enabled, unregister the video preprocessing using
AGVideoPreProcessing.deregisterVideoPreprocessing()and update thefilterButtonwith the white image.
fileprivate var isFiltering = false {
didSet {
guard let agoraKit = agoraKit else {
return
}
if isFiltering {
AGVideoPreProcessing.registerVideoPreprocessing(agoraKit)
filterButton?.image = NSImage(named: "btn_filter_blue")
} else {
AGVideoPreProcessing.deregisterVideoPreprocessing(agoraKit)
filterButton?.image = NSImage(named: "btn_filter")
}
}
}The chatMessageVC variable manages the chat message list.
The isInputing variable is set to false as the default. When this is set:
-
Based on the current value of
isInputing- The
messageTextFieldis activated/deactivated usingbecomeFirstResponder()/resignFirstResponder(). - The
messageInputerViewis hidden/unhidden.
- The
-
The
messageButtonimage is updated usingmessageButton?.setImage().
fileprivate var chatMessageVC: ChatMessageViewController?
fileprivate var isInputing = false {
didSet {
if isInputing {
messageTextField?.becomeFirstResponder()
} else {
messageTextField?.resignFirstResponder()
}
messageInputerView?.isHidden = !isInputing
messageButton?.image = NSImage(named: isInputing ? "btn_message_blue" : "btn_message")
}
}The viewDidLoad() method initializes the RoomViewController:
- Set the
roomNameLabeltext toroomName. - Set
messageInputerView.wantsLayertotrue. - Set the
messageInputerViewlayer background color to semi-transparent black and set thecornerRadiusproperty to2. - Invoke
setupWindowListView(). - Load the Agora RTC engine SDK using
loadAgoraKit().
override func viewDidLoad() {
super.viewDidLoad()
roomNameLabel.stringValue = roomName
messageInputerView.wantsLayer = true
messageInputerView.layer?.backgroundColor = NSColor(hex: 0x000000, alpha: 0.75).cgColor
messageInputerView.layer?.cornerRadius = 2
setupWindowListView()
loadAgoraKit()
}The viewDidAppear() method is triggered when the view appears on the screen. Set the view's window configuration style using configStyle().
override func viewDidAppear() {
super.viewDidAppear()
configStyle(of: view.window!)
}The prepare() segue method manages the navigation for the RoomViewController. If the segueId is VideoVCEmbedChatMessageVC, set chatMessageVC to the ChatMessageViewController; otherwise do nothing.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let segueId = segue.identifier else {
return
}
switch segueId {
case "VideoVCEmbedChatMessageVC":
chatMessageVC = segue.destination as? ChatMessageViewController
default:
break
}
}The methods in this section manage the methods for the IKImageBrowserView class.
extension RoomViewController {
...
}The numberOfItems method returns the number of items for the image browser. Return the value of windows.items.count.
override func numberOfItems(inImageBrowser aBrowser: IKImageBrowserView!) -> Int {
return windows.items.count
}The itemAt method returns the item at the specified index for the image browser. Return the value of windows.items[index].
override func imageBrowser(_ aBrowser: IKImageBrowserView!, itemAt index: Int) -> Any! {
let item = windows.items[index]
return item
}The cellWasDoubleClickedAt method is triggered when a cell in the image browser is double-clicked.
If aBrowser has no selected index or the index is less than windows.items.count, invoke return.
Otherwise, use the index to retrieve the window from windows.items and start sharing the window using startShareWindow() and set screenSharingStatus to .sharing.
override func imageBrowser(_ aBrowser: IKImageBrowserView!, cellWasDoubleClickedAt index: Int) {
guard let selected = aBrowser.selectionIndexes() else {
return
}
let index = selected.first
guard index! < windows.items.count else {
return
}
let window = windows.items[index!].window
startShareWindow(window!)
screenSharingStatus = .sharing
}These IBAction methods map to the UI elements for the RoomViewController:
The doMessageClicked() method is invoked by the messageButton UI button and updates isInputing.
The doMessageInput() method is invoked by the messageTextField UI text field. If the text field is not empty:
- Send the
textusingsend(). - Clear the text field by setting the
stringValueproperty to an empty string.
@IBAction func doMessageClicked(_ sender: NSButton) {
isInputing = !isInputing
}
...
@IBAction func doMessageInput(_ sender: NSTextField) {
let text = sender.stringValue
if !text.isEmpty {
send(text: text)
sender.stringValue = ""
}
}The doMuteVideoClicked() method is invoked by the muteVideoButton UI button and updates videoMuted.
The doMuteAudioClicked() method is invoked by the muteAudioButton UI button and updates audioMuted.
@IBAction func doMuteVideoClicked(_ sender: NSButton) {
videoMuted = !videoMuted
}
@IBAction func doMuteAudioClicked(_ sender: NSButton) {
audioMuted = !audioMuted
}The doShareScreenClicked() method is invoked by the screenSharingButton UI button and updates screenSharingStatus to the value of screenSharingStatus.nextStatus().
The doFilterClicked() method is invoked by the filterButton UI button action and updates isFiltering.
@IBAction func doShareScreenClicked(_ sender: NSButton) {
screenSharingStatus = screenSharingStatus.nextStatus()
}
@IBAction func doFilterClicked(_ sender: NSButton) {
isFiltering = !isFiltering
}The private methods for the RoomViewController are created as functions in a private extension.
private extension RoomViewController {
...
}- Create the configStyle() Method
- Create the updateInterface() Method
- Create Session Methods
- Create the UI Control Methods
They configStyle() method sets the style of the application window.
- Set the window
delegatetoself. - Set the window's
collectionBehaviorproperty to an array containing.fullScreenPrimary. - Set the window minimum, maximum, and current size:
- Set the window's minimum size to
960x600usingCGSize(). - Set the window's maximum size to the largest value possible using
CGFloat.greatestFiniteMagnitudeandCGSize(). - Set the current size to
minSizeusingwindow.setContentSize().
- Set the window's minimum size to
func configStyle(of window: NSWindow) {
window.styleMask.insert([.fullSizeContentView, .miniaturizable])
window.delegate = self
window.collectionBehavior = [.fullScreenPrimary]
let minSize = CGSize(width: 960, height: 600)
window.minSize = minSize
window.maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
window.setContentSize(minSize)
}The updateInterface() method handles layout updates for the video session.
Do one of the following:
- If
sessionsis not empty, continue with the remaining code. - If
sessionsis empty, invokereturn.
func updateInterface(with sessions: [VideoSession]) {
guard !sessions.isEmpty else {
return
}
...
}Update the videoViewLayout properties:
- Initialize a local variable
selfSessiontosessions.first. - Set the
selfViewproperty toselfSession.hostingView. - Set the
selfSizeproperty toselfSession.size. - Initialize a local variable
peerVideoViewsto an empty array. - For each session in
sessions, append the sessio.n'shostingView. - Set the
videoViewsproperty topeerVideoViews - Set the
fullViewproperty todoubleClickFullSession?.hostingView. - Set the
containerViewproperty tocontainerView.
Update the video views using videoViewLayout.layoutVideoViews().
let selfSession = sessions.first!
videoViewLayout.selfView = selfSession.hostingView
videoViewLayout.selfSize = selfSession.size
var peerVideoViews = [VideoView]()
for i in 1..<sessions.count {
peerVideoViews.append(sessions[i].hostingView)
}
videoViewLayout.videoViews = peerVideoViews
videoViewLayout.fullView = doubleClickFullSession?.hostingView
videoViewLayout.containerView = containerView
videoViewLayout.layoutVideoViews()Invoke updateSelfViewVisiable().
If the number of sessions is greater than or equal to 3, set doubleClickEnabled to true. Otherwise, set doubleClickEnabled to false and doubleClickFullSession to nil.
updateSelfViewVisiable()
if sessions.count >= 3 {
doubleClickEnabled = true
} else {
doubleClickEnabled = false
doubleClickFullSession = nil
}The fetchSession() method returns the VideoSession for a specified user. Loop through videoSessions until the session.uid matches the uid.
func fetchSession(of uid: UInt) -> VideoSession? {
for session in videoSessions {
if session.uid == uid {
return session
}
}
return nil
}The videoSession() method returns the VideoSession for the user.
The difference between this method and the fetchSession() method is that if no fetchSession() exists a new VideoSession object is created and appended to videoSessions.
func videoSession(of uid: UInt) -> VideoSession {
if let fetchedSession = fetchSession(of: uid) {
return fetchedSession
} else {
let newSession = VideoSession(uid: uid)
videoSessions.append(newSession)
return newSession
}
}The setVideoMuted() method starts/stops the video for a specified user. The VideoSession is retrieved using fetchSession() to apply muted to the isVideoMuted property.
func setVideoMuted(_ muted: Bool, forUid uid: UInt) {
fetchSession(of: uid)?.isVideoMuted = muted
}The updateSelfViewVisiable() method sets the user view to hidden/not hidden. If the number of videoSessions is 2, determine if the view is hidden using videoMuted and shouldCompressSelfView. Otherwise, hide the view.
func updateSelfViewVisiable() {
guard let selfView = videoSessions.first?.hostingView else {
return
}
if videoSessions.count == 2 {
selfView.isHidden = (videoMuted || shouldCompressSelfView)
} else {
selfView.isHidden = false
}
}The setupWindowListView() method updates the windowListView:
- Allow the width to be resizable using
windowListView.setContentResizingMask(). - Set
IKImageBrowserBackgroundColorKeyto semi-transparent white usingwindowListView.setValue(). - Set the
IKImageBrowserCellsTitleAttributesKey:- Retrieve the old attributes value using the
windowListView.value(). - Create a new local variable
attributresusingoldAttributres.mutableCopy(). - Set the
NSForegroundColorAttributeNameofattributresto white usingattributres.setObject(). - Set the title attributes using
windowListView.setValue().
- Retrieve the old attributes value using the
func setupWindowListView() {
windowListView.setContentResizingMask(Int(NSAutoresizingMaskOptions.viewWidthSizable.rawValue))
windowListView.setValue(NSColor(white: 0, alpha: 0.75), forKey:IKImageBrowserBackgroundColorKey)
let oldAttributres = windowListView.value(forKey: IKImageBrowserCellsTitleAttributesKey) as! NSDictionary
let attributres = oldAttributres.mutableCopy() as! NSMutableDictionary
attributres.setObject(NSColor.white, forKey: NSForegroundColorAttributeName as NSCopying)
windowListView.setValue(attributres, forKey:IKImageBrowserCellsTitleAttributesKey)
}The showWindowList() method shows/hides the windowListView.
If shouldShow is true:
- Invoke
windows.getList()andwindowListView?.reloadData()to update and reload the list data. - Show the
windowListViewby setting theisHiddenproperty tofalse.
If shouldShow is false, hide the windowListView by setting the isHidden property to true.
func showWindowList(_ shouldShow: Bool) {
if shouldShow {
windows.getList()
windowListView?.reloadData()
windowListView?.isHidden = false
} else {
windowListView?.isHidden = true
}
}The alert() method appends an alert message to the chat message box using chatMessageVC?.append().
func alert(string: String) {
guard !string.isEmpty else {
return
}
chatMessageVC?.append(alert: string)
}The windowShouldClose() is a public method required by NSWindowDelegate and is triggered before the window closes.
Invoke leaveChannel() and return false.
extension RoomViewController: NSWindowDelegate {
func windowShouldClose(_ sender: Any) -> Bool {
leaveChannel()
return false
}
}The methods applying the Agora SDK are placed within a private extension for the RoomViewController.
private extension RoomViewController {
...
}- Create the loadAgoraKit() Method
- Create the addLocalSession() Method
- Create the leaveChannel() Method
- Create the Screen Share Methods
- Create the send() Method
- Create the AgoraRtcEngineDelegate
The loadAgoraKit() method initializes the Agora RTC engine using AgoraRtcEngineKit.sharedEngine():
-
Set the channel profile to
.communication, enable video usingagoraKit.enableVideo(), and set thevideoProfileusingagoraKit.setVideoProfile(). -
Invoke
addLocalSession()and start the preview usingagoraKit.startPreview(). -
If
encryptionSecretis not empty, set the encryption usingagoraKit.setEncryptionMode()andagoraKit.setEncryptionSecret(). -
Join the channel
roomNameusingagoraKit.joinChannel():
- If the
codeis equal to0, the channel join is successful. Disable the idle timer usingsetIdleTimerActive. - If the channel join is not successful, display an error message alert using
self.alert().
- Complete the method with
agoraKit.createDataStream()to create a data stream for the joined channel.
func loadAgoraKit() {
agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: KeyCenter.AppId, delegate: self)
agoraKit.setChannelProfile(.communication)
agoraKit.enableVideo()
agoraKit.setVideoProfile(videoProfile, swapWidthAndHeight: false)
addLocalSession()
agoraKit.startPreview()
if let encryptionType = encryptionType, let encryptionSecret = encryptionSecret , !encryptionSecret.isEmpty {
agoraKit.setEncryptionMode(encryptionType.modeString())
agoraKit.setEncryptionSecret(encryptionSecret)
}
let code = agoraKit.joinChannel(byToken: nil, channelId: roomName, info: nil, uid: 0, joinSuccess: nil)
if code != 0 {
DispatchQueue.main.async(execute: {
self.alert(string: "Join channel failed: \(code)")
})
}
agoraKit.createDataStream(&dataChannelId, reliable: true, ordered: true)
}The addLocalSession() method appends the local video session to the videoSessions and sets up the local video view using agoraKit.setupLocalVideo().
If MediaInfo is available for the videoProfile, set the media info property for the local session using localSession.mediaInfo.
func addLocalSession() {
let localSession = VideoSession.localSession()
videoSessions.append(localSession)
agoraKit.setupLocalVideo(localSession.canvas)
if let mediaInfo = MediaInfo(videoProfile: videoProfile) {
localSession.mediaInfo = mediaInfo
}
}The leaveChannel() method enables the user to leave the video session.
- Clear the local video and leave the channel by applying
nilas the parameter foragoraKit.setupLocalVideo()andagoraKit.leaveChannel(). - Stop the video preview using
agoraKit.stopPreview()and setisFilteringtofalse. - Loop through
videoSessionsand remove itshostingViewfrom the superview usingremoveFromSuperview(). - Clear the video sessions array using
videoSessions.removeAll(). - Complete the method by invoking the room to close using
delegate?.roomVCNeedClose().
func leaveChannel() {
agoraKit.setupLocalVideo(nil)
agoraKit.leaveChannel(nil)
agoraKit.stopPreview()
isFiltering = false
for session in videoSessions {
session.hostingView.removeFromSuperview()
}
videoSessions.removeAll()
delegate?.roomVCNeedClose(self)
}The startShareWindow() method starts screen sharing.
Capture the screen specified by the windowId using agoraKit?.startScreenCapture().
Turn on screen share for the first item in the videoSessions using hostingView.switchToScreenShare() if any of the following are true:
windowIdis equal to0window.nameis equal toAgora Video Callwindow.nameis equal toFull Screen
func startShareWindow(_ window: Window) {
let windowId = window.id
agoraKit?.startScreenCapture(UInt(windowId), withCaptureFreq: 15, bitRate: 0, andRect: CGRect.zero )
videoSessions.first?.hostingView.switchToScreenShare(windowId == 0 || window.name == "Agora Video Call" || window.name == "Full Screen")
}The stopShareWindow() method stops screen sharing.
Stop the screen capture the screen specified by using agoraKit?.stopScreenCapture() and turn of screen share for the first item in the videoSessions using hostingView.switchToScreenShare().
func stopShareWindow() {
agoraKit?.stopScreenCapture()
videoSessions.first?.hostingView.switchToScreenShare(false)
}The send() method sends a new message to the stream.
Ensure that the dataChannelId is greater than 0 and that the text.data is valid before applying the following:
- Send the message to the stream using
agoraKit.sendStreamMessage(). - Append the message to the chat message view using
chatMessageVC?.append().
func send(text: String) {
if dataChannelId > 0, let data = text.data(using: String.Encoding.utf8) {
agoraKit.sendStreamMessage(dataChannelId, data: data)
chatMessageVC?.append(chat: text, fromUid: 0)
}
}The AgoraRtcEngineDelegate methods are added through an extension for the RoomViewController.
extension RoomViewController: AgoraRtcEngineDelegate {
...
}- Create the rtcEngine Connection Methods
- Create the errorCode Event Listener
- Create the firstRemoteVideoDecodedOfUid Event Listener
- Create the firstLocalVideoFrameWith Event Listener
- Create the didOfflineOfUid Event Listener
- Create the didVideoMuted Event Listener
- Create the remoteVideoStats Event Listener
- Create the Device Changed Event Listener
- Create the receiveStreamMessageFromUid Event Listener
- Create the didOccurStreamMessageErrorFromUid Event Listener
The rtcEngineConnectionDidInterrupted() method displays an alert with the error message Connection Interrupted.
The rtcEngineConnectionDidLost() method displays an alert with the error message Connection Lost.
func rtcEngineConnectionDidInterrupted(_ engine: AgoraRtcEngineKit) {
alert(string: "Connection Interrupted")
}
func rtcEngineConnectionDidLost(_ engine: AgoraRtcEngineKit) {
alert(string: "Connection Lost")
}The didOccurError event listener is triggered when the Agora RTC engine generates an error.
Display an alert with the error code value errorCode.rawValue.
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
alert(string: "errorCode \(errorCode.rawValue)")
}The firstRemoteVideoDecodedOfUid event listener is triggered when the first remote video is decoded.
-
Retrieve the video session of the user using the
videoSession(). -
Set the session dimensions using the
userSession.sizeand update the media info using theuserSession.updateMediaInfo(). -
Complete the method by setting up the remote video using
agoraKit.setupRemoteVideo().
func rtcEngine(_ engine: AgoraRtcEngineKit, firstRemoteVideoDecodedOfUid uid: UInt, size: CGSize, elapsed: Int) {
let userSession = videoSession(of: uid)
let sie = size.fixedSize()
userSession.size = sie
userSession.updateMediaInfo(resolution: size)
agoraKit.setupRemoteVideo(userSession.canvas)
}The firstLocalVideoFrameWith event listener is triggered when the first local video frame has elapsed.
Ensure that selfSession is the first item in the videoSessions before applying the following:
- Set the dimensions of the video session using
selfSession.size. - Update the video interface using
updateInterface().
// first local video frame
func rtcEngine(_ engine: AgoraRtcEngineKit, firstLocalVideoFrameWith size: CGSize, elapsed: Int) {
if let selfSession = videoSessions.first {
selfSession.size = size.fixedSize()
updateInterface(with: videoSessions)
}
}The didOfflineOfUid is triggered when a user goes offline.
Loop through the videoSessions to retrieve the video session of the offline user:
- If the video session is found, remove the session
hostingViewfrom the superview usingremoveFromSuperview().. - If the offline user session is
doubleClickFullSession, setdoubleClickFullSessiontonil.
// user offline
func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
var indexToDelete: Int?
for (index, session) in videoSessions.enumerated() {
if session.uid == uid {
indexToDelete = index
}
}
if let indexToDelete = indexToDelete {
let deletedSession = videoSessions.remove(at: indexToDelete)
deletedSession.hostingView.removeFromSuperview()
if let doubleClickFullSession = doubleClickFullSession , doubleClickFullSession == deletedSession {
self.doubleClickFullSession = nil
}
}
}The didVideoMuted is triggered when a user turns off video.
Set the video to off using setVideoMuted().
// video muted
func rtcEngine(_ engine: AgoraRtcEngineKit, didVideoMuted muted: Bool, byUid uid: UInt) {
setVideoMuted(muted, forUid: uid)
}The remoteVideoStats event is triggered when a metric changes for the Agora RTC engine.
Retrieve the video session for the user using fetchSession() and update the resolution, height, and fps using session.updateMediaInfo().
//remote stat
func rtcEngine(_ engine: AgoraRtcEngineKit, remoteVideoStats stats: AgoraRtcRemoteVideoStats) {
if let session = fetchSession(of: stats.uid) {
session.updateMediaInfo(resolution: CGSize(width: CGFloat(stats.width), height: CGFloat(stats.height)), bitRate: Int(stats.receivedBitrate), fps: Int(stats.receivedFrameRate))
}
}The device changed event listener is triggered when the device changes.
Set a device notification with the DeviceListChangeNotificationKey and the device type using the NotificationCenter.default.post().
func rtcEngine(_ engine: AgoraRtcEngineKit, device deviceId: String, type deviceType: AgoraMediaDeviceType, stateChanged state: Int) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: DeviceListChangeNotificationKey), object: NSNumber(value: deviceType.rawValue))
}
The receiveStreamMessageFromUid is triggered when a message is received from a user.
The method checks that the message string is not empty before appending it to the chat message view using chatMessageVC?.append().
//data channel
func rtcEngine(_ engine: AgoraRtcEngineKit, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) {
guard let string = String(data: data, encoding: String.Encoding.utf8) , !string.isEmpty else {
return
}
chatMessageVC?.append(chat: string, fromUid: Int64(uid))
}The didOccurStreamMessageErrorFromUid is triggered when a user message error occurs and then displays the error using chatMessageVC?.append().
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurStreamMessageErrorFromUid uid: UInt, streamId: Int, error: Int, missed: Int, cached: Int) {
chatMessageVC?.append(alert: "Data channel error: \(error)")
}ChatMessageViewController.swift defines and connects application functionality with the ChatMessageViewController UI.
- Add Global Variables and Superclass Overrides
- Create append() Methods
- Create the UITableViewDataSource Object
The ChatMessageViewController defines the IBOutlet variable messageTableView, which maps to the table created in the ChatMessageViewController UI.
Initialize the private variable messageList to manage the array of messages for the chat.
import Cocoa
class ChatMessageViewController: NSViewController {
@IBOutlet weak var messageTableView: NSTableView!
fileprivate var messageList = [Message]()
...
}The append() methods are used to add messages and alerts to the message window.
-
The
append()method for achatcreates a newMessageobject of type.chatand invokes theappend()method for themessage. -
The
append()method for analertcreates a newMessageobject of type.alertand invokes theappend()method formessage.
func append(chat text: String, fromUid uid: Int64) {
let message = Message(text: text, type: .chat)
append(message: message)
}
func append(alert text: String) {
let message = Message(text: text, type: .alert)
append(message: message)
}The append() method for a message is created in an extension for the ChatMessageViewController.
The message is added to the messageList.
When the messageList contains more than 20 messages, delete the first message in the array using updateMessageTable().
private extension ChatMessageViewController {
func append(message: Message) {
messageList.append(message)
var deleted: Message?
if messageList.count > 20 {
deleted = messageList.removeFirst()
}
updateMessageTable(withDeleted: deleted)
}
...
}The updateMessageTable() method is a helper method to handle messages for the chat view.
-
Check that the
messageTableViewexists. If it does not exist, stop the method usingreturn. -
If
deletedis equal tonil, remove the first message usingtableView.removeRows(). -
Retrieve the
IndexSetfor the last message by usingmessageList.count - 1. -
Add the new message to the table using
tableView.insertRows(). -
Display the last message on the screen using
tableView.scrollRowToVisible().
func updateMessageTable(withDeleted deleted: Message?) {
guard let tableView = messageTableView else {
return
}
if deleted != nil {
tableView.removeRows(at: IndexSet(integer: 0), withAnimation: NSTableViewAnimationOptions())
}
let lastRow = messageList.count - 1
tableView.insertRows(at: IndexSet(integer: lastRow), withAnimation: NSTableViewAnimationOptions())
tableView.scrollRowToVisible(lastRow)
}The tableView data source method is defined in an extension to the ChatMessageViewController.
Return a messageList.count as the number of rows in the table section.
extension ChatMessageViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return messageList.count
}
}The tableView delegate methods are defined in an extension to the ChatMessageViewController.
Retrieve each cell for the table:
-
Create the table cell using
tableView.make(). -
Set the cell
messageusingcell.setand return the resulting cell.
extension ChatMessageViewController: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cell = tableView.make(withIdentifier: "messageCell", owner: self) as! ChatMessageCellView
let message = messageList[row]
cell.set(with: message)
return cell
}
...
}Set the height for each cell in the table:
- Initialize a local
defaultHeightvariable to24. - Retrieve the text for the current row using
messageList[row].text. - Retrieve the first column of the table using
tableView.tableColumns.first. - Initialize a local
widthvariable tocolumn.width - 24. - Create a bounding rectangle for the text using
string.boundingRect(). - Set the text height using
textRect.height + 6and ensure that the result is at leastdefaultHeight. - Return the resulting
textHeight.
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
let defaultHeight: CGFloat = 24
let string: NSString = messageList[row].text as NSString
let column = tableView.tableColumns.first!
let width = column.width - 24
let textRect = string.boundingRect(with: NSMakeSize(width, 0), options: [.usesLineFragmentOrigin], attributes: [NSFontAttributeName: NSFont.systemFont(ofSize: 12)])
var textHeight = textRect.height + 6
if textHeight < defaultHeight {
textHeight = defaultHeight;
}
return textHeight;
}DevicesViewController.swift defines and connects application functionality with the DevicesViewController UI.
The DevicesViewController class has two global variables that serve as notification key constants.
The remaining code in this section is contained within the NSViewController declaration.
import Cocoa
let DeviceListChangeNotificationKey = "io.agora.deviceListChangeNotification"
let VolumeChangeNotificationKey = "io.agora.volumeChangeNotification"
class DevicesViewController: NSViewController {
...
}- Define IBOutlet Variables
- Define Public and Private Variables
- Create Superclass Override Methods
- Create IBAction Methods
The DevicesViewController class defines a set of UI elements for input device, output device, and camera controls using IBOutlet variables.
| Input Device Variable | Description |
|---|---|
inputDevicePopUpButton |
Dropdown menu for the list of available input devices |
inputDeviceVolSlider |
Volume control for the selected input device |
intputDeviceTestButton |
Button to test the selected input device |
inputDeviceVolLevelIndicator |
Volume level indicator for the selected input device |
@IBOutlet weak var inputDevicePopUpButton: NSPopUpButton!
@IBOutlet weak var inputDeviceVolSlider: NSSlider!
@IBOutlet weak var intputDeviceTestButton: NSButton!
@IBOutlet weak var inputDeviceVolLevelIndicator: NSLevelIndicator!| Output Device Variable | Description |
|---|---|
outputDevicePopUpButton |
Dropdown menu for the list of available output devices |
outputDeviceVolSlider |
Volume control for the selected output device |
outputDeviceTestButton |
Button to test the selected output device |
@IBOutlet weak var outputDevicePopUpButton: NSPopUpButton!
@IBOutlet weak var outputDeviceVolSlider: NSSlider!
@IBOutlet weak var outputDeviceTestButton: NSButton!| Output Device Variable | Description |
|---|---|
cameraPopUpButton |
Dropdown menu for the list of available camera devices |
cameraTestButton |
Button to test the selected camera device |
cameraPreviewView |
View to display the video from the selected camera device |
@IBOutlet weak var cameraPopUpButton: NSPopUpButton!
@IBOutlet weak var cameraTestButton: NSButton!
@IBOutlet weak var cameraPreviewView: NSView!The DevicesViewController class has two public variables and many private variables.
-
The
agoraKitvariable is the Agora RTC engine, which connects the sample application to the Agora SDK. -
The
couldTestvariable is set totrueas a default and acts as the indicator if the device can be tested.
var agoraKit: AgoraRtcEngineKit!
var couldTest = trueDeclare a set of private variables for the recording, playout, and capture devices.
| Variable | Description |
|---|---|
recordingDeviceId |
ID of the current recording device |
recordingDevices |
Array of recording devices |
playoutDeviceId |
ID of the current playout device |
playoutDevices |
Array of playout devices |
captureDeviceId |
ID of the current capture device |
captureDevices |
Array of capture devices |
fileprivate var recordingDeviceId: String?
fileprivate var recordingDevices = [AgoraRtcDeviceInfo]()
fileprivate var playoutDeviceId: String?
fileprivate var playoutDevices = [AgoraRtcDeviceInfo]()
fileprivate var captureDeviceId: String?
fileprivate var captureDevices = [AgoraRtcDeviceInfo]()Declare a set of private variables that apply changes to the sample application using didSet.
The isInputTesting variable is set to false as a default. When the value changes:
- Change the configuration of
intputDeviceTestButtonusingconfig(). - If
isInputTestingistrue, start the recording device test usingagoraKit?.startRecordingDeviceTest(), Otherwise, stop the test usingagoraKit?.stopRecordingDeviceTest(). - Display/hide
inputDeviceVolLevelIndicatorby applyingisInputTestingto theisHiddenproperty.
fileprivate var isInputTesting = false {
didSet {
config(button: intputDeviceTestButton, isTesting: isInputTesting)
if isInputTesting {
agoraKit?.startRecordingDeviceTest(200)
} else {
agoraKit?.stopRecordingDeviceTest()
}
inputDeviceVolLevelIndicator?.isHidden = !isInputTesting
}
}The isOutputTesting variable is set to false as a default. When the value changes:
-
Change the configuration of the
outputDeviceTestButtonusingconfig(). -
If
isOutputTestingistrue, start the playback device test usingagoraKit?.startPlaybackDeviceTest(). Otherwise, stop the test usingagoraKit?.stopPlaybackDeviceTest().Note: Ensure that the
pathfor the test audio asset is valid before invoking theagoraKit?.startPlaybackDeviceTest(). -
Display/hide the
inputDeviceVolLevelIndicatorby applyingisInputTestingto theisHiddenproperty.
fileprivate var isOutputTesting = false {
didSet {
config(button: outputDeviceTestButton, isTesting: isOutputTesting)
if isOutputTesting {
if let path = Bundle.main.path(forResource: "test", ofType: "wav") {
agoraKit?.startPlaybackDeviceTest(path)
}
} else {
agoraKit?.stopPlaybackDeviceTest()
}
}
}The isCameraputTesting variable is set to false as a default. When the value changes:
-
Change the configuration of the
cameraTestButtonusingconfig(). -
If
isCameraputTestingistrue, ensure that theviewfor the video preview is valid and start the playback device test usingagoraKit?.startCaptureDeviceTest(). Otherwise, stop the test usingagoraKit?.stopCaptureDeviceTest().
fileprivate var isCameraputTesting = false {
didSet {
config(button: cameraTestButton, isTesting: isCameraputTesting)
if isCameraputTesting {
if let view = cameraPreviewView {
agoraKit?.startCaptureDeviceTest(view)
}
} else {
agoraKit?.stopCaptureDeviceTest()
}
}
}The deviceVolume variable is set to 0 as a default. When the value changes, set the inputDeviceVolLevelIndicator?.integerValue to deviceVolume.
fileprivate var deviceVolume = 0 {
didSet {
inputDeviceVolLevelIndicator?.integerValue = deviceVolume
}
}The methods in this section override the NSViewController superclass methods that are invoked when changes to the view occur.
The viewDidLoad() method is triggered when the view loads into the sample application.
- Update the
wantsLayerproperty of theviewtotrueand the layer'sbackgroundColorproperty to white. - Update the
wantsLayerproperty of thecameraPreviewViewtotrueand the layer'sbackgroundColorproperty to black. - Update the button style configurations using
configButtonStyle(). - Load the devices using
loadDevices().
override func viewDidLoad() {
super.viewDidLoad()
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.white.cgColor
cameraPreviewView.wantsLayer = true
cameraPreviewView.layer?.backgroundColor = NSColor.black.cgColor
configButtonStyle()
loadDevices()
}The viewWillAppear() method is triggered when the view appears on the screen.
Set the configuration style of view.window using configStyle().
override func viewWillAppear() {
super.viewWillAppear()
configStyle(of: view.window!)
}The viewWillDisappear() method is triggered the view is hidden from the screen.
If couldTest is true:
- Set
isInputTestingtofalseifisInputTestingis valid. - Set
isOutputTestingtofalseifisOutputTestingis valid. - Set
isCameraputTestingtofalseifisCameraputTestingis valid.
override func viewWillDisappear() {
super.viewWillDisappear()
if couldTest {
if isInputTesting {
isInputTesting = false
}
if isOutputTesting {
isOutputTesting = false
}
if isCameraputTesting {
isCameraputTesting = false
}
}
}- Create Input/Output Device Change Methods
- Create Input/Output Test Methods
- Create Input/Output Volume Change Methods
- Create Camera Methods
The doInputDeviceChanged() method is applied to the input device dropdown menu created in the DevicesViewController UI.
- If
isInputTestingistrue, set the value tofalse. - Retrieve the
deviceIdusing theindexOfSelectedItemproperty of therecordingDevicesdropdown menu and set the selected device usingagoraKit.setDevice().
@IBAction func doInputDeviceChanged(_ sender: NSPopUpButton) {
if isInputTesting {
isInputTesting = false
}
if let deviceId = recordingDevices[sender.indexOfSelectedItem].deviceId {
agoraKit.setDevice(.audioRecording, deviceId: deviceId)
}
}The doOutputDeviceChanged() method is applied to the output device dropdown menu created in the DevicesViewController UI.
- If
isOutputTestingistrue, set the value tofalse. - Retrieve the
deviceIdusing theindexOfSelectedItemproperty of theplayoutDevicesdropdown menu and set the selected device usingagoraKit.setDevice().
@IBAction func doOutputDeviceChanged(_ sender: NSPopUpButton) {
if isOutputTesting {
isOutputTesting = false
}
if let deviceId = playoutDevices[sender.indexOfSelectedItem].deviceId {
agoraKit.setDevice(.audioPlayout, deviceId: deviceId)
}
}The doInputDeviceChanged() method is applied to the input device Test button created in the DevicesViewController UI.
Update the value of isInputTesting.
@IBAction func doInputDeviceTestClicked(_ sender: NSButton) {
isInputTesting = !isInputTesting
}The doInputDeviceChanged() method is applied to the output device Test button created in the DevicesViewController UI.
Update the value of isOutputTesting.
@IBAction func doOutputDeviceTestClicked(_ sender: NSButton) {
isOutputTesting = !isOutputTesting
}The doInputDeviceChanged() method is applied to the input device Volume slider created in the DevicesViewController UI.
Retrieve the input volume using the sender.intValue and set the device volume using agoraKit.setDeviceVolume().
@IBAction func doInputVolSliderChanged(_ sender: NSSlider) {
let vol = sender.intValue
agoraKit.setDeviceVolume(.audioRecording, volume: vol)
}The doOutputVolSliderChanged() method is applied to the output device Volume slider created in the DevicesViewController UI.
Retrieve the output volume using the sender.intValue and set the device volume using agoraKit.setDeviceVolume().
@IBAction func doOutputVolSliderChanged(_ sender: NSSlider) {
let vol = sender.intValue
agoraKit.setDeviceVolume(.audioPlayout, volume: vol)
}The doCameraChanged() method is applied to the camera device dropdown menu created in the DevicesViewController UI.
- If
isCameraputTestingistrue, set the value tofalse. - Retrieve the
deviceIdusing theindexOfSelectedItemproperty of thecaptureDevicesdropdown menu and set the selected device usingagoraKit.setDevice().
@IBAction func doCameraChanged(_ sender: NSPopUpButton) {
if isCameraputTesting {
isCameraputTesting = false
}
if let deviceId = captureDevices[sender.indexOfSelectedItem].deviceId {
agoraKit.setDevice(.videoCapture, deviceId: deviceId)
}
}The doCameraTestClicked() method is applied to the camera device Test button created in the DevicesViewController UI.
Update the value of isCameraputTesting.
@IBAction func doCameraTestClicked(_ sender: NSButton) {
isCameraputTesting = !isCameraputTesting
}The configuration private methods for DevicesViewController are contained within two sets of extensions.
private extension DevicesViewController {
...
}
private extension DevicesViewController {
...
}The first extension contains methods that set the configuration and styles of the UI elements.
The configStyle() method configures the style of the window.
Insert the full-sized content view to the style mask using styleMask.insert().
Configure the window properties.
window Property |
Value | Description |
|---|---|---|
titlebarAppearsTransparent |
true |
Makes the window's title bar transparent |
isMovableByWindowBackground |
true |
Enables the window to move by dragging on its background |
minSize |
CGSize(width: 600, height: 600) |
Minimum size of the window |
maxSize |
CGSize(width: 600, height: 600) |
Maximum size of the window |
func configStyle(of window: NSWindow) {
window.styleMask.insert(.fullSizeContentView)
window.titlebarAppearsTransparent = true
window.isMovableByWindowBackground = true
window.minSize = CGSize(width: 600, height: 600)
window.maxSize = CGSize(width: 600, height: 600)
}The configButtonStyle() method configures the style of the buttons.
- Set the
intputDeviceTestButton,outputDeviceTestButton, andcameraTestButtonbuttons to non-testing mode usingconfig(). - Set the
intputDeviceTestButton,outputDeviceTestButton, andcameraTestButtonbuttons to hidden/not hidden using theisHiddenproperty.
func configButtonStyle() {
config(button: intputDeviceTestButton, isTesting: false)
config(button: outputDeviceTestButton, isTesting: false)
config(button: cameraTestButton, isTesting: false)
intputDeviceTestButton.isHidden = !couldTest
outputDeviceTestButton.isHidden = !couldTest
cameraTestButton.isHidden = !couldTest
}The config() method configures the title of a button using the title property.
If isTesting is true, set the title to Stop Test. Otherwise, set it to Test.
func config(button: NSButton, isTesting: Bool) {
button.title = isTesting ? "Stop Test" : "Test"
}The second extension contains methods that load and update the UI elements.
The loadDevices() method loads the selected devices for testing.
Load the playout, recording, and video capture devices using loadDevice().
Create a notification observer for the DeviceListChangeNotificationKey using NotificationCenter.default.addObserver(). When the event listener is triggered, verify that the notify.object is a number and that its type is valid before loading the device using self?.loadDevice().
If couldTest is true, create a notification observer for the VolumeChangeNotificationKey using NotificationCenter.default.addObserver(). When the event listener is triggered, verify that the notify.object is a number and set the device volume using self?.deviceVolume.
func loadDevices() {
loadDevice(of: .audioPlayout)
loadDevice(of: .audioRecording)
loadDevice(of: .videoCapture)
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: DeviceListChangeNotificationKey), object: nil, queue: nil) { [weak self] (notify) in
if let obj = notify.object as? NSNumber, let type = AgoraMediaDeviceType(rawValue: obj.intValue) {
self?.loadDevice(of: type)
}
}
if couldTest {
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: VolumeChangeNotificationKey), object: nil, queue: nil, using: { [weak self] (notify) in
if let obj = notify.object as? NSNumber {
self?.deviceVolume = obj.intValue
}
})
}
}The loadDevice() method loads a selected device.
Ensure that the device type is a valid device type before continuing with the remaining actions in the method.
Retrieve the device ID from the type using the agoraKit.getDeviceId() and apply changes to one of the following device types based on the value of type:
| Value | Description |
|---|---|
.audioRecording |
Recording device |
.audioPlayout |
Playout device |
.videoCapture |
video capture device |
- Set the devices array with the value of
devices. - Set the device ID with the value of
deviceId. - Update the dropdown menu for the device using
updatePopUpButton().
Complete the method by updating the volume using updateVolume().
func loadDevice(of type: AgoraMediaDeviceType) {
guard let devices = agoraKit.enumerateDevices(type)! as NSArray as? [AgoraRtcDeviceInfo] else {
return
}
let deviceId = agoraKit.getDeviceId(type)
switch type {
case .audioRecording:
recordingDevices = devices
recordingDeviceId = deviceId
updatePopUpButton(inputDevicePopUpButton, withValue: deviceId, inValueList: devices)
case .audioPlayout:
playoutDevices = devices
playoutDeviceId = deviceId
updatePopUpButton(outputDevicePopUpButton, withValue: deviceId, inValueList: devices)
case .videoCapture:
captureDevices = devices
captureDeviceId = deviceId
updatePopUpButton(cameraPopUpButton, withValue: deviceId, inValueList: devices)
default:
break
}
updateVolume(of: type)
}The updatePopUpButton() method updates the contents of the popup button.
- Clear the contents of the button using
button.removeAllItems(). - Iterate through the supplied
AgoraRtcDeviceInfoarray usinglist.map()and add eachinfo.deviceNameusingbutton.addItems(). - Iterate through the
listand add eachinfo.deviceIdtodeviceIds. - Verify that the
valueis not null and that thedeviceIds.index()is valid, then set the selected item with theindexusing thebutton.selectItem ().
func updatePopUpButton(_ button: NSPopUpButton, withValue value: String?, inValueList list: [AgoraRtcDeviceInfo]) {
button.removeAllItems()
button.addItems(withTitles: list.map({ (info) -> String in
return info.deviceName!
}))
let deviceIds = list.map { (info) -> String in
return info.deviceId!
}
if let value = value, let index = deviceIds.index(of: value) {
button.selectItem(at: index)
}
}The updateVolume() method updates the volume of one of the following devices, based on the value of the type:
| Type | UI Element name |
|---|---|
.audioRecording |
inputDeviceVolSlider |
.audioPlayout |
outputDeviceVolSlider |
Retrieve the volume using agoraKit.getDeviceVolume() and set the volume level to vol using the intValue property.
func updateVolume(of type: AgoraMediaDeviceType) {
switch type {
case .audioRecording:
let vol = agoraKit.getDeviceVolume(type)
inputDeviceVolSlider.intValue = vol
case .audioPlayout:
let vol = agoraKit.getDeviceVolume(type)
outputDeviceVolSlider.intValue = vol
default:
return
}
}SettingsViewController.swift defines and connects application functionality with the SettingsViewController UI.
The settingsVC() protocol method is used by external classes to update the video profile.
import Cocoa
protocol SettingsVCDelegate: class {
func settingsVC(_ settingsVC: SettingsViewController, closeWithProfile videoProfile: AgoraVideoProfile)
}| Variable | Description |
|---|---|
profilePopUpButton |
IBOutlet variable. Maps to the profile popup button created in the SettingsViewController UI. |
videoProfile |
Agora video profile |
delegate |
Optional SettingsVCDelegate object |
class SettingsViewController: NSViewController {
@IBOutlet weak var profilePopUpButton: NSPopUpButton!
var videoProfile: AgoraVideoProfile!
var delegate: SettingsVCDelegate?
...
}The viewDidLoad() method is invoked when the application loads the view.
Set view.wantsLayer to true and the view layer background color to NSColor.white.cgColor.
override func viewDidLoad() {
super.viewDidLoad()
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.white.cgColor
loadProfileItems()
}The private doProfileChanged() method sets the videoProfile with AgoraVideoProfile objects and is initialized with AgoraVideoProfile.validProfileList().
@IBAction func doProfileChanged(_ sender: NSPopUpButton) {
let profile = AgoraVideoProfile.validProfileList()[sender.indexOfSelectedItem]
videoProfile = profile
}The doConfirmClicked() IBAction method is invoked by the Confirm button in the UI layout. This method updates the video profile by invoking delegate?.settingsVC().
@IBAction func doConfirmClicked(_ sender: NSButton) {
delegate?.settingsVC(self, closeWithProfile: videoProfile)
}The loadProfileItems() method is set within a private extension and populates the profilePopUpButton UI object with an array of AgoraVideoProfile objects.
Loop through the items in the AgoraVideoProfile.validProfileList() and add items to the UI using profilePopUpButton.addItems().
Select a default item using profilePopUpButton.selectItem().
private extension SettingsViewController {
func loadProfileItems() {
profilePopUpButton.addItems(withTitles: AgoraVideoProfile.validProfileList().map { (res) -> String in
return res.description()
})
profilePopUpButton.selectItem(withTitle: videoProfile.description())
}
}- 1 to 1 Video Tutorial for MacOS/Swift
- Agora Video SDK samples are also available for the following platforms:
- OpenVideoCall for iOS (Swift)
- OpenVideoCall for iOS (Objective-C)
- OpenVideoCall for Android
- OpenVideoCall for Windows
- OpenVideoCall for MacOS
This software is licensed under the MIT License (MIT). View the license.











