即時(shí)通訊整個(gè)頁面的搭建(三)

即時(shí)通訊整個(gè)頁面的搭建(一)
即時(shí)通訊整個(gè)頁面的搭建(二)
繼續(xù)寫

前面兩篇主要寫完了, 界面上的問題, 這一篇就說說消息的發(fā)送接收過程(包含錄語音, 發(fā)視頻照片等等), 進(jìn)行了簡單的封裝

消息體的整個(gè)生成過程基本上都在IMChatToolBar這個(gè)類里, 整個(gè)消息模型的生成寫了類方法, 傳入必要的參數(shù)就可以, 當(dāng)然這不是所有的都是通用的代碼, 基本上邏輯都是差不多的

從錄制語音開始, 主要代碼都在IMAudioTool里, 兩個(gè)比較重要的Api

- (void)playWithFileName:(NSString *)fileName returnBeforeFileName:(ReplacePlayFileName)replaceBlock withFinishBlock:(FinishPlayBlock)playFinish;

- (void)recorderVoiceVolumeView:(IMVoiceVolumeView *)volumeView finishBlock:(FinishEncodeBlock)finishBlock;

@end

這是實(shí)現(xiàn)

- (void)recorderVoiceVolumeView:(IMVoiceVolumeView *)volumeView finishBlock:(FinishEncodeBlock)finishBlock{
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        self.isCancel = NO;
        self.seconds = 0;
        self.finishBlock = finishBlock;
        self.volumeView = volumeView;
        [self.timer invalidate];
        self.timer = nil;
        [self timer];
        
        if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0"] != NSOrderedAscending) {
            //7.0第一次運(yùn)行會(huì)提示,是否允許使用麥克風(fēng)
            AVAudioSession *session = [AVAudioSession sharedInstance];
            NSError *sessionError;
            //AVAudioSessionCategoryPlayAndRecord用于錄音和播放
            [session setCategory:AVAudioSessionCategoryPlayAndRecord error:&sessionError];
            if(session == nil)
                NSLog(@"Error creating session: %@", [sessionError description]);
            else
                [session setActive:YES error:nil];
        }
        
        NSString *fileName = [NSString stringWithFormat:@"%d%d", (int)[[NSDate date] timeIntervalSince1970], arc4random() % 100000];
        
        self.oldFileName = fileName;
        
        [self audioRecorderWithFileName:fileName];
        
        [self.audioRecorder record];
        
    });
    
}

[self audioRecorderWithFileName:fileName];實(shí)現(xiàn)如下

- (void)audioRecorderWithFileName:(NSString *)fileName{
    
    AVAudioSession * audioSession = [AVAudioSession sharedInstance];
    
    NSError* error1;
    
    [audioSession setCategory:AVAudioSessionCategoryRecord error: &error1];
    
    NSString *filePath =[[IMBaseAttribute dataAudioPath] stringByAppendingPathComponent:fileName];
    
            //錄音設(shè)置
    NSMutableDictionary *recordSetting = [[NSMutableDictionary alloc]init];
    //設(shè)置錄音格式  AVFormatIDKey==kAudioFormatLinearPCM
    [recordSetting setValue:[NSNumber numberWithInt:kAudioFormatLinearPCM] forKey:AVFormatIDKey];
    //設(shè)置錄音采樣率(Hz) 如:AVSampleRateKey==8000/44100/96000(影響音頻的質(zhì)量)
    [recordSetting setValue:[NSNumber numberWithFloat:11025.0] forKey:AVSampleRateKey];
    //錄音通道數(shù)  1 或 2
    [recordSetting setValue:[NSNumber numberWithInt:2] forKey:AVNumberOfChannelsKey];
//    //線性采樣位數(shù)  8、16、24、32
//    [recordSetting setValue:[NSNumber numberWithInt:16] forKey:AVLinearPCMBitDepthKey];
    //錄音的質(zhì)量
    [recordSetting setValue:[NSNumber numberWithInt:AVAudioQualityMedium] forKey:AVEncoderAudioQualityKey];
    
    NSError *error;
    //初始化
    self.audioRecorder = [[AVAudioRecorder alloc]initWithURL:[NSURL fileURLWithPath:filePath] settings:recordSetting error:&error];
    //開啟音量檢測(cè)
    self.audioRecorder.meteringEnabled = YES;
    [self.audioRecorder recordForDuration:(NSTimeInterval) IMAudioMaxDurtion];
    self.audioRecorder.delegate = self;
}

