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官網(http://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開源下載地址

在這裏插入圖片描述