流媒體始終是大衆生活娛樂最爲重要的一個部分,同時也是技術開發中比較有難度的,尤爲是直播,不只功能是點播沒法替代的,開發難度也要比點播大,里約奧運會等重大致育賽事你們只能經過直播觀看比賽,體會現場觀看的緊張和刺激,點播是沒法作到的。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萬+碼農成長充電第一站,陪有夢想的你一塊兒成長。