iOS 实现stream边录边传功能

背景

网上有很多帖子写这个功能,但是大部分零零碎碎,没办法直接用。
本文把思路整理下,并且真正可用,有问题可以微信我(lishi_655)。
如果您觉得好请帮忙点个赞。Thanks♪(・ω・)ノ

实现步骤:
  1. 首先是边录边压缩录音流,参考代码MLAudioRecorder
    。本文使用了其中的三个类:录音类MLAudioRecorder ,pcm转mp3类Mp3RecordWriter,音量大小监听类MLAudioMeterObserver。
  2. 对获得的二进制录音流,使用我封装的CCVoiceUploader来上传流。关于上传的思路,网上有帖子写,例如这篇,但是不完整,且有问题。Stack Overflow上的问答也没有可用的。流的传输还需要参考苹果论坛中官方回复才能理解写法。苹果开发人员还是厉害。
  3. 您需要基于1和2封装一个管理和错误处理类,如果需要可以微信找我要代码。
框架github链接

realtimeVideoStream

代码具体实现:
  1. 对于此步,直接参考MLAudioRecorder中的代码,自己写个demo,看下能否录制声音,并打印二进制测试下。
  2. 此步的代码如下
#import 

NS_ASSUME_NONNULL_BEGIN

@class SXVoiceUploader;
@protocol  SXVoiceUploaderDelegate

-(void)uploader:(SXVoiceUploader *)uploader didFinishUploadStreamAndGetResult:(NSDictionary *)dic;

-(void)uploader:(SXVoiceUploader *)uploader didUploadStatus:(int)status description:(NSString *)description;

@end


@interface SXVoiceUploader : NSObject

@property(nonatomic,weak)id delegate;

@property(nonatomic,copy)NSString *token;

@property(nonatomic,copy)NSString *userid;


-(void)connectSeverWithContent:(NSString *)content;

-(void)uploadData:(NSData *)data;

-(void)endUpload;


@end

NS_ASSUME_NONNULL_END

//
//  CCVoiceUploader.m
//  QuqiClass
//
//  Created by lishi on 2020/2/13.
//  Copyright © 2020 李诗. All rights reserved.
//

#import "SXVoiceUploader.h"
#import "SXVoiceDataTask.h"


//#define voiceAuthToken @""

@interface SXVoiceUploader ()

@property(nonatomic,strong)NSURLSessionUploadTask *uploadTask;

@property(nonatomic,strong)NSOutputStream *outputStream;

@property(nonatomic,strong)NSInputStream *bodyStream;

@property(nonatomic,assign)BOOL isEnd;

@property(nonatomic,strong)NSMutableData *responseData;

@property(nonatomic,assign)BOOL hasSpaceAvailable;

@property(nonatomic,assign)BOOL isWriting;

@property(nonatomic,assign)int64_t alreadyRecord;

@property(nonatomic,assign)int64_t alreadyUpload;

@property(nonatomic,strong)NSTimer *timer;

@property(nonatomic,strong)NSMutableArray *dataTaskArr;


@end

@implementation SXVoiceUploader



-(void)connectSeverWithContent:(NSString *)content{
    // 1.初始化
    _uploadTask = nil;
    _outputStream = nil;
    _bodyStream = nil;

    _isEnd = NO;
    _responseData = [NSMutableData data];
    _hasSpaceAvailable = NO;
    
    _alreadyRecord = 0;
    _alreadyUpload = 0;
    _dataTaskArr = [NSMutableArray new];

    
    // 2.配置参数
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];


    NSURL *r_url = [NSURL URLWithString:@"你的地址"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:r_url];
    request.HTTPMethod = @"POST";
    [request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
    request.timeoutInterval = 30;

    // 设置请求头
    [request setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];
    [request setValue:@"Keep-Alive" forHTTPHeaderField:@"Connection"];
    [request setValue:self.token forHTTPHeaderField:@"Authorization"];
    [request setValue:content forHTTPHeaderField:@"word_name"];
    [request setValue:@"stream.wav" forHTTPHeaderField:@"myWavfile"];
    [request setValue:self.userid?:@"gu001" forHTTPHeaderField:@"user_id"];

    NSURLSessionUploadTask *uploadTask = [session uploadTaskWithStreamedRequest:request];
    self.uploadTask = uploadTask;


    // 3.任务执行
    [uploadTask resume];

    [self startTaskScaner];

}


-(void)uploadData:(NSData *)data{
    
    _alreadyRecord += [data length];
    
    SXVoiceDataTask *task = [SXVoiceDataTask new];
    task.data = data;
    task.hasUpload = NO;
    [_dataTaskArr addObject:task];
    
}


