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


ARCallPlus 简介

ARCallPlus 是 anyRTC 开源的音视频通话项目,同时支持iOS、Android、Web等平台。本文主要介绍音视频通话 ARUICalling 模块 iOS 本地库的封装。

源码下载

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

  • GitHub 开源地址
  • App Store 下载地址

开发环境

  • 开发工具:Xcode13 真机运行

  • 开发语言:Objective-C、Swift

项目结构

arcallplus_structure

核心 API:

  • ARUILogin(登录 API)
  • ARUICalling(通话 API)
  • ARUICallingListerner(通话回调)

内部核心 API:

  • ARTCCalling(音视频)
  • ARTCCallingDelegate(音视频回调)
  • ARTCCalling+Signal(实时消息)

核心 API 和回调

@interface ARUICalling : NSObject

+ (instancetype)shareInstance;

/// 通话接口
/// @param users 用户信息
/// @param type 呼叫类型:视频/语音
- (void)call:(NSArray *)users type:(ARUICallingType)type;

/// 通话回调
/// @param listener 回调
- (void)setCallingListener:(id)listener;

/// 设置铃声,建议在30s以内,只支持本地音频文件
/// @param filePath 音频文件路径
- (void)setCallingBell:(NSString *)filePath;

/// 开启静音模式(默认关)
- (void)enableMuteMode:(BOOL)enable;

/// 开启自定义路由(默认关)
/// @param enable 打开后,在onStart回调中,会收到对应的ViewController对象,可以自行决定视图展示方式
- (void)enableCustomViewRoute:(BOOL)enable;

@end


@protocol ARUICallingListerner 

// 收到呼叫时,先通过此方法询问是否可以唤起被叫UI.
// 返回为true,直接唤起UI。返回为false,内部返回忙线
// 不实现默认直接可以唤起UI
- (BOOL)shouldShowOnCallView NS_SWIFT_NAME(shouldShowOnCallView());

/// 呼叫开始回调。主叫、被叫均会触发;
/// 被叫触发时,会将控制器通过监听回调出来,由接入方决定显示方案。
/// @param userIDs 本次通话用户id(自己除外)
/// @param type 通话类型:视频\音频
/// @param role 通话角色:主叫\被叫
/// @param viewController 提供Calling功能页面给调用方,可以让用户在此基础上自定义
- (void)callStart:(NSArray *)userIDs type:(ARUICallingType)type role:(ARUICallingRole)role viewController:(UIViewController * _Nullable)viewController NS_SWIFT_NAME(callStart(userIDs:type:role:viewController:));

/// 通话结束回调
/// @param userIDs 本次通话用户id(自己除外)
/// @param type 通话类型:视频\音频
/// @param role 通话角色:主叫\被叫
/// @param totalTime 通话时长
- (void)callEnd:(NSArray *)userIDs type:(ARUICallingType)type role:(ARUICallingRole)role totalTime:(float)totalTime NS_SWIFT_NAME(callEnd(userIDs:type:role:totalTime:));

/// 通话事件回调
/// @param event 回调事件类型
/// @param type 通话类型:视频\音频
/// @param role 通话角色:主叫\被叫
/// @param message 事件
- (void)onCallEvent:(ARUICallingEvent)event type:(ARUICallingType)type role:(ARUICallingRole)role message:(NSString *)message NS_SWIFT_NAME(onCallEvent(event:type:role:message:));

/// 推送事件回调
/// @param userIDs 不在线的用户id
/// @param type 通话类型:视频\音频
- (void)onPushToOfflineUser:(NSArray *)userIDs type:(ARUICallingType)type;

@end

示例代码

效果展示(点对点音频通话)

arcallplus_audio

代码实现
- (ARtcEngineKit *)rtcEngine {
    if (!_rtcEngine) {
        /// 实例化音视频引擎对象
        _rtcEngine = [ARtcEngineKit sharedEngineWithAppId:[ARUILogin getSdkAppID] delegate:self];
        /// 直播模式
        [_rtcEngine setChannelProfile: ARChannelProfileLiveBroadcasting];
        [_rtcEngine setClientRole: ARClientRoleBroadcaster];
        /// 编码配置
        ARVideoEncoderConfiguration *configuration = [[ARVideoEncoderConfiguration alloc] init];
        configuration.dimensions = CGSizeMake(960, 540);
        configuration.frameRate = 15;
        configuration.bitrate = 500;
        [_rtcEngine setVideoEncoderConfiguration:configuration];
        /// 启用说话者音量提示
        [_rtcEngine enableAudioVolumeIndication:2000 smooth:3 report_vad: YES];
        /// 开启美颜
        [_rtcEngine setBeautyEffectOptions:YES options:[[ARBeautyOptions alloc]init]];
    }
    return _rtcEngine;
}