設(shè)置采樣率的時(shí)候,設(shè)置老是變聲, 一點(diǎn)點(diǎn)調(diào)出來的, 由此我猜想, 變聲軟件是不是改這個(gè)采樣率的當(dāng)?shù)赖淖兟曅Ч麤]有去深究這個(gè)東西
錄音的格式安卓和iOS要保持一致, 所以同一轉(zhuǎn)碼成MP3格式的, 轉(zhuǎn)碼用的lame這個(gè)庫, 可以谷歌一下, 如果下載下來不支持64位, 可以直接到我的demo去扒下來, 轉(zhuǎn)碼的代碼demo里有,這個(gè)就不貼了, 轉(zhuǎn)碼之后就是上傳到服務(wù)器了, 發(fā)送成功之后, 就是發(fā)送到聊天服務(wù)器了

看看拍視頻, 照片的IMImagePickerManager

/**
 *  視頻, 照片
 *
 *  @param souceType      Picker的souceType
 *  @param viewController 需要一個(gè)跳轉(zhuǎn)的控制器
 *  @param finishAction   選擇成功之后的回調(diào)
 */
+ (void)showImagePickerWithSouceType:(ImagePickerSouceType)souceType withViewController:(UIViewController *)viewController finishAction:(IMImagePickerFinishAction)finishAction;
+ (void)showImagePickerWithSouceType:(ImagePickerSouceType)souceType withViewController:(UIViewController *)viewController finishAction:(IMImagePickerFinishAction)finishAction{
    [IMImagePickerManager shareInstance].souceType = souceType;
    [IMImagePickerManager shareInstance].finishAction = finishAction;
    UIImagePickerController *picker = [[UIImagePickerController alloc] init];
    picker.delegate = [IMImagePickerManager shareInstance];
    switch (souceType) {
        case ImagePickerSoucePhotoType:
            picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
            break;
        case ImagePickerSouceCameraType:
            picker.sourceType = UIImagePickerControllerSourceTypeCamera;
            picker.mediaTypes = @[(NSString *)kUTTypeImage];
            break;
        case ImagePickerSouceVedioType:
            picker.sourceType = UIImagePickerControllerCameraCaptureModeVideo;
            picker.mediaTypes = @[(NSString *)kUTTypeMovie];
            picker.videoMaximumDuration = 10;
            break;
        default:
            break;
    }
    [viewController presentViewController:picker animated:YES completion:nil];
}

mediaTypes是一個(gè)數(shù)組, 可以穿多個(gè)類型, 就就像系統(tǒng)的拍照一樣,左右滑動(dòng)切換模式, 拍視頻可以限制最大的時(shí)長, 不過沒有倒計(jì)時(shí)的提示, 到時(shí)間了, 就會(huì)自動(dòng)停止, 一個(gè)Alert提示, 這個(gè)UIImagePickerController控制器也不能繼承, 想自定義也不行, 暫且先這樣
選擇照片比較簡單, 選擇完之后, 我是先寫入本地聊天文件的路徑, 然后回調(diào)出文件名, 拿到文件名, 拼接上對(duì)應(yīng)的路徑之后, 上傳圖片就好了
拍照片的時(shí)候, 橫屏拍攝的時(shí)候會(huì)有旋轉(zhuǎn)90°的問題, 粘貼處理的代碼(谷歌出來的):

- (UIImage *)fixOrientation:(UIImage *)aImage {
    
    // No-op if the orientation is already correct
    if (aImage.imageOrientation == UIImageOrientationUp)
        return aImage;
    
    // We need to calculate the proper transformation to make the image upright.
    // We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored.
    CGAffineTransform transform = CGAffineTransformIdentity;
    
    switch (aImage.imageOrientation) {
        case UIImageOrientationDown:
        case UIImageOrientationDownMirrored:
            transform = CGAffineTransformTranslate(transform, aImage.size.width, aImage.size.height);
            transform = CGAffineTransformRotate(transform, M_PI);
            break;
            
        case UIImageOrientationLeft:
        case UIImageOrientationLeftMirrored:
            transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
            transform = CGAffineTransformRotate(transform, M_PI_2);
            break;
            
        case UIImageOrientationRight:
        case UIImageOrientationRightMirrored:
            transform = CGAffineTransformTranslate(transform, 0, aImage.size.height);
            transform = CGAffineTransformRotate(transform, -M_PI_2);
            break;
        default:
            break;
    }
    
    switch (aImage.imageOrientation) {
        case UIImageOrientationUpMirrored:
        case UIImageOrientationDownMirrored:
            transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
            transform = CGAffineTransformScale(transform, -1, 1);
            break;
            
        case UIImageOrientationLeftMirrored:
        case UIImageOrientationRightMirrored:
            transform = CGAffineTransformTranslate(transform, aImage.size.height, 0);
            transform = CGAffineTransformScale(transform, -1, 1);
            break;
        default:
            break;
    }
    
    // Now we draw the underlying CGImage into a new context, applying the transform
    // calculated above.
    CGContextRef ctx = CGBitmapContextCreate(NULL, aImage.size.width, aImage.size.height,
                                             CGImageGetBitsPerComponent(aImage.CGImage), 0,
                                             CGImageGetColorSpace(aImage.CGImage),
                                             CGImageGetBitmapInfo(aImage.CGImage));
    CGContextConcatCTM(ctx, transform);
    switch (aImage.imageOrientation) {
        case UIImageOrientationLeft:
        case UIImageOrientationLeftMirrored:
        case UIImageOrientationRight:
        case UIImageOrientationRightMirrored:
            // Grr...
            CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.height,aImage.size.width), aImage.CGImage);
            break;
            
        default:
            CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.width,aImage.size.height), aImage.CGImage);
            break;
    }
    
    // And now we just create a new UIImage from the drawing context
    CGImageRef cgimg = CGBitmapContextCreateImage(ctx);
    UIImage *img = [UIImage imageWithCGImage:cgimg];
    CGContextRelease(ctx);
    CGImageRelease(cgimg);
    return img;
}

