即时通讯
1. 即时通讯简介
即时通讯英文名为:Instant Messaging,简称IM。
即时通讯(Instant messaging,简称IM)是一个终端服务,允许两人或多人使用网路即时的传递文字讯息、档案、语音与视频交流。即时通讯按使用用途分为企业即时通讯和网站即时通讯,根据装载的对象又可分为手机即时通讯和PC即时通讯,手机即时通讯代表是QQ,微信。
2. 即时通讯的代表作
主流的代表:Skype/QQ/Google Talk/WhatsApp/Instagram/LINE/Kik/Wechat/Facebook Messenger/Yahoo! Messenger/MSN Messenger/ICQ/IChat
3. 如何实现即时通讯
即时通讯实现需要开发者写一个通讯协议,比如服务器的通讯协议是一致的,服务器跟服务器之间进行数据的传输,A客户端和B客户端就能进行数据的传输。
协议:定义一个标准,如何传输数据和客户端如何通讯。4. iOS中如何实现即时通讯
- 使用Socket写一个通讯协议(
自己写一个协议
) - 使用
XMPPframework
第三方框架 - 使用国内第三方框架
融云
- 使用国内第三框架
环信
- 使用国内第三方框架
LeanCloud
- 使用国内第三方框架
阿里悟空
- ...
5. 以上几种方式简单分析
各行各业的App使用的通讯框架各有差异,但是实现的功能都是相似的,目前站在程序员的角度来观看,环信提供的接口和服务器都是相对要稳定很多,最重要的是他们的客服有几次凌晨来咨询我环信使用得怎么样。都快感动爬了。
简单介绍下两款比较新的框架
LeanCloud
:是网易推出的即时通讯云服务器,使用这个框架的公司目前主要是网易新闻、网易云音乐和网易花田等其他的App。
阿里悟空
:阿里抱着对社交一直不死心的心态下推出的阿里悟空即时通讯云,主要App案例是大姨吗、钉钉等
6. 先研究环信的使用
EaseMob简介
环信官网:http://www.easemob.com
环信是北京易掌云峰科技有限公司推出的即时通讯云平台,环信将基于移动互联网的即时通讯能力通过云端开放的 Rest API 和客户端 SDK 包的方式提供给开发者和企业。
环信全面支持iOS、Android、Web等多种平台,在流量、电量、长连接、语音、位置、安全等能力做了极致的优化,让移动开发者摆脱繁重的移动IM通讯底层开发,最大限度地缩短产品开发周期,最短的时间内让App拥有移动IM能力。
简单的说:只要集成了EaseMobSDK,然后做简单的配置,实现简单的代码便能让你的App实现聊天的功能
环信是基于Jabber/XMPP协议的即时通讯服务器
接下里实现的效果
EaseMobSDK的导入
1. 提前准备
- 下载iOS的环信SDK
- 注册环信即时通讯云账号
- 登陆到管理后台
- 在我的应用中创建一个应用
- 在苹果的个人开发中心创建一个推送证书(当然不创建也没用关系,只是不能推送消息而已)
- 创建完证书导出p12文件
- 在我的应用中点击你的应用选择推送证书
- 新增证书选择p12文件上传
2. SDK导入
- 将下载完的环信SDK中的EaseMobSDK拖入到项目中
- EaseMobSDK中的lib文件夹中包含以下两个.a文件
- libEaseMobClientSDK:包含所有功能
- libEaseMobClientSDKLite:不包含实时语音
- 所以只需要保留一个
- 同时需要在include文件夹中也需要删除一个文件夹
- EaseMobSDK目录结构
- EaseMobSDK
- include(包含对应功能服务的头文件)
- CallService(语音服务)
- ChatService(聊天服务)
- EaseMobClientSDK(客户端主要使用的SDK头文件)
- Utility(硬件相关接口和错误码定义)
- lib(静态库)
- resources(资源文件)
- include(包含对应功能服务的头文件)
- EaseMobSDK
- 在AppDelegate中的didFinishLaunchingWithOptions注册EaseMobSDK
// 注册SDK// kEaseMobAppKey:环信后台管理->我的应用->对应的应用->应用概述->应用标识// kEaseMobPushName:环信后台管理->我的应用->对应的应用->应用概述->推送证书->iOS->证书名称[[EaseMob sharedInstance] registerSDKWithAppKey:kEaseMobAppKey apnsCertName:kEaseMobPushName];
- 此时会报很多错误
- 需要导入框架
- MobileCoreServices.framework
- CFNetwork.framework
- libEaseMobClientSDKLite.a
- libsqlite3.dylib
- libstdc++.6.0.9.dylib
- libz.dylib
- libiconv.dylib
- libresolv.dylib
- libxml2.dylib
- 需要对象做配置
- Build Settings->Linking->Other Linker Flags 中 添加-ObjC 或者 force_load 静态库路径
- 需要导入框架
- SDK集成完毕
应用程序生命周期方法中实现环信中对应的方法
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{ [[EaseMob sharedInstance] application:application didFinishLaunchingWithOptions:launchOptions]; return YES;}// App进入后台- (void)applicationDidEnterBackground:(UIApplication *)application{ [[EaseMob sharedInstance] applicationDidEnterBackground:application];}// App将要从后台返回- (void)applicationWillEnterForeground:(UIApplication *)application{ [[EaseMob sharedInstance] applicationWillEnterForeground:application];}// 申请处理时间- (void)applicationWillTerminate:(UIApplication *)application{ [[EaseMob sharedInstance] applicationWillTerminate:application];}
EaseMob项目架构的搭建
1. 创建根控制器
- rootNavigationController:根导航控制器
- rootViewController:控制器所有的共同的设置应该在这里设置
- contentView:继承自UIScrollView替代控制的根view
EaseMob 注册
注意点:
- 注册账号不能为中文
- 在环信后台管理创建应用时需要选择开放注册
聊天管理器
- 获取聊天管理器对象后,可以做登陆、聊天等操作
- 获取方式[EaseMob sharedInstance].chatManager
- 聊天管理器其实就是遵守了一堆功能操作的协议
注册账号的方式
/*! @method @brief 在聊天服务器上创建账号 @discussion @param username 用户名 @param password 密码 @param pError 错误信息 @result 是否注册成功 */- (BOOL)registerNewAccount:(NSString *)username password:(NSString *)password error:(EMError **)pError;/*! @method @brief 异步方法, 在聊天服务器上创建账号 @discussion 在注册过程中, EMChatManagerLoginDelegate中的didRegisterNewAccount:password:error:回调会被触发 @param username 用户名 @param password 密码 @result */- (void)asyncRegisterNewAccount:(NSString *)username password:(NSString *)password;/*! @method @brief 异步方法, 在聊天服务器上创建账号 @discussion @param username 用户名 @param password 密码 @param completion 回调 @param aQueue 回调时的线程 @result */- (void)asyncRegisterNewAccount:(NSString *)username password:(NSString *)password withCompletion:(void (^)(NSString *username, NSString *password, EMError *error))completion onQueue:(dispatch_queue_t)aQueue;
- 我们一般是使用异步block方式注册
- 其它的功能一般也是使用异步block方式
EaseMob登陆
登陆方式
- 使用异步block方式登陆
/*! @method @brief 使用用户名密码登录聊天服务器 @discussion 如果登陆失败, 返回nil @param username 用户名 @param password 密码 @param pError 错误信息 @result 登录后返回的用户信息 */- (NSDictionary *)loginWithUsername:(NSString *)username password:(NSString *)password error:(EMError **)pError;/*! @method @brief 异步方法, 使用用户名密码登录聊天服务器 @discussion 在登陆过程中, EMChatManagerLoginDelegate中的didLoginWithInfo:error:回调会被触发 @param username 用户名 @param password 密码 @result */- (void)asyncLoginWithUsername:(NSString *)username password:(NSString *)password;/*! @method @brief 异步方法, 使用用户名密码登录聊天服务器 @discussion @param username 用户名 @param password 密码 @param completion 回调 @param aQueue 回调时的线程 @result */- (void)asyncLoginWithUsername:(NSString *)username password:(NSString *)password completion:(void (^)(NSDictionary *loginInfo, EMError *error))completion onQueue:(dispatch_queue_t)aQueue;
- 关闭打印数据
[[EaseMob sharedInstance] registerSDKWithAppKey:kEaseMobAppKey apnsCertName:kEaseMobPushName otherConfig:@{kSDKConfigEnableConsoleLogger:@(NO)}];
- 查看登陆成功的信息
- 登陆成功之后切换窗口的跟控制器
- 在AppDelegate中提供一个登陆成功的方法用来切换控制器
2. 自动登陆
- 实现原理
- 在登陆成功之后将登陆信息存储到沙盒中
- 下次程序启动从沙盒中拿到用户名和密码直接调用登陆的接口
- 以上操作环信SDK已经做好了,我们只需要设置自动登陆的属性即可(setIsAutoLoginEnabled)
- 登陆完成调用代理方法
// 自动登陆完成的回调方法- (void)didAutoLoginWithInfo:(NSDictionary *)loginInfo error:(EMError *)error{ NSLog(@"loginInfo = %@",loginInfo); [MBProgressHUD hideAllHUDsForView:self.window animated:YES]; if (error) { [[TKAlertCenter defaultCenter]postAlertWithMessage:@"登陆失败"]; }else{ [[TKAlertCenter defaultCenter]postAlertWithMessage:@"登陆成功"]; [self loginSuccess]; }}
- 登陆完来到主页,设置tabbar的图片和文字颜色
3. 重新连接
- 使用真机调试
- 添加代理,遵守代理协议EMChatManagerDelegate
- 实现代理方法即可
/** * 即将自动连接 */- (void)willAutoReconnect{ NSLog(@"即将重新连接"); self.title = @"连接中...";}/** * 自动连接结束 * */- (void)didAutoReconnectFinishedWithError:(NSError *)error{ NSLog(@"连接完成"); if (!error) { self.title = @"聊天"; }}/** * 连接状态发生改变调用 * */- (void)didConnectionStateChanged:(EMConnectionState)connectionState{ switch (connectionState) { case eEMConnectionConnected: NSLog(@"连接成功"); self.title = @"连接成功"; break; case eEMConnectionDisconnected: NSLog(@"连接失败"); self.title = @"连接失败"; break; default: break; }}
EaseMob退出登陆
1. 退出登陆
- 主动退出登陆
- 被动退出登陆
- 账号多处登陆被顶
- 正在登陆的账号在服务端被移除
2. 退出登陆的方式
/*! @method @brief 注销当前登录用户 @discussion 当接收到【didLoginFromOtherDevice】和【didRemovedFromServer】的回调时,调用此方法,isUnbind传NO @param isUnbind 是否解除device token @param pError 错误信息 @result 返回注销信息 */- (NSDictionary *)logoffWithUnbindDeviceToken:(BOOL)isUnbind error:(EMError **)pError;/*! @method @brief 异步方法, 注销当前登录用户 @discussion 当接收到【didLoginFromOtherDevice】和【didRemovedFromServer】的回调时,调用此方法,isUnbind传NO @result 完成后【didLogoffWithError:】回调会被触发. */- (void)asyncLogoffWithUnbindDeviceToken:(BOOL)isUnbind;/*! @method @brief 异步方法, 注销当前登录用户 @discussion 当接收到【didLoginFromOtherDevice】和【didRemovedFromServer】的回调时,调用此方法,isUnbind传NO @param completion 回调 @param aQueue 回调时的线程 @result */- (void)asyncLogoffWithUnbindDeviceToken:(BOOL)isUnbind completion:(void (^)(NSDictionary *info, EMError *error))completion onQueue:(dispatch_queue_t)aQueue;
- 建议主动退出登陆isUnbind 传YES,被迫退出登陆传NO
- 退出成功后在AppDelegate里提供切换控制器方法,并且设置不再自动登陆
EaseMob添加好友
通讯录界面搭建
- 在导航栏左侧添加一个添加按钮
- 点击按钮的时候弹出输入框
添加好友
- 方式一
- 要发送添加好友的username 和请求信息
- 返回的BOOL值YES代表请求添加好友成功,NO代表失败
BOOL addSuccess = [[EaseMob sharedInstance].chatManager addBuddy:addBuddyNameField.text message:addBuddyMsgField.text error:nil]; if (addSuccess) { [[TKAlertCenter defaultCenter] postAlertWithMessage:@"添加好友请求成功"]; }
- 方式二
- 要发送添加好友的username 和请求信息
- 发送将好友分到哪个分组中
- 返回的BOOL值YES代表请求添加好友成功,NO代表失败
BOOL addSuccess = [[EaseMob sharedInstance].chatManager addBuddy:addBuddyNameField.text message:addBuddyMsgField.text toGroups:@[@"XMG"] error:nil] if (addSuccess) { [[TKAlertCenter defaultCenter] postAlertWithMessage:@"添加好友请求成功"]; }
添加好友成功
- 在添加好友成功之后没有刷新表格
- 也就是没有调用didUpdateBuddyList代理方法
- 那么可以实现didAcceptedByBuddy代理方法
- 在didAcceptedByBuddy中重新获取好友列表并且刷新表格
EaseMob获取好友列表
获取好友列表
- 如果每次都需要请求好友列表用户体验会不好
- 所以我们需要在一次请求到好友列表之后存储到本地数据库
- 这些操作环信已经给我们做好了
- 获取本地好友列表
[[EaseMob sharedInstance].chatManager buddyList];
- 如果本地没有那么再去服务端获取
[[EaseMob sharedInstance].chatManager asyncFetchBuddyListWithCompletion:^(NSArray *buddyList, EMError *error) { NSLog(@"====%@",buddyList); _buddies = buddyList; } onQueue:nil];
EaseMob接收好友请求
使用代理方法处理
- 设置代理
- 实现代理方法
- (void)didReceiveBuddyRequest:(NSString *)username message:(NSString *)message{}
- 在代理方法中可以做相应的处理
- 同意添加请求
BOOL isSuccess = [[EaseMob sharedInstance].chatManager acceptBuddyRequest:username error:nil];
- 拒绝添加请求
BOOL isSuccess = [[EaseMob sharedInstance].chatManager rejectBuddyRequest:username reason:@"不想加" error:nil];
EaseMob删除好友
1. 当前用户移除好友
- 实现tableView的代理方法
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{}
- 删除好友
[[EaseMob sharedInstance].chatManager removeBuddy:buddy.username removeFromRemote:YES error:nil];
2. 当前用户被好友移除
- 会调用以下代理方法
- (void)didRemovedByBuddy:(NSString *)username{}
EaseMob聊天界面的搭建
1.主要的设计
- 封装底部的工具条
- 添加四个子控件
- 默认设置发送语音按钮隐藏
- 当点击发送语音的时候隐藏输入框显示语音按钮
- 当输入文字的时候点击键盘上的return使用block方式通知控制器
- 点击加号按钮隐藏键盘弹出自定义view
- 自定义view中添加发送图片、语音和视频按钮
- 封装模仿微信聊天的Cell
EaseMob发送好友消息
发送消息
- 使用异步发送
[[EaseMob sharedInstance].chatManager asyncSendMessage:msg progress:nil prepare:^(EMMessage *message, EMError *error) { NSLog(@"即将发送消息"); } onQueue:nil completion:^(EMMessage *message, EMError *error) { if (!error) { NSLog(@"发送消息成功"); } } onQueue:nil];
- 发送一条消息需要创建一个消息对象
// 创建一个消息对象EMMessage *msg = [[EMMessage alloc]initWithReceiver:ctr.buddy.username bodies:@[body]];
- 创建一个消息对象需要创建一个消息体
// 创建一个消息体EMTextMessageBody *body = [[EMTextMessageBody alloc]initWithChatObject:chatText];
- 创建一个消息体需要创建一个文本消息实例
// 创建一个文本消息实例EMChatText *chatText = [[EMChatText alloc]initWithText:textField.text];
2.消息发送成功之后的操作
- 将消息存储到数组中
- 刷新表格
- 清空输入框
- 滚动到tableView的底部
EaseMob接收好友消息
1.接收在线消息
- 设置代理
- 实现代理方法
// 接收到好友消息- (void)didReceiveMessage:(EMMessage *)message{ NSLog(@"message =====%@",message);}
2.接收聊天消息需要注意
- 判断是否是与当前好友聊天
// 判断是不是当前好友if (![message.from isEqualToString:self.buddy.username]) return;
- 判断消息体的类型(单聊、群聊、聊天室)
// 判断消息类型// 单聊、群聊、聊天室if (message.messageType != eMessageTypeChat) return;
- 获取消息体中的内容
- 添加到数组中
- 刷新表格
- 滚动到最后一行
id body = [message.messageBodies firstObject];if ([body isKindOfClass:[EMTextMessageBody class]]) { EMTextMessageBody *textBody = body; NSLog(@"text = %@ message = %@",textBody.text,textBody.message); [_dataSources addObject:textBody.message]; // 刷新表格 [_tableView reloadData]; [self scrollLastRow];}
EaseMob发送语音消息
监听按钮的点击状态
// 开始录音- (void)start:(XMGButton *)btn{ if (self.delegate && [self.delegate respondsToSelector:@selector(toolViewRecord:withType:)]) { [self.delegate toolViewRecord:btn withType:XMGToolViewRecordStart]; }}// 结束录音- (void)stop:(XMGButton *)btn{ if (self.delegate && [self.delegate respondsToSelector:@selector(toolViewRecord:withType:)]) { [self.delegate toolViewRecord:btn withType:XMGToolViewRecordStop]; }}// 退出录音- (void)cancel:(XMGButton *)btn{ if (self.delegate && [self.delegate respondsToSelector:@selector(toolViewRecord:withType:)]) { [self.delegate toolViewRecord:btn withType:XMGToolViewRecordCancel]; }}
- 正在录音:UIControlEventTouchDown
- 调用环信EMCDDeviceManager的开始录音方法
[[EMCDDeviceManager sharedInstance] asyncStartRecordingWithFileName:fileName completion:^(NSError *error) { if (!error) { NSLog(@"====正在录音 %@",fileName); } }];
- 自定义文件名
- 为了避免文件名重复所以使用当前时间加上一个随机数
- 录音结束:UIControlEventTouchUpInside
- 调用环信EMCDDeviceManager的停止录音方法
[[EMCDDeviceManager sharedInstance] asyncStopRecordingWithCompletion:^(NSString *recordPath, NSInteger aDuration, NSError *error) { NSLog(@"====录音完成 %@",recordPath); if (!error) { // 将消息发送给好友 [self sendVoiceWithFileName:recordPath duration:aDuration]; } }];
- 将消息发送给好友:调用发送消息的方法
[[EaseMob sharedInstance].chatManager asyncSendMessage:msgObj progress:self prepare:^(EMMessage *message, EMError *error) { NSLog(@"准备发送语音"); } onQueue:nil completion:^(EMMessage *message, EMError *error) { if (!error) { NSLog(@"语音发送成功"); [_dataSources addObject:message]; [_tableView reloadData]; [self scrollLastRow]; }else{ NSLog(@"语音发送失败"); } } onQueue:nil];
- 需要创建一个消息对象
EMMessage *msgObj = [[EMMessage alloc]initWithReceiver:self.buddy.username bodies:@[voiceBody]];
- 需要创建一个语音消息体
EMVoiceMessageBody *voiceBody = [[EMVoiceMessageBody alloc]initWithChatObject:chatVoice];
- 需要创建一个语音对象
EMChatVoice *chatVoice = [[EMChatVoice alloc]initWithFile:fileName displayName:@"audio"];
- 需要实现IEMChatProgressDelegate代理方法
/*! @method @brief 设置进度 @discussion 用户需实现此接口用以支持进度显示 @param progress 值域为0到1.0的浮点数 @param message 某一条消息的progress @param messageBody 某一条消息某个body的progress @result */-(void)setProgress:(float)progress forMessage:(EMMessage *)message forMessageBody:(id
)messageBody; - 语音发送成功:添加数据/刷新表格/滚动到最后一行
- 退出录音:UIControlEventTouchUpOutside
- 目前没有任何操作
EaseMob播放语音消息
点击消息按钮即刻播放语音
开始播放
- 获取当前的消息体
id msgBody = self.message.messageBodies[0];
- 判断消息体是否为语音消息体
if ([msgBody isKindOfClass:[EMVoiceMessageBody class]])
- 获取语音消息体
EMVoiceMessageBody *voiceBody = msgBody;
- 获取语音路径
NSString *voicePath = voiceBody.localPath;
- 判断该路径本地是否存在
NSFileManager *manager = [NSFileManager defaultManager];if (![manager fileExistsAtPath:voicePath]) {
- 如果不存在获取服务器上的语音路径
voicePath = voiceBody.remotePath;
- 播放
[[EMCDDeviceManager sharedInstance] asyncPlayingWithPath:voicePath completion:^(NSError *error) { NSLog(@"播放完成"); }];
结束播放
// 停止播放- (void)stopPlaying;
EaseMob发送图片
1.自定义底部更多功能模块
// 添加更多功能 XMGAnyView *anyView = [[XMGAnyView alloc]initWithImageBlock:^{ NSLog(@"点击了图片按钮"); // 跳转到图片选择器 UIImagePickerController *picker = [[UIImagePickerController alloc]init]; picker.delegate = ctr; picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; [ctr presentViewController:picker animated:YES completion:nil]; } callBtnBlock:^{ NSLog(@"点击了电话按钮"); } videoBlock:^{ NSLog(@"点击了视频按钮"); }]; anyView.frame = CGRectMake(0, kWeChatScreenHeight, kWeChatScreenWidth, 200); [[UIApplication sharedApplication].keyWindow addSubview:anyView]; self.anyView = anyView;
2.选择完一张图片直接发送
- 在imagePickerController:didFinishPickingMediaWithInfo代理方法中处理
- 隐藏选择器
- 取出选择的图片
- 发送图片消息
[[EaseMob sharedInstance].chatManager asyncSendMessage:msg progress:self prepare:^(EMMessage *message, EMError *error) { NSLog(@"准备发送图片"); } onQueue:nil completion:^(EMMessage *message, EMError *error) { if (!error) { NSLog(@"图片发送成功"); [_dataSources addObject:message]; [_tableView reloadData]; [self scrollLastRow]; } } onQueue:nil];
- 需要创建图片消息
EMMessage *msg = [[EMMessage alloc]initWithReceiver:self.buddy.username bodies:@[body]];
- 需要创建图片消息体
// 第一个参数的原图片// 第二个参数是预览图片 如果传nil环信默认帮我们生成EMImageMessageBody *body = [[EMImageMessageBody alloc]initWithImage:chatImage thumbnailImage:nil];
- 需要创建环信图片对象
EMChatImage *chatImage = [[EMChatImage alloc]initWithUIImage:image displayName:@"image"];
3.显示图片
- 需要在cell判断消息的类型是否为图片消息
[msgBody isKindOfClass:[EMImageMessageBody class]]
- 在cell中都是显示预览图片
NSString *imgPath = imgBody.thumbnailLocalPath;
- 判断本地图片是否存在
NSFileManager *file = [NSFileManager defaultManager];NSURL *url = nil;if ([file fileExistsAtPath:imgPath]) { url = [NSURL fileURLWithPath:imgPath];}else{ url = [NSURL URLWithString:imgBody.thumbnailRemotePath];}
- 使用SDWebImage设置图片
[_chatBtn sd_setImageWithURL:url forState:UIControlStateNormal];
- 查看大图的原理也是一样
EaseMob查看图片
1.点击图片的跳转到图片浏览器
- 使用代理通知控制器
#pragma mark - 展示大图片代理方法- (void)chatCellShowImageWithMessage:(EMMessage *)msg
- 保存点击图片的EMMessage
imageMsg = msg;
- 创建图片浏览器
MWPhotoBrowser *browser = [[MWPhotoBrowser alloc] initWithDelegate:self];
- 跳转到图片浏览器
[self.navigationController pushViewController:browser animated:YES];
- 实现浏览器显示多少张图片的代理方法
#pragma mark - MWPhotoBrowserDelegate-(NSUInteger)numberOfPhotosInPhotoBrowser:(MWPhotoBrowser *)photoBrowser { return 1;}
- 实现浏览器显示图片的代理方法
-(id)photoBrowser:(MWPhotoBrowser *)photoBrowser photoAtIndex:(NSUInteger)index { EMImageMessageBody *body = imageMsg.messageBodies[0]; // 预览图片的路径 NSString *imgPath = body.localPath; // 判断本地图片是否存在 NSFileManager *file = [NSFileManager defaultManager]; // 使用SDWebImage设置图片 NSURL *url = nil; if ([file fileExistsAtPath:imgPath]) { return [MWPhoto photoWithImage:[UIImage imageWithContentsOfFile:imgPath]]; }else{ url = [NSURL URLWithString:body.remotePath]; return [MWPhoto photoWithURL:url]; }}
EaseMob电话聊天
1.自定义底部更多功能模块
// 添加更多功能 XMGAnyView *anyView = [[XMGAnyView alloc]initWithImageBlock:^{ NSLog(@"点击了图片按钮"); // 跳转到图片选择器 UIImagePickerController *picker = [[UIImagePickerController alloc]init]; picker.delegate = ctr; picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; [ctr presentViewController:picker animated:YES completion:nil]; } callBtnBlock:^{ NSLog(@"点击了电话按钮"); // 电话聊天 } videoBlock:^{ NSLog(@"点击了视频按钮"); }]; anyView.frame = CGRectMake(0, kWeChatScreenHeight, kWeChatScreenWidth, 200); [[UIApplication sharedApplication].keyWindow addSubview:anyView]; self.anyView = anyView;
- 点击电话聊天按钮使用callManager调用电话请求方法
// self.buddy.username:当前聊天的好友(非自己)// timeout: 超时时间(0:环信默认设置超时时间)[[EaseMob sharedInstance].callManager asyncMakeVoiceCall:self.buddy.username timeout:50 error:nil];
- 添加实时通话的代理
[[EaseMob sharedInstance].callManager addDelegate:self delegateQueue:nil];
- 遵守EMCallManagerDelegate协议
- 实现实时通话状态变化的代理方法
// callSession:实时通话的会话// reason:发生变化的原因-(void)callSessionStatusChanged:(EMCallSession *)callSession changeReason:(EMCallStatusChangedReason)reason error:(EMError *)error
- 只要当前状态是连接成功的就跳转到通话的界面
if (callSession.status == eCallSessionStatusConnected) { XMGCallController *callCtr = [[XMGCallController alloc]init]; // 将当前的会话传到下一个界面进行处理 callCtr.m_session = callSession; [self presentViewController:callCtr animated:YES completion:nil]; }
2.在实时通话界面(XMGCallController)
- 同意通话按钮
// 即刻可以通话聊天[[EaseMob sharedInstance].callManager asyncAnswerCall:self.m_session.sessionId];// 通话时间开始计时self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(startTimer) userInfo:nil repeats:YES];
- (void)startTimer{ self.time ++; int hour = self.time/3600; int min = (self.time - hour * 3600)/60; int sec = self.time - hour* 3600 - min * 60; if (hour > 0) { timeLabel.text = [NSString stringWithFormat:@"%i:%i:%i",hour,min,sec]; }else if(min > 0){ timeLabel.text = [NSString stringWithFormat:@"%i:%i",min,sec]; }else{ timeLabel.text = [NSString stringWithFormat:@"00:%i",sec]; }}
- 拒绝通话按钮
[[EaseMob sharedInstance].callManager asyncEndCall:self.m_session.sessionId reason:eCallReasonNull];
EaseMob视频聊天
1.自定义底部更多功能模块
// 添加更多功能 XMGAnyView *anyView = [[XMGAnyView alloc]initWithImageBlock:^{ NSLog(@"点击了图片按钮"); // 跳转到图片选择器 UIImagePickerController *picker = [[UIImagePickerController alloc]init]; picker.delegate = ctr; picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; [ctr presentViewController:picker animated:YES completion:nil]; } callBtnBlock:^{ NSLog(@"点击了电话按钮"); // 电话聊天 [[EaseMob sharedInstance].callManager asyncMakeVoiceCall:self.buddy.username timeout:50 error:nil]; } videoBlock:^{ NSLog(@"点击了视频按钮"); [[EaseMob sharedInstance].callManager asyncMakeVideoCall:self.buddy.username timeout:50 error:nil]; }]; anyView.frame = CGRectMake(0, kWeChatScreenHeight, kWeChatScreenWidth, 200); [[UIApplication sharedApplication].keyWindow addSubview:anyView]; self.anyView = anyView;
- 与实时通话一样在代理方法中跳转到视频界面
if (callSession.status == eCallSessionStatusConnected) { XMGCallController *callCtr = [[XMGCallController alloc]init]; // 将当前的会话传到下一个界面进行处理 callCtr.m_session = callSession; [self presentViewController:callCtr animated:YES completion:nil]; }
2.在实时通话界面(XMGCallController)
2.1 如果当前的实时通话为视频通话
if (self.m_session.type == eCallSessionTypeVideo)
2.2 初始化方法
- 大窗口显示层(用于显示对方传过来的视频)
_openGLView = [[OpenGLView20 alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)]; _openGLView.backgroundColor = [UIColor clearColor]; _openGLView.sessionPreset = AVCaptureSessionPreset352x288; [self.view addSubview:_openGLView];
- 小窗口视图(显示自己的摄像头拍照的内容)
CGFloat width = 80; CGFloat height = _openGLView.frame.size.height / _openGLView.frame.size.width * width; _smallView = [[UIView alloc] initWithFrame:CGRectMake(self.view.frame.size.width - 90, 50, width, height)]; _smallView.backgroundColor = [UIColor clearColor]; [self.view addSubview:_smallView];
- 创建会话层(当前视频的会话)
_session = [[AVCaptureSession alloc] init]; [_session setSessionPreset:_openGLView.sessionPreset];
- 创建、配置输入设备
AVCaptureDevice *device; NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; for (AVCaptureDevice *tmp in devices) { if (tmp.position == AVCaptureDevicePositionFront) { device = tmp; break; } } NSError *error = nil; _captureInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; [_session beginConfiguration]; if(!error){ [_session addInput:_captureInput]; }
- 创建、配置输出
_captureOutput = [[AVCaptureVideoDataOutput alloc] init]; _captureOutput.videoSettings = _openGLView.outputSettings; _captureOutput.minFrameDuration = CMTimeMake(1, 15); _captureOutput.alwaysDiscardsLateVideoFrames = YES; dispatch_queue_t outQueue = dispatch_queue_create("com.gh.cecall", NULL); [_captureOutput setSampleBufferDelegate:self queue:outQueue]; [_session addOutput:_captureOutput]; [_session commitConfiguration];
- 小窗口显示层
_smallCaptureLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session]; _smallCaptureLayer.frame = CGRectMake(0, 0, width, height); _smallCaptureLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; [_smallView.layer addSublayer:_smallCaptureLayer];
2.2 基本设置
- 开始会话
[_session startRunning];
- 将按钮显示在屏幕的最前面
[self.view bringSubviewToFront:contentView];
- 视频时对方的图像显示区域
self.m_session.displayView = _openGLView;
3. 实现视频输出的代理方法
- 在创建、配置输出设置的输出代理
- 遵守协议:AVCaptureVideoDataOutputSampleBufferDelegate
- 实现代理方法
-(void)captureOutput:(AVCaptureOutput *)captureOutputdidOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{ if (self.m_session.status != eCallSessionStatusAccepted) { return; }#warning 捕捉数据输出,根据自己需求可随意更改 CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); if(CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) { UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0); UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1); size_t width = CVPixelBufferGetWidth(imageBuffer); size_t height = CVPixelBufferGetHeight(imageBuffer); size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0); size_t bytesrow1 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1); if (_imageDataBuffer == nil) { _imageDataBuffer = (UInt8 *)malloc(width * height * 3 / 2); } UInt8 *pY = bufferPtr; UInt8 *pUV = bufferPtr1; UInt8 *pU = _imageDataBuffer + width * height; UInt8 *pV = pU + width * height / 4; for(int i =0; i < height; i++) { memcpy(_imageDataBuffer + i * width, pY + i * bytesrow0, width); } for(int j = 0; j < height / 2; j++) { for(int i = 0; i < width / 2; i++) { *(pU++) = pUV[i<<1]; *(pV++) = pUV[(i<<1) + 1]; } pUV += bytesrow1; } YUV420spRotate90(bufferPtr, _imageDataBuffer, width, height); [[EaseMob sharedInstance].callManager processPreviewData:(char *)bufferPtr width:width height:height]; /*We unlock the buffer*/ CVPixelBufferUnlockBaseAddress(imageBuffer, 0); }}
- 我们可以对摄像头采集的YUV420sp数据做很多的转换,这里直接使用环信的算法即可
void YUV420spRotate90(UInt8 * dst, UInt8* src, size_t srcWidth, size_t srcHeight){ size_t wh = srcWidth * srcHeight; size_t uvHeight = srcHeight >> 1;//uvHeight = height / 2 size_t uvWidth = srcWidth>>1; size_t uvwh = wh>>2; //旋转Y int k = 0; for(int i = 0; i < srcWidth; i++) { int nPos = wh-srcWidth; for(int j = 0; j < srcHeight; j++) { dst[k] = src[nPos + i]; k++; nPos -= srcWidth; } } for(int i = 0; i < uvWidth; i++) { int nPos = wh+uvwh-uvWidth; for(int j = 0; j < uvHeight; j++) { dst[k] = src[nPos + i]; dst[k+uvwh] = src[nPos + i+uvwh]; k++; nPos -= uvWidth; } }}
- 完成以上操作视频功能即可完成
EaseMob群组聊天(XMGGroupController)
1. 创建群组
- 使用聊天管理创建群组
// Subject: 群名称// description: 群描述// invitees: 群成员// initialWelcomeMessage: 欢迎语// 群组设置[[EaseMob sharedInstance].chatManager asyncCreateGroupWithSubject:groupNameField.text description:descriptionMsgField.text invitees:@[@"test",@"test3"] initialWelcomeMessage:@"欢迎加入" styleSetting:groupSetting completion:^(EMGroup *group, EMError *error) { if (!error) { [[TKAlertCenter defaultCenter] postAlertWithMessage:@"创建群组成功"]; [self.dataSource addObject:group]; [tableView reloadData]; } } onQueue:nil];
- 群组设置
// 群组的配置EMGroupStyleSetting *groupSetting = [[EMGroupStyleSetting alloc]init];// 设置群组的类型 groupSetting.groupStyle = eGroupStyle_Default;// 群组最大人员数groupSetting.groupMaxUsersCount = 150;
2. 获取群列表
- 首先获取本地群组列表
[self.dataSource addObjectsFromArray:[[EaseMob sharedInstance].chatManager groupList]];
- 如果本地没有那么就获取后台数据
// 如果本地没有 那么就获取后台数据if (self.dataSource.count == 0) { [[EaseMob sharedInstance].chatManager asyncFetchMyGroupsListWithCompletion:^(NSArray *groups, EMError *error) { if (!error) { [self.dataSource addObjectsFromArray:groups]; [tableView reloadData]; } } onQueue:nil];}
3. 群组聊天
- 点击某个群组跳转到聊天界面(XMGChatController)
// 设置是否是群聊- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ XMGChatController *chatCtr = [[XMGChatController alloc]initWithIsGroup:YES]; [chatCtr setHidesBottomBarWhenPushed:YES]; chatCtr.group = self.dataSource[indexPath.row]; [self.navigationController pushViewController:chatCtr animated:YES];}
- 获取聊天记录需要判断是否为群组
// 从本地数据库获取聊天记录(通过会话对象获取) EMConversationType type = self.isGroup ? eConversationTypeGroupChat : eConversationTypeChat; NSString *chatter = self.isGroup ? self.group.groupId : self.buddy.username; // 与当前好友的会话 EMConversation *conversation = [[EaseMob sharedInstance].chatManager conversationForChatter:chatter conversationType:type]; NSArray *messages = [conversation loadAllMessages]; _dataSources = [NSMutableArray arrayWithArray:messages];
- 使用异步发送文本聊天
[[EaseMob sharedInstance].chatManager asyncSendMessage:msg progress:nil prepare:^(EMMessage *message, EMError *error) { NSLog(@"即将发送消息"); } onQueue:nil completion:^(EMMessage *message, EMError *error) { if (!error) { NSLog(@"发送消息成功"); } } onQueue:nil];
在创建消息对象前需要判断接受者是否是群组
// 判断是否是群消息NSString *receiver = ctr.isGroup ? ctr.group.groupId : ctr.buddy.username;
- 发送一条消息需要创建一个消息对象
// 创建一个消息对象EMMessage *msg = [[EMMessage alloc]initWithReceiver:ctr.buddy.username bodies:@[body]];
设置消息类型是单聊还是群聊
msg.messageType = ctr.isGroup ? eMessageTypeGroupChat:eMessageTypeChat;
- 创建一个消息对象需要创建一个消息体
// 创建一个消息体EMTextMessageBody *body = [[EMTextMessageBody alloc]initWithChatObject:chatText];
- 创建一个消息体需要创建一个文本消息实例
// 创建一个文本消息实例EMChatText *chatText = [[EMChatText alloc]initWithText:textField.text];
- 将消息存储到数组中
- 刷新表格
- 清空输入框
- 滚动到tableView的底部