//MARK: - Publish Method

- (void)call:(NSArray *)userIDs type:(CallType)type {
    /// 发起通话
    if (!self.isOnCalling) {
        self.curLastModel.inviter = [ARUILogin getUserID];
        self.curLastModel.action = CallAction_Call;
        self.curLastModel.calltype = type;
        self.curRoomID = [NSString stringWithFormat:@"%d", [ARTCCallingUtils generateRoomID]];
        self.isMembers = userIDs.count >= 2 ? YES : NO;
        self.calleeUserIDs = [@[] mutableCopy];
        
        self.curType = type;
        self.isOnCalling = YES;
        self.isBeingCalled = NO;
        [self joinRoom];
        [self createMemberChannel];
    }
    
    // 如果不在当前邀请列表,则新增
    NSMutableArray *newInviteList = [NSMutableArray array];
    for (NSString *userID in userIDs) {
        if (![self.curInvitingList containsObject:userID]) {
            [newInviteList addObject:userID];
        }
    }
    
    [self.curInvitingList addObjectsFromArray:newInviteList];
    [self.calleeUserIDs addObjectsFromArray:newInviteList];
    
    if (!(self.curInvitingList && self.curInvitingList.count > 0)) return;
    self.currentCallingUserID = newInviteList.firstObject;
    for (NSString *userID in self.curInvitingList) {
        [self invite:userID action:CallAction_Call];
    }
}

- (void)accept:(BOOL)isVideo {
    /// 接受当前通话
    ARLog(@"Calling - accept Call");
    if (!isVideo) {
        [self.rtcEngine disableVideo];
        self.curType = CallType_Audio;
        if ([self canDelegateRespondMethod:@selector(onSwitchToAudio:message:)]) {
            [self.delegate onSwitchToAudio:YES message:@""];
        }
    }
    
    [self joinRoom];
    self.currentCallingUserID = self.curSponsorForMe;
    [self invite:self.curSponsorForMe action:CallAction_Accept];
    self.isCallSucess = YES;
    [self dealWithException:10];
}

- (void)reject {
    /// 拒绝当前通话
    ARLog(@"Calling - reject Call");
    [self invite:self.curSponsorForMe action:CallAction_Reject];
    self.isOnCalling = NO;
    [self.rtcEngine disableVideo];
}

- (void)hangup {
    /// 主动挂断通话
    __block BOOL hasCallUser = NO;
    [self.curRoomList enumerateObjectsUsingBlock:^(NSString *user, NSUInteger idx, BOOL * _Nonnull stop) {
        if ((user && user.length > 0) && ![self.curInvitingList containsObject:user]) {
            // 还有正在通话的用户
            hasCallUser = YES;
            [self invite:user action:CallAction_End];
            *stop = YES;
        }
    }];
    
    /// 主叫需要取消未接通的通话
    if (hasCallUser == NO) {
        ARLog(@"Calling - GroupHangup Send CallAction_Cancel");
        [self.curInvitingList enumerateObjectsUsingBlock:^(NSString *invitedId, NSUInteger idx, BOOL * _Nonnull stop) {
            [self invite:invitedId action:CallAction_Cancel];
        }];
    }

    [self leaveRoom];
    self.isOnCalling = NO;
}


- (void)switchToAudio {
    /// 切换到语音通话(通话中)
    self.curType = CallType_Audio;
    [self.rtcEngine disableVideo];
    [self invite:self.currentCallingUserID action:CallAction_SwitchToAudio];
    
    if ([self canDelegateRespondMethod:@selector(onSwitchToAudio:message:)]) {
        [self.delegate onSwitchToAudio:YES message:@""];
    }
}