-(void)endUpload{
    _isEnd = YES;
}

-(void)stopStream{
//    NSLog(@"将要关闭流传输");
    self.outputStream.delegate = nil;
    [self.outputStream close];
}


-(void)add:(NSString *)str toData:(NSMutableData *)data{
    [data appendData:[str dataUsingEncoding:NSUTF8StringEncoding]];
}

// MARK:NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
 needNewBodyStream:(void (^)(NSInputStream * _Nullable bodyStream))completionHandler{
    
    // 绑定输入输出流
    
    NSInputStream *inputStream = nil;
    NSOutputStream *outputStream = nil;
    [NSStream getBoundStreamsWithBufferSize:8000*16 inputStream:&inputStream outputStream:&outputStream];
    
    self.bodyStream = inputStream;
    
    self.outputStream = outputStream;
    self.outputStream.delegate = self;
    [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [self.outputStream open];
    
    completionHandler(self.bodyStream);
}


-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend{
    NSString *logStr = [NSString stringWithFormat: @"SXMDDSDK=>已上传:%lld,总上传:%lld,期望上传:%lld",bytesSent,totalBytesSent,totalBytesExpectedToSend];
    if (self.delegate) {
        [self.delegate uploader:self didUploadStatus:10000 description:logStr];
    }
    
    _alreadyUpload += bytesSent;
    
    if (_isEnd && _alreadyRecord == totalBytesSent) {
        
        
        [self stopTaskScaner];
        [self stopStream];
       
//        NSLog(@"上传完毕");
    }
}


// 以下三个方法收到数据
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    
    NSHTTPURLResponse *res = (NSHTTPURLResponse *)response;
    if (res.statusCode != 200) {
        
        NSString *dis = [NSString stringWithFormat:@"SXMDDSDK=>获取结果失败   %@",res.description];
        if (self.delegate) {
            [self.delegate uploader:self didUploadStatus:10004 description:dis];
            [self.delegate uploader:self didFinishUploadStreamAndGetResult:@{}];
        }
    }else{
        NSString *dis = @"SXMDDSDK=>获取结果成功";
        if (self.delegate) {
            [self.delegate uploader:self didUploadStatus:10000 description:dis];
        }
        completionHandler(NSURLSessionResponseAllow);
    }
    
}


-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    
    NSString *dis =  [NSString stringWithFormat:@"SXMDDSDK=>已经录制数据:%lld,已经上传数据:%lld",_alreadyRecord,_alreadyUpload];
    if (self.delegate) {
        [self.delegate uploader:self didUploadStatus:10000 description:dis];
    }
//    NSLog(@"%@",dis);
    
    //拼接数据
    [self.responseData appendData:data];
}


-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    
    //解析数据
    NSString *response = [[NSString alloc] initWithData:self.responseData encoding:NSUTF8StringEncoding];
//    NSLog(@"%@",response);
    NSData *jsondata = [response dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary * json = [NSJSONSerialization JSONObjectWithData:jsondata options:0 error:nil];
    if (self.delegate) {
        [self.delegate uploader:self didFinishUploadStreamAndGetResult:json];
    }
    // 关闭任务
    [self stopTaskScaner];
}


// MARK: NSStreamDelegate

-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
    switch (eventCode) {
        case NSStreamEventNone:
            NSLog(@"NSStreamEventNone");
            break;

        case NSStreamEventOpenCompleted:
           
            NSLog(@"NSStreamEventOpenCompleted");
            break;

        case NSStreamEventHasBytesAvailable: {
            NSLog(@"NSStreamEventHasBytesAvailable");
            
            
        } break;

        case NSStreamEventHasSpaceAvailable: {
            NSLog(@"NSStreamEventHasSpaceAvailable");
            _hasSpaceAvailable = YES;
            
        } break;

        case NSStreamEventErrorOccurred:
            NSLog(@"NSStreamEventErrorOccurred");
            break;

        case NSStreamEventEndEncountered:
            NSLog(@"NSStreamEventEndEncountered");
            break;

        default:
            break;
    }
}

//MARK: - 上传任务系统
-(void)startTaskScaner{
    if (self.timer==nil) {
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(scanTask) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
        self.timer = timer;
    }
}

