IOS技术分享| ARCallPlus 开源项目(二)

540 阅读5分钟

ARCallPlus 简介

ARCallPlus 是 anyRTC 开源的音视频通话项目,同时支持iOS、Android、Web等平台。上一篇我们介绍了ARUICalling 开源组件的封装,本篇主要介绍如何通过 ARUICalling 组件来实现音视频通话效果。

源码下载

三行代码、二十分钟应用内构建,实现音视频通话。本项目已上架App Store,欢迎下载体验。

开发环境

  • 开发工具:Xcode13 真机运行

  • 开发语言:Objective-C、Swift

项目结构

arcallplus_structure

示例 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>

arcall_plus_camera

  • 推送权限(可选) arcall_plus_remotenotifi

步骤三:初始化组件

anyRTC 为 App 开发者签发的 App ID。每个项目都应该有一个独一无二的 App ID。如果你的开发包里没有 App ID,请从anyRTC官网(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;

示例代码

效果展示(注册登录)

arcallplus_register

代码实现
        /// 检查是否登录
    /// - 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)
    }

效果展示(主页我的)

arcallplus_main

代码实现
    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")
    }


效果展示(呼叫通话)

arcallplus_call

代码实现

    @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开源下载地址。

Github开源下载地址

在这里插入图片描述