- (void)startRemoteView:(NSString *)userID view:(UIView *)view {
    /// 开启远程用户视频渲染
    ARLog(@"Calling - startRemoteView userID = %@", userID);
    if (userID.length != 0) {
        ARtcVideoCanvas *canvas = [[ARtcVideoCanvas alloc] init];
        canvas.uid = userID;
        canvas.view = view;
        [self.rtcEngine setupRemoteVideo:canvas];
    }
}

- (void)stopRemoteView:(NSString *)userID {
    /// 关闭远程用户视频渲染
    ARLog(@"Calling - stopRemoteView userID = %@", userID);
    ARtcVideoCanvas *canvas = [[ARtcVideoCanvas alloc] init];
    canvas.uid = userID;
    canvas.view = nil;
    [self.rtcEngine setupRemoteVideo:canvas];
}

- (void)openCamera:(BOOL)frontCamera view:(UIView *)view {
    /// 打开摄像头
    ARLog(@"Calling - openCamera");
    if (self.curType == CallType_Video) {
        [self.rtcEngine enableVideo];
    }
    ARtcVideoCanvas *canvas = [[ARtcVideoCanvas alloc] init];
    canvas.uid = [ARUILogin getUserID];
    canvas.view = view;
    [self.rtcEngine setupLocalVideo:canvas];
    [self.rtcEngine startPreview];
    self.isFrontCamera = frontCamera;
}

效果展示(点对点视频通话)

arcallplus_video

代码实现
//MARK: - ARtcEngineDelegate

- (void)rtcEngine:(ARtcEngineKit *)engine didOccurError:(ARErrorCode)errorCode {
    /// 发生错误回调
    ARLog(@"Calling - didOccurError = %ld", (long)errorCode);
}

- (void)rtcEngine:(ARtcEngineKit *)engine firstRemoteVideoDecodedOfUid:(NSString *)uid size:(CGSize)size elapsed:(NSInteger)elapsed {
    ARLog(@"Calling - firstRemoteVideoDecodedOfUid = %@", uid);
}

- (void)rtcEngine:(ARtcEngineKit *)engine didJoinedOfUid:(NSString *)uid elapsed:(NSInteger)elapsed {
    /// 远端用户/主播加入回调
    ARLog(@"Calling - didJoinedOfUid = %@", uid);
    // C2C curInvitingList 不要移除 userID,如果是自己邀请的对方,这里移除后,最后发结束信令的时候找不到人
    [self dealWithException:0];
    [self removeTimer:uid];
    if ([self.curInvitingList containsObject:uid]) {
        [self.curInvitingList removeObject:uid];
    }
    if (![self.curRoomList containsObject:uid]) {
        [self.curRoomList addObject:uid];
    }
    // C2C 通话要计算通话时长
    if ([self canDelegateRespondMethod:@selector(onUserEnter:)]) {
        [self.delegate onUserEnter:uid];
    }
}

- (void)rtcEngine:(ARtcEngineKit *)engine didOfflineOfUid:(NSString *)uid reason:(ARUserOfflineReason)reason {
    /// 远端用户(通信场景)/主播(直播场景)离开当前频道回调
    ARLog(@"Calling - didOfflineOfUid = %@", uid);
    // C2C curInvitingList 不要移除 userID,如果是自己邀请的对方,这里移除后,最后发结束信令的时候找不到人
    if (self.isMembers || (!self.isMembers && reason == ARUserOfflineReasonQuit)) {
        if ([self.curInvitingList containsObject:uid]) {
            [self.curInvitingList removeObject:uid];
        }
        if ([self.curRoomList containsObject:uid]) {
            [self.curRoomList removeObject:uid];
        }
        if ([self canDelegateRespondMethod:@selector(onUserLeave:)]) {
            [self.delegate onUserLeave:uid];
        }
        [self preExitRoom];
    } else if (reason == ARUserOfflineReasonDropped) {
        [self dealWithException:10];
    }
}

- (void)rtcEngine:(ARtcEngineKit *)engine remoteVideoStateChangedOfUid:(NSString *)uid state:(ARVideoRemoteState)state reason:(ARVideoRemoteStateReason)reason elapsed:(NSInteger)elapsed {
    /// 远端视频状态发生改变回调
    if (reason == ARVideoRemoteStateReasonRemoteMuted || reason == ARVideoRemoteStateReasonRemoteUnmuted) {
        if ([self canDelegateRespondMethod:@selector(onUserVideoAvailable:available:)]) {
            [self.delegate onUserVideoAvailable:uid available:(reason == ARVideoRemoteStateReasonRemoteMuted) ? NO : YES];
        }
    }
}

