- 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.framework
libresolv.tbdroomview-
CoreWLAN.framework`CoreAudio.framework
CoreMedia.framework
AudioToolbox.framework
AgoraRtcEngineKit.framework
VideoToolbox.framework
AVFoundation.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
videoProfile
variable is initialized with the default Agora video profile usingAgoraVideoProfile.defaultProfile()
. - The
agoraKit
private variable is declared as anAgoraRtcEngineKit
object and represents the Agora RTC engine. - The
encryptionType
private 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
wantsLayer
property totrue
. - Set the view layer's
backgroundColor
color 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.videoProfile
to the currentvideoProfile
. - Set
settingsVC.delegate
toself
.
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.roomName
tosender
. - Set
roomVC.encryptionSecret
to the text entered in theencryptionTextField
. - Set
roomVC.encryptionType
to the currentencryptionType
. - Set
roomVC.videoProfile
to the currentvideoProfile
. - Set
roomVC.delegate
toself
.
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.agoraKit
toagoraKit
. - Set
devicesVC.couldTest
totrue
.
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
window
is valid, apply the remaining code. - If
window
is 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.fullScreen
option. - Add two additional options
.fullSizeContentView
and.miniaturizable
to 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
size
usingCGSize()
. - Set the
minSize
and themaxSize
tosize
and 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 = size
The 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.stringValue
tolegalString
, 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
isHidden
property of thebuttonContainerView
,messageTableContainerView
, androomNameLabel
to the new value ofshouldHideFlowViews
. -
If the screen sharing status is
.list
mode, set thescreenSharingStatus
to.none
. -
Manage the
messageTextField
and themessageInputerView
based onshouldHideFlowViews
.- If
shouldHideFlowViews
istrue
, remove the focus from themessageTextField
using theresignFirstResponder()
and hide themessageInputerView
by setting itsisHidden
property totrue
. - If
shouldHideFlowViews
isfalse
, set the focus to themessageTextField
using thebecomeFirstResponder()
and show themessageInputerView
by setting itsisHidden
property 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
muteVideoButton
image 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
screenSharingButton
image is updated. - If the old value is
.sharing
, the screen share stopsstopShareWindow()
. - if
screenSharingStatus
is 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
agoraKit
is verified. - If filtering is enabled, set the video preprocessing using
AGVideoPreProcessing.registerVideoPreprocessing()
and update thefilterButton
with the blue image. - If filtering is not enabled, unregister the video preprocessing using
AGVideoPreProcessing.deregisterVideoPreprocessing()
and update thefilterButton
with 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
messageTextField
is activated/deactivated usingbecomeFirstResponder()
/resignFirstResponder()
. - The
messageInputerView
is hidden/unhidden.
- The
-
The
messageButton
image 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
roomNameLabel
text toroomName
. - Set
messageInputerView.wantsLayer
totrue
. - Set the
messageInputerView
layer background color to semi-transparent black and set thecornerRadius
property 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
text
usingsend()
. - Clear the text field by setting the
stringValue
property 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
delegate
toself
. - Set the window's
collectionBehavior
property to an array containing.fullScreenPrimary
. - Set the window minimum, maximum, and current size:
- Set the window's minimum size to
960
x600
usingCGSize()
. - Set the window's maximum size to the largest value possible using
CGFloat.greatestFiniteMagnitude
andCGSize()
. - Set the current size to
minSize
usingwindow.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
sessions
is not empty, continue with the remaining code. - If
sessions
is empty, invokereturn
.
func updateInterface(with sessions: [VideoSession]) {
guard !sessions.isEmpty else {
return
}
...
}
Update the videoViewLayout
properties:
- Initialize a local variable
selfSession
tosessions.first
. - Set the
selfView
property toselfSession.hostingView
. - Set the
selfSize
property toselfSession.size
. - Initialize a local variable
peerVideoViews
to an empty array. - For each session in
sessions
, append the sessio.n'shostingView
. - Set the
videoViews
property topeerVideoViews
- Set the
fullView
property todoubleClickFullSession?.hostingView
. - Set the
containerView
property 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
IKImageBrowserBackgroundColorKey
to semi-transparent white usingwindowListView.setValue()
. - Set the
IKImageBrowserCellsTitleAttributesKey
:- Retrieve the old attributes value using the
windowListView.value()
. - Create a new local variable
attributres
usingoldAttributres.mutableCopy()
. - Set the
NSForegroundColorAttributeName
ofattributres
to 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
windowListView
by setting theisHidden
property 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 thevideoProfile
usingagoraKit.setVideoProfile()
. -
Invoke
addLocalSession()
and start the preview usingagoraKit.startPreview()
. -
If
encryptionSecret
is not empty, set the encryption usingagoraKit.setEncryptionMode()
andagoraKit.setEncryptionSecret()
. -
Join the channel
roomName
usingagoraKit.joinChannel()
:
- If the
code
is 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
nil
as the parameter foragoraKit.setupLocalVideo()
andagoraKit.leaveChannel()
. - Stop the video preview using
agoraKit.stopPreview()
and setisFiltering
tofalse
. - Loop through
videoSessions
and remove itshostingView
from 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
:
windowId
is equal to0
window.name
is equal toAgora Video Call
window.name
is 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.size
and 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
hostingView
from the superview usingremoveFromSuperview()
.. - If the offline user session is
doubleClickFullSession
, setdoubleClickFullSession
tonil
.
// 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 achat
creates a newMessage
object of type.chat
and invokes theappend()
method for themessage
. -
The
append()
method for analert
creates a newMessage
object of type.alert
and 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
messageTableView
exists. If it does not exist, stop the method usingreturn
. -
If
deleted
is equal tonil
, remove the first message usingtableView.removeRows()
. -
Retrieve the
IndexSet
for 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
message
usingcell.set
and 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
defaultHeight
variable 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
width
variable tocolumn.width - 24
. - Create a bounding rectangle for the text using
string.boundingRect()
. - Set the text height using
textRect.height + 6
and 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
agoraKit
variable is the Agora RTC engine, which connects the sample application to the Agora SDK. -
The
couldTest
variable is set totrue
as a default and acts as the indicator if the device can be tested.
var agoraKit: AgoraRtcEngineKit!
var couldTest = true
Declare 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
intputDeviceTestButton
usingconfig()
. - If
isInputTesting
istrue
, start the recording device test usingagoraKit?.startRecordingDeviceTest()
, Otherwise, stop the test usingagoraKit?.stopRecordingDeviceTest()
. - Display/hide
inputDeviceVolLevelIndicator
by applyingisInputTesting
to theisHidden
property.
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
outputDeviceTestButton
usingconfig()
. -
If
isOutputTesting
istrue
, start the playback device test usingagoraKit?.startPlaybackDeviceTest()
. Otherwise, stop the test usingagoraKit?.stopPlaybackDeviceTest().
Note: Ensure that the
path
for the test audio asset is valid before invoking theagoraKit?.startPlaybackDeviceTest()
. -
Display/hide the
inputDeviceVolLevelIndicator
by applyingisInputTesting
to theisHidden
property.
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
cameraTestButton
usingconfig()
. -
If
isCameraputTesting
istrue
, ensure that theview
for 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
wantsLayer
property of theview
totrue
and the layer'sbackgroundColor
property to white. - Update the
wantsLayer
property of thecameraPreviewView
totrue
and the layer'sbackgroundColor
property 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
isInputTesting
tofalse
ifisInputTesting
is valid. - Set
isOutputTesting
tofalse
ifisOutputTesting
is valid. - Set
isCameraputTesting
tofalse
ifisCameraputTesting
is 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
isInputTesting
istrue
, set the value tofalse
. - Retrieve the
deviceId
using theindexOfSelectedItem
property of therecordingDevices
dropdown 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
isOutputTesting
istrue
, set the value tofalse
. - Retrieve the
deviceId
using theindexOfSelectedItem
property of theplayoutDevices
dropdown 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
isCameraputTesting
istrue
, set the value tofalse
. - Retrieve the
deviceId
using theindexOfSelectedItem
property of thecaptureDevices
dropdown 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
, andcameraTestButton
buttons to non-testing mode usingconfig()
. - Set the
intputDeviceTestButton
,outputDeviceTestButton
, andcameraTestButton
buttons to hidden/not hidden using theisHidden
property.
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
AgoraRtcDeviceInfo
array usinglist.map()
and add eachinfo.deviceName
usingbutton.addItems()
. - Iterate through the
list
and add eachinfo.deviceId
todeviceIds
. - Verify that the
value
is not null and that thedeviceIds.index()
is valid, then set the selected item with theindex
using 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())
}
}