-(void)scanTask{
//    NSLog(@"扫描任务");
    if (_dataTaskArr.count == 0) {
        // 没有录音数据到来,等待
    }else{
        if (!_hasSpaceAvailable) {
            // 如果没有上传空间,则等待
        }else{
            // 遍历任务数组,找到第一个没完成的任务
            SXVoiceDataTask *task = nil;
            for (int i = 0; i<_dataTaskArr.count; i++) {
                if (!_dataTaskArr[i].hasUpload) {
                    task = _dataTaskArr[i];
                }
            }
            
            if (task != nil) {
                
                if (_isWriting) {
                    return;
                }
                _isWriting = YES;
                
                NSUInteger len = [task.data length];
                Byte *byteData = (Byte *)malloc(len);
                memcpy(byteData, [task.data bytes], len);
                
                NSUInteger ret = [self.outputStream write:byteData maxLength:len];
                if (ret <0) {
                    NSString *logStr =  @"SXMDDSDK=>写入流失败";
                    if (self.delegate) {
                        [self.delegate uploader:self didUploadStatus:10002 description:logStr];
                    }
                    _isWriting = NO;
                    return;
                }
                if (ret != len) {
                    NSString *logStr = [NSString stringWithFormat:@"SXMDDSDK=>写入流缺省,写入:%zd,需写入%zd",ret,len];
                    if (self.delegate) {
                        [self.delegate uploader:self didUploadStatus:10003 description:logStr];
                    }
                    
                    _isWriting = NO;
                    return;
                }
                NSString *logStr = [NSString stringWithFormat:@"SXMDDSDK=>写入流成功%zd",len];
                if (self.delegate) {
                    [self.delegate uploader:self didUploadStatus:10000 description:logStr];
                }
                task.hasUpload = YES;// 标记为已上传
                
                _isWriting = NO;
                _hasSpaceAvailable = false;
                
            }else{
                // 如果当前任务列表所有任务都完成,则不处理
                
            }
            
        }
    }
    
}

-(void)dealloc{
    [self stopTaskScaner];
}


-(void)stopTaskScaner{
    if (self.timer) {
        if ([self.timer respondsToSelector:@selector(isValid)]) {
            if ([self.timer isValid]) {
                [self.timer invalidate];
                self.timer = nil;
            }
        }
    }
}

@end

代码思路:

  1. 确定好http-header字段,封装并开启流任务
 [request setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];
    [request setValue:@"Keep-Alive" forHTTPHeaderField:@"Connection"];
    [request setValue:@"您的校验token" forHTTPHeaderField:@"Authorization"];
    [request setValue:content forHTTPHeaderField:@"word_name"];
    [request setValue:@"stream.wav" forHTTPHeaderField:@"myWavfile"];

    NSURLSessionUploadTask *uploadTask = [session uploadTaskWithStreamedRequest:request];
  1. 流任务开启后会调用NSURLSessionTaskDelegate中初始流方法,注意流是在http-body中上传。这个http会不断读取inputStream中的流,outputStream和inputStream绑定后,向outputStream写入数据,inputStream会读出数据。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
 needNewBodyStream:(void (^)(NSInputStream * _Nullable bodyStream))completionHandler{
    
    // 绑定输入输出流
    NSLog(@"%s",__func__);
    
    NSInputStream *inputStream = nil;
    NSOutputStream *outputStream = nil;
  // 设置流缓冲区,必须比录音分块的data大。一次装不下一个data
    [NSStream getBoundStreamsWithBufferSize:8000*16 inputStream:&inputStream outputStream:&outputStream];
    
    self.bodyStream = inputStream;
    
// 开启写入流,此时stream的代理方法会执行,NSStreamEventHasSpaceAvailable即可写入数据
    self.outputStream = outputStream;
    self.outputStream.delegate = self;
    [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [self.outputStream open];
    
    completionHandler(self.bodyStream);
}

3.之后我们打开outputStream,监听代理其可写状态,并保存可写状态。有些状态需要我们处理,这里略去错误处理

-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
    switch (eventCode) {
        case NSStreamEventNone:
            NSLog(@"NSStreamEventNone");
            break;

        case NSStreamEventOpenCompleted:
           
            NSLog(@"NSStreamEventOpenCompleted");
            break;

        case NSStreamEventHasBytesAvailable: {
            NSLog(@"NSStreamEventHasBytesAvailable");
            
            
        } break;

        case NSStreamEventHasSpaceAvailable: {
            NSLog(@"NSStreamEventHasSpaceAvailable");
        // 置可写标记位
            _hasSpaceAvailable = YES;
            
        } break;

        case NSStreamEventErrorOccurred:
            NSLog(@"NSStreamEventErrorOccurred");
            break;

        case NSStreamEventEndEncountered:
            NSLog(@"NSStreamEventEndEncountered");
            break;

        default:
            break;
    }
}

4.接下来就是等待录音数据到来,外层会调用如下方法把数据传来。
这是先把数据转为byte字节码,然后当可写标记时,写入到outputStream。写完后把可写标记置为否。这里的NSLog的异常信息您需要在错误处理类中记录和处理。这里略去。这里我们使用一个任务队列来存数据,并每隔0.2s扫描一次来上传数据

