美文网首页
iOS 实现stream边录边传功能

iOS 实现stream边录边传功能

作者: 后青春期的诗大喵 | 来源:发表于2020-02-14 22:12 被阅读0次
    背景

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

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

    realtimeVideoStream

    代码具体实现:
    1. 对于此步,直接参考MLAudioRecorder中的代码,自己写个demo,看下能否录制声音,并打印二进制测试下。
    2. 此步的代码如下
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @class SXVoiceUploader;
    @protocol  SXVoiceUploaderDelegate<NSObject>
    
    -(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 <SXVoiceUploaderDelegate>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 ()<NSURLSessionTaskDelegate,NSStreamDelegate>
    
    @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 <SXVoiceDataTask *>*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边录边传功能

          本文链接:https://www.haomeiwen.com/subject/pzajfhtx.html