diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 5e73510..6e61a2b 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -27,7 +27,6 @@ jobs: uses: actions/checkout@v3 - name: Build DocC 🛠 run: | - sudo xcode-select -s /Applications/Xcode_14.0.app; xcodebuild docbuild -scheme AgoraUIKit -derivedDataPath /tmp/docbuild -destination 'generic/platform=iOS'; $(xcrun --find docc) process-archive \ transform-for-static-hosting /tmp/docbuild/Build/Products/Debug-iphoneos/AgoraUIKit.doccarchive \ diff --git a/.github/workflows/swift-build-lint.yml b/.github/workflows/swift-build-lint.yml index fc8eab5..45d3cbf 100644 --- a/.github/workflows/swift-build-lint.yml +++ b/.github/workflows/swift-build-lint.yml @@ -23,3 +23,7 @@ jobs: DESTINATION: 'generic/platform=iOS' - name: Pod Lint 🔎 run: pod lib lint AgoraUIKit_iOS.podspec --allow-warnings --skip-import-validation --include-podspecs='AgoraRtmControl_iOS.podspec' + - name: Print Version 🔤 + run: | + echo '### Build passed :rocket:' >> $GITHUB_STEP_SUMMARY + echo "Version: $(grep 'static let version' Sources/Agora-Video-UIKit/AgoraUIKit.swift | sed -e 's,.*\"\(.*\)\",\1,')" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/AgoraRtmControl_iOS.podspec b/AgoraRtmControl_iOS.podspec index 73912cc..a6701be 100644 --- a/AgoraRtmControl_iOS.podspec +++ b/AgoraRtmControl_iOS.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.name = 'AgoraRtmControl_iOS' s.module_name = 'AgoraRtmControl' - s.version = ENV['LIB_VERSION'] || '4.0.0' + s.version = ENV['LIB_VERSION'] || '4.0.1' s.summary = 'Agora Real-time Messaging Wrapper.' s.description = <<-DESC diff --git a/AgoraUIKit_iOS.podspec b/AgoraUIKit_iOS.podspec index bb7fad5..2d528cd 100644 --- a/AgoraUIKit_iOS.podspec +++ b/AgoraUIKit_iOS.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.name = 'AgoraUIKit_iOS' s.module_name = 'AgoraUIKit' - s.version = ENV['LIB_VERSION'] || '4.0.0' + s.version = ENV['LIB_VERSION'] || '4.0.1' s.summary = 'Agora video session UIKit template.' s.description = <<-DESC @@ -26,7 +26,7 @@ Use this Pod to create a video UIKit view that can be easily added to your iOS a s.static_framework = true s.source_files = 'Sources/Agora-Video-UIKit/*' - s.dependency 'AgoraRtcEngine_iOS', '4.0.0.4' + s.dependency 'AgoraRtcEngine_iOS', '~> 4.0.1' s.dependency 'AgoraRtmControl_iOS', "#{s.version.to_s}" end diff --git a/Package.swift b/Package.swift index 59a5bb5..74d510c 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( .package( name: "AgoraRtcKit", url: "https://github.com/AgoraIO/AgoraRtcEngine_iOS", - revision: "4.0.0-r.4" + .upToNextMinor(from: Version(4, 0, 1)) ), .package( name: "AgoraRtmKit", diff --git a/README.md b/README.md index 9a76258..f0a8d29 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Agora UIKit for iOS +# Agora Video UI Kit for iOS

diff --git a/Sources/Agora-Video-UIKit/AgoraCollectionViewer.swift b/Sources/Agora-Video-UIKit/AgoraCollectionViewer.swift index 461a7be..efd37ce 100644 --- a/Sources/Agora-Video-UIKit/AgoraCollectionViewer.swift +++ b/Sources/Agora-Video-UIKit/AgoraCollectionViewer.swift @@ -11,11 +11,13 @@ import UIKit import AppKit #endif -/// Collection View to display all connected users camera feeds -public class AgoraCollectionViewer: MPCollectionView { +/// Collection View to display all connected users camera feeds. Used in the streamerCollectionView. +internal class AgoraCollectionViewer: MPCollectionView { static let cellSpacing: CGFloat = 5 - public static var flowLayout: MPCollectionViewFlowLayout { + + /// Details for the collection list of participants camera feeds. + static var flowLayout: MPCollectionViewFlowLayout { let flowLayout = MPCollectionViewFlowLayout() flowLayout.itemSize = CGSize(width: 100, height: 100) flowLayout.scrollDirection = .horizontal diff --git a/Sources/Agora-Video-UIKit/AgoraSingleVideoView+RtmDelegate.swift b/Sources/Agora-Video-UIKit/AgoraSingleVideoView+RtmDelegate.swift index 3f8d388..1374d51 100644 --- a/Sources/Agora-Video-UIKit/AgoraSingleVideoView+RtmDelegate.swift +++ b/Sources/Agora-Video-UIKit/AgoraSingleVideoView+RtmDelegate.swift @@ -19,10 +19,21 @@ public protocol SingleVideoViewDelegate: AnyObject { #if canImport(AgoraRtmControl) /// RTM Controller class for managing RTM messages var rtmController: AgoraRtmController? { get set } + /// Create and send request to user to mute/unmute a device + /// - Parameters: + /// - uid: RTM User ID to send the request to + /// - str: String from the action label to + /// - Returns: Boolean stating if the request was valid or not func createRequest( to uid: UInt, fromString str: String ) -> Bool + /// Create and send request to mute/unmute a device + /// - Parameters: + /// - rtcId: RTC User ID to send the request to + /// - mute: Whether the device should be muted or unmuted + /// - device: Type of device (camera/microphone) + /// - isForceful: Whether the request should force its way through, otherwise a request is made. Cannot forcefully unmute. func sendMuteRequest(to rtcId: UInt, mute: Bool, device: AgoraVideoViewer.MutingDevices, isForceful: Bool) #endif diff --git a/Sources/Agora-Video-UIKit/AgoraUIKit.swift b/Sources/Agora-Video-UIKit/AgoraUIKit.swift index 0727b57..d1214e1 100644 --- a/Sources/Agora-Video-UIKit/AgoraUIKit.swift +++ b/Sources/Agora-Video-UIKit/AgoraUIKit.swift @@ -8,38 +8,40 @@ import Foundation import AgoraRtcKit -/// Agora UIKit data structure. Access `AgoraUIKit.current` for information -/// about your UIKit version. +/// Agora UIKit data structure. Access ``AgoraUIKit/AgoraUIKit/current`` for information +/// about your Video UI Kit version. public struct AgoraUIKit: Codable { /// Instance of the current AgoraUIKit instance. public static var current: AgoraUIKit { AgoraUIKit(version: AgoraUIKit.version, platform: AgoraUIKit.platform, framework: AgoraUIKit.framework) } /// Platform that is being used: ios, macos, android, unknown - fileprivate(set) var platform: String + public fileprivate(set) var platform: String /// Version of UIKit being used - fileprivate(set) var version: String + public fileprivate(set) var version: String /// Framework type of UIKit. "native", "flutter", "reactnative" - fileprivate(set) var framework: String + public fileprivate(set) var framework: String /// Version of UIKit being used - static let version = "4.0.0" + public static let version = "4.0.1" /// Framework type of UIKit. "native", "flutter", "reactnative" - static let framework = "native" + public static let framework = "native" #if os(iOS) /// Platform that is being used: ios, macos, android, unknown - static let platform = "ios" + public static let platform = "ios" #elseif os(macOS) /// Platform that is being used: ios, macos, android, unknown - static let platform = "macos" + public static let platform = "macos" #else /// Platform that is being used: ios, macos, android, unknown - static let platform = "unknown" + public static let platform = "unknown" #endif fileprivate init(version: String, platform: String, framework: String) { self.version = version self.platform = platform self.framework = framework } + /// Get the Video UI Kit details in a pretty printed string format. Used for print statements. + /// - Returns: String of the version, platform and framework. func prettyPrint() -> String { """ version: \(version) @@ -47,9 +49,23 @@ public struct AgoraUIKit: Codable { framework: \(framework) """ } + /// Initialiser from a decoder. Used for internal purposes only + /// - Parameter decoder: Decoder object that is used to set all the properties. + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.platform = try container.decode(String.self, forKey: .platform) + self.version = try container.decode(String.self, forKey: .version) + self.framework = try container.decode(String.self, forKey: .framework) + } + /// Converts an unsigned UInt32 to a regular signed Int. This is to handle User Id's across multiple platforms. + /// - Parameter uint: Unigned integer userId + /// - Returns: Signed integer userId public static func uintToInt(_ uint: UInt) -> Int { Int(Int32(bitPattern: UInt32(uint))) } + /// Converts a regular Int to an unsigned UInt32. This is to handle User Id's across multiple platforms. + /// - Parameter userInt: Signed integer userId + /// - Returns: Unsigned integer userId public static func intToUInt(_ userInt: Int) -> UInt { UInt(UInt32(bitPattern: Int32(userInt))) } diff --git a/Sources/Agora-Video-UIKit/AgoraVideoViewer+AgoraExtensions.swift b/Sources/Agora-Video-UIKit/AgoraVideoViewer+AgoraExtensions.swift index 729b469..97f68c9 100644 --- a/Sources/Agora-Video-UIKit/AgoraVideoViewer+AgoraExtensions.swift +++ b/Sources/Agora-Video-UIKit/AgoraVideoViewer+AgoraExtensions.swift @@ -1,5 +1,5 @@ // -// AgoraVideoViewer.swift +// AgoraVideoViewer+AgoraExtensions.swift // Agora-Video-UIKit // // Created by Max Cobb on 09/09/2021. diff --git a/Sources/Agora-Video-UIKit/AgoraVideoViewer+Token.swift b/Sources/Agora-Video-UIKit/AgoraVideoViewer+Token.swift index 0046349..798413c 100644 --- a/Sources/Agora-Video-UIKit/AgoraVideoViewer+Token.swift +++ b/Sources/Agora-Video-UIKit/AgoraVideoViewer+Token.swift @@ -19,6 +19,13 @@ extension AgoraVideoViewer { case invalidURL } + /// Update the token currently in use by the Agora SDK. Used to not interrupt an active video session. + /// - Parameter newToken: new token to be applied to the current connection. + @objc open func updateToken(_ newToken: String) { + self.currentRtcToken = newToken + self.agkit.renewToken(newToken) + } + /// Requests the token from our backend token service /// - Parameter urlBase: base URL specifying where the token server is located /// - Parameter channelName: Name of the channel we're requesting for diff --git a/Sources/Agora-Video-UIKit/AgoraVideoViewer+VideoControl.swift b/Sources/Agora-Video-UIKit/AgoraVideoViewer+VideoControl.swift index a529a9e..5e48132 100644 --- a/Sources/Agora-Video-UIKit/AgoraVideoViewer+VideoControl.swift +++ b/Sources/Agora-Video-UIKit/AgoraVideoViewer+VideoControl.swift @@ -24,7 +24,6 @@ extension AgoraVideoViewer { self.agkit.setExternalVideoSource( agoraSettings.externalVideoSettings.enabled, useTexture: agoraSettings.externalVideoSettings.texture, -// encodedFrame: agoraSettings.externalVideoSettings.encoded sourceType: agoraSettings.externalVideoSettings.encoded ? .encodedVideoFrame : .videoFrame ) if self.agoraSettings.externalAudioSettings.enabled { @@ -52,17 +51,12 @@ extension AgoraVideoViewer { !self.checkPermissions( mediaType: .video, callback: { err in - if err == nil { + if err == nil { // if permissions are now granted DispatchQueue.main.async { - // if permissions are now granted self.setCam(to: enabled, completion: completion) } - } else { - completion?(false) - } - }) { - return - } + } else { completion?(false) } + }) { return } self.agoraSettings.cameraEnabled = enabled self.agkit.enableLocalVideo(enabled) @@ -100,14 +94,11 @@ extension AgoraVideoViewer { completion?(true) return } - if enabled, - self.connectionData.channel != nil, - !self.checkPermissions( + if enabled, self.connectionData.channel != nil, !self.checkPermissions( mediaType: .audio, callback: { err in - if err == nil { + if err == nil { // if permissions are now granted DispatchQueue.main.async { - // if permissions are now granted self.setMic(to: enabled, completion: completion) } } else { completion?(false) } @@ -154,7 +145,6 @@ extension AgoraVideoViewer { ssButton.backgroundColor = ssButton.isSelected ? .systemGreen : .systemGray #elseif os(macOS) ssButton.layer?.backgroundColor = (ssButton.isOn ? NSColor.systemGreen : NSColor.systemGray).cgColor - if ssButton.isOn { self.startSharingScreen() } else { self.agkit.stopScreenCapture() } #endif @@ -174,7 +164,6 @@ extension AgoraVideoViewer { parameters.bitrate = 1000 parameters.captureMouseCursor = true self.agkit.startScreenCapture(byDisplayId: UInt32(displayId), regionRect: rectangle, captureParams: parameters) -// self.agkit.setScreenCaptureContentHint(contentHint) #endif } @@ -250,13 +239,15 @@ extension AgoraVideoViewer { /// A token will only be fetched if a token URL is provided in AgoraSettings. /// Default: `false` /// - uid: UID to be set when user joins the channel, default will be 0. + /// - mediaOptions: Media options such as custom audio/video tracks, subscribing options etc. public func join( channel: String, as role: AgoraClientRole = .broadcaster, - fetchToken: Bool = false, uid: UInt? = nil + fetchToken: Bool = false, uid: UInt? = nil, + mediaOptions: AgoraRtcChannelMediaOptions? = nil ) { if self.connectionData == nil { fatalError("No app ID is provided") } guard fetchToken else { - self.join(channel: channel, with: self.currentRtcToken, as: role, uid: uid) + self.join(channel: channel, with: self.currentRtcToken, as: role, uid: uid, mediaOptions: mediaOptions) return } if let tokenURL = self.agoraSettings.tokenURL { @@ -266,7 +257,7 @@ extension AgoraVideoViewer { switch result { case .success(let token): DispatchQueue.main.async { - self.join(channel: channel, with: token, as: role, uid: uid) + self.join(channel: channel, with: token, as: role, uid: uid, mediaOptions: mediaOptions) } case .failure(let err): AgoraVideoViewer.agoraPrint(.error, message: "Could not fetch token from server: \(err)") @@ -283,24 +274,28 @@ extension AgoraVideoViewer { /// - token: Valid token to join the channel /// - role: [AgoraClientRole](https://docs.agora.io/en/Video/API%20Reference/oc/Constants/AgoraClientRole.html) to join the channel as. Default: `.broadcaster` /// - uid: UID to be set when user joins the channel, default will be 0. + /// - mediaOptions: Media options such as custom audio/video tracks, subscribing options etc. /// - Returns: `Int32?` representing Agora's joinChannelByToken response. If response is `nil`, /// that means it has continued on another thread, or you area already in the channel. @discardableResult public func join( channel: String, with token: String?, - as role: AgoraClientRole = .broadcaster, uid: UInt? = nil + as role: AgoraClientRole = .broadcaster, uid: UInt? = nil, + mediaOptions: AgoraRtcChannelMediaOptions? = nil ) -> Int32? { if self.connectionData == nil { fatalError("No app ID is provided") } if role == .broadcaster { if !self.checkForPermissions(self.activePermissions, callback: { error in if error != nil { return } DispatchQueue.main.async { - self.join(channel: channel, with: token, as: role, uid: uid) + self.join(channel: channel, with: token, as: role, uid: uid, mediaOptions: mediaOptions) } }) { return nil } } if self.connectionData.channel != nil { - self.handleAlreadyInChannel(channel: channel, with: token, as: role, uid: uid) + self.handleAlreadyInChannel( + channel: channel, with: token, as: role, uid: uid, mediaOptions: mediaOptions + ) return nil } self.userRole = role @@ -315,9 +310,8 @@ extension AgoraVideoViewer { byToken: token, channelId: channel, uid: self.userID, - mediaOptions: AgoraRtcChannelMediaOptions() - ) - // Delegate method is called upon success + mediaOptions: mediaOptions ?? AgoraRtcChannelMediaOptions() + ) // Delegate method is called upon success } #if canImport(AgoraRtmControl) @@ -346,7 +340,8 @@ extension AgoraVideoViewer { internal func handleAlreadyInChannel( channel: String, with token: String?, - as role: AgoraClientRole = .broadcaster, uid: UInt? = nil + as role: AgoraClientRole = .broadcaster, uid: UInt? = nil, + mediaOptions: AgoraRtcChannelMediaOptions? = nil ) { if self.connectionData.channel == channel { AgoraVideoViewer.agoraPrint(.verbose, message: "We are already in a channel") @@ -354,7 +349,7 @@ extension AgoraVideoViewer { if self.leaveChannel() < 0 { AgoraVideoViewer.agoraPrint(.error, message: "Could not leave current channel") } else { - self.join(channel: channel, with: token, as: role, uid: uid) + self.join(channel: channel, with: token, as: role, uid: uid, mediaOptions: mediaOptions) } } @@ -385,15 +380,8 @@ extension AgoraVideoViewer { return leaveChannelRtn } - /// Update the token currently in use by the Agora SDK. Used to not interrupt an active video session. - /// - Parameter newToken: new token to be applied to the current connection. - @objc open func updateToken(_ newToken: String) { - self.currentRtcToken = newToken - self.agkit.renewToken(newToken) - } - /// Leave any open channels and kills the Agora Engine instance. - @objc open func exit() { + @objc open func exit(stopPreview: Bool = true) { self.leaveChannel(stopPreview: true) AgoraRtcEngineKit.destroy() } diff --git a/Sources/Agora-Video-UIKit/AgoraVideoViewer.swift b/Sources/Agora-Video-UIKit/AgoraVideoViewer.swift index 577a390..2f7a127 100644 --- a/Sources/Agora-Video-UIKit/AgoraVideoViewer.swift +++ b/Sources/Agora-Video-UIKit/AgoraVideoViewer.swift @@ -162,6 +162,7 @@ open class AgoraVideoViewer: MPView, SingleVideoViewDelegate { get { self.connectionData.rtcId } set { self.connectionData.rtcId = newValue } } + /// Storing struct for holding data about the connection to Agora service. internal var connectionData: AgoraConnectionData! /// Gets and sets the role for the user. Either `.audience` or `.broadcaster`. @@ -260,19 +261,18 @@ open class AgoraVideoViewer: MPView, SingleVideoViewDelegate { #endif // Had issues with `self.style == .collection`, so changed to switch case switch self.style { - case .collection: - rtnView.isHidden = true - default: - rtnView.isHidden = false + case .collection: rtnView.isHidden = true + default: rtnView.isHidden = false } return rtnView }() /// AgoraRtcEngineKit being used by this AgoraVideoViewer. lazy public internal(set) var agkit: AgoraRtcEngineKit = { - let engine = AgoraRtcEngineKit.sharedEngine( - withAppId: connectionData.appId, delegate: self - ) + let engine = AgoraRtcEngineKit.sharedEngine(withAppId: connectionData.appId, delegate: self) + + // This helps us know how many people are using the Video UI Kit. + engine.setParameters("{\"rtc.using_ui_kit\": 1}") engine.enableAudioVolumeIndication(1000, smooth: 3, reportVad: self.agoraSettings.reportLocalVolume) engine.setChannelProfile(.liveBroadcasting) if self.agoraSettings.usingDualStream { @@ -316,7 +316,6 @@ open class AgoraVideoViewer: MPView, SingleVideoViewDelegate { } // MARK: Storyboard Settings - /// Used by storyboard to set the AgoraVideoViewer appID. @IBInspectable var appID: String = "" { didSet { @@ -370,9 +369,7 @@ open class AgoraVideoViewer: MPView, SingleVideoViewDelegate { } } else if self.style == .grid { return self.userVideoLookup.filter { ($0.key != self.userID || self.agoraSettings.showSelf) } - } else { - return [:] - } + } else { return [:] } } /// Video views to be displayed in the pinned collection view. diff --git a/Sources/Agora-Video-UIKit/AgoraViewer.swift b/Sources/Agora-Video-UIKit/AgoraViewer.swift index 909251b..3bb84fc 100644 --- a/Sources/Agora-Video-UIKit/AgoraViewer.swift +++ b/Sources/Agora-Video-UIKit/AgoraViewer.swift @@ -58,8 +58,11 @@ public struct AgoraViewer: UIViewRepresentable { /// - channel: Channel name to join. /// - token: Valid token to join the channel. /// - role: AgoraClientRole to join the channel as. Default: .broadcaster. - public func join(channel: String, with token: String?, as role: AgoraClientRole) { - self.viewer.join(channel: channel, with: token, as: role) + /// - mediaOptions: Media options such as custom audio/video tracks, subscribing options etc. + public func join( + channel: String, with token: String?, as role: AgoraClientRole, + mediaOptions: AgoraRtcChannelMediaOptions? = nil) { + self.viewer.join(channel: channel, with: token, as: role, mediaOptions: mediaOptions) } } #endif