zoukankan      html  css  js  c++  java
  • 即时通讯之环信视频语音实时通话与单聊和群聊实现

    即时通讯

    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中如何实现即时通讯

    1. 使用Socket写一个通讯协议(自己写一个协议
    2. 使用XMPPframework第三方框架
    3. 使用国内第三方框架融云
    4. 使用国内第三框架环信
    5. 使用国内第三方框架LeanCloud
    6. 使用国内第三方框架阿里悟空
    7. ...

    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.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<IEMMessageBody>)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 <MWPhoto>)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 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];
    // 设置群组的类型
     <!--@constant eGroupStyle_PrivateOnlyOwnerInvite 私有群组,只能owner权限的人邀请人加入-->
     <!--@constant eGroupStyle_PrivateMemberCanInvite 私有群组,owner和member权限的人可以邀请人加入-->
     <!--@constant eGroupStyle_PublicJoinNeedApproval 公开群组,允许非群组成员申请加入,需要管理员同意才能真正加入该群组-->
     <!--@constant eGroupStyle_PublicOpenJoin         公开群组,允许非群组成员加入,不需要管理员同意-->
     <!--@constant eGroupStyle_PublicAnonymous        公开匿名群组,允许非群组成员加入,不需要管理员同意-->
     <!--@constant eGroupStyle_Default                默认群组类型-->
    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. 那么发送语音和图片的也需要判断是否是群组聊天

  • 相关阅读:
    5.21 CSS样式表练习
    5.20 c#验证码练习
    5.20 邮箱注册,及网页嵌套,知识点复习
    5.19 网页注册练习
    5.19练习标签及其 定义
    5.16 兔子生兔子,日期时间练习
    5.15 复习;共5题
    5.11 集合 与 特殊集合
    5.11 集合与特殊集合
    WinForm1
  • 原文地址:https://www.cnblogs.com/qimashejian/p/5074807.html
Copyright © 2011-2022 走看看