流媒体始终是大众生活娱乐最为重要的一个部分,同时也是技术开发中比较有难度的,尤为是直播,不只功能是点播没法替代的,开发难度也要比点播大,里约奥运会等重大致育赛事你们只能经过直播观看比赛,体会现场观看的紧张和刺激,点播是没法作到的。web
现在咱们也会有直播回看和下载的需求,一些APP包括咱们本身的项目也已经实现了这些功能,网上讲解这部分技术的知识相对较少,并且有不少都不是很靠谱,我这里抛砖引玉,给你们提供一种思路,仅供参考。因此建议你们理解个人思路,尽可能不要直接拿来用在项目里,后面我会详细讲解有哪些地方在应用到项目中须要额外的处理。数组
注意: 一、本文不适合初级iOS开发者,须要有必定的开发经验,和对流媒体技术的基本概念和开发技术的了解,例如本文不会讲解什么是TS、AAC和M3U8等概念,这些知识网上不少,你们能够自行查阅理解,这里就赘述了; 二、直播的回看和下载相对于音视频的播放开发难度要大一些,数据处理的思路也比较复杂,因此为了你们能更快的理解和接受,本文着重核心功能的讲解,以避免过多的代码对理解产生干扰,好比咱们拿到一个M3U8连接,咱们要判断这个连接是不是http或者https的,其次要去除连接中的空白字符,注意空白字符不必定是空格,还有多是回车、TAB等其余的空白字符,处理起来也比较繁琐,本文不对这些作过多处理,默认M3U8连接是有效的,小伙伴们在实际项目中要对这些地方作处理,避免所以出现bug; 三、鉴于HLS直播的回看和下载网上可参考的资料太少,若是观看本文的小伙伴有更好的实现方案,欢迎留言,对本文的实现方案提出建议,感激涕零。缓存
回看服务器
HLS直播的回看功能有2种实现方案,2种方案都须要借助服务器。微信
一、第一种方案是服务器将实时获取的TS(AAC音频处理流程同样,后面不赘述)文件片断存储到指定的路径下,当客户端请求某一时间段的回看节目时,服务器取出相对应的TS,打包这些TS片断生成.M3U8索引文件和播放连接,返回给客户端,这是客户端拿到的播放连接和直播的连接是同样的,播放的处理流程也是同样的,只不过这时的直播只能播放一段时间。网络
二、第二种方案是服务器将制定节目的直播内容使用FFMPEG转码成MP4和3GP等点播源,生成播放链接返回给客户端播放就能够了。
注意: 因为回看要借助服务器实现,这里就不附上实现的代码了,客户端的实现比较简单,拿到播放源直接播放就能够了,后面要讲的下载和回看的第一种方案是同样的,都是将TS片断下载下来,能够参考后面的内容。架构
三、两中方案的优缺点分析:
①第一种方案对于服务器来讲处理比较简单,只须要将TS存储并打包便可。对于客户端来讲播放很简单,同时HLS的传输效率也要更高一些,播放速度会很快,可是涉及到调整视频进度、截取视频某一帧图片,监听视频播放状态这些就比较麻烦了。回看的内容虽然也是直播的内容,可是在用户看来无所谓点播和直播,这些已是播放过的节目,天然能够调整进度。这里给出一种调整进度的方案,根据客户端的时间戳向服务器获取相应的TS片断。例以下面这个连接:app
self.playerUrl = @"http://cctv2.vtime.cntv.wscdns.com:8000/live/no/204_/seg0/index.m3u8?begintime=1469509516000";
这个连接有一个参数:begintime,从命名咱们能够看出是要传输一个播放源从哪里开始播放的时间戳,服务器拿到这个参数后会生成对应的数据返回给客户端播放,这里就能够实现精准的进度控制了。
②第二种方案对于服务器来讲要繁琐些,多了一步制做点播源的步骤。对于客户端,第二种方案的好处是直接拿到的是点播的播放源,不管是进度调整、获取帧率图和播放状态的控制都很简单,虽然播放速度相对与HLS来讲会慢一点,但影响并不大。同时因为服务器已经将每个节目转码成功,若是用户要下载这些节目观看,客户端的实现也比较简单。这种方案的缺点是不够灵活,用户只能以节目为时间单位进行回看,没法像第一种方案同样,以时间戳为单位回看,精细度不够。
总结 两种回看方案并无优略之分,具体采用哪种,要看具体项目的需求,小伙伴们在开发过程当中要注意和服务器的联调测试,尤为是第一种方案,M3U8的各类tag设置的不许确也会形成各类播放错误,并无那么容易实现,固然服务器那边也会有一些第三方库能够直接用,因此对于有些开发经验的服务器工程师仍是比较容易实现的。机器学习
下载tcp
下载的流程比较复杂,为了让小伙伴更容易理解,我不会按照个人代码一步步讲解,这样只会让人头晕脑胀,意义不大。我这里按照我在学习新知识时比较容易理解知识的经验来说解。
咱们在学习时,若是只是拿来别人的代码一行行看,遇到不会的查阅,而后再下面的,没一会就头晕了,相信你们都有过这种经验,效果很是差,并且做者在写这些代码的时候并非逐字逐行的写的,而是一次次优化改动得来的,经过代码咱们很难明白做者写代码的逻辑和心路历程,自控力强的多看几遍屡清楚思路能看明白,自控力稍差的可能就放弃了,下面讲解下个人讲解思路和学习方法。
学习思路
实现思路
实现思路能够分为4大步:解码、下载、打包、播放。
说明: 一、本文借鉴了iOS端M3U8第三方库的处理流程,因为这个第三方库长时间没有维护和更新,而且采用了ASI做为网络请求,直接采用会给项目带来大量的警告和错误,还会致使没法适配各类架构等问题,处理起来非常繁琐和棘手,而且即便配置成功,也是没法直接使用的,仍是须要改动第三方库的不少地方,因此我这里模仿M3U8库的部分处理逻辑,同时网络请求使用AFN,固然这里建议你们对AFN作一层封装后再使用,避免AFN升级换代带来没必要要的麻烦。 二、本文封装了一个名为“ZYLDecodeTool”的工具类,负责调度每一步。
HLS下载流程
#import <Foundation/Foundation.h> #import "M3U8Playlist.h" @class ZYLM3U8Handler; @protocol ZYLM3U8HandlerDelegate <NSObject> /** * 解析M3U8链接失败 */ - (void)praseM3U8Finished:(ZYLM3U8Handler *)handler; /** * 解析M3U8成功 */ - (void)praseM3U8Failed:(ZYLM3U8Handler *)handler; @end @interface ZYLM3U8Handler : NSObject /** * 解码M3U8 */ - (void)praseUrl:(NSString *)urlStr; /** * 传输成功或者失败的代理 */ @property (weak, nonatomic)id <ZYLM3U8HandlerDelegate> delegate; /** * 存储TS片断的数组 */ @property (strong, nonatomic) NSMutableArray *segmentArray; /** * 打包获取的TS片断 */ @property (strong, nonatomic) M3U8Playlist *playList; /** * 存储原始的M3U8数据 */ @property (copy, nonatomic) NSString *oriM3U8Str; @end ZYLM3U8Handler.m文件 #import "ZYLM3U8Handler.h" #import "M3U8SegmentModel.h" @implementation ZYLM3U8Handler #pragma mark - 解析M3U8连接 - (void)praseUrl:(NSString *)urlStr { //判断是不是HTTP链接 if (!([urlStr hasPrefix:@"http://"] || [urlStr hasPrefix:@"https://"])) { if (self.delegate != nil && [self.delegate respondsToSelector:@selector(praseM3U8Failed:)]) { [self.delegate praseM3U8Failed:self]; } return; } //解析出M3U8 NSError *error = nil; NSStringEncoding encoding; NSString *m3u8Str = [[NSString alloc] initWithContentsOfURL:[NSURL URLWithString:urlStr] usedEncoding:&encoding error:&error];//这一步是耗时操做,要在子线程中进行 self.oriM3U8Str = m3u8Str; /*注意一、请看代码下方注意1*/ if (m3u8Str == nil) { if (self.delegate != nil && [self.delegate respondsToSelector:@selector(praseM3U8Failed:)]) { [self.delegate praseM3U8Failed:self]; } return; } //解析TS文件 NSRange segmentRange = [m3u8Str rangeOfString:@"#EXTINF:"]; if (segmentRange.location == NSNotFound) { //M3U8里没有TS文件 if (self.delegate != nil && [self.delegate respondsToSelector:@selector(praseM3U8Failed:)]) { [self.delegate praseM3U8Failed:self]; } return; } if (self.segmentArray.count > 0) { [self.segmentArray removeAllObjects]; } //逐个解析TS文件,并存储 while (segmentRange.location != NSNotFound) { //声明一个model存储TS文件连接和时长的model M3U8SegmentModel *model = [[M3U8SegmentModel alloc] init]; //读取TS片断时长 NSRange commaRange = [m3u8Str rangeOfString:@","]; NSString* value = [m3u8Str substringWithRange:NSMakeRange(segmentRange.location + [@"#EXTINF:" length], commaRange.location -(segmentRange.location + [@"#EXTINF:" length]))]; model.duration = [value integerValue]; //截取M3U8 m3u8Str = [m3u8Str substringFromIndex:commaRange.location]; //获取TS下载连接,这须要根据具体的M3U8获取连接,能够根据本身公司的需求 NSRange linkRangeBegin = [m3u8Str rangeOfString:@","]; NSRange linkRangeEnd = [m3u8Str rangeOfString:@".ts"]; NSString* linkUrl = [m3u8Str substringWithRange:NSMakeRange(linkRangeBegin.location + 2, (linkRangeEnd.location + 3) - (linkRangeBegin.location + 2))]; model.locationUrl = linkUrl; [self.segmentArray addObject:model]; m3u8Str = [m3u8Str substringFromIndex:(linkRangeEnd.location + 3)]; segmentRange = [m3u8Str rangeOfString:@"#EXTINF:"]; } /*注意二、请看代码下方注意2*/ //已经获取了全部TS片断,继续打包数据 [self.playList initWithSegmentArray:self.segmentArray]; self.playList.uuid = @"moive1"; //到此数据TS解析成功,经过代理发送成功消息 if (self.delegate != nil && [self.delegate respondsToSelector:@selector(praseM3U8Finished:)]) { [self.delegate praseM3U8Finished:self]; } } #pragma mark - getter - (NSMutableArray *)segmentArray { if (_segmentArray == nil) { _segmentArray = [[NSMutableArray alloc] init]; } return _segmentArray; } - (M3U8Playlist *)playList { if (_playList == nil) { _playList = [[M3U8Playlist alloc] init]; } return _playList; } @end
注意:
一、下面就是解析出来的M3U8索引数据,#EXTINF:10表示的是这段TS的时长是10秒,57b3f432.ts这里表示的是每个TS的文件名,有的M3U8这里直接是一个完成的http连接。前面说到咱们要拼接处每个TS文件的下载连接,这里应该如何拼接呢,在一开始作这里的时候,我也费解了一段时间,查阅了一些资料和博文都不靠谱,因此不建议你们根据这些不靠谱的信息拼接连接,我这里总结出来的经验是,TS文件通常都存储在.M3U8索引文件所在的路径,只须要将TS文件名替换到.M3U8索引便可,固然最靠谱的作法和大家的服务器小伙伴协商好下载路径。
#EXTM3U #EXT-X-VERSION:2 #EXT-X-MEDIA-SEQUENCE:102 #EXT-X-TARGETDURATION:12 #EXTINF:10, 57b3f432.ts #EXTINF:12, 57b3f43c.ts #EXTINF:9, 57b3f446.ts
二、M3U8Playlist是一个存储一个M3U8数据的Model,存储的是TS下载连接数组,数组的数量。uuid设置为固定的"moive1",主要是用来拼接统一的缓存路径。
#import <Foundation/Foundation.h> #import "M3U8Playlist.h" @class ZYLVideoDownLoader; @protocol ZYLVideoDownLoaderDelegate <NSObject> /** * 下载成功 */ - (void)videoDownloaderFinished:(ZYLVideoDownLoader *)videoDownloader; /** * 下载失败 */ - (void)videoDownloaderFailed:(ZYLVideoDownLoader *)videoDownloader; @end @interface ZYLVideoDownLoader : NSObject @property (strong, nonatomic) M3U8Playlist *playList; /** * 记录原始的M3U8 */ @property (copy, nonatomic) NSString *oriM3U8Str; /** * 下载TS数据 */ - (void)startDownloadVideo; /** * 储存正在下载的数组 */ @property (strong, nonatomic) NSMutableArray *downLoadArray; /** * 下载成功或者失败的代理 */ @property (weak, nonatomic) id <ZYLVideoDownLoaderDelegate> delegate; /** * 建立M3U8文件 */ - (void)createLocalM3U8file; @end
下载器ZYLVideoDownLoader.m文件
#import "ZYLVideoDownLoader.h" #import "M3U8SegmentModel.h" #import "SegmentDownloader.h" @interface ZYLVideoDownLoader () <SegmentDownloaderDelegate> @property (assign, nonatomic) NSInteger index;//记录一共多少TS文件 @property (strong, nonatomic) NSMutableArray *downloadUrlArray;//记录全部的下载连接 @property (assign, nonatomic) NSInteger sIndex;//记录下载成功的文件的数量 @end @implementation ZYLVideoDownLoader -(instancetype)init { self = [super init]; if (self) { self.index = 0; self.sIndex = 0; } return self; } #pragma mark - 下载TS数据 - (void)startDownloadVideo { //首相检查是否存在路径 [self checkDirectoryIsCreateM3U8:NO]; __weak __typeof(self)weakSelf = self; /*注意1,请看下方注意1*/ //将解析的数据打包成一个个独立的下载器装进数组 [self.playList.segmentArray enumerateObjectsUsingBlock:^(M3U8SegmentModel *obj, NSUInteger idx, BOOL * _Nonnull stop) { //检查此下载对象是否存在 __block BOOL isE = NO; [weakSelf.downloadUrlArray enumerateObjectsUsingBlock:^(NSString *inObj, NSUInteger inIdx, BOOL * _Nonnull inStop) { if ([inObj isEqualToString:obj.locationUrl]) { //已经存在 isE = YES; *inStop = YES; } else { //不存在 isE = NO; } }]; if (isE) { //存在 } else { //不存在 NSString *fileName = [NSString stringWithFormat:@"id%ld.ts", (long)weakSelf.index]; SegmentDownloader *sgDownloader = [[SegmentDownloader alloc] initWithUrl:[@"http://111.206.23.22:55336/tslive/c25_ct_btv2_btvwyHD_smooth_t10/" stringByAppendingString:obj.locationUrl] andFilePath:weakSelf.playList.uuid andFileName:fileName withDuration:obj.duration withIndex:weakSelf.index]; sgDownloader.delegate = weakSelf; [weakSelf.downLoadArray addObject:sgDownloader]; [weakSelf.downloadUrlArray addObject:obj.locationUrl]; weakSelf.index++; } }]; /*注意2,请看下方注意2*/ //根据新的数据更改新的playList __block NSMutableArray *newPlaylistArray = [[NSMutableArray alloc] init]; [self.downLoadArray enumerateObjectsUsingBlock:^(SegmentDownloader *obj, NSUInteger idx, BOOL * _Nonnull stop) { M3U8SegmentModel *model = [[M3U8SegmentModel alloc] init]; model.duration = obj.duration; model.locationUrl = obj.fileName; model.index = obj.index; [newPlaylistArray addObject:model]; }]; if (newPlaylistArray.count > 0) { self.playList.segmentArray = newPlaylistArray; } //打包完成开始下载 [self.downLoadArray enumerateObjectsUsingBlock:^(SegmentDownloader *obj, NSUInteger idx, BOOL * _Nonnull stop) { obj.flag = YES; [obj start]; }]; } #pragma mark - 检查路径 - (void)checkDirectoryIsCreateM3U8:(BOOL)isC { //建立缓存路径 NSString *pathPrefix = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0]; NSString *saveTo = [[pathPrefix stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:self.playList.uuid]; NSFileManager *fm = [NSFileManager defaultManager]; //路径不存在就建立一个 BOOL isD = [fm fileExistsAtPath:saveTo]; if (isD) { //存在 } else { //不存在 BOOL isS = [fm createDirectoryAtPath:saveTo withIntermediateDirectories:YES attributes:nil error:nil]; if (isS) { NSLog(@"路径不存在建立成功"); } else { NSLog(@"路径不存在建立失败"); } } } #pragma mark - SegmentDownloaderDelegate /*注意3,请看下方注意3*/ #pragma mark - 数据下载成功 - (void)segmentDownloadFinished:(SegmentDownloader *)downloader { //数据下载成功后再数据源中移除当前下载器 self.sIndex++; if (self.sIndex >= 3) { //每次下载完成后都要建立M3U8文件 [self createLocalM3U8file]; //证实全部的TS已经下载完成 [self.delegate videoDownloaderFinished:self]; } } #pragma mark - 数据下载失败 - (void)segmentDownloadFailed:(SegmentDownloader *)downloader { [self.delegate videoDownloaderFailed:self]; } #pragma mark - 进度更新 - (void)segmentProgress:(SegmentDownloader *)downloader TotalUnitCount:(int64_t)totalUnitCount completedUnitCount:(int64_t)completedUnitCount { //NSLog(@"下载进度:%f", completedUnitCount * 1.0 / totalUnitCount * 1.0); } /*注意4,请看下方注意4*/ #pragma mark - 建立M3U8文件 - (void)createLocalM3U8file { [self checkDirectoryIsCreateM3U8:YES]; //建立M3U8的连接地址 NSString *path = [[[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0] stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:self.playList.uuid] stringByAppendingPathComponent:@"movie.m3u8"]; //拼接M3U8连接的头部具体内容 //NSString *header = @"#EXTM3U\n#EXT-X-VERSION:2\n#EXT-X-MEDIA-SEQUENCE:371\n#EXT-X-TARGETDURATION:12\n"; NSString *header = [NSString stringWithFormat:@"#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-TARGETDURATION:15\n"]; //填充M3U8数据 __block NSString *tsStr = [[NSString alloc] init]; [self.playList.segmentArray enumerateObjectsUsingBlock:^(M3U8SegmentModel *obj, NSUInteger idx, BOOL * _Nonnull stop) { //文件名 NSString *fileName = [NSString stringWithFormat:@"id%ld.ts", obj.index]; //文件时长 NSString* length = [NSString stringWithFormat:@"#EXTINF:%ld,\n",obj.duration]; //拼接M3U8 tsStr = [tsStr stringByAppendingString:[NSString stringWithFormat:@"%@%@\n", length, fileName]]; }]; //M3U8头部和中间拼接,到此咱们完成的新的M3U8连接的拼接 header = [header stringByAppendingString:tsStr]; /*注意5,请看下方注意5*/ header = [header stringByAppendingString:@"#EXT-X-ENDLIST"]; //拼接完成,存储到本地 NSMutableData *writer = [[NSMutableData alloc] init]; NSFileManager *fm = [NSFileManager defaultManager]; //判断m3u8是否存在,已经存在的话就再也不从新建立 if ([fm fileExistsAtPath:path isDirectory:nil]) { //存在这个连接 NSLog(@"存在这个连接"); } else { //不存在这个连接 NSString *saveTo = [[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0] stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:self.playList.uuid]; BOOL isS = [fm createDirectoryAtPath:saveTo withIntermediateDirectories:YES attributes:nil error:nil]; if (isS) { NSLog(@"建立目录成功"); } else { NSLog(@"建立目录失败"); } } [writer appendData:[header dataUsingEncoding:NSUTF8StringEncoding]]; BOOL bSucc = [writer writeToFile:path atomically:YES]; if (bSucc) { //成功 NSLog(@"M3U8数据保存成功"); } else { //失败 NSLog(@"M3U8数据保存失败"); } NSLog(@"新数据\n%@", header); } #pragma mark - 删除缓存文件 - (void)deleteCache { //获取缓存路径 NSString *pathPrefix = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0]; NSString *saveTo = [[pathPrefix stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:@"moive1"]; NSFileManager *fm = [NSFileManager defaultManager]; //路径不存在就建立一个 BOOL isD = [fm fileExistsAtPath:saveTo]; if (isD) { //存在 NSArray *deleteArray = [_downloadUrlArray subarrayWithRange:NSMakeRange(0, _downloadUrlArray.count - 20)]; //清空当前的M3U8文件 [deleteArray enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL * _Nonnull stop) { BOOL isS = [fm removeItemAtPath:[saveTo stringByAppendingPathComponent:[NSString stringWithFormat:@"%@", obj]] error:nil]; if (isS) { NSLog(@"多余路径存在清空成功%@", obj); } else { NSLog(@"多余路径存在清空失败%@", obj); } }]; } } #pragma mark - getter - (NSMutableArray *)downLoadArray { if (_downLoadArray == nil) { _downLoadArray = [[NSMutableArray alloc] init]; } return _downLoadArray; } - (NSMutableArray *)downloadUrlArray { if (_downloadUrlArray == nil) { _downloadUrlArray = [[NSMutableArray alloc] init]; } return _downloadUrlArray; } @end
注意:
TS文件下载器
上面的下载器将每个TS文件单独封装,单独下载,下面咱们来看看每个TS文件是如何下载的
TS文件下载器 SegmentDownloader.h文件
#import <Foundation/Foundation.h> @class SegmentDownloader; @protocol SegmentDownloaderDelegate <NSObject> /** * 下载成功 */ - (void)segmentDownloadFinished:(SegmentDownloader *)downloader; /** * 下载失败 */ - (void)segmentDownloadFailed:(SegmentDownloader *)downloader; /** * 监听进度 */ - (void)segmentProgress:(SegmentDownloader *)downloader TotalUnitCount:(int64_t)totalUnitCount completedUnitCount:(int64_t)completedUnitCount; @end @interface SegmentDownloader : NSObject @property (nonatomic, copy) NSString *fileName; @property (nonatomic, copy) NSString *filePath; @property (nonatomic, copy) NSString *downloadUrl; @property (assign, nonatomic) NSInteger duration; @property (assign, nonatomic) NSInteger index; /** * 标记这个下载器是否正在下载 */ @property (assign, nonatomic) BOOL flag; /** * 初始化TS下载器 */ - (instancetype)initWithUrl:(NSString *)url andFilePath:(NSString *)path andFileName:(NSString *)fileName withDuration:(NSInteger)duration withIndex:(NSInteger)index; /** * 传递数据下载成功或者失败的代理 */ @property (strong, nonatomic) id <SegmentDownloaderDelegate> delegate; /** * 开始下载 */ - (void)start; @end TS文件下载器 SegmentDownloader.m文件 #import "SegmentDownloader.h" #import <AFNetworking.h> @interface SegmentDownloader () @property (strong, nonatomic) AFHTTPRequestSerializer *serializer; @property (strong, nonatomic) AFURLSessionManager *downLoadSession; @end @implementation SegmentDownloader #pragma mark - 初始化TS下载器 - (instancetype)initWithUrl:(NSString *)url andFilePath:(NSString *)path andFileName:(NSString *)fileName withDuration:(NSInteger)duration withIndex:(NSInteger)index { self = [super init]; if (self) { self.downloadUrl = url; self.filePath = path; self.fileName = fileName; self.duration = duration; self.index = index; } return self; } #pragma mark - 开始下载 - (void)start { //首先检查此文件是否已经下载 if ([self checkIsDownload]) { //下载了 [self.delegate segmentDownloadFinished:self]; return; } else { //没下载 } //首先拼接存储数据的路径 __block NSString *path = [[[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0] stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:self.filePath] stringByAppendingPathComponent:self.fileName]; /*注意1,请查看下方注意1*/ //这里使用AFN下载,并将数据同时存储到沙盒目录制定的目录中 NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.downloadUrl]]; __block NSProgress *progress = nil; NSURLSessionDownloadTask *downloadTask = [self.downLoadSession downloadTaskWithRequest:request progress:&progress destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) { //在这里告诉AFN数据存储的路径和文件名 NSURL *documentsDirectoryURL = [NSURL fileURLWithPath:path isDirectory:NO]; return documentsDirectoryURL; } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) { if (error == nil) { //下载成功 //NSLog(@"路径%@保存成功", filePath); [self.delegate segmentDownloadFinished:self]; } else { //下载失败 [self.delegate segmentDownloadFailed:self]; } [progress removeObserver:self forKeyPath:@"completedUnitCount"]; }]; //添加对进度的监听 [progress addObserver:self forKeyPath:@"completedUnitCount" options:NSKeyValueObservingOptionNew context:nil]; //开始下载 [downloadTask resume]; } #pragma mark - 检查此文件是否下载过 - (BOOL)checkIsDownload { //获取缓存路径 NSString *pathPrefix = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0]; NSString *saveTo = [[pathPrefix stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:self.filePath]; NSFileManager *fm = [NSFileManager defaultManager]; __block BOOL isE = NO; //获取缓存路径下的全部的文件名 NSArray *subFileArray = [fm subpathsAtPath:saveTo]; [subFileArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { //判断是否已经缓存了此文件 if ([self.fileName isEqualToString:[NSString stringWithFormat:@"%@", obj]]) { //已经下载 isE = YES; *stop = YES; } else { //没有存在 isE = NO; } }]; return isE; } #pragma mark - 监听进度 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(NSProgress *)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"completedUnitCount"]) { [self.delegate segmentProgress:self TotalUnitCount:object.totalUnitCount completedUnitCount:object.completedUnitCount]; } } #pragma mark - getter - (AFHTTPRequestSerializer *)serializer { if (_serializer == nil) { _serializer = [AFHTTPRequestSerializer serializer]; } return _serializer; } - (AFURLSessionManager *)downLoadSession { if (_downLoadSession == nil) { NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; _downLoadSession = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration]; } return _downLoadSession; } @end
注意:
一、这里使用AFN的AFURLSessionManager下载数据并缓存数据到本地,同时能够经过这里得到下载的进度;
二、因为这里是本身下载TS文件,全部如果咱们的项目中有直接操做视频数据的需求,就能够在这里获取视频数据进行处理了。具体的下载流程,你们参考代码便可。
三、为了直观的看到TS文件的下载过程,小伙伴们能够在模拟器上运行DEMO,而后进入到沙盒目录下,能够看到数据的实时更新,以下图:
TS文件下载过程
播放
TS文件下载完成了,.M3U8索引文件也建立好了,那么如何播放呢,看着一段段零散的TS文件,咱们难道要一段段播放给用户看吗?这样显然不合理,这里咱们要使用HLS直播播放技术,模拟服务器和客户端的交互的过程,因此咱们在本地创建一个http服务器,让HLS访问本地的http服务器就能够播放了,下面看看具体的实现过程
创建本地的http服务器
这里咱们使用iOS端颇有名也很好用的CocoaHTTPServer第三方库创建http服务器,能够直接cocoaPods导入工程,导入后建立服务器,代码以下:
- (void)openServer { [DDLog addLogger:[DDTTYLogger sharedInstance]]; self.httpServer=[[HTTPServer alloc]init]; [self.httpServer setType:@"_http._tcp."]; [self.httpServer setPort:9479]; NSString *pathPrefix = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0]; NSString *webPath = [pathPrefix stringByAppendingPathComponent:@"Downloads"]; [self.httpServer setDocumentRoot:webPath]; NSLog(@"服务器路径:%@", webPath); NSError *error; if ([self.httpServer start:&error]) { NSLog(@"开启HTTP服务器 端口:%hu",[self.httpServer listeningPort]); } else{ NSLog(@"服务器启动失败错误为:%@",error); } }
注意:
一、[self.httpServer setPort:9479];这里是设置服务器端口,端口号写一个不容易重复的便可,避免用户手机其余APP也创建了端口号同样的服务器,致使服务器创建失败,或者数据混乱,另外用模拟器在本地创建的服务器,是直接创建的mac上的,能够把播放连接直接给vlc打开播放;
二、[self.httpServer setDocumentRoot:webPath];这一步在给服务器设置路径的时候,必定要注意和缓存TS数据的路径一致;
三、解码工具类中使用了一些定时器,小伙伴们在使用的时候,要记得声明一个销毁解码工具类的方法,在这个方法里销毁定时器等,避免页面没法销毁的bug。
播放
服务器页创建好了,那么播放连接是什么呢?懂一些网络技术的小伙伴可能已经猜到了,服务器是创建在本地的,网络里127.0.0.1是本地IP地址,所以播放链接是:@"http://127.0.0.1:9479/moive1/movie.m3u8", 将这个链接直接交给AVPlayer就能够播放了,用VLC打开,不只能够播放,还能够调整进度。当下载了一些文件后,退出APP,即便在没有网络的状况下打开,也能够正常播放,如图:
手机播放:VLC播放
总结
到这里咱们已经实现了M3U8直播的回看和下载。
本文为小伙伴们提供了一种思路,整个实现过程仍是有些复杂的,须要小伙伴们反复理解,固然有必定的音视频开发技术理解起来就简单多了,本文并无对M3U8作过多技术讲解,这方面的知识能够查阅苹果官方文档:HLS苹果官方资料,这里只是挑出一些问题讲解一下,最终可否理解还要靠小伙伴们本身的努力,若在文中发现错误请及时指正,感激涕零。
知识是无价的,互相学习最重
原创做者:张云龙
校 验:逆流的鱼yuiop
原文连接:https://www.jianshu.com/p/b0db841ed6d3yuiop
欢迎关注个人微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提高•职场突围•思惟跃迁,20万+码农成长充电第一站,陪有梦想的你一块儿成长。