- (void)rtcEngine:(ARtcEngineKit *)engine remoteAudioStateChangedOfUid:(NSString *)uid state:(ARAudioRemoteState)state reason:(ARAudioRemoteStateReason)reason elapsed:(NSInteger)elapsed {
    /// 远端音频状态发生改变回调
    if (reason == ARAudioRemoteReasonRemoteMuted || reason == ARAudioRemoteReasonRemoteUnmuted) {
        if ([self canDelegateRespondMethod:@selector(onUserAudioAvailable:available:)]) {
            [self.delegate onUserAudioAvailable:uid available:(reason == ARAudioRemoteReasonRemoteMuted) ? NO : YES];
        }
    }
}

- (void)rtcEngine:(ARtcEngineKit *)engine reportAudioVolumeIndicationOfSpeakers:(NSArray *)speakers totalVolume:(NSInteger)totalVolume {
    /// 提示频道内谁正在说话、说话者音量及本地用户是否在说话的回调
    if ([self canDelegateRespondMethod:@selector(onUserVoiceVolume:volume:)]) {
        for (ARtcAudioVolumeInfo *info in speakers) {
            if ([info.uid isEqualToString:@"0"]) {
                [self.delegate onUserVoiceVolume:[ARUILogin getUserID] volume:(UInt32)info.volume];
            } else {
                [self.delegate onUserVoiceVolume:info.uid volume:(UInt32)info.volume];
            }
        }
    }
}

- (void)rtcEngine:(ARtcEngineKit *)engine connectionChangedToState:(ARConnectionStateType)state reason:(ARConnectionChangedReason)reason {
    //ARLog(@"Calling - rtc connectionStateChanged state = %ld reason = %ld", (long)state, (long)reason);
}

- (void)rtcEngine:(ARtcEngineKit *)engine didVideoSubscribeStateChange:(NSString *)channel withUid:(NSString *)uid oldState:(ARStreamSubscribeState)oldState newState:(ARStreamSubscribeState)newState elapseSinceLastState:(NSInteger)elapseSinceLastState {
    ARLog(@"Calling - didVideoSubscribeStateChange = %@ %@ %lu %lu", channel, uid, (unsigned long)oldState, (unsigned long)newState);
}

效果展示(多人视频通话)

arcallplus_videos

代码实现

- (void)addSignalListener {
    ARUILogin.kit.aRtmDelegate = self;
    self.callEngine.callDelegate = self;
    /// 用户进入后台推送问题
    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(enterBackground:) name:UIApplicationWillResignActiveNotification object:nil];
    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(becomeActive:) name:UIApplicationWillEnterForegroundNotification object:nil];
}

