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

语言: CN / TW / HK

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官网(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 *)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开源下载地址

在这里插入图片描述