即时通讯之环信视频语音实时通话与单聊和群聊实现

发布时间:2021-11-27 06:22:46

即时通讯
1. 即时通讯简介

即时通讯英文名为:Instant Messaging,简称IM。


即时通讯(Instant messaging,简称IM)是一个终端服务,允许两人或多人使用网路即时的传递文字讯息、档案、语音与视频交流。即时通讯按使用用途分为企业即时通讯和网站即时通讯,根据装载的对象又可分为手机即时通讯和PC即时通讯,手机即时通讯代表是QQ,微信。


2. 即时通讯的代表作

主流的代表:Skype/QQ/Google Talk/WhatsApp/Instagram/LINE/Kik/Wechat/* 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(资源文件)在AppDelegate中的didFinishLaunchingWithOptions注册EaseMobSDK

// 注册SDK
// kEaseMobAppKey:环信后台管理->我的应用->对应的应用->应用概述->应用标识
// kEaseMobPushName:环信后台管理->我的应用->对应的应用->应用概述->推送证书->iOS->证书名称
[[EaseMob sharedInstance] registerSDKWithAppKey:kEaseMobAppKey apnsCertName:kEaseMobPushName];

此时会报很多错误
需要导入框架
MobileCoreServices.frameworkCFNetwork.frameworklibEaseMobClientSDKLite.alibsqlite3.dyliblibstdc++.6.0.9.dyliblibz.dyliblibiconv.dyliblibresolv.dyliblibxml2.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 *)captureOutput
didOutputSampleBuffer:(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的底部
4. 那么发送语音和图片的也需要判断是否是群组聊天

转载于:https://www.cnblogs.com/qimashejian/p/5074807.html






相关资源:Linux 下语音实时的一种实现方法

相关文档

  • 如何巧剥荔枝巧剥荔枝的方法 荔枝吃过量可引发低血糖过量吃荔枝
  • 如何通过QQ里的好友然后添加到微信里的好友
  • 教师转正工作心得总结
  • 在建房屋商用租赁合同样本一
  • 野生蜜蜂是怎样诱捕的呢
  • 在微信上买流量用不完下个月可以接着用吗
  • 把不良儿童视频归为邪典片?你真的了解吗
  • 盘点科目二考试失分点
  • 勉励小学生的话
  • 潭面无风镜未磨一句中诗人把什么比作未经打磨的铜镜 潭面无风镜未磨用什么比作什么
  • 那些不结婚的人后来怎么样了
  • 天津5A级旅游景点都有哪些
  • 钱鹿怎么设置提现密码 钱鹿APP提现密码设置图文教程
  • 抽烟对口腔的危害
  • 初中一年级语文老师学期总结
  • 20年后的地球一个惊喜
  • 脸部注射玻尿酸副作用
  • android drawable自定义xml
  • 政府调研员个人自我总结
  • 合并BN层到卷积层,提升模型前向推理的速度
  • 泡菜饭煎饼的做法怎么做
  • 七年级班主任工作计划
  • 分段线性插值法实验报告_计算方法实验报告习题1(浙大版)
  • 表达描述爱的英语情景口语
  • 男人有狐臭,必定会长寿
  • 海洋馆游玩的心得体会小学
  • 初中德育标兵先进事迹优秀范文
  • 小豆子发芽日记
  • onclick 绑定在 input上的坑,无效
  • linq子查询--学习linq的资料和笔记(五)
  • 猜你喜欢

  • 秦皇岛市中贤幕墙装饰工程有限公司烟台分公司企业信用报告-天眼查
  • 广州秦安经济发展公司深圳分公司企业信用报告-天眼查
  • 2019最新高考地理一轮复* 课后限时集训16 人口增长模式与人口合理容量 湘教版(考试专用)
  • 给客户的鸡年新年祝福语
  • 泰兴市信达物资有限公司企业信用报告-天眼查
  • 小学三年级写景作文:漓江烟雨
  • 《摆渡人》读后感心得体会
  • 【推荐】2019秋九年级物理全册第十五章《电流和电路》检测题课件新版新人教版.ppt
  • 武汉泽迪建材有限责任公司企业信用报告-天眼查
  • 慈善情暖万家倡议书
  • 九年级化学全册第7章 第2节 第3课时 中和反应课件(新版)沪教版
  • php操作mysql数据库
  • 某园区网络主干光缆敷设及分中心机房建设方案
  • 2018秋八年级历史上册 第三单元 资产阶级民族革命与中华民国的建立 第9课 辛亥革命作业
  • 关于旅游管理应用型本科加强实践教学环节的思考
  • 电脑浏览器都是广告怎么办
  • 母猪子宫内膜炎的防治及治疗效果对比及治疗效果对比
  • 海南省机械设备成套安装公司企业信用报告-天眼查
  • 温州美食之锦绣鱼丝
  • 温州第五十一中学2004年度第一学期期中考试
  • 初中音乐教学工作计划
  • 放下便是轻松作文【初中初一1200字】
  • 白腹锦鸡繁殖及食性的初步观察
  • 高尔夫草坪养护全年计划
  • 2017届高考地理关于交通的模拟题和答案
  • 商务会计人员个人简历表格
  • 广东省惠来县2012-2013学年高二化学上学期期中试题新人教版
  • 局部解剖学(头部) ppt课件
  • 同时性消化道重复癌文献复*
  • 实用的八句英语短语
  • 河南省2015年初级防水工模拟试题
  • 北邮电磁场实验 迈克尔逊干涉实验
  • 绥化市兴和高丽米业有限公司企业信用报告-天眼查
  • 2020年哪些大学缩短了中秋国庆假期国庆假期缩短了几天
  • 【老会计经验】存货损失的税收筹划
  • 医学检验师简历模板
  • 自然辩证法课后思考题题答案整理
  • 软件设计师--判定覆盖,判定条件覆盖,条件组合覆盖--一个栗子
  • 2019幼儿园毕业典礼家长代表发言稿4
  • 高原地区老年消化性溃疡225例临床分析
  • 计算机工程学院结构化布线实验室设备清单
  • 人美版小学美术三年级上册台历的设计PPT课件
  • 电脑版