- (void)removeSignalListener {
    self.callEngine.callDelegate = nil;
    self.callEngine = nil;
    [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
    [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
}

- (void)invite:(NSString *)receiver action:(CallAction)action {
    if (action == CallAction_Call) {
        /// 发起呼叫邀请
        NSMutableArray *arr = [[NSMutableArray alloc] initWithObjects:[ARUILogin getUserID], nil];
        [arr addObjectsFromArray:self.calleeUserIDs];
        
        NSMutableArray *infoArr = [NSMutableArray array];
        for (NSInteger i = 0; i < arr.count; i++) {
            ARCallUser *user = [ARUILogin getCallUserInfo:arr[i]];
            [infoArr addObject:[NSObject ar_dictionaryWithObject: user]];
        }
        
        NSDictionary *dic = @{@"Mode": @(self.curType == CallType_Video ? 0 : 1),
                             @"Conference": [NSNumber numberWithBool:self.isMembers],
                             @"ChanId": self.curRoomID,
                             @"UserData": arr,
                             @"UserInfo": infoArr
        };
        ARtmLocalInvitation *localInvitation = [[ARtmLocalInvitation alloc] initWithCalleeId:receiver];
        localInvitation.content = [ARTCCallingUtils dictionary2JsonStr:dic];
        [self.callEngine sendLocalInvitation:localInvitation completion:^(ARtmInvitationApiCallErrorCode errorCode) {
            ARLog(@"sendLocalInvitation code = %ld", (long)errorCode);
        }];
        [self.callingDic setObject:localInvitation forKey:receiver];
    } else if (action == CallAction_Cancel) {
        /// 取消呼叫邀请
        id invitation = [self.callingDic objectForKey:receiver];
        if (invitation) {
            ARtmLocalInvitation *localInvitation = (ARtmLocalInvitation *)invitation;
            [self.callEngine cancelLocalInvitation:localInvitation completion:^(ARtmInvitationApiCallErrorCode errorCode) {
                ARLog(@"cancelLocalInvitation code = %ld", (long)errorCode);
            }];
        }
    } else if (action == CallAction_Accept) {
        /// 接受呼叫邀请
        id invitation = [self.calledDic objectForKey:receiver];
        if (invitation) {
            ARtmRemoteInvitation *remoteInvitation = (ARtmRemoteInvitation *)invitation;
            NSDictionary *dic = @{@"Mode": @(self.curType == CallType_Video ? 0: 1), @"Conference": [NSNumber numberWithBool:self.isMembers]};
            remoteInvitation.response = [ARTCCallingUtils dictionary2JsonStr:dic];
            [self.callEngine acceptRemoteInvitation:remoteInvitation completion:^(ARtmInvitationApiCallErrorCode errorCode) {
                ARLog(@"acceptRemoteInvitation code = %ld", (long)errorCode);
            }];
        }
    } else if (action == CallAction_Reject) {
        /// 拒绝呼叫邀请
        id invitation = [self.calledDic objectForKey:receiver];
        if (invitation) {
            ARtmRemoteInvitation *remoteInvitation = (ARtmRemoteInvitation *)invitation;
            [self.callEngine refuseRemoteInvitation:remoteInvitation completion:^(ARtmInvitationApiCallErrorCode errorCode) {
                ARLog(@"refuseRemoteInvitation code = %ld", (long)errorCode);
            }];
        }
    } else if (action == CallAction_SwitchToAudio) {
        /// 切换成语音通话
        NSDictionary *dic = [[NSDictionary alloc] initWithObjectsAndKeys:@"SwitchAudio", @"Cmd",nil];
        ARtmMessage *message = [[ARtmMessage alloc] initWithText:[ARTCCallingUtils dictionary2JsonStr:dic]];
        [self sendPeerMessage:message user:receiver];
    } else if (action == CallAction_End) {
        /// 通话中断
        if (!self.isMembers) {
            NSDictionary *dic = [[NSDictionary alloc] initWithObjectsAndKeys:@"EndCall", @"Cmd",nil];
            ARtmMessage *message = [[ARtmMessage alloc] initWithText:[ARTCCallingUtils dictionary2JsonStr:dic]];
            [self sendPeerMessage:message user:receiver];
        }
    }
}

- (void)preExitRoom {
    /// 当前房间中存在成员,不能自动退房
    if (self.curRoomList.count > 0) return;
    
    /// 存在正在呼叫的通话
    if (self.curInvitingList.count >= 1) {
        return;
    }
    
    [self exitRoom];
}

- (void)exitRoom {
    ARLog(@"Calling - exitRoom");
    if ([self canDelegateRespondMethod:@selector(onCallEnd)]) {
        [self.delegate onCallEnd];
    }
    
    for (NSString *uid in self.timerDic.allKeys) {
        [self removeTimer:uid];
    }
    
    [self dealWithException:0];
    [self leaveRoom];
    self.isOnCalling = NO;
    
    if(UIApplication.sharedApplication.applicationState == UIApplicationStateBackground) {
        [self logout];
    }
}

// MARK: - privite

- (ARtmCallKit *)callEngine {
    return [ARUILogin.kit getRtmCallKit];
}

- (void)sendPeerMessage:(ARtmMessage *)message user:(NSString *)uid {
    ARLog(@"Calling - sendPeerMessage = %@", message.text);
    ARtmSendMessageOptions *options = [[ARtmSendMessageOptions alloc] init];
    [[ARUILogin kit] sendMessage:message toPeer:uid sendMessageOptions:options completion:^(ARtmSendPeerMessageErrorCode errorCode) {
        ARLog(@"Calling - SendPeerMessage code = %ld", (long)errorCode);
    }];
}

- (void)logout {
    if (!self.isOnCalling && ARUILogin.kit != nil) {
        [ARUILogin.kit logoutWithCompletion:nil];
        self.interrupt = YES;
    }
}

- (void)enterBackground:(NSNotification *)notification {
    [self logout];
}

- (void)becomeActive:(NSNotification *)notification {
    if (!self.isOnCalling && self.interrupt) {
        [ARUILogin.kit loginByToken:nil user:ARUILogin.getUserID completion:nil];
        self.interrupt = NO;
    }
}

//MARK: - ARtmDelegate

- (void)rtmKit:(ARtmKit *)kit connectionStateChanged:(ARtmConnectionState)state reason:(ARtmConnectionChangeReason)reason {
    ARLog(@"Calling - rtm connectionStateChanged state = %ld reason = %ld", (long)state, (long)reason);
    if (reason == ARtmConnectionChangeReasonRemoteLogin) {
        [self.delegate onError:401 msg:@"RemoteLogin"];
        [self exitRoom];
        return;
    }
    
    if (!self.isMembers) {
        if (state == ARtmConnectionStateDisconnected || state == ARtmConnectionStateReconnecting) {
            self.isReconnection = YES;
            [self dealWithException: 30];
            
        } else if (state == ARtmConnectionStateConnected) {
            [self dealWithException:0];
            
            if (self.isReconnection && self.isOnCalling && self.currentCallingUserID) {
                /// 兼容异常
                [self dealWithException:10];
                self.isReconnection = NO;
                NSDictionary *dic = [[NSDictionary alloc] initWithObjectsAndKeys:@"CallState", @"Cmd",nil];
                ARtmMessage *message = [[ARtmMessage alloc] initWithText:[ARTCCallingUtils dictionary2JsonStr:dic]];
                [self sendPeerMessage:message user:self.currentCallingUserID];
            }
        }
    }
}

- (void)rtmKit:(ARtmKit *)kit messageReceived:(ARtmMessage *)message fromPeer:(NSString *)peerId {
    /// 收到点对点消息回调
    ARLog(@"Calling - messageReceived text = %@ ", message.text);
    if (message.text.length != 0) {
        NSDictionary *dic = [ARTCCallingUtils jsonSring2Dictionary:message.text];
        NSString *value = [dic objectForKey:@"Cmd"];
        if ([value isEqualToString:@"SwitchAudio"]) {
            /// 切换成语音通话
            if ([self canDelegateRespondMethod:@selector(onSwitchToAudio:message:)]) {
                [self.delegate onSwitchToAudio:YES message:@""];
            }
            self.curType = CallType_Audio;
            
        } else if ([value isEqualToString:@"EndCall"]) {
            /// 结束通话
            if (!self.isMembers) {
                if ([self canDelegateRespondMethod:@selector(onUserLeave:)]) {
                    [self.delegate onUserLeave:peerId];
                }
                [self exitRoom];
            }
        } else if ([value isEqualToString:@"CallState"]) {
            /// 确认通话状态
            NSDictionary *dic;
            if (self.isCallSucess) {
                dic = @{@"Cmd": @"CallStateResult", @"state": @(2), @"Mode": (self.curType == CallType_Video ? @(0) : @(1))};
            } else {
                dic= @{@"Cmd": @"CallStateResult", @"state": @(1)};
            }
            ARtmMessage *message = [[ARtmMessage alloc] initWithText:[ARTCCallingUtils dictionary2JsonStr:dic]];
            [self sendPeerMessage:message user:peerId];
            
        } else if ([value isEqualToString:@"CallStateResult"]) {
            /// 对方通话状态回复结果
            [self dealWithException:0];
            
            int state = [[dic objectForKey:@"state"] intValue];
            if (state == 0) {
                /// 已挂断
                [self exitRoom];
            } else if (state == 1) {
                /// 呼叫等待
            } else {
                /// 已同意
                int mode = [[dic objectForKey:@"Mode"] intValue];
                if (self.curType == CallType_Video && mode == 1) {
                    self.curType = CallType_Audio;
                    [self switchToAudio];
                }
            }
        }
    }
}

// MARK: - ARtmCallDelegate

- (void)rtmCallKit:(ARtmCallKit * _Nonnull)callKit localInvitationReceivedByPeer:(ARtmLocalInvitation * _Nonnull)localInvitation {
    /// 被叫已收到呼叫邀请
    ARLog(@"Calling - localInvitationReceivedByPeer");
}

- (void)rtmCallKit:(ARtmCallKit * _Nonnull)callKit localInvitationAccepted:(ARtmLocalInvitation * _Nonnull)localInvitation withResponse:(NSString * _Nullable) response {
    /// 被叫已接受呼叫邀请
    ARLog(@"Calling - localInvitationAccepted response = %@", response);
    [self.callingDic removeObjectForKey:localInvitation.calleeId];
    if (response != nil) {
        NSDictionary * dic = [ARTCCallingUtils jsonSring2Dictionary:response];
        if (self.curType == CallType_Video && [[dic objectForKey:@"Mode"] intValue] == 1) {
            if ([self canDelegateRespondMethod:@selector(onSwitchToAudio:message:)]) {
                [self.delegate onSwitchToAudio:YES message:@""];
            }
            self.curType = CallType_Audio;
        }
    }
    
    self.isCallSucess = YES;
}

- (void)rtmCallKit:(ARtmCallKit * _Nonnull)callKit localInvitationRefused:(ARtmLocalInvitation * _Nonnull)localInvitation withResponse:(NSString * _Nullable) response {
    /// 被叫已拒绝呼叫邀请
    ARLog(@"Calling - localInvitationRefused");
    [self.callingDic removeObjectForKey:localInvitation.calleeId];
    
    BOOL isBusy = NO;
    if (localInvitation.response.length != 0) {
        NSDictionary * dic = [ARTCCallingUtils jsonSring2Dictionary:localInvitation.response];
        if ([dic.allValues containsObject:@"Calling"]) {
            isBusy = YES;
        }
    }
    
    if (self.delegate) {
        NSString *uid = localInvitation.calleeId;
        if ([self.curInvitingList containsObject:uid]) {
            [self.curInvitingList removeObject:uid];
        }
        if (isBusy) {
            if ([self canDelegateRespondMethod:@selector(onLineBusy:)]) {
                [self.delegate onLineBusy:localInvitation.calleeId];
            }
        } else {
            if ([self canDelegateRespondMethod:@selector(onReject:)]) {
                [self.delegate onReject:uid];
            }
        }
        [self preExitRoom];
    }
}

- (void)rtmCallKit:(ARtmCallKit * _Nonnull)callKit localInvitationCanceled:(ARtmLocalInvitation * _Nonnull)localInvitation {
    /// 呼叫邀请已被取消
    ARLog(@"Calling - localInvitationCanceled");
    [self.callingDic removeObjectForKey:localInvitation.calleeId];
}

- (void)rtmCallKit:(ARtmCallKit * _Nonnull)callKit localInvitationFailure:(ARtmLocalInvitation * _Nonnull)localInvitation errorCode:(ARtmLocalInvitationErrorCode)errorCode {
    /// 呼叫邀请发送失败
    ARLog(@"Calling - localInvitationFailure");
    NSString *calleeId = localInvitation.calleeId;
    [self.callingDic removeObjectForKey:calleeId];
    
    if ([self canDelegateRespondMethod:@selector(onNoResp:)]) {
        [self.delegate onNoResp:calleeId];
    }
    if ([self.curInvitingList containsObject:calleeId]) {
        [self.curInvitingList removeObject:calleeId];
    }
    [self preExitRoom];
}

- (void)rtmCallKit:(ARtmCallKit * _Nonnull)callKit remoteInvitationReceived:(ARtmRemoteInvitation * _Nonnull)remoteInvitation {
    /// 收到一个呼叫邀请
    ARLog(@"Calling - remoteInvitationReceived");
    [self.calledDic setObject:remoteInvitation forKey:remoteInvitation.callerId];
    if (!self.isOnCalling) {
        self.isOnCalling = YES;
        self.curSponsorForMe = remoteInvitation.callerId;
        self.currentCallingUserID = remoteInvitation.callerId;
        
        NSDictionary *dic = [ARTCCallingUtils jsonSring2Dictionary:remoteInvitation.content];
        self.isMembers = [[dic objectForKey:@"Conference"] boolValue];
        self.curRoomID = [dic objectForKey:@"ChanId"];
        CallType type = ([[dic objectForKey:@"Mode"] intValue] == 0) ? CallType_Video : CallType_Audio;
        self.curType = type;
        if ([dic.allKeys containsObject:@"UserInfo"]) {
            NSArray *infoArr = [dic objectForKey:@"UserInfo"];
            for (NSInteger i = 0; i < infoArr.count; i++) {
                ARCallUser *user = [ARCallUser ar_objectWithDictionary: infoArr[i]];
                [ARUILogin setCallUserInfo:user];
            }
        }
        
        if (self.isMembers) {
            /// 多人通话
            NSArray *arr = [dic objectForKey:@"UserData"];
            [self.delegate onInvited:remoteInvitation.callerId userIds:arr isFromGroup:NO callType:type];
            [self createMemberChannel];
            
            /// 30s
            for (NSInteger i = 0; i < arr.count; i++) {
                NSString *uid = arr[i];
                /// 被叫对其他受邀者倒计时 -- 异常处理
                if (![uid isEqualToString:[ARUILogin getUserID]] && ![uid isEqualToString:self.curSponsorForMe]) {
                    __block NSInteger totalTime = 0;
                    NSTimeInterval interval = 1.0;
                    __weak typeof(self) weakSelf = self;
                    NSString *timerName = [ARTCGCDTimer timerTask:^{
                        totalTime += (NSInteger)interval;
                        if (totalTime == 30) {
                            if ([weakSelf canDelegateRespondMethod:@selector(onNoResp:)]) {
                                [weakSelf.delegate onNoResp:uid];
                                [weakSelf removeTimer: uid];
                            }
                        }
                        ARLog(@"%@ ==> %ld \n", uid, (long)totalTime);
                    } start:0 interval:interval repeats:YES async:NO];
                    [self.timerDic setObject:timerName forKey:uid];
                }
            }
        } else {
            /// 单人通话
            [self.delegate onInvited:remoteInvitation.callerId userIds:@[[ARUILogin getUserID]] isFromGroup:NO callType:type];
        }
    } else {
        NSDictionary *dic = [[NSDictionary alloc] initWithObjectsAndKeys:@"Calling", @"Cmd",nil];
        remoteInvitation.response = [ARTCCallingUtils dictionary2JsonStr:dic];
        [self invite:remoteInvitation.callerId action:CallAction_Reject];
    }
}

- (void)rtmCallKit:(ARtmCallKit * _Nonnull)callKit remoteInvitationRefused:(ARtmRemoteInvitation * _Nonnull)remoteInvitation {
    /// 拒绝呼叫邀请成功
    ARLog(@"Calling - remoteInvitationRefused");
    [self.calledDic removeObjectForKey:remoteInvitation.callerId];
}

- (void)rtmCallKit:(ARtmCallKit * _Nonnull)callKit remoteInvitationAccepted:(ARtmRemoteInvitation * _Nonnull)remoteInvitation {
    /// 接受呼叫邀请成功
    ARLog(@"Calling - remoteInvitationAccepted");
    [self.calledDic removeObjectForKey:remoteInvitation.callerId];
}

- (void)rtmCallKit:(ARtmCallKit * _Nonnull)callKit remoteInvitationCanceled:(ARtmRemoteInvitation * _Nonnull)remoteInvitation {
    /// 主叫已取消呼叫邀请
    ARLog(@"Calling - remoteInvitationCanceled");
    [self.calledDic removeObjectForKey:remoteInvitation.callerId];
    
    if ([self.curSponsorForMe isEqualToString:remoteInvitation.callerId]) {
        [self exitRoom];
        if ([self canDelegateRespondMethod:@selector(onCallingCancel:)]) {
            [self.delegate onCallingCancel:remoteInvitation.callerId];
        }
    }
}

结束语

最后,ARCallPlus开源项目中还存在一些bug和待完善的功能点。有不足之处欢迎大家指出issues。最后再贴一下 Github开源下载地址。

Github开源下载地址。

在这里插入图片描述