-(void)uploadData:(NSData *)data{
    
    _alreadyRecord += [data length];
    
    SXVoiceDataTask *task = [SXVoiceDataTask new];
    task.data = data;
    task.hasUpload = NO;
    [_dataTaskArr addObject:task];
    
}

// 扫描任务并上传
-(void)scanTask{
//    NSLog(@"扫描任务");
    if (_dataTaskArr.count == 0) {
        // 没有录音数据到来,等待
    }else{
        if (!_hasSpaceAvailable) {
            // 如果没有上传空间,则等待
        }else{
            // 遍历任务数组,找到第一个没完成的任务
            SXVoiceDataTask *task = nil;
            for (int i = 0; i<_dataTaskArr.count; i++) {
                if (!_dataTaskArr[i].hasUpload) {
                    task = _dataTaskArr[i];
                }
            }
            
            if (task != nil) {
                
                if (_isWriting) {
                    return;
                }
                _isWriting = YES;
                
                NSUInteger len = [task.data length];
                Byte *byteData = (Byte *)malloc(len);
                memcpy(byteData, [task.data bytes], len);
                
                NSUInteger ret = [self.outputStream write:byteData maxLength:len];
                if (ret <0) {
                    NSString *logStr =  @"SXMDDSDK=>写入流失败";
                    if (self.delegate) {
                        [self.delegate uploader:self didUploadStatus:10002 description:logStr];
                    }
                    _isWriting = NO;
                    return;
                }
                if (ret != len) {
                    NSString *logStr = [NSString stringWithFormat:@"SXMDDSDK=>写入流缺省,写入:%zd,需写入%zd",ret,len];
                    if (self.delegate) {
                        [self.delegate uploader:self didUploadStatus:10003 description:logStr];
                    }
                    
                    _isWriting = NO;
                    return;
                }
                NSString *logStr = [NSString stringWithFormat:@"SXMDDSDK=>写入流成功%zd",len];
                if (self.delegate) {
                    [self.delegate uploader:self didUploadStatus:10000 description:logStr];
                }
                task.hasUpload = YES;// 标记为已上传
                
                _isWriting = NO;
                _hasSpaceAvailable = false;
                
            }else{
                // 如果当前任务列表所有任务都完成,则不处理
                
            }
            
        }
    }
    
}

5.上述4的方法会在录音过程中不断调用。当用户关闭录音时,此时我们关闭录音,新数据不会产生,然后做一个标记,在监听方法中判断流已经上传完毕,上传完毕后调用stopstream方法,这个方法会上传一个http-body为空的报文,这时服务端就知道我们结束了上传了。

// 关闭录音时,流可能尚未传输完成,所以只能做一个标记
-(void)endUpload{
    _isEnd = YES;
}

-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend{
    NSString *logStr = [NSString stringWithFormat: @"SXMDDSDK=>已上传:%lld,总上传:%lld,期望上传:%lld",bytesSent,totalBytesSent,totalBytesExpectedToSend];
    if (self.delegate) {
        [self.delegate uploader:self didUploadStatus:10000 description:logStr];
    }
    
    _alreadyUpload += bytesSent;
    
    if (_isEnd && _alreadyRecord == totalBytesSent) {
        
        
        [self stopTaskScaner];
        [self stopStream];
       
//        NSLog(@"上传完毕");
    }
}
-(void)stopStream{
//    NSLog(@"将要关闭流传输");
    self.outputStream.delegate = nil;
    [self.outputStream close];
}

6.当5完成后,服务端会返回响应和响应数据,如下为这些数据的组装。不再赘述。

-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    
    NSHTTPURLResponse *res = (NSHTTPURLResponse *)response;
    if (res.statusCode != 200) {
        NSLog(@"获取结果失败");
        NSLog(@"%@",res.debugDescription);
    }else{
        NSLog(@"获取结果成功");
        completionHandler(NSURLSessionResponseAllow);
    }
    
}


-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
     NSLog(@"%s",__func__);
    
    //拼接数据
    [self.responseData appendData:data];
}


-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    NSLog(@"%s",__func__);
    
    //解析数据
    NSLog(@"%@",[[NSString alloc]initWithData:self.responseData encoding:NSUTF8StringEncoding]);
}

  1. 至此,拿到数据,可以传给管理类,整个边录边上传流程就结束了。我们在管理类中,也可以从mp3压缩器Mp3RecordWriter,拿到本地的mp3文件。

因为之前没玩过流上传,所以大概查资料和整理了两天时间。希望写这个文章能节约您的开发时间,很需要您的赞。

你可能感兴趣的:(iOS 实现stream边录边传功能)