基本上的原理根據(jù)旋轉(zhuǎn)的imageOrientation來判斷旋轉(zhuǎn)的角度, 然后讓照片旋轉(zhuǎn)成正常的方向, 再畫出來(不對(duì)的話, 就當(dāng)我胡說??)

視頻跟語音一樣,也要轉(zhuǎn)碼, 同一一種格式(不轉(zhuǎn)這個(gè)文件太大了, 也受不了??), 轉(zhuǎn)碼的代碼和獲取視頻的第一幀的代碼就不貼了,可以去demo里看

一起來看看上傳的文件的類吧(基于AFN2.6的, 坑的就是進(jìn)度沒有3.0的block回調(diào)), 主要說上傳時(shí)候的進(jìn)度監(jiān)聽

@interface IMUploadProgressDelegate : NSObject
// 進(jìn)度的block
@property(nonatomic, copy)void(^progressBlock)(CGFloat progress);
// 通過key獲取這個(gè)監(jiān)聽者
+ (instancetype)uploadProgressDelegateWithKey:(NSString *)key;
@end

@implementation IMUploadProgressDelegate

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(NSProgress *)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    if ([change[@"new"] floatValue] == 1) {
        [object removeObserver:self forKeyPath:keyPath];
    }
    if (self.progressBlock) {
        self.progressBlock([change[@"new"] floatValue]);
    }
}

+ (instancetype)uploadProgressDelegateWithKey:(NSString *)key{
    id obj = [[IMBaseAttribute shareIMBaseAttribute].uploadProgressDict valueForKey:key];
    
    if (obj && [obj isKindOfClass:[self class]]) {
        return obj;
    } else {
        return nil;
    }
}

@end

下面添加這個(gè)監(jiān)聽者的代碼, 是在上傳文件的代碼里

IMUploadProgressDelegate *progressDelegate = [[IMUploadProgressDelegate alloc] init];
    
[progress addObserver:progressDelegate forKeyPath:@"fractionCompleted" options:NSKeyValueObservingOptionNew context:nil];
    
[[IMBaseAttribute shareIMBaseAttribute].uploadProgressDict setValue:progressDelegate forKey:fileName];

上傳圖片語音視頻的進(jìn)度都是通過它來監(jiān)聽的, 通過文件名去存取這個(gè)類, 進(jìn)度改變回調(diào), 改變模型的值, 刷新這一行

[[IMUploadProgressDelegate uploadProgressDelegateWithKey:soucePath] setProgressBlock:^(CGFloat progress) {
       item.uploadProgress = progress;
       MainQueueBlock(^{
           if ([weakSelf.delegate respondsToSelector:@selector(IMChatToolBar:imBaseItem:)]) {
                [weakSelf.delegate IMChatToolBar:weakSelf imBaseItem:item];
            }
       })
}];

IMBaseAttribute在項(xiàng)目中,充當(dāng)了管家的角色, 很多東西通過它去配置的,

@property(nonatomic, assign)CGFloat normalMargin;

@property(nonatomic, assign)CGFloat headImageViewWidth;

@property(nonatomic, strong)UIFont *nameLabelFont;

@property(nonatomic, strong)UIColor *nameLabelTextColor;

@property(nonatomic, strong)UIFont *nameInfoLabelFont;

@property(nonatomic, strong)UIColor *nameInfoLabelTextColor;

@property(nonatomic, strong)UIFont *contentTextFont;

@property(nonatomic, strong)UIColor *contentTextColor;

@property(nonatomic, assign)CGFloat headImageViewCornerRadius;

@property(nonatomic, assign)CGFloat bufferMaxWidth;

@property(nonatomic, assign)CGFloat contentTextInsetMargin;

@property(nonatomic, assign)CGFloat messageBodyImageWidth;

@property(nonatomic, assign)CGFloat messageBodyVoiceWidth;

@property(nonatomic, assign)CGFloat messageBodyVoiceHeight;

