集成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

 

06-27 18:27