IOS技术分享| ARCallPlus 开源项目(二)
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开源下载地址。