diff --git a/.AgoraUIKit_macOS.podspec b/.AgoraUIKit_macOS.podspec index 2351d0e9..85921e3f 100644 --- a/.AgoraUIKit_macOS.podspec +++ b/.AgoraUIKit_macOS.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'AgoraUIKit_macOS' - s.version = '1.6.4' + s.version = '1.6.8' s.summary = 'Agora video session AppKit template.' s.description = <<-DESC diff --git a/AgoraUIKit_iOS.podspec b/AgoraUIKit_iOS.podspec index a2c375c3..12b723d8 100644 --- a/AgoraUIKit_iOS.podspec +++ b/AgoraUIKit_iOS.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'AgoraUIKit_iOS' - s.version = '4.0.0-preview.4' + s.version = '4.0.0-preview.5' s.summary = 'Agora video session UIKit template.' s.description = <<-DESC diff --git a/Sources/Agora-UIKit/AgoraRtmController+Helpers.swift b/Sources/Agora-UIKit/AgoraRtmController+Helpers.swift new file mode 100644 index 00000000..415666a8 --- /dev/null +++ b/Sources/Agora-UIKit/AgoraRtmController+Helpers.swift @@ -0,0 +1,159 @@ +// +// AgoraRtmController+Helpers.swift +// +// +// Created by Max Cobb on 30/09/2021. +// + +import AgoraRtmKit + +// MARK: Helper Methods +extension AgoraRtmController { + /// Type of decoded message coming from other users + public enum DecodedRtmAction { + /// Mute is when a user is requesting another user to mute or unmute a device + case mute(_: MuteRequest) + /// DecodedRtmAction type containing data about a user (local or remote) + case userData(_: UserData) + /// Message that contains a small action request, such as a ping or requesting a user's data + case genericAction(_: RtmGenericRequest) + } + + /// Decode message to a compatible DecodedRtmMessage type. + /// - Parameters: + /// - data: Raw data input, should be utf8 encoded JSON string of MuteRequest or UserData. + /// - rtmId: Sender Real-time Messaging ID. + /// - Returns: DecodedRtmMessage enum of the appropriate type. + internal static func decodeRawRtmData(data: Data, from rtmId: String) -> DecodedRtmAction? { + let decoder = JSONDecoder() + if let userData = try? decoder.decode(UserData.self, from: data) { + return .userData(userData) + } else if let muteReq = try? decoder.decode(MuteRequest.self, from: data) { + return .mute(muteReq) + } else if let genericRequest = try? decoder.decode(RtmGenericRequest.self, from: data) { + return .genericAction(genericRequest) + } + return nil + } + + /// Share local UserData to all connected channels. + /// Call this method when personal details are updated. + open func broadcastPersonalData() { + for channel in self.channels { self.sendPersonalData(to: channel.value) } + } + + /// Share local UserData to a specific channel + /// - Parameter channel: Channel to share UserData with. + open func sendPersonalData(to channel: AgoraRtmChannel) { + self.sendRaw(message: self.personalData, channel: channel) { sendMsgState in + switch sendMsgState { + case .errorOk: + AgoraVideoViewer.agoraPrint( + .verbose, message: "Personal data sent to channel successfully" + ) + case .errorFailure, .errorTimeout, .tooOften, + .invalidMessage, .errorNotInitialized, .notLoggedIn: + AgoraVideoViewer.agoraPrint( + .error, message: "Could not send message to channel \(sendMsgState.rawValue)" + ) + @unknown default: + AgoraVideoViewer.agoraPrint(.error, message: "Could not send message to channel (unknown)") + } + } + } + + /// Share local UserData to a specific RTM member + /// - Parameter member: Member to share UserData with. + open func sendPersonalData(to member: String) { + self.sendRaw(message: self.personalData, member: member) { sendMsgState in + switch sendMsgState { + case .ok: + AgoraVideoViewer.agoraPrint( + .verbose, message: "Personal data sent to member successfully" + ) + case .failure, .timeout, .tooOften, .invalidMessage, .notInitialized, .notLoggedIn, + .peerUnreachable, .cachedByServer, .invalidUserId, .imcompatibleMessage: + AgoraVideoViewer.agoraPrint( + .error, message: "Could not send message to channel \(sendMsgState.rawValue)" + ) + @unknown default: + AgoraVideoViewer.agoraPrint(.error, message: "Could not send message to channel (unknown)") + } + } + } + + /// Send a raw codable message over RTM to the channel. + /// - Parameters: + /// - message: Codable message to send over RTM. + /// - channel: String channel name to send the message to. + /// - callback: Callback, to see if the message was sent successfully. + public func sendRaw( + message: Value, channel: String, + callback: @escaping (AgoraRtmSendChannelMessageErrorCode) -> Void + ) where Value: Codable { + if let channel = self.channels[channel], let data = try? JSONEncoder().encode(message) { + channel.send( + AgoraRtmRawMessage(rawData: data, description: "AgoraUIKit"), completion: callback + ) + } + } + + /// Create raw message from codable object + /// - Parameter codableObj: Codable object to be sent over the Real-time Messaging network. + /// - Returns: AgoraRtmRawMessage that is ready to be sent across the Agora Real-time Messaging network. + public static func createRawRtm(from codableObj: Value) -> AgoraRtmRawMessage? where Value: Codable { + if let data = try? JSONEncoder().encode(codableObj) { + return AgoraRtmRawMessage(rawData: data, description: "AgoraUIKit") + } + AgoraVideoViewer.agoraPrint(.error, message: "Message could not be encoded to JSON") + return nil + } + + /// Send a raw codable message over RTM to the channel + /// - Parameters: + /// - message: Codable message to send over RTM + /// - channel: AgoraRtmChannel to send the message over + /// - callback: Callback, to see if the message was sent successfully. + public func sendRaw( + message: Value, channel: AgoraRtmChannel, + callback: @escaping (AgoraRtmSendChannelMessageErrorCode) -> Void + ) where Value: Codable { + if let rawMsg = AgoraRtmController.createRawRtm(from: message) { + channel.send(rawMsg, completion: callback) + return + } + callback(.invalidMessage) + } + + /// Send a raw codable message over RTM to a member + /// - Parameters: + /// - message: Codable message to send over RTM + /// - channel: member, or RTM ID to send the message to + /// - callback: Callback, to see if the message was sent successfully. + public func sendRaw( + message: Value, member: String, + callback: @escaping (AgoraRtmSendPeerMessageErrorCode) -> Void + ) where Value: Codable { + guard let rawMsg = AgoraRtmController.createRawRtm(from: message) else { + callback(.imcompatibleMessage) + return + } + self.rtmKit.send(rawMsg, toPeer: member, completion: callback) + } + + /// Send a raw codable message over RTM to a member + /// - Parameters: + /// - message: Codable message to send over RTM + /// - channel: member, or RTC User ID to send the message to + /// - callback: Callback, to see if the message was sent successfully. + public func sendRaw( + message: Value, user: UInt, + callback: @escaping (AgoraRtmSendPeerMessageErrorCode) -> Void + ) where Value: Codable { + if let rtmId = self.rtcLookup[user] { + self.sendRaw(message: message, member: rtmId, callback: callback) + } else { + callback(.peerUnreachable) + } + } +} diff --git a/Sources/Agora-UIKit/AgoraRtmController.swift b/Sources/Agora-UIKit/AgoraRtmController.swift index c1bbe9fe..bd8e2bfc 100644 --- a/Sources/Agora-UIKit/AgoraRtmController.swift +++ b/Sources/Agora-UIKit/AgoraRtmController.swift @@ -26,6 +26,14 @@ public protocol RtmControllerDelegate: AnyObject { var videoLookup: [UInt: AgoraSingleVideoView] { get } /// The role for the user. Either `.audience` or `.broadcaster`. var userRole: AgoraClientRole { get set } + /// Delegate for the AgoraVideoViewer, used for some important callback methods. + var agoraViewerDelegate: AgoraVideoViewerDelegate? { get } +} + +public extension AgoraVideoViewer { + var agoraViewerDelegate: AgoraVideoViewerDelegate? { + return self.delegate + } } extension AgoraVideoViewer: RtmControllerDelegate { @@ -53,20 +61,31 @@ open class AgoraRtmController: NSObject { weak var delegate: RtmControllerDelegate! /// Status of the RTM Engine - public enum LoginStatus { + public enum RTMStatus { + /// Initialisation failed + case initFailed /// Login has not been attempted case offline + /// RTM is initialising, process is not yet complete + case initialising /// Currently attempting to log in case loggingIn /// RTM has logged in case loggedIn + /// RTM is logged in, and connected to the current channel + case connected /// RTM Login Failed case loginFailed(AgoraRtmLoginErrorCode) } /// Status of the RTM Engine - public internal(set) var loginStatus: LoginStatus = .offline -// var videoViewer: AgoraVideoViewer - var rtmKit: AgoraRtmKit + public internal(set) var rtmStatus: RTMStatus = .initialising { + didSet { + self.delegate.agoraViewerDelegate?.rtmStateChanged(from: oldValue, to: self.rtmStatus) + } + } + + /// Reference to the Agora Real-time Messaging engine used by this class. + public internal(set) var rtmKit: AgoraRtmKit /// Lookup remote user RTM ID based on their RTC ID public internal(set) var rtcLookup: [UInt: String] = [:] /// Get remote user data from their RTM ID @@ -134,12 +153,13 @@ open class AgoraRtmController: NSObject { self.rtmKit = rtmKit } else { return nil } super.init() + self.rtmStatus = .offline self.rtmKit.agoraRtmDelegate = self self.rtmLogin {_ in} } func rtmLogin(completion: @escaping (AgoraRtmLoginErrorCode) -> Void) { - self.loginStatus = .loggingIn + self.rtmStatus = .loggingIn if let tokenURL = self.agoraSettings.tokenURL { AgoraRtmController.fetchRtmToken(urlBase: tokenURL, userId: self.connectionData.rtmId) { fetchResult in switch fetchResult { @@ -168,7 +188,7 @@ open class AgoraRtmController: NSObject { open func rtmLoggedIn(code: AgoraRtmLoginErrorCode) { switch code { case .ok, .alreadyLogin: - self.loginStatus = .loggedIn + self.rtmStatus = .loggedIn for step in self.afterLoginSteps { step() } self.afterLoginSteps.removeAll() return @@ -179,7 +199,7 @@ open class AgoraRtmController: NSObject { @unknown default: AgoraVideoViewer.agoraPrint(.error, message: "unknown login code") } - self.loginStatus = .loginFailed(code) + self.rtmStatus = .loginFailed(code) } /// Joins an RTM channel. @@ -192,7 +212,7 @@ open class AgoraRtmController: NSObject { (String, AgoraRtmChannel, AgoraRtmJoinChannelErrorCode) -> Void )? = nil ) { - switch loginStatus { + switch rtmStatus { case .offline: self.rtmLogin { err in if err == .ok || err == .alreadyLogin { @@ -203,8 +223,9 @@ open class AgoraRtmController: NSObject { } case .loggingIn: self.afterLoginSteps.append { self.joinChannel(named: channel, callback: callback) } - case .loginFailed(let loginErr): print("login failed: \(loginErr.rawValue)") - case .loggedIn: + case .loginFailed(let loginErr): + AgoraVideoViewer.agoraPrint(.error, message: "login failed: \(loginErr.rawValue)") + case .loggedIn, .connected: guard let newChannel = self.rtmKit.createChannel(withId: channel, delegate: self) else { return } @@ -212,6 +233,12 @@ open class AgoraRtmController: NSObject { callback?(channel, newChannel, $0) self.rtmChannelJoined(name: channel, channel: newChannel, code: $0) } + case .initialising: + self.afterLoginSteps.append { + self.joinChannel(named: channel, callback: callback) + } + case .initFailed: + AgoraVideoViewer.agoraPrint(.error, message: "Cannot log into a channel if RTM failed") } } @@ -243,6 +270,7 @@ open class AgoraRtmController: NSObject { ) { switch code { case .channelErrorOk: + self.rtmStatus = .connected self.sendPersonalData(to: channel) self.channels[name] = channel case .channelErrorFailure, .channelErrorRejected, .channelErrorInvalidArgument, @@ -255,134 +283,3 @@ open class AgoraRtmController: NSObject { } } } - -// MARK: Helper Methods -extension AgoraRtmController { - /// Type of decoded message coming from other users - public enum DecodedRtmAction { - /// Mute is when a user is requesting another user to mute or unmute a device - case mute(_: MuteRequest) - /// DecodedRtmAction type containing data about a user (local or remote) - case userData(_: UserData) - /// Message that contains a small action request, such as a ping or requesting a user's data - case genericAction(_: RtmGenericRequest) - } - - /// Decode message to a compatible DecodedRtmMessage type. - /// - Parameters: - /// - data: Raw data input, should be utf8 encoded JSON string of MuteRequest or UserData. - /// - rtmId: Sender Real-time Messaging ID. - /// - Returns: DecodedRtmMessage enum of the appropriate type. - internal static func decodeRawRtmData(data: Data, from rtmId: String) -> DecodedRtmAction? { - let decoder = JSONDecoder() - if let userData = try? decoder.decode(UserData.self, from: data) { - return .userData(userData) - } else if let muteReq = try? decoder.decode(MuteRequest.self, from: data) { - return .mute(muteReq) - } else if let genericRequest = try? decoder.decode(RtmGenericRequest.self, from: data) { - return .genericAction(genericRequest) - } - return nil - } - - /// Share local UserData to all connected channels. - /// Call this method when personal details are updated. - open func broadcastPersonalData() { - for channel in self.channels { self.sendPersonalData(to: channel.value) } - } - - /// Share local UserData to a specific channel - /// - Parameter channel: Channel to share UserData with. - open func sendPersonalData(to channel: AgoraRtmChannel) { - self.sendRaw(message: self.personalData, channel: channel) { sendMsgState in - switch sendMsgState { - case .errorOk: - AgoraVideoViewer.agoraPrint( - .verbose, message: "Personal data sent to channel successfully" - ) - case .errorFailure, .errorTimeout, .tooOften, - .invalidMessage, .errorNotInitialized, .notLoggedIn: - AgoraVideoViewer.agoraPrint( - .error, message: "Could not send message to channel \(sendMsgState.rawValue)" - ) - @unknown default: - AgoraVideoViewer.agoraPrint(.error, message: "Could not send message to channel (unknown)") - } - } - } - - /// Share local UserData to a specific RTM member - /// - Parameter member: Member to share UserData with. - open func sendPersonalData(to member: String) { - self.sendRaw(message: self.personalData, member: member) { sendMsgState in - switch sendMsgState { - case .ok: - AgoraVideoViewer.agoraPrint( - .verbose, message: "Personal data sent to member successfully" - ) - case .failure, .timeout, .tooOften, .invalidMessage, .notInitialized, .notLoggedIn, - .peerUnreachable, .cachedByServer, .invalidUserId, .imcompatibleMessage: - AgoraVideoViewer.agoraPrint( - .error, message: "Could not send message to channel \(sendMsgState.rawValue)" - ) - @unknown default: - AgoraVideoViewer.agoraPrint(.error, message: "Could not send message to channel (unknown)") - } - } - } - - func sendRaw( - message: Value, channel: String, - callback: @escaping (AgoraRtmSendChannelMessageErrorCode) -> Void - ) where Value: Codable { - if let channel = self.channels[channel], let data = try? JSONEncoder().encode(message) { - channel.send( - AgoraRtmRawMessage(rawData: data, description: "AgoraUIKit"), completion: callback - ) - } - } - - /// Create raw message from codable object - /// - Parameter codableObj: Codable object to be sent over the Real-time Messaging network. - /// - Returns: AgoraRtmRawMessage that is ready to be sent across the Agora Real-time Messaging network. - internal static func createRawRtm(from codableObj: Value) -> AgoraRtmRawMessage? where Value: Codable { - if let data = try? JSONEncoder().encode(codableObj) { - return AgoraRtmRawMessage(rawData: data, description: "AgoraUIKit") - } - AgoraVideoViewer.agoraPrint(.error, message: "Message could not be encoded to JSON") - return nil - } - - func sendRaw( - message: Value, channel: AgoraRtmChannel, - callback: @escaping (AgoraRtmSendChannelMessageErrorCode) -> Void - ) where Value: Codable { - if let rawMsg = AgoraRtmController.createRawRtm(from: message) { - channel.send(rawMsg, completion: callback) - return - } - callback(.invalidMessage) - } - - func sendRaw( - message: Value, member: String, - callback: @escaping (AgoraRtmSendPeerMessageErrorCode) -> Void - ) where Value: Codable { - guard let rawMsg = AgoraRtmController.createRawRtm(from: message) else { - callback(.imcompatibleMessage) - return - } - self.rtmKit.send(rawMsg, toPeer: member, completion: callback) - } - - func sendRaw( - message: Value, user: UInt, - callback: @escaping (AgoraRtmSendPeerMessageErrorCode) -> Void - ) where Value: Codable { - if let rtmId = self.rtcLookup[user] { - self.sendRaw(message: message, member: rtmId, callback: callback) - } else { - callback(.peerUnreachable) - } - } -} diff --git a/Sources/Agora-UIKit/AgoraSettings.swift b/Sources/Agora-UIKit/AgoraSettings.swift index 89fbe367..156f8138 100644 --- a/Sources/Agora-UIKit/AgoraSettings.swift +++ b/Sources/Agora-UIKit/AgoraSettings.swift @@ -20,6 +20,9 @@ public struct AgoraSettings { /// Delegate for Agora RTM Channel callbacks public weak var rtmChannelDelegate: AgoraRtmChannelDelegate? + /// Whether RTM should be initialised and used + public var rtmEnabled: Bool = true + /// URL to fetch tokens from. If supplied, this package will automatically fetch tokens /// when the Agora Engine indicates it will be needed. /// It will follow the URL pattern found in @@ -107,6 +110,7 @@ public struct AgoraSettings { /// External video source settings parameters public var externalVideoSettings: ExternalVideoSettings = .allFalse + /// External video source settings parameters @available(*, deprecated, renamed: "externalVideoSettings") public var externalVideoSource: ExternalVideoSettings { get { self.externalVideoSettings } diff --git a/Sources/Agora-UIKit/AgoraUIKit.swift b/Sources/Agora-UIKit/AgoraUIKit.swift index 8370eba5..756d5a68 100644 --- a/Sources/Agora-UIKit/AgoraUIKit.swift +++ b/Sources/Agora-UIKit/AgoraUIKit.swift @@ -22,7 +22,7 @@ public struct AgoraUIKit: Codable { /// Framework type of UIKit. "native", "flutter", "reactnative" fileprivate(set) var framework: String /// Version of UIKit being used - static let version = "4.0.0-preview.4" + static let version = "4.0.0-preview.5" /// Framework type of UIKit. "native", "flutter", "reactnative" static let framework = "native" #if os(iOS) diff --git a/Sources/Agora-UIKit/AgoraVideoViewer+AgoraRtcEngineDelegate.swift b/Sources/Agora-UIKit/AgoraVideoViewer+AgoraRtcEngineDelegate.swift index a4bf0ec3..df83bf52 100644 --- a/Sources/Agora-UIKit/AgoraVideoViewer+AgoraRtcEngineDelegate.swift +++ b/Sources/Agora-UIKit/AgoraVideoViewer+AgoraRtcEngineDelegate.swift @@ -162,6 +162,15 @@ extension AgoraVideoViewer: AgoraRtcEngineDelegate { ) } + /** + Occurs when the local user successfully joins a specified channel. + + - Parameters: + - engine: AgoraRtcEngineKit object + - channel: The channel name. + - uid: The user ID. + - elapsed: The time elapsed (ms) from the local user calling `joinChannelByToken` until this event occurs. + */ open func rtcEngine( _ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int diff --git a/Sources/Agora-UIKit/AgoraVideoViewer+Buttons.swift b/Sources/Agora-UIKit/AgoraVideoViewer+Buttons.swift index ac5b4034..c63e2279 100644 --- a/Sources/Agora-UIKit/AgoraVideoViewer+Buttons.swift +++ b/Sources/Agora-UIKit/AgoraVideoViewer+Buttons.swift @@ -48,6 +48,9 @@ extension AgoraVideoViewer { let buttonSize = self.agoraSettings.buttonSize let buttonMargin = self.agoraSettings.buttonMargin + if builtinButtons.isEmpty { + return + } buttons.enumerated().forEach({ (elem) in let button = elem.element #if os(iOS) diff --git a/Sources/Agora-UIKit/AgoraVideoViewer+VideoControl.swift b/Sources/Agora-UIKit/AgoraVideoViewer+VideoControl.swift index 19389ccf..b2df02d1 100644 --- a/Sources/Agora-UIKit/AgoraVideoViewer+VideoControl.swift +++ b/Sources/Agora-UIKit/AgoraVideoViewer+VideoControl.swift @@ -324,10 +324,17 @@ extension AgoraVideoViewer { /// Initialise RTM to send messages across the network. open func setupRtmController(joining channel: String) { + if !self.agSettings.rtmEnabled { return } if self.rtmController == nil { - self.rtmController = AgoraRtmController(delegate: self) + DispatchQueue.global(qos: .utility).async { + self.rtmController = AgoraRtmController(delegate: self) + if self.rtmController == nil { + AgoraVideoViewer.agoraPrint(.error, message: "Error initialising RTM") + } else { + self.rtmController?.joinChannel(named: channel) + } + } } - self.rtmController?.joinChannel(named: channel) } internal func handleAlreadyInChannel( diff --git a/Sources/Agora-UIKit/AgoraVideoViewer.swift b/Sources/Agora-UIKit/AgoraVideoViewer.swift index 5703384e..1f4826c2 100644 --- a/Sources/Agora-UIKit/AgoraVideoViewer.swift +++ b/Sources/Agora-UIKit/AgoraVideoViewer.swift @@ -47,6 +47,11 @@ public protocol AgoraVideoViewerDelegate: AnyObject { /// A pong request has just come back to the local user, indicating that someone is still present in RTM /// - Parameter peerId: RTM ID of the remote user that sent the pong request. func incomingPongRequest(from peerId: String) + /// State of RTM has changed + /// - Parameters: + /// - oldState: Previous state of RTM + /// - newState: New state of RTM + func rtmStateChanged(from oldState: AgoraRtmController.RTMStatus, to newState: AgoraRtmController.RTMStatus) } public extension AgoraVideoViewerDelegate { @@ -65,6 +70,9 @@ public extension AgoraVideoViewerDelegate { func extraButtons() -> [NSButton] { [] } #endif func incomingPongRequest(from peerId: String) {} + func rtmStateChanged( + from oldState: AgoraRtmController.RTMStatus, to newState: AgoraRtmController.RTMStatus + ) {} } /// View to contain all the video session objects, including camera feeds and buttons for settings @@ -138,6 +146,17 @@ open class AgoraVideoViewer: MPView, SingleVideoViewDelegate { set { self.connectionData.appToken = newValue } } + /// Status of the RTM Engine + var rtmState: AgoraRtmController.RTMStatus { + if let rtmc = self.rtmController { + return rtmc.rtmStatus + } else if self.agSettings.rtmEnabled { + return .initFailed + } else { + return .offline + } + } + lazy internal var floatingVideoHolder: MPCollectionView = { let collView = AgoraCollectionViewer() self.addSubview(collView)