IOS 技术分享| ARCallPlus 开源项目(二)
- 2022 年 4 月 27 日
本文字数:6655 字
阅读完需:约 22 分钟

ARCallPlus 简介
ARCallPlus 是 anyRTC 开源的音视频通话项目,同时支持 iOS、Android、Web 等平台。上一篇我们介绍了 ARUICalling 开源组件的封装,本篇主要介绍如何通过 ARUICalling 组件来实现音视频通话效果。
源码下载
三行代码、二十分钟应用内构建,实现音视频通话。本项目已上架 App Store,欢迎下载体验。
开发环境
开发工具:Xcode13 真机运行
开发语言:Objective-C、Swift
项目结构
示例 demo 目录:
LoginViewController (登录)
RegisterViewController (注册)
MainViewController (首页)
CallingViewController(发起音视频通话)
MineViewController (我的)
ARUICalling 组件核心 API:
ARUILogin(登录 API)
ARUICalling(通话 API)
ARUICallingListerner(通话回调)
组件集成
步骤一:导入 ARUICalling 组件
通过 cocoapods 导入组件,具体步骤如下:
在您的工程 Podfile 文件同一级目录下创建 ARUICalling 文件夹。
从 Github 下载代码,然后将 ARUICalling/iOS/ 目录下的 Source、Resources 文件夹 和 ARUICalling.podspec 文件拷贝到您在 步骤 1 创建的 ARUICalling 文件夹下。
在您的 Podfile 文件中添加以下依赖,之后执行 pod install 命令,完成导入。
# :path => "指向ARUICalling.podspec的相对路径"pod 'ARUICalling', :path => "ARUICalling/ARUICalling.podspec", :subspecs => ["RTC"]
步骤二:配置权限
使用音视频功能,需要授权麦克风和摄像头的使用权限。
<key>NSCameraUsageDescription</key><string>ARCallPlus请求访问麦克风用于视频通话?</string><key>NSMicrophoneUsageDescription</key><string>ARCallPlus请求访问麦克风用于语音交流?</string>
推送权限(可选)
步骤三:初始化组件
anyRTC 为 App 开发者签发的 App ID。每个项目都应该有一个独一无二的 App ID。如果你的开发包里没有 App ID,请从 anyRTC 官网(https://www.anyrtc.io)申请一个新的 App ID
/// 初始化 ARUILogin.initWithSdkAppID(AppID) /// 登录 ARUILogin.login(localUserModel!) { success() print("Calling - login sucess") } fail: { code in failed(code.rawValue) print("Calling - login fail") }
步骤四:实现音视频通话
/// 发起通话ARUICalling.shareInstance().call(users: ["123"], type: .video)/// 通话回调ARUICalling.shareInstance().setCallingListener(listener: self)
步骤五:离线推送(可选)如果您的业务场景需要在 App 的进程被杀死后或者 App 退到后台后,还可以正常接收到音视频通话请求,就需要为 ARUICalling 组件增加推送功能,可参考 demo 中推送逻辑(极光推送为例)。
// MARK: - ARUICallingListerner
/// 推送事件回调/// @param userIDs 不在线的用户id/// @param type 通话类型:视频\音频- (void)onPushToOfflineUser:(NSArray<NSString *> *)userIDs type:(ARUICallingType)type;
示例代码
效果展示(注册登录)
代码实现
/// 检查是否登录 /// - Returns: 是否存在 func existLocalUserData() -> Bool { if let cacheData = UserDefaults.standard.object(forKey: localUserDataKey) as? Data { if let cacheUser = try? JSONDecoder().decode(LoginModel.self, from: cacheData) { localUserModel = cacheUser localUid = cacheUser.userId /// 获取 Authorization exists(uid: localUid!) { } failed: { error in } return true } } return false } /// 查询设备信息是否存在 /// - Parameters: /// - uid: 用户id /// - success: 成功回调 /// - failed: 失败回调 func exists(uid: String, success: @escaping ()->Void, failed: @escaping (_ error: Int)->Void) { ARNetWorkHepler.getResponseData("jpush/exists", parameters: ["uId": uid, "appId": AppID] as [String : AnyObject], headers: false) { [weak self] result in let code = result["code"].rawValue as! Int if code == 200 { let model = LoginModel(jsonData: result["data"]) if model.device != 2 { /// 兼容异常问题 self?.register(uid: model.userId, nickName: model.userName, headUrl: model.headerUrl, success: { success() }, failed: { error in failed(error) }) } else { self?.localUserModel = model do { let cacheData = try JSONEncoder().encode(model) UserDefaults.standard.set(cacheData, forKey: localUserDataKey) } catch { print("Calling - Save Failed") } success() } } else { failed(code) } } error: { error in print("Calling - Exists Error") self.receiveError(code: error) } } /// 初始化设备信息 /// - Parameters: /// - uid: 用户id /// - nickName: 用户昵称 /// - headUrl: 用户头像 /// - success: 成功回调 /// - failed: 失败回调 func register(uid: String, nickName: String, headUrl: String, success: @escaping ()->Void, failed: @escaping (_ error: Int)->Void) { ARNetWorkHepler.getResponseData("jpush/init", parameters: ["appId": AppID, "uId": uid, "device": 2, "headerImg": headUrl, "nickName": nickName] as [String : AnyObject], headers: false) { [weak self]result in print("Calling - Server init Sucess") let code = result["code"].rawValue as! Int if code == 200 { let model = LoginModel(jsonData: result["data"]) self?.localUserModel = model do { let cacheData = try JSONEncoder().encode(model) UserDefaults.standard.set(cacheData, forKey: localUserDataKey) } catch { print("Calling - Save Failed") } success() } else { failed(code) } success() } error: { error in print("Calling - Server init Error") self.receiveError(code: error) } }
/// 当前用户登录 /// - Parameters: /// - success: 成功回调 /// - failed: 失败回调 @objc func loginRTM(success: @escaping ()->Void, failed: @escaping (_ error: NSInteger)->Void) { ARUILogin.initWithSdkAppID(AppID) ARUILogin.login(localUserModel!) { success() print("Calling - login sucess") } fail: { code in failed(code.rawValue) print("Calling - login fail") } /// 配置极光别名 JPUSHService.setAlias(localUid, completion: { iResCode, iAlias, seq in }, seq: 0) }
效果展示(主页我的)
代码实现
func setupUI() { addLoading() navigationItem.leftBarButtonItem = barButtonItem view.addSubview(bgImageView) view.addSubview(collectionView) bgImageView.snp.makeConstraints { make in make.edges.equalToSuperview() } collectionView.snp.makeConstraints { make in make.edges.equalToSuperview() } } func loginRtm() { ProfileManager.shared.loginRTM { [weak self] in guard let self = self else { return } UIView.animate(withDuration: 0.8) { self.loadingView.alpha = 0 } completion: { result in self.loadingView.removeFromSuperview() } CallingManager.shared.addListener() print("Calling - LoginRtm Sucess") } failed: { [weak self] error in guard let self = self else { return } if error == 9 { self.loadingView.removeFromSuperview() self.refreshLoginState() } print("Calling - LoginRtm Fail") } } var menus: [MenuItem] = [ MenuItem(imageName: "icon_lock", title: "隐私条例"), MenuItem(imageName: "icon_log", title: "免责声明"), MenuItem(imageName: "icon_register", title: "anyRTC官网"), MenuItem(imageName: "icon_time", title: "发版时间", subTitle: "2022.03.10"), MenuItem(imageName: "icon_sdkversion", title: "SDK版本", subTitle: String(format: "V %@", "1.0.0")), MenuItem(imageName: "icon_appversion", title: "软件版本", subTitle: String(format: "V %@", Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! CVarArg)) ]
override func viewDidLoad() { super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations // self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller. // self.navigationItem.rightBarButtonItem = self.editButtonItem view.backgroundColor = UIColor(hexString: "#F5F6FA") navigationItem.leftBarButtonItem = barButtonItem tableView.tableFooterView = UIView() tableView.tableHeaderView = headView tableView.tableHeaderView?.height = ARScreenHeight * 0.128 tableView.separatorColor = UIColor(hexString: "#DCDCDC") }
效果展示(呼叫通话)
代码实现
@objc func sendCalling() { CallingManager.shared.callingType = callType! let type: ARUICallingType = (callType == .video || callType == .videos) ? .video : .audio ARUICalling.shareInstance().call(users: selectedUsers!, type: type) } class CallingManager: NSObject { @objc public static let shared = CallingManager() private var callingVC = UIViewController() public var callingType: CallingType = .audio func addListener() { ARUICalling.shareInstance().setCallingListener(listener: self) ARUICalling.shareInstance().enableCustomViewRoute(enable: true) }}
extension CallingManager: ARUICallingListerner { func shouldShowOnCallView() -> Bool { /// 作为被叫是否拉起呼叫页面,若为 false 直接 reject 通话 return true } func callStart(userIDs: [String], type: ARUICallingType, role: ARUICallingRole, viewController: UIViewController?) { print("Calling - callStart") if let vc = viewController { callingVC = vc; vc.modalPresentationStyle = .fullScreen let topVc = topViewController() topVc.present(vc, animated: false, completion: nil) } } func callEnd(userIDs: [String], type: ARUICallingType, role: ARUICallingRole, totalTime: Float) { print("Calling - callEnd") callingVC.dismiss(animated: true) {} } func onCallEvent(event: ARUICallingEvent, type: ARUICallingType, role: ARUICallingRole, message: String) { print("Calling - onCallEvent event = \(event.rawValue) type = \(type.rawValue)") if event == .callRemoteLogin { ProfileManager.shared.removeAllData() ARAlertActionSheet.showAlert(titleStr: "账号异地登录", msgStr: nil, style: .alert, currentVC: topViewController(), cancelBtn: "确定", cancelHandler: { action in ARUILogin.logout() AppUtils.shared.showLoginController() }, otherBtns: nil, otherHandler: nil) } }}
推送模块
代码实现
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. ///【注册通知】通知回调代理 let entity: JPUSHRegisterEntity = JPUSHRegisterEntity() entity.types = NSInteger(UNAuthorizationOptions.alert.rawValue) | NSInteger(UNAuthorizationOptions.sound.rawValue) | NSInteger(UNAuthorizationOptions.badge.rawValue) JPUSHService.register(forRemoteNotificationConfig: entity, delegate: self) ///【初始化sdk】 JPUSHService.setup(withOption: launchOptions, appKey: jpushAppKey, channel: channel, apsForProduction: isProduction) changeBadgeNumber() return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { /// sdk注册DeviceToken JPUSHService.registerDeviceToken(deviceToken) }
extension CallingManager: ARUICallingListerner { func onPush(toOfflineUser userIDs: [String], type: ARUICallingType) { print("Calling - toOfflineUser \(userIDs)") ProfileManager.shared.processPush(userIDs: userIDs, type: callingType) }} /// 推送接口 /// - Parameters: /// - userIDs: 离线人员id /// - type: 呼叫类型( 0/1/2/3:p2p音频呼叫/p2p视频呼叫/群组音频呼叫/群组视频呼叫) func processPush(userIDs: [String], type: CallingType) { ARNetWorkHepler.getResponseData("jpush/processPush", parameters: ["caller": localUid as Any, "callee": userIDs, "callType": type.rawValue, "pushType": 0, "title": "ARCallPlus"] as [String : AnyObject], headers: true) { result in print("Calling - Offline Push Sucess == \(result)") } error: { error in print("Calling - Offline Push Error") self.receiveError(code: error) } }
结束语
最后,ARCallPlus 开源项目中还存在一些 bug 和待完善的功能点。有不足之处欢迎大家指出 issues。最后再贴一下 Github 开源下载地址。
版权声明: 本文为 InfoQ 作者【anyRTC开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/ce8a040a1f0a36fa04a5293d2】。文章转载请联系作者。
anyRTC开发者
实时交互,万物互联! 2020.08.10 加入
实时交互,万物互联,全球实时互动云服务商领跑者!










评论