即時通訊之環信視頻語音實時通話與單聊和群聊實現


即時通訊

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: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. 那么發送語音和圖片的也需要判斷是否是群組聊天


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM