diff --git a/Sources/Agora-Video-UIKit/AgoraCameraSourcePush.swift b/Sources/Agora-Video-UIKit/AgoraCameraSourcePush.swift index 55d7c4f..2d21a13 100644 --- a/Sources/Agora-Video-UIKit/AgoraCameraSourcePush.swift +++ b/Sources/Agora-Video-UIKit/AgoraCameraSourcePush.swift @@ -106,7 +106,7 @@ public protocol AgoraCameraSourcePushDelegate: AnyObject { open class AgoraCameraSourcePush: NSObject { fileprivate var delegate: AgoraCameraSourcePushDelegate? - private var localVideoPreview: CustomVideoSourcePreview? + internal var localVideoPreview: CustomVideoSourcePreview? /// Active capture session public let captureSession: AVCaptureSession diff --git a/Sources/Agora-Video-UIKit/AgoraCollectionViewer.swift b/Sources/Agora-Video-UIKit/AgoraCollectionViewer.swift index efd37ce..9099449 100644 --- a/Sources/Agora-Video-UIKit/AgoraCollectionViewer.swift +++ b/Sources/Agora-Video-UIKit/AgoraCollectionViewer.swift @@ -227,7 +227,7 @@ extension AgoraVideoViewer: MPCollectionViewDelegate, MPCollectionViewDataSource if self.agoraSettings.showSelf { self.collectionViewVideos = Array(self.userVideoLookup.values) } else { - self.collectionViewVideos = Array(self.userVideoLookup.filter { $0.key != self.userID}.values) + self.collectionViewVideos = Array(self.userVideoLookup.filter { $0.key != 0 }.values) } default: self.collectionViewVideos.removeAll() @@ -251,7 +251,7 @@ extension AgoraVideoViewer: MPCollectionViewDelegate, MPCollectionViewDataSource var myActiveSpeaker: UInt? switch self.style { case .pinned: - myActiveSpeaker = self.overrideActiveSpeaker ?? self.activeSpeaker ?? self.userID + myActiveSpeaker = self.overrideActiveSpeaker ?? self.activeSpeaker ?? 0 default: break } diff --git a/Sources/Agora-Video-UIKit/AgoraConnectionData.swift b/Sources/Agora-Video-UIKit/AgoraConnectionData.swift index 1f56f4c..2ca8500 100644 --- a/Sources/Agora-Video-UIKit/AgoraConnectionData.swift +++ b/Sources/Agora-Video-UIKit/AgoraConnectionData.swift @@ -22,7 +22,7 @@ public struct AgoraConnectionData { /// Token to be used to connect to a RTM channel, can be nil. public var rtmToken: String? /// Channel the object is connected to. This cannot be set with the initialiser. - public var channel: String? + public internal(set) var channel: String? /// Agora Real-time Communication Identifier (Agora Video/Audio SDK). public var rtcId: UInt /// Agora Real-time Messaging Identifier (Agora RTM SDK). diff --git a/Sources/Agora-Video-UIKit/AgoraSettings.swift b/Sources/Agora-Video-UIKit/AgoraSettings.swift index d1a8ea8..7f08c20 100644 --- a/Sources/Agora-Video-UIKit/AgoraSettings.swift +++ b/Sources/Agora-Video-UIKit/AgoraSettings.swift @@ -182,6 +182,8 @@ public struct AgoraSettings { /// and camera to not be activated at all. public var cameraEnabled: Bool = true + public internal(set) var previewEnabled: Bool = false + /// Show the icon for remote user video feeds to request mute/unmute of devices public var showRemoteRequestOptions: Bool = true diff --git a/Sources/Agora-Video-UIKit/AgoraUIKit.docc/Tutorials/UsingSwiftUI.tutorial b/Sources/Agora-Video-UIKit/AgoraUIKit.docc/Tutorials/UsingSwiftUI.tutorial index d893e5e..3b0e63a 100644 --- a/Sources/Agora-Video-UIKit/AgoraUIKit.docc/Tutorials/UsingSwiftUI.tutorial +++ b/Sources/Agora-Video-UIKit/AgoraUIKit.docc/Tutorials/UsingSwiftUI.tutorial @@ -10,7 +10,7 @@ Let's make a SwiftUI Video Call App! After creating a new SwiftUI project with Xcode, you must add the Swift Package of Agora Video UI Kit using this URL: - `https://github.com/AgoraIO-Community/VideoUIKit-iOS` + `https://github.com/AgoraIO-Community/VideoUIKit-macOS` And add camera and microphone permissions to your Info.plist: diff --git a/Sources/Agora-Video-UIKit/AgoraUIKit.swift b/Sources/Agora-Video-UIKit/AgoraUIKit.swift index c4d5643..98d3b17 100644 --- a/Sources/Agora-Video-UIKit/AgoraUIKit.swift +++ b/Sources/Agora-Video-UIKit/AgoraUIKit.swift @@ -25,7 +25,7 @@ public struct AgoraUIKit: Codable { /// Framework type of UIKit. "native", "flutter", "reactnative" public fileprivate(set) var framework: String /// Version of UIKit being used - public static let version = "4.0.6" + public static let version = "4.1.0" /// Framework type of UIKit. "native", "flutter", "reactnative" public static let framework = "native" #if os(iOS) diff --git a/Sources/Agora-Video-UIKit/AgoraVideoViewer+AgoraRtcEngineDelegate.swift b/Sources/Agora-Video-UIKit/AgoraVideoViewer+AgoraRtcEngineDelegate.swift index 5539f59..3671eca 100644 --- a/Sources/Agora-Video-UIKit/AgoraVideoViewer+AgoraRtcEngineDelegate.swift +++ b/Sources/Agora-Video-UIKit/AgoraVideoViewer+AgoraRtcEngineDelegate.swift @@ -22,8 +22,8 @@ extension AgoraVideoViewer: AgoraRtcEngineDelegate { ) { let isHost = newRole == .broadcaster if !isHost { - self.userVideoLookup.removeValue(forKey: self.userID) - } else if self.userVideoLookup[self.userID] == nil { + self.userVideoLookup.removeValue(forKey: 0) + } else if self.userVideoLookup[0] == nil { self.addLocalVideo() } @@ -73,7 +73,7 @@ extension AgoraVideoViewer: AgoraRtcEngineDelegate { videoView.audioMuted = state == .stopped } else if state != .stopped { self.addUserVideo(with: uid).audioMuted = false - if self.activeSpeaker == nil && uid != self.userID { + if self.activeSpeaker == nil && uid != 0 { self.activeSpeaker = uid } } @@ -156,7 +156,7 @@ extension AgoraVideoViewer: AgoraRtcEngineDelegate { switch state { case .decoding: self.addUserVideo(with: uid).videoMuted = false - if self.activeSpeaker == nil && uid != self.userID { + if self.activeSpeaker == nil && uid != 0 { self.activeSpeaker = uid } case .stopped: @@ -207,8 +207,14 @@ extension AgoraVideoViewer: AgoraRtcEngineDelegate { error: AgoraLocalVideoStreamError, sourceType: AgoraVideoSourceType ) { switch state { - case .capturing, .stopped: - self.userVideoLookup[self.userID]?.videoMuted = state == .stopped + case .capturing: + if !self.agoraSettings.previewEnabled { + self.addLocalVideo()?.videoMuted = false + } + case .stopped: + if !self.agoraSettings.previewEnabled { + self.videoLookup[0]?.videoMuted = true + } default: break } @@ -234,8 +240,14 @@ extension AgoraVideoViewer: AgoraRtcEngineDelegate { error: AgoraAudioLocalError ) { switch state { - case .recording, .stopped: - self.userVideoLookup[self.userID]?.audioMuted = state == .stopped + case .recording: + if !self.agoraSettings.previewEnabled { + self.addLocalVideo()?.audioMuted = false + } + case .stopped: + if !self.agoraSettings.previewEnabled { + self.videoLookup[0]?.audioMuted = true + } default: break } diff --git a/Sources/Agora-Video-UIKit/AgoraVideoViewer+Buttons.swift b/Sources/Agora-Video-UIKit/AgoraVideoViewer+Buttons.swift index caeed2b..1cb3b65 100644 --- a/Sources/Agora-Video-UIKit/AgoraVideoViewer+Buttons.swift +++ b/Sources/Agora-Video-UIKit/AgoraVideoViewer+Buttons.swift @@ -30,10 +30,10 @@ extension AgoraVideoViewer { containerSize = CGSize(width: containerSize.height, height: containerSize.width) frameOriginY = (self.bounds.height - CGFloat(contWidth)) / 2 if self.agoraSettings.buttonPosition == .left { - frameOriginX = 30 + frameOriginX = 20 resizeMask = [.flexibleTopMargin, .flexibleRightMargin, .flexibleBottomMargin] } else { - frameOriginX = self.bounds.width - self.agoraSettings.buttonSize - 20 - 10 + frameOriginX = self.bounds.width - containerSize.width - 20 resizeMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleBottomMargin] } case .bottom: break diff --git a/Sources/Agora-Video-UIKit/AgoraVideoViewer+JoinChannel.swift b/Sources/Agora-Video-UIKit/AgoraVideoViewer+JoinChannel.swift index ee05a83..7c515fc 100644 --- a/Sources/Agora-Video-UIKit/AgoraVideoViewer+JoinChannel.swift +++ b/Sources/Agora-Video-UIKit/AgoraVideoViewer+JoinChannel.swift @@ -82,12 +82,13 @@ extension AgoraVideoViewer { /// - Returns: An integer representing Agora's joinChannelByToken response. If response is `nil`, /// that means it has continued on another thread due to requesting camera/mic permissions, /// or you area already in the channel. If the response is 0, everything is fine. - @discardableResult - public func join( + @discardableResult public func join( channel: String, with token: String?, as role: AgoraClientRole = .broadcaster, uid: UInt? = nil, mediaOptions: AgoraRtcChannelMediaOptions? = nil ) -> Int32? { + // Once we join the channel, preview is not relevant. + self.agoraSettings.previewEnabled = false if self.connectionData == nil { fatalError("No app ID is provided") } if role == .broadcaster { if !self.checkForPermissions(self.activePermissions, callback: { error in @@ -160,27 +161,32 @@ extension AgoraVideoViewer { /// Leave channel stops all preview elements /// - Parameters: - /// - stopPreview: Stops the local preview and the video - /// - leaveChannelBlock: This callback indicates that a user leaves a channel, and provides the statistics of the call. + /// - stopPreview: Stops the local preview and the video + /// - leaveChannelBlock: This callback indicates that a user leaves a channel, and provides the statistics of the call. /// - Returns: Same return as AgoraRtcEngineKit.leaveChannel, 0 means no problem, less than 0 means there was an issue leaving - @discardableResult - @objc open func leaveChannel( + @discardableResult @objc open func leaveChannel( stopPreview: Bool = true, _ leaveChannelBlock: ((AgoraChannelStats) -> Void)? = nil ) -> Int32 { + self.agoraSettings.previewEnabled = !stopPreview guard let chName = self.connectionData.channel else { AgoraVideoViewer.agoraPrint(.error, message: "Not in a channel, could not leave") // Returning 0 to just say we are not in a channel return 0 } self.connectionData.channel = nil - self.agkit.setupLocalVideo(nil) - self.customCamera?.stopCapture() - if stopPreview, self.userRole == .broadcaster { agkit.stopPreview() } + if stopPreview, self.userRole == .broadcaster { + agkit.stopPreview() + self.agkit.setupLocalVideo(nil) + self.customCamera?.stopCapture() + } + self.userVideoLookup = self.userVideoLookup.filter { + if !stopPreview, $0.key == 0 { return true } + $0.value.removeFromSuperview() + return false + } self.activeSpeaker = nil self.remoteUserIDs = [] - self.userVideoLookup = [:] - self.backgroundVideoHolder.subviews.forEach { $0.removeFromSuperview() } - self.controlContainer?.isHidden = true + self.controlContainer?.isHidden = stopPreview let leaveChannelRtn = self.agkit.leaveChannel(leaveChannelBlock) defer { if leaveChannelRtn == 0 { delegate?.leftChannel(chName) } } return leaveChannelRtn diff --git a/Sources/Agora-Video-UIKit/AgoraVideoViewer+LocalVideo.swift b/Sources/Agora-Video-UIKit/AgoraVideoViewer+LocalVideo.swift index ded05f9..09c1b01 100644 --- a/Sources/Agora-Video-UIKit/AgoraVideoViewer+LocalVideo.swift +++ b/Sources/Agora-Video-UIKit/AgoraVideoViewer+LocalVideo.swift @@ -17,11 +17,11 @@ extension AgoraVideoViewer: AgoraCameraSourcePushDelegate { /// Adds the local video feed to the user video collections. /// - Returns: The newly created (or already created) local video feed container. internal func addLocalVideo() -> AgoraSingleVideoView? { - if self.userID == 0 || self.userVideoLookup[self.userID] != nil { - return self.userVideoLookup[self.userID] + if self.userVideoLookup[0] != nil { + return self.userVideoLookup[0] } let vidView = AgoraSingleVideoView( - uid: self.userID, micColor: self.agoraSettings.colors.micFlag + uid: 0, micColor: self.agoraSettings.colors.micFlag ) vidView.canvas.renderMode = self.agoraSettings.videoRenderMode self.agkit.setupLocalVideo(vidView.canvas) @@ -31,13 +31,53 @@ extension AgoraVideoViewer: AgoraCameraSourcePushDelegate { vidView.customCameraView = CustomVideoSourcePreview(frame: .zero) vidView.customCameraView?.isHidden = true self.customCamera = AgoraCameraSourcePush(delegate: self, localVideoPreview: vidView.customCameraView) - customCamera?.startCapture(ofDevice: device) } - self.userVideoLookup[self.userID] = vidView + self.userVideoLookup[0] = vidView return vidView } + internal func removeLocalVideo() { + guard let localVideo = self.userVideoLookup[0] else { + return + } + self.agkit.setupLocalVideo(nil) + if !self.agoraSettings.externalVideoSettings.enabled { + self.agkit.stopPreview() + } else if self.agoraSettings.externalVideoSettings.captureDevice != nil { + localVideo.customCameraView?.removeFromSuperview() + self.customCamera?.stopCapture() + self.customCamera?.localVideoPreview = nil + } + localVideo.removeFromSuperview() + self.userVideoLookup.removeValue(forKey: 0) + } + + /// Initialises the pre-call view. This shows the local Video and lets the user adjust their scene before joining a call. + /// Do not call this method if you're already in a channel. + public func startPrecallVideo() { + guard !self.agoraSettings.previewEnabled, self.connectionData.channel == nil else { + return + } + self.agoraSettings.previewEnabled = true + if self.userRole == .audience { + self.setRole(to: .broadcaster) + } + self.addLocalVideo()?.videoMuted = !agoraSettings.cameraEnabled + self.addLocalVideo()?.audioMuted = !agoraSettings.micEnabled + self.rtcEngine(rtcEngine, didClientRoleChanged: .audience, newRole: .broadcaster, newRoleOptions: .none) + } + + /// Stops the precall view if we are not in a channel and preview is enabled + public func stopPrecallVideo() { + guard self.agoraSettings.previewEnabled, self.connectionData.channel == nil else { + return + } + self.removeLocalVideo() + self.controlContainer?.isHidden = true + self.agoraSettings.previewEnabled = false + } + /// Set or change the current capture device. /// - Parameter captureDevice: Desired AVCaptureDevice to be set up. /// - Returns: Returns true if successful, else false. @@ -72,7 +112,7 @@ extension AgoraVideoViewer: AgoraCameraSourcePushDelegate { // once we have the video frame, we can push to agora sdk self.agkit.pushExternalVideoFrame(videoFrame) - if let localUser = userVideoLookup[self.userID], localUser.videoMuted { + if let localUser = userVideoLookup[0], localUser.videoMuted { self.rtcEngine( self.agkit, localVideoStateChangedOf: AgoraVideoLocalState.capturing, error: .OK, sourceType: AgoraVideoSourceType.camera diff --git a/Sources/Agora-Video-UIKit/AgoraVideoViewer+Ordering.swift b/Sources/Agora-Video-UIKit/AgoraVideoViewer+Ordering.swift index 886f8d4..e3361a9 100644 --- a/Sources/Agora-Video-UIKit/AgoraVideoViewer+Ordering.swift +++ b/Sources/Agora-Video-UIKit/AgoraVideoViewer+Ordering.swift @@ -35,7 +35,7 @@ extension AgoraVideoViewer { /// Randomly select an activeSpeaker that is not the local user @objc open func setRandomSpeaker() { - if let randomNotMe = self.userVideoLookup.keys.shuffled().filter({ $0 != self.userID }).randomElement() { + if let randomNotMe = self.userVideoLookup.keys.shuffled().filter({ $0 != 0 }).randomElement() { // active speaker has left, reassign activeSpeaker to a random member self.activeSpeaker = randomNotMe } else { @@ -81,6 +81,11 @@ extension AgoraVideoViewer { } } + open override func layoutSubviews() { + super.layoutSubviews() + self.reorganiseVideos() + } + /// Display grid when there are only two video members fileprivate func gridForTwo() { // when there are 2 videos we display them ontop of eachother @@ -102,7 +107,7 @@ extension AgoraVideoViewer { .width, .height, .maxYMargin, .minYMargin, .maxXMargin, .minXMargin ] #endif - if self.agoraSettings.usingDualStream && self.userID != keyVals.key { + if self.agoraSettings.usingDualStream && keyVals.key != 0 { self.agkit.setRemoteVideoStream( keyVals.key, type: self.agoraSettings.gridThresholdHighBitrate > 2 ? .high : .low @@ -143,7 +148,7 @@ extension AgoraVideoViewer { #elseif os(macOS) videoSessionView.autoresizingMask = [.width, .height, .maxYMargin, .minYMargin, .maxXMargin, .minXMargin] #endif - if self.agoraSettings.usingDualStream && videoID != self.userID { + if self.agoraSettings.usingDualStream && videoID != 0 { self.agkit.setRemoteVideoStream( videoID, type: vidCounts <= self.agoraSettings.gridThresholdHighBitrate ? .high : .low diff --git a/Sources/Agora-Video-UIKit/AgoraVideoViewer+VideoControl.swift b/Sources/Agora-Video-UIKit/AgoraVideoViewer+VideoControl.swift index 61d8c3d..64c5bc6 100644 --- a/Sources/Agora-Video-UIKit/AgoraVideoViewer+VideoControl.swift +++ b/Sources/Agora-Video-UIKit/AgoraVideoViewer+VideoControl.swift @@ -54,6 +54,9 @@ extension AgoraVideoViewer { ).cgColor #endif } + if self.agoraSettings.previewEnabled { + self.addLocalVideo()?.videoMuted = !self.agoraSettings.cameraEnabled + } } /// Manually set the camera to be enabled or disabled. @@ -94,6 +97,8 @@ extension AgoraVideoViewer { error: .OK, sourceType: AgoraVideoSourceType.camera ) } + } else { + _ = enabled ? self.agkit.startPreview() : self.agkit.stopPreview() } updateCamButton() @@ -127,7 +132,7 @@ extension AgoraVideoViewer { return } self.agoraSettings.micEnabled = enabled - self.userVideoLookup[self.userID]?.audioMuted = !self.agoraSettings.micEnabled + self.userVideoLookup[0]?.audioMuted = !self.agoraSettings.micEnabled self.agkit.muteLocalAudioStream(!self.agoraSettings.micEnabled) if self.agoraSettings.micEnabled { // This is only enabled. If you want to disable it then do so manually. diff --git a/Sources/Agora-Video-UIKit/AgoraVideoViewer.swift b/Sources/Agora-Video-UIKit/AgoraVideoViewer.swift index 6de442e..ffa08e0 100644 --- a/Sources/Agora-Video-UIKit/AgoraVideoViewer.swift +++ b/Sources/Agora-Video-UIKit/AgoraVideoViewer.swift @@ -112,6 +112,9 @@ open class AgoraVideoViewer: MPView, SingleVideoViewDelegate { /// as well as agora video configuration. public internal(set) var agoraSettings: AgoraSettings + internal var previewEnabled: Bool { + self.agoraSettings.previewEnabled + } #if canImport(AgoraRtmControl) /// Controller class for managing RTM messages public var rtmController: AgoraRtmController? @@ -273,7 +276,7 @@ open class AgoraVideoViewer: MPView, SingleVideoViewDelegate { 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.setParameters(#"{"rtc.using_ui_kit": 1}"#) engine.enableAudioVolumeIndication(1000, smooth: 3, reportVad: self.agoraSettings.reportLocalVolume) engine.setChannelProfile(.liveBroadcasting) if self.agoraSettings.usingDualStream { @@ -366,10 +369,10 @@ open class AgoraVideoViewer: MPView, SingleVideoViewDelegate { return [:] } return self.userVideoLookup.filter { - $0.key == (self.overrideActiveSpeaker ?? self.activeSpeaker ?? self.userID) + $0.key == (self.overrideActiveSpeaker ?? self.activeSpeaker ?? 0) } } else if self.style == .grid { - return self.userVideoLookup.filter { ($0.key != self.userID || self.agoraSettings.showSelf) } + return self.userVideoLookup.filter { ($0.key != 0 || self.agoraSettings.showSelf) } } else { return [:] } } diff --git a/Tests/Agora-UIKit-Tests/RtmMessagingTests.swift b/Tests/Agora-UIKit-Tests/RtmMessagingTests.swift index ca3b447..ee12023 100644 --- a/Tests/Agora-UIKit-Tests/RtmMessagingTests.swift +++ b/Tests/Agora-UIKit-Tests/RtmMessagingTests.swift @@ -30,8 +30,7 @@ final class RtmMessagesTests: XCTestCase { XCTAssertEqual((unencodedJSON["mute"] as? Bool), muteReq.mute, "mute invalid!") XCTAssertEqual((unencodedJSON["device"] as? Int), muteReq.device, "device invalid!") XCTAssertEqual((unencodedJSON["isForceful"] as? Bool), muteReq.isForceful, "mute invalid!") - let msgTextValid = "{\"rtcId\":999,\"mute\":true,\"messageType\":" - + "\"MuteRequest\",\"device\":0,\"isForceful\":true}" + let msgTextValid = #"{"rtcId":999,"mute":true,"messageType":"MuteRequest","device":0,"isForceful":true}"# XCTAssertEqual(rtmMessage.text, msgTextValid, "Message text not matching mstTextValid")