iOS 实现stream边录边传功能
2020-02-14 本文已影响0人
后青春期的诗大喵
背景
网上有很多帖子写这个功能,但是大部分零零碎碎,没办法直接用。
本文把思路整理下,并且真正可用,有问题可以微信我(lishi_655)。
如果您觉得好请帮忙点个赞。Thanks♪(・ω・)ノ
实现步骤:
- 首先是边录边压缩录音流,参考代码MLAudioRecorder
。本文使用了其中的三个类:录音类MLAudioRecorder ,pcm转mp3类Mp3RecordWriter,音量大小监听类MLAudioMeterObserver。 - 对获得的二进制录音流,使用我封装的CCVoiceUploader来上传流。关于上传的思路,网上有帖子写,例如这篇,但是不完整,且有问题。Stack Overflow上的问答也没有可用的。流的传输还需要参考苹果论坛中官方回复才能理解写法。苹果开发人员还是厉害。
- 您需要基于1和2封装一个管理和错误处理类,如果需要可以微信找我要代码。
框架github链接
代码具体实现:
- 对于此步,直接参考MLAudioRecorder中的代码,自己写个demo,看下能否录制声音,并打印二进制测试下。
- 此步的代码如下
#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
代码思路:
- 确定好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];
- 流任务开启后会调用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]);
}
- 至此,拿到数据,可以传给管理类,整个边录边上传流程就结束了。我们在管理类中,也可以从mp3压缩器Mp3RecordWriter,拿到本地的mp3文件。
因为之前没玩过流上传,所以大概查资料和整理了两天时间。希望写这个文章能节约您的开发时间,很需要您的赞。😆