集成iOS SDk前的准备:
(如果需要推送消息,则要到苹果官网上制作证书,再到环信后台制作推送证书http://www.easemob.com/docs/ios/IOSSDKPrepare/#registerDeveloper)
注册环信开发者账号并创建后台应用,
登陆地址:https://console.easemob.com/?comeFrom=easemobHome
注册模式为开放注册,填写对应信息后得到AppKey
需要用到的内容:
EMCallManagerDelegate语音视频代理
AVFoundation音频输出
EMCallSession会话信息
音视频集成过程:
1、在环信官网申请一个AppKey
2、导入SDK
环信官方下载SDK和官方Demo,打开Xcode工程将下载的iOS_IM_SDK_V3.6.0文件夹下的EaseUI(UI框架)文件夹与HyphenateFullSDK(包含实时通话)文件夹或HyphenateSDK(不包含实时通话功能的SDK)添加到项目工程中(注意勾选Copy items if needed、Create groups与Add to targets)
或
用cocoapods添加HyphenateSDK(不包含实时通话功能的SDK)、HyphenateFullSDK(包含实时通话的SDK)和EaseUI(集成sdk到项目中)
#Lite版本(不带实时音视频通话功能)
pod 'HyphenateLite'
#Full版本(带实时音视频通话功能)
pod 'Hyphenate'
在Podfile目录下执行指令pod install
注:用到一些Demo中的内容3rdparty、chatSDK、ChatUI
3、设置工程属性:
选择工程名-->TARGETS-->General-->Embedded Binaries添加Hyphenate.framework
HyphenateLite.framework
向General→Linked Frameworks andLibraies 中添加
HyphenateLite.framework(将【Required】改为【Optional】)
Hyphenate.framework(将【Required】改为【Optional】)
向Build Phases →Link Binary With Libraries 中添加依赖库:
CoreMedia.framework
AudioToolbox.framework
AVFoundation.framework
MobileCoreServices.framework
ImageIO.framework
libc++.dylib
libz.dylib
libstdc++.6.0.9.dylib
libsqlite3.dylib
libiconv.dylib
官方漏掉了的系统库:
CoreMotion.framework
UserNotifications.framework
AssetsLibrary.framework
MapKit.framework
Photos.framework
libbz2.1.0.dylib
后端云LeanCloud需要引入的依赖库:
libicucore.dylib
SystemConfiguration.framework
CoreTelephony.framework
CoreLocation.framework
(如果使用的是xcode7+,后缀为tbd)
(SDK不支持bitcode,Build Settings → 搜索bitcode →Enable Bitcode 中设置NO,此时编译不报错,集成成功)
4、编写页面逻辑,会话列表+聊天界面(环信有提供列表页,在EaseUI的子文件夹EMUIKit的子文件夹ViewController就已经包含了UI页面,找到EMiOSDemo→Class→Call+EMDemoHelper添加到自己的工程中,修改对应的EMDemoHelper工具类中的报错)
提醒:在集成时必须向工程导入Helper模块,然后在根据需求导入其他模块。
环信UI模块依赖于以下三方库:
pod 'Bugly'
pod 'Masonry'
pod 'MJRefresh'
pod 'MBProgressHUD', '~> 1.1.0'
pod 'SDWebImage', '~> 4.0'
pod 'SDWebImage/GIF'
pod 'FLAnimatedImage', '~> 1.0'
pod 'Hyphenate'
5、增加隐私权限,在工程info.plist文件里面添加隐私权限,用于Chat聊天模块发送图片,语音,视频,位置消息使用
Privacy - Photo Library Usage Description 需要访问您的相册
Privacy - Microphone Usage Description 需要访问您的麦克风(是否允许此使用你的麦克风)
Privacy - Camera Usage Description 需要访问您的相机(是否允许使用你的相机)
Privacy - Location Always Usage Description 需要您的同意,才能在使用期间访问位置
Privacy - Location When In Use Usage Description 需要您的同意,才能始终访问位置
6、.pch配置(从官方的Demo中的.pch中粘贴即可)
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
//包含实时音视频功能SDK 的头文件
#import <Hyphenate/Hyphenate.h>
// UI 头文件
#import "EMHeaders.h"
#endif
或
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
//不包含实时音视频功能SDK 的头文件
#import <HyphenateLite/HyphenateLite.h>
// UI 头文件
#import "EMHeaders.h"
#endif
7、初始化SDK及登录环信服务器
设置Appdelegate,参考Appdelegate.m文件即可
在Appdelegate中导入#import <Hyphenate/Hyphenate.h>头文件,然后在
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
方法中使用如下代码实现初始化即可:
// appkey替换成自己在环信管理后台注册应用中的appkey
EMOptions *options = [EMOptions optionsWithAppkey:@"appkey"];
// apnsCertName是证书名称,可以先传nil,等后期配置apns推送(苹果推送通知服务)时在传入证书名称
options.apnsCertName = nil;
[[EMClient sharedClient] initializeSDKWithOptions:options];
例如:
EMOptions *options = [EMOptions optionsWithAppkey:@"douser#istore"];
options.apnsCertName = @"istore_dev";
[[EMClient sharedClient] initializeSDKWithOptions:options];
// APP进入后台
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[[EMClient sharedClient] applicationDidEnterBackground:application];
}
// APP将要从后台返回
- (void)applicationWillEnterForeground:(UIApplication *)application
{
[[EMClient sharedClient] applicationWillEnterForeground:application];
}
注册模式分两种,开放注册和授权注册。
只有开放注册时,才可以客户端注册。开放注册是为了测试使用,正式环境中不推荐使用该方式注册环信账号。
授权注册的流程应该是您服务器通过环信提供的REST API 注册,之后保存到您的服务器或返回给客户端。
EMError *error = [[EMClient sharedClient] registerWithUsername:@"8001" password:@"111111"];
if (error==nil) {
NSLog(@"注册成功");
}
登录环信服务器:调用SDK 的登录接口进行的操作。
//传入在应用(appkey)下注册的IM用户8001,密码111111,用于登录环信服务器
EMError *error = [[EMClient sharedClient] loginWithUsername:@"8001" password:@"111111"];
if (!error) {
NSLog(@"登录成功");
}
如果在集成调试阶段,可以在初始化环信SDK 完成之后,就调用登录方法。
如果项目上线,建议开发者在登录自己服务器成功之后,再调用环信SDK 登录方法使用用户绑定的环信id登录环信服务器(开发者给自己用户在自己服务器创建账号的同时,调用环信的rest 接口在给用户授权注册一个环信id,一起返回给app 端,app 端拿到用户的账号密码以及环信id 密码分别登录自己的服务器以及环信服务器)。
8、初始化聊天界面
向工程导入Chat文件
// ConversationId接收消息方的环信ID:@"user2"
// type聊天类型:EMConversationTypeChat 单聊类型
// createIfNotExist 如果会话不存在是否创建会话:YES
EMChatViewController *chatController = [[EMChatViewController alloc] initWithConversationId:@"user2" type:EMConversationTypeChat createIfNotExist:YES];
[self.navigationController pushViewController:chatController animated:YES];
有导航的话,可以用push 方式跳转到聊天页面发消息测试,也就是用登录的user1 给user2 发消息,没有导航的话,可以用present 方式跳转到聊天页面。
集成实时音视频通话
9、集成实时音视频通话
向工程导入Chat文件
在初始化SDK 完成之后,在初始化SDK所在的类引入头文件:
#import "DemoCallManager.h" // 1v1实时通话功能的头文件
#import "DemoConfManager.h" // 多人实时通话功能的头文件
添加:
[DemoCallManager sharedManager]; // 初始化1v1实时通话功能的单例
[DemoConfManager sharedManager]; // 初始化多人实时通话功能的单例
提醒:在主控制器中添加[EMDemoHelper shareHelper].mainVC = self;
在聊天页面中下方,点击语音,视频通话图标按钮即可使用。
10、1V1实时通话
配置属性
进行音视频之前,设置全局的音视频属性,具体属性有哪些请查看头文件*EMCallOptions*
EMCallOptions *options = [[EMClient sharedClient].callManager getCallOptions];
//当对方不在线时,是否给对方发送离线消息和推送,并等待对方回应
options.isSendPushIfOffline = NO;
//设置视频分辨率:自适应分辨率、352 * 288、640 * 480、1280 * 720
options.videoResolution = EMCallVideoResolutionAdaptive;
//最大视频码率,范围50 < videoKbps < 5000, 默认0, 0为自适应,建议设置为0
options.maxVideoKbps = 0;
//最小视频码率
options.minVideoKbps = 0;
//是否固定视频分辨率,默认为NO
options.isFixedVideoResolution = NO;
[[EMClient sharedClient].callManager setCallOptions:options];
具体实现可以参考Demo: DemoCallManager 和EMCallViewController
发起实时通话
用户可以调用发起语音或者视频API 向在线用户发起实时通话。
/*!
* 发起实时会话
*
* @param aType 通话类型
* @param aRemoteName 被呼叫的用户(不能与自己通话)
* @param aExt 通话扩展信息,会传给被呼叫方
* @param aCompletionBlock 完成的回调
*/
- (void)startCall:(EMCallType)aType
remoteName:(NSString *)aRemoteName
ext:(NSString *)aExt
completion:(void (^)(EMCallSession *aCallSession, EMError *aError))aCompletionBlock;
示例代码:创建视频通话
void (^completionBlock)(EMCallSession *, EMError *) = ^(EMCallSession *aCallSession, EMError *aError){
//创建通话实例是否成功
//TODO: code
};
[[EMClient sharedClient].callManager startCall:EMCallTypeVideo remoteName:aUsername record:ext:nil completion:^(EMCallSession *aCallSession, EMError *aError) {
completionBlock(aCallSession, aError);
}];
被叫方同意实时通话
接收到通话时调用此API 同意实时通话。
/*!
* 接收方同意通话请求
*
* @param aCallId 通话ID
*
* @result 错误信息
*/
- (EMError *)answerIncomingCall:(NSString *)aCallId;
//调用:
//EMError *error = nil;
//error = [[EMClient sharedClient].callManager answerIncomingCall:@"sessionId"];
结束实时通话
根据不同场景可以选择结束会话的原因。
例如:拒接选择EMCallEndReasonDecline,主动挂断选择EMCallEndReasonHangup。
typedef enum{
EMCallEndReasonHangup = 0, /*! 对方挂断*/
EMCallEndReasonNoResponse, /*! 对方没有响应*/
EMCallEndReasonDecline, /*! 对方拒接*/
EMCallEndReasonBusy, /*! 对方占线*/
EMCallEndReasonFailed, /*! 失败*/
EMCallEndReasonUnsupported, /*! 功能不支持*/
}EMCallEndReason;
/*!
* 结束通话
*
* @param aCallId 通话的ID
* @param aReason 结束原因
*
* @result 错误
*/
- (EMError *)endCall:(NSString *)aCallId
reason:(EMCallEndReason)aReason;
//调用:
//[[EMClient sharedClient].callManager endCall:@"sessionId" reason:aReason];
创建通话页面
SDK提供了用于显示本地视频的页面类*EMCallLocalView*,显示对方视频的页面类*EMCallRemoteView*,建议在同意接通视频通话之后再初始化EMCallRemoteView页面。
//前提:EMCallSession *callSession 存在
CGFloat width = 80;
CGFloat height = self.view.frame.size.height / self.view.frame.size.width * width;
callSession.localVideoView = [[EMCallLocalView alloc] initWithFrame:CGRectMake(self.view.frame.size.width - 90, CGRectGetMaxY(_statusLabel.frame), width, height)];
[self.view addSubview:callSession.localVideoView];
//同意接听视频通话之后
callSession.remoteVideoView = [[EMCallRemoteView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
//设置视频页面缩放方式
callSession.remoteVideoView.scaleMode = EMCallViewScaleModeAspectFill;
[self.view addSubview:_callSession.remoteVideoView];
实时通话相关API
暂停恢复实时通话的数据传输相关API。
/*!
* 暂停语音数据传输
*
* @result 错误
*/
- (EMError *)pauseVoice;
/*!
* 恢复语音数据传输
*
* @result 错误
*/
- (EMError *)resumeVoice;
/*!
* 暂停视频图像数据传输
*
* @result 错误
*/
- (EMError *)pauseVideo;
/*!
* 恢复视频图像数据传输
*
* @result 错误
*/
- (EMError *)resumeVideo;
实时通话前后摄像头切换相关API
#pragma mark - Camera
/*!
* 设置使用前置摄像头还是后置摄像头,默认使用前置摄像头
*
* @param aIsFrontCamera 是否使用前置摄像头, YES使用前置, NO使用后置
*/
- (void)switchCameraPosition:(BOOL)aIsFrontCamera;
实时通话相关的回调
注册实时通话回调
//注册实时通话回调
[[EMClient sharedClient].callManager addDelegate:self delegateQueue:nil];
//移除实时通话回调
[[EMClient sharedClient].callManager removeDelegate:self];
相关回调说明:
/*!
* 用户A拨打用户B,用户B会收到这个回调
*
* @param aSession 会话实例
*/
- (void)callDidReceive:(EMCallSession *)aSession;
/*!
* 通话通道建立完成,用户A和用户B都会收到这个回调
*
* @param aSession 会话实例
*/
- (void)callDidConnect:(EMCallSession *)aSession;
/*!
* 用户B同意用户A拨打的通话后,双方都会收到这个回调
*
* @param aSession 会话实例
*/
- (void)callDidAccept:(EMCallSession *)aSession;
/*!
* 1. 用户A或用户B结束通话后,对方会收到该回调
* 2. 通话出现错误,双方都会收到该回调
*
* @param aSession 会话实例
* @param aReason 结束原因
* @param aError 错误
*/
- (void)callDidEnd:(EMCallSession *)aSession
reason:(EMCallEndReason)aReason
error:(EMError *)aError;
/*!
* 用户A和用户B正在通话中,用户A中断或者继续数据流传输时,用户B会收到该回调
*
* @param aSession 会话实例
* @param aType 改变类型
*/
- (void)callStateDidChange:(EMCallSession *)aSession
type:(EMCallStreamingStatus)aType;
弱网检测
通过回调通知应用当前实时通话网络状态。
typedef enum{
EMCallNetworkStatusNormal = 0, /*! 正常*/
EMCallNetworkStatusUnstable, /*! 不稳定*/
EMCallNetworkStatusNoData, /*! 没有数据*/
}EMCallNetworkStatus;
/*!
* 用户A和用户B正在通话中,用户A的网络状态出现不稳定,用户A会收到该回调
*
* @param aSession 会话实例
* @param aStatus 当前状态
*/
- (void)callNetworkDidChange:(EMCallSession *)aSession
status:(EMCallNetworkStatus)aStatus
离线发推送
配置属性
EMCallOptions *options = [[EMClient sharedClient].callManager getCallOptions];
//当对方不在线时,是否给对方发送离线消息和推送,并等待对方回应
options.isSendPushIfOffline = NO;
[[EMClient sharedClient].callManager setCallOptions:options];
监听回调
[[EMClient sharedClient].callManager setBuilderDelegate:self];
处理回调
- (void)callRemoteOffline:(NSString *)aRemoteName
{
NSString *text = [[EMClient sharedClient].callManager getCallOptions].offlineMessageText;
EMTextMessageBody *body = [[EMTextMessageBody alloc] initWithText:text];
NSString *fromStr = [EMClient sharedClient].currentUsername;
EMMessage *message = [[EMMessage alloc] initWithConversationID:aRemoteName from:fromStr to:aRemoteName body:body ext:@{@"em_apns_ext":@{@"em_push_title":text}}];
message.chatType = EMChatTypeChat;
[[EMClient sharedClient].chatManager sendMessage:message progress:nil completion:nil];
}
自定义视频数据
配置属性
//进行1v1自定义视频之前,必须设置EMCallOptions.enableCustomizeVideoData=YES
EMCallOptions *options = [[EMClient sharedClient].callManager getCallOptions];
options.enableCustomizeVideoData = YES;
[[EMClient sharedClient].callManager startCall:aType remoteName:aUsername ext:@"123" completion:^(EMCallSession *aCallSession, EMError *aError) {
completionBlock(aCallSession, aError);
}];
//进行默认1v1视频之前,必须设置EMCallOptions.enableCustomizeVideoData=NO
EMCallOptions *options = [[EMClient sharedClient].callManager getCallOptions];
options.enableCustomizeVideoData = NO;
[[EMClient sharedClient].callManager startCall:aType remoteName:aUsername ext:@"123" completion:^(EMCallSession *aCallSession, EMError *aError) {
completionBlock(aCallSession, aError);
}];
自定义摄像头数据
设置EMCallOptions.enableCustomizeVideoData=YES 后,必须自定义摄像头数据。
接口
/*!
* \~chinese
* 自定义本地视频数据
*
* @param aSampleBuffer 视频采样缓冲区
* @param aRotation 旋转方向
* @param aCallId 1v1会话实例ID,即[EMCallSession callId]
* @param aCompletionBlock 完成后的回调
*/
- (void)inputVideoSampleBuffer:(CMSampleBufferRef)aSampleBuffer
rotation:(UIDeviceOrientation)aRotation
callId:(NSString *)aCallId
completion:(void (^)(EMError *aError))aCompletionBlock;
/*!
* \~chinese
* 自定义本地视频数据
*
* @param aPixelBuffer 视频像素缓冲区
* @param aCallId 1v1会话实例ID,即[EMCallSession callId]
* @param aTime 视频原始数据时间戳,CMTime time = CMSampleBufferGetPresentationTimeStamp((CMSampleBufferRef)sampleBuffer);
* @param aRotation 旋转方向
* @param aCompletionBlock 完成后的回调
*/
- (void)inputVideoPixelBuffer:(CVPixelBufferRef)aPixelBuffer
sampleBufferTime:(CMTime)aTime
rotation:(UIDeviceOrientation)aRotation
callId:(NSString *)aCallId
completion:(void (^)(EMError *aError))aCompletionBlock;
11、集成其他模块
会话列表
向工程中导入Conversation 文件
头文件:#import “EMConversationsViewController.h”
初始化页面跳转(导航跳转示例):
EMConversationsViewController *conversationVC = [[EMConversationsViewController alloc] init];
[self.navigationController pushViewController:conversationVC animated:YES];
好友列表
向工程中导入Contact 文件
头文件:#import “EMContactsViewController.h”
初始化页面跳转(导航跳转示例):
EMContactsViewController *contactVC= [[EMContactsViewController alloc] init];
[self.navigationController pushViewController:contactVC animated:YES];
群组
向工程中导入Group 文件
头文件:#import “EMGroupsViewController.h”
初始化页面跳转(导航跳转示例):
EMGroupsViewController *groupVC= [[EMContactsViewController alloc] init];
[self.navigationController pushViewController:groupVC animated:YES];
聊天室
向工程中导入Chatroom 文件
头文件:#import “EMChatroomsViewController.h”
初始化页面跳转(导航跳转示例):
EMChatroomsViewController *chatRoomVC= [[EMChatroomsViewController alloc] init];
[self.navigationController pushViewController:chatRoomVC animated:YES];
12、集成动态库上传AppStore
由于iOS 编译的特殊性,为了方便开发者使用,我们将i386, x86_64, armv7, arm64 几个平台都合并到了一起,所以使用动态库上传appstore 时需要将i386 , x86_64 两个平台删除后,才能正常提交审核
在SDK 当前路径下执行以下命令删除i386, x86_64两个平台
bak文件是备份目录,上传appstore之后需要替换回bak 目录下的SDK
实时音视频版本Hyphenate.framework
mkdir ./bak
cp -r Hyphenate.framework ./bak
lipo Hyphenate.framework/Hyphenate -thin armv7 -output Hyphenate_armv7
lipo Hyphenate.framework/Hyphenate -thin arm64 -output Hyphenate_arm64
lipo -create Hyphenate_armv7 Hyphenate_arm64 -output Hyphenate
mv Hyphenate Hyphenate.framework/
不包含实时音视频版本HyphenateLite.framework
mkdir ./bak
cp -r HyphenateLite.framework ./bak
lipo HyphenateLite.framework/HyphenateLite -thin armv7 -output HyphenateLite_armv7
lipo HyphenateLite.framework/HyphenateLite -thin arm64 -output HyphenateLite_arm64
lipo -create HyphenateLite_armv7 HyphenateLite_arm64 -output HyphenateLite
mv HyphenateLite HyphenateLite.framework/
。。。。
EMiOSDemo说明:
Account:主要是demo 的注册,登录
AppDelegate:主要是demo 中初始化环信SDK,注册推送等
Call:demo的实时语音视频通话功能模块(包含1v1 实时通话以及多人实时通话的功能)
Chat:demo的聊天功能模块
Contact:demo的好友列表功能模块
Conversation:demo的会话列表功能模块
EMDemoHelper:demo的单例类,主要是全局监听接收消息,好友,群组,聊天室等相关事件的回调,从而进行对应的处理
Group:demo的群组功能模块
Helper:demo的功能性文件,自定义库和页面,第三方库,全局通用的配置模块
Home:demo的根控制器页面
Notification:demo的好友,群组相关请求通知的页面
Settings:demo的功能设置页面
Chatroom:聊天室模块
补充内容:
1、添加代理方法为:
[[EMClient sharedClient].callManager addDelegate:self delegateQueue:nil];
使用到的代理方法主要有:
- (void)callDidReceive:(EMCallSession *)aSession
//用户A拨打用户B用户B会收到这个回调、你希望在哪个页面可以监听被呼叫就把这个方法写在里面,记得遵守协议;
- (void)callDidConnect:(EMCallSession *)aSession //通话通道完成,可以在这里创建音频输出设备和环境AVAudioSession
- (void)callDidAccept:(EMCallSession *)aSession //用户B同意用户A的通话请求后,用户A会收到这个回调
- (void)callDidEnd:(EMCallSession )aSession reason:(EMCallEndReason)aReason error:(EMError )aError //用户A或用户B挂断后对方会收到这个回调。或者通话出现错误、双方都会收到该回调
2、创建一个语音或视频通话:
/*
* @param aType 通话类型
* @param aRemoteName 被呼叫的用户(不能与自己通话)
* @param aExt 通话扩展信息,会传给被呼叫方
* @param aCompletionBlock 完成的回调
*/
[[EMClient sharedClient].callManager startCall:aType remoteName:aRemoteName ext:aExt completion:^(EMCallSession *aCallSession, EMError *aError) {
if (!aError) {//创建成功
}else{
}
}];
同意别人的会话邀请:
/*
_callSession.callId会话ID
*/
[[EMClient sharedClient].callManager answerIncomingCall:_callSession.callId];
结束通话:
/*
_callSession.callId会话ID
aReason 挂断原因 (EMCallEndReason)
*/
[[EMClient sharedClient].callManager endCall:_callSession.callId reason:aReason];
3、对方挂断语言,可能要发送相应的消息(对方拒接 挂断等)在结束实时通话的回调中
- (void)callDidEnd:(EMCallSession *)aSession
reason:(EMCallEndReason)aReason
error:(EMError *)aError
根据EMCallEndReason这个枚举看通话结束的原因,去发送NSNotification通知
然后在聊天页面EaseMessageViewController.m中去监听这个通知,然后在通知方法中去插入消息,可参考以下代码:
NSString *insertStr = @"对方已挂断";
EMTextMessageBody *body = [[EMTextMessageBody alloc] initWithText:insertStr];
NSString *from = [[EMClient sharedClient] currentUsername];
//生成Message
EMMessage *message = [[EMMessage alloc] initWithConversationID:self.conversation.conversationId from:from to:self.conversation.conversationId body:body ext:nil];
message.chatType = EMChatTypeChat;// 设置为单聊消息
message.status = EMMessageStatusSucceed;
message.direction = callEnder;
[self addMessageToDataSource:message progress:nil];
[self.conversation insertMessage:message error:nil];
4、二次封装:
init方法重写(实时通话的代理注册EMChatManagerDelegate, EMCallManagerDelegate)
摄像头捕捉画面需要进行OutputAudioPort设置
如果要拨打语音通话可以修改拨打方法,添加通话类型参数
相关链接:
iOS接入环信单聊(+实时音视频):https://www.jianshu.com/p/fb1c1262f129
iOS环信视频语音 细节处理(挂断电话逻辑处理):https://www.imgeek.org/article/825309145
iOS如何集成环信实现即时通讯:https://jingyan.baidu.com/article/b87fe19e543a55521835681e.html
iOS环信视频通话的二次封装:https://www.jianshu.com/p/bafc400632f3
iOS基于环信SDK实现即时通讯-语音、视频聊天:https://blog.csdn.net/create_pro/article/details/64438747
IOS快速集成环信IM - 基于官方的Demo优化,5分钟集成环信IM功能:https://www.imgeek.org/article/825307886