@property(nonatomic, assign)CGFloat messageBodyVedioHeight;

@property(nonatomic, assign)CGFloat messageBodyVedioWidth;

@property(nonatomic, assign)CGFloat timeViewHeight;

@property(nonatomic, strong)NSMutableDictionary *downloadProgressDict;

@property(nonatomic, strong)NSMutableDictionary *uploadProgressDict;

+ (NSString*)dataVedioPath;

+ (NSString*)dataAudioPath;

+ (NSString*)dataPicturePath;

上傳下載的進(jìn)度監(jiān)聽的對(duì)象都放在這個(gè)兩個(gè)字典里,很多字體顏色, 間距的大小的配置都是放在這個(gè)里面的, 初始化的屬性的都放在了+ (void)load方法里

下載的監(jiān)聽也是一樣的, 當(dāng)點(diǎn)擊不同消息類型的Cell的時(shí)候, 去下載這個(gè)文件, 在下載中的, 時(shí)候不能再去點(diǎn)擊, 這個(gè)要判斷的

基本上消息的發(fā)送就是這么多了

播放視頻, 查看圖片的這個(gè)就不說啦, 播放語音的時(shí)候, 有一個(gè)靠近聽筒的監(jiān)聽:

#pragma mark - 監(jiān)聽聽筒or揚(yáng)聲器
- (void) handleNotification:(BOOL)state
{
    [[UIDevice currentDevice] setProximityMonitoringEnabled:state]; //建議在播放之前設(shè)置yes,播放結(jié)束設(shè)置NO,這個(gè)功能是開啟紅外感應(yīng)
    
    if(state)//添加監(jiān)聽
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(sensorStateChange:) name:@"UIDeviceProximityStateDidChangeNotification"
                                                   object:nil];
    else//移除監(jiān)聽
        [[NSNotificationCenter defaultCenter] removeObserver:self name:@"UIDeviceProximityStateDidChangeNotification" object:nil];
}

//處理監(jiān)聽觸發(fā)事件
-(void)sensorStateChange:(NSNotificationCenter *)notification;
{
    //如果此時(shí)手機(jī)靠近面部放在耳朵旁,那么聲音將通過聽筒輸出,并將屏幕變暗(省電?。?    if ([[UIDevice currentDevice] proximityState] == YES)
    {
        NSLog(@"Device is close to user");
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
    }
    else
    {
        NSLog(@"Device is not close to user");
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
    }
}

最后說說緩存:

  • 聊天內(nèi)容的緩存(FMDB)
  • 聊天附件的緩存

至于聊天內(nèi)容肯定是要建一張表, 也可以建多張表, 我demo里是我是建了多張表的, 通過一個(gè)integer值去創(chuàng)建的, 這樣在刪除聊天內(nèi)容的時(shí)候, 可以通過刪除對(duì)應(yīng)的表即可, 其他的聊天記錄不影響, 一張表的話, 刪除某個(gè)人的聊天記錄的時(shí)候, 可能就沒有那么快了, 我看微信與QQ聊天記錄都是一起清除的, 可能是放在一個(gè)數(shù)據(jù)庫里, 直接刪除了這張表吧(猜測(cè)而已)

聊天文件的緩存我這里也是分開存儲(chǔ)的, 微信刪除聊天附件的時(shí)候, 對(duì)應(yīng)一個(gè)聊天對(duì)象做的緩存, QQ則是清除緩存文件, 沒有對(duì)應(yīng)的刪除某個(gè)人的, 當(dāng)然具體怎么做的, 我就不清楚了

總結(jié)

只有自己寫了才知道坑在哪里, 蹚過去, 就成長了
這個(gè)demo里還有很多缺陷
比如輸入框, 多行輸入調(diào)高度, 文件下載的時(shí)候斷網(wǎng)的情況下, 也沒有暫停下載的功能等等, 細(xì)節(jié)需要優(yōu)化, 等有時(shí)間的??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,045評(píng)論 25 709
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,329評(píng)論 4 61
  • 夜已深,淺眠中,三言兩語 天已明,夢(mèng)未醒,窸窸窣窣 ??? 言談間,少氛圍,寥寥數(shù)語 望一友,深交流,無所不說
    貳字棼閱讀 191評(píng)論 0 0
  • 主要的編碼有三種:ASCII、Unicode、utf-8 ASCII僅包含大小寫的英文字母和部分標(biāo)點(diǎn)符號(hào)Unico...
    Jornathon閱讀 168評(píng)論 0 0
  • 公公的麥田 春雨深情地?fù)砦侵G油油的關(guān)中千里沃野,幾天時(shí)間,沉睡了一冬的麥苗已經(jīng)精神抖擻地在風(fēng)里...
    玫露雪閱讀 531評(píng)論 3 3

友情鏈接更多精彩內(nèi)容