|
| 1 | +// |
| 2 | +// JoinChannelVC.swift |
| 3 | +// APIExample |
| 4 | +// |
| 5 | +// Created by 张乾泽 on 2020/4/17. |
| 6 | +// Copyright © 2020 Agora Corp. All rights reserved. |
| 7 | +// |
| 8 | +import UIKit |
| 9 | +import AGEVideoLayout |
| 10 | +import AgoraRtcKit |
| 11 | + |
| 12 | +class LiveStreamingEntry : UIViewController |
| 13 | +{ |
| 14 | + @IBOutlet weak var joinButton: UIButton! |
| 15 | + @IBOutlet weak var channelTextField: UITextField! |
| 16 | + let identifier = "LiveStreaming" |
| 17 | + var role:AgoraClientRole = .broadcaster |
| 18 | + |
| 19 | + override func viewDidLoad() { |
| 20 | + super.viewDidLoad() |
| 21 | + } |
| 22 | + |
| 23 | + func getRoleAction(_ role: AgoraClientRole) -> UIAlertAction{ |
| 24 | + return UIAlertAction(title: "\(role.description())", style: .default, handler: {[unowned self] action in |
| 25 | + self.role = role |
| 26 | + self.doJoin() |
| 27 | + }) |
| 28 | + } |
| 29 | + |
| 30 | + |
| 31 | + @IBAction func doJoinPressed(sender: UIButton) { |
| 32 | + guard let _ = channelTextField.text else {return} |
| 33 | + //resign channel text field |
| 34 | + channelTextField.resignFirstResponder() |
| 35 | + |
| 36 | + //display role picker |
| 37 | + let alert = UIAlertController(title: "Pick Role".localized, message: nil, preferredStyle: UIDevice.current.userInterfaceIdiom == .pad ? UIAlertController.Style.alert : UIAlertController.Style.actionSheet) |
| 38 | + alert.addAction(getRoleAction(.broadcaster)) |
| 39 | + alert.addAction(getRoleAction(.audience)) |
| 40 | + alert.addCancelAction() |
| 41 | + present(alert, animated: true, completion: nil) |
| 42 | + } |
| 43 | + |
| 44 | + func doJoin() { |
| 45 | + guard let channelName = channelTextField.text else {return} |
| 46 | + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) |
| 47 | + // create new view controller every time to ensure we get a clean vc |
| 48 | + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} |
| 49 | + newViewController.title = channelName |
| 50 | + newViewController.configs = ["channelName":channelName, "role":self.role] |
| 51 | + self.navigationController?.pushViewController(newViewController, animated: true) |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +class LiveStreamingMain: BaseViewController { |
| 56 | + var foregroundVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) |
| 57 | + var backgroundVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) |
| 58 | + @IBOutlet weak var foregroundVideoContainer:UIView! |
| 59 | + @IBOutlet weak var backgroundVideoContainer:UIView! |
| 60 | + @IBOutlet weak var clientRoleToggleView:UIView! |
| 61 | + @IBOutlet weak var ultraLowLatencyToggleView:UIView! |
| 62 | + @IBOutlet weak var clientRoleToggle:UISwitch! |
| 63 | + @IBOutlet weak var ultraLowLatencyToggle:UISwitch! |
| 64 | + var remoteUid: UInt? { |
| 65 | + didSet { |
| 66 | + foregroundVideoContainer.isHidden = !(role == .broadcaster && remoteUid != nil) |
| 67 | + } |
| 68 | + } |
| 69 | + var agoraKit: AgoraRtcEngineKit! |
| 70 | + var role: AgoraClientRole = .broadcaster { |
| 71 | + didSet { |
| 72 | + foregroundVideoContainer.isHidden = !(role == .broadcaster && remoteUid != nil) |
| 73 | + ultraLowLatencyToggle.isEnabled = role == .audience |
| 74 | + } |
| 75 | + } |
| 76 | + var isLocalVideoForeground = false { |
| 77 | + didSet { |
| 78 | + if isLocalVideoForeground { |
| 79 | + foregroundVideo.setPlaceholder(text: "Local Host".localized) |
| 80 | + backgroundVideo.setPlaceholder(text: "Remote Host".localized) |
| 81 | + } else { |
| 82 | + foregroundVideo.setPlaceholder(text: "Remote Host".localized) |
| 83 | + backgroundVideo.setPlaceholder(text: "Local Host".localized) |
| 84 | + } |
| 85 | + } |
| 86 | + } |
| 87 | + var isUltraLowLatencyOn: Bool = false |
| 88 | + |
| 89 | + // indicate if current instance has joined channel |
| 90 | + var isJoined: Bool = false |
| 91 | + |
| 92 | + override func viewDidLoad() { |
| 93 | + super.viewDidLoad() |
| 94 | + |
| 95 | + // layout render view |
| 96 | + foregroundVideoContainer.addSubview(foregroundVideo) |
| 97 | + backgroundVideoContainer.addSubview(backgroundVideo) |
| 98 | + foregroundVideo.bindFrameToSuperviewBounds() |
| 99 | + backgroundVideo.bindFrameToSuperviewBounds() |
| 100 | + |
| 101 | + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() |
| 102 | + let config = AgoraRtcEngineConfig() |
| 103 | + config.appId = KeyCenter.AppId |
| 104 | + config.channelProfile = .liveBroadcasting |
| 105 | + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) |
| 106 | + agoraKit.setLogFile(LogUtils.sdkLogPath()) |
| 107 | + |
| 108 | + // get channel name from configs |
| 109 | + guard let channelName = configs["channelName"] as? String, let clientRole = configs["role"] as? AgoraClientRole else {return} |
| 110 | + |
| 111 | + role = clientRole |
| 112 | + // for audience put local video in foreground |
| 113 | + isLocalVideoForeground = role == .audience |
| 114 | + // if inital role is broadcaster, do not show audience options |
| 115 | + clientRoleToggleView.isHidden = role == .broadcaster |
| 116 | + ultraLowLatencyToggleView.isHidden = role == .broadcaster |
| 117 | + |
| 118 | + // make this room live broadcasting room |
| 119 | + updateClientRole(role) |
| 120 | + |
| 121 | + // enable video module and set up video encoding configs |
| 122 | + agoraKit.enableVideo() |
| 123 | + |
| 124 | + // Set audio route to speaker |
| 125 | + agoraKit.setDefaultAudioRouteToSpeakerphone(true) |
| 126 | + |
| 127 | + // start joining channel |
| 128 | + // 1. Users can only see each other after they join the |
| 129 | + // same channel successfully using the same app id. |
| 130 | + // 2. If app certificate is turned on at dashboard, token is needed |
| 131 | + // when joining channel. The channel name and uid used to calculate |
| 132 | + // the token has to match the ones used for channel join |
| 133 | + let option = AgoraRtcChannelMediaOptions() |
| 134 | + option.publishCameraTrack = .of(true) |
| 135 | + option.clientRoleType = .of((Int32)(AgoraClientRole.broadcaster.rawValue)) |
| 136 | + |
| 137 | + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, uid: 0, mediaOptions: option) |
| 138 | + if result != 0 { |
| 139 | + // Usually happens with invalid parameters |
| 140 | + // Error code description can be found at: |
| 141 | + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html |
| 142 | + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html |
| 143 | + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + /// make myself a broadcaster |
| 148 | + func becomeBroadcaster() { |
| 149 | + guard let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, |
| 150 | + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, |
| 151 | + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else { |
| 152 | + LogUtils.log(message: "invalid video configurations, failed to become broadcaster", level: .error) |
| 153 | + return |
| 154 | + } |
| 155 | + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, |
| 156 | + frameRate: fps, |
| 157 | + bitrate: AgoraVideoBitrateStandard, |
| 158 | + orientationMode: orientation, mirrorMode: .auto)) |
| 159 | + |
| 160 | + // set up local video to render your local camera preview |
| 161 | + let videoCanvas = AgoraRtcVideoCanvas() |
| 162 | + videoCanvas.uid = 0 |
| 163 | + // the view to be binded |
| 164 | + videoCanvas.view = localVideoCanvas() |
| 165 | + videoCanvas.renderMode = .hidden |
| 166 | + agoraKit.setupLocalVideo(videoCanvas) |
| 167 | + // you have to call startPreview to see local video |
| 168 | + agoraKit.startPreview() |
| 169 | + |
| 170 | + agoraKit.setClientRole(.broadcaster, options: nil) |
| 171 | + } |
| 172 | + |
| 173 | + /// make myself an audience |
| 174 | + func becomeAudience() { |
| 175 | + // unbind view |
| 176 | + agoraKit.setupLocalVideo(nil) |
| 177 | + // You have to provide client role options if set to audience |
| 178 | + let options = AgoraClientRoleOptions() |
| 179 | + options.audienceLatencyLevel = isUltraLowLatencyOn ? .ultraLowLatency : .lowLatency |
| 180 | + agoraKit.setClientRole(.audience, options: options) |
| 181 | + } |
| 182 | + |
| 183 | + func localVideoCanvas() -> UIView { |
| 184 | + return isLocalVideoForeground ? foregroundVideo.videoView : backgroundVideo.videoView |
| 185 | + } |
| 186 | + |
| 187 | + func remoteVideoCanvas() -> UIView { |
| 188 | + return isLocalVideoForeground ? backgroundVideo.videoView : foregroundVideo.videoView |
| 189 | + } |
| 190 | + |
| 191 | + @IBAction func onTapForegroundVideo(_ sender:UIGestureRecognizer) { |
| 192 | + isLocalVideoForeground = !isLocalVideoForeground |
| 193 | + |
| 194 | + let localVideoCanvas = AgoraRtcVideoCanvas() |
| 195 | + localVideoCanvas.uid = 0 |
| 196 | + localVideoCanvas.renderMode = .hidden |
| 197 | + localVideoCanvas.view = self.localVideoCanvas() |
| 198 | + |
| 199 | + let remoteVideoCanvas = AgoraRtcVideoCanvas() |
| 200 | + remoteVideoCanvas.renderMode = .hidden |
| 201 | + remoteVideoCanvas.view = self.remoteVideoCanvas() |
| 202 | + |
| 203 | + agoraKit.setupLocalVideo(localVideoCanvas) |
| 204 | + if let uid = remoteUid { |
| 205 | + remoteVideoCanvas.uid = uid |
| 206 | + agoraKit.setupRemoteVideo(remoteVideoCanvas) |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + @IBAction func onToggleClientRole(_ sender:UISwitch) { |
| 211 | + let role:AgoraClientRole = sender.isOn ? .broadcaster : .audience |
| 212 | + updateClientRole(role) |
| 213 | + } |
| 214 | + |
| 215 | + fileprivate func updateClientRole(_ role:AgoraClientRole) { |
| 216 | + self.role = role |
| 217 | + if(role == .broadcaster) { |
| 218 | + becomeBroadcaster() |
| 219 | + } else { |
| 220 | + becomeAudience() |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + @IBAction func onToggleUltraLowLatency(_ sender:UISwitch) { |
| 225 | + updateUltraLowLatency(sender.isOn) |
| 226 | + } |
| 227 | + |
| 228 | + fileprivate func updateUltraLowLatency(_ enabled:Bool) { |
| 229 | + if(self.role == .audience) { |
| 230 | + self.isUltraLowLatencyOn = enabled |
| 231 | + updateClientRole(.audience) |
| 232 | + } |
| 233 | + } |
| 234 | + |
| 235 | + override func willMove(toParent parent: UIViewController?) { |
| 236 | + if parent == nil { |
| 237 | + // leave channel when exiting the view |
| 238 | + if isJoined { |
| 239 | + agoraKit.leaveChannel { (stats) -> Void in |
| 240 | + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) |
| 241 | + } |
| 242 | + } |
| 243 | + } |
| 244 | + } |
| 245 | +} |
| 246 | + |
| 247 | +/// agora rtc engine delegate events |
| 248 | +extension LiveStreamingMain: AgoraRtcEngineDelegate { |
| 249 | + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out |
| 250 | + /// what is happening |
| 251 | + /// Warning code description can be found at: |
| 252 | + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html |
| 253 | + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html |
| 254 | + /// @param warningCode warning code of the problem |
| 255 | + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { |
| 256 | + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) |
| 257 | + } |
| 258 | + |
| 259 | + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand |
| 260 | + /// to let user know something wrong is happening |
| 261 | + /// Error code description can be found at: |
| 262 | + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html |
| 263 | + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html |
| 264 | + /// @param errorCode error code of the problem |
| 265 | + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { |
| 266 | + LogUtils.log(message: "error: \(errorCode)", level: .error) |
| 267 | + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") |
| 268 | + } |
| 269 | + |
| 270 | + /// callback when the local user joins a specified channel. |
| 271 | + /// @param channel |
| 272 | + /// @param uid uid of local user |
| 273 | + /// @param elapsed time elapse since current sdk instance join the channel in ms |
| 274 | + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { |
| 275 | + isJoined = true |
| 276 | + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) |
| 277 | + } |
| 278 | + |
| 279 | + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event |
| 280 | + /// @param uid uid of remote joined user |
| 281 | + /// @param elapsed time elapse since current sdk instance join the channel in ms |
| 282 | + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { |
| 283 | + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) |
| 284 | + |
| 285 | + //record remote uid |
| 286 | + remoteUid = uid |
| 287 | + |
| 288 | + // Only one remote video view is available for this |
| 289 | + // tutorial. Here we check if there exists a surface |
| 290 | + // view tagged as this uid. |
| 291 | + let videoCanvas = AgoraRtcVideoCanvas() |
| 292 | + videoCanvas.uid = uid |
| 293 | + // the view to be binded |
| 294 | + videoCanvas.view = remoteVideoCanvas() |
| 295 | + videoCanvas.renderMode = .hidden |
| 296 | + agoraKit.setupRemoteVideo(videoCanvas) |
| 297 | + } |
| 298 | + |
| 299 | + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event |
| 300 | + /// @param uid uid of remote joined user |
| 301 | + /// @param reason reason why this user left, note this event may be triggered when the remote user |
| 302 | + /// become an audience in live broadcasting profile |
| 303 | + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { |
| 304 | + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) |
| 305 | + |
| 306 | + //clear remote uid |
| 307 | + if(remoteUid == uid){ |
| 308 | + remoteUid = nil |
| 309 | + } |
| 310 | + |
| 311 | + // to unlink your view from sdk, so that your view reference will be released |
| 312 | + // note the video will stay at its last frame, to completely remove it |
| 313 | + // you will need to remove the EAGL sublayer from your binded view |
| 314 | + let videoCanvas = AgoraRtcVideoCanvas() |
| 315 | + videoCanvas.uid = uid |
| 316 | + // the view to be binded |
| 317 | + videoCanvas.view = nil |
| 318 | + videoCanvas.renderMode = .hidden |
| 319 | + agoraKit.setupRemoteVideo(videoCanvas) |
| 320 | + } |
| 321 | +} |
0 commit comments