HLS直播(M3U8)回看和下載功能的實現

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

下載的流程比較複雜,爲了讓小夥伴更容易理解,我不會按照個人代碼一步步講解,這樣只會讓人頭暈腦脹,意義不大。我這裏按照我在學習新知識時比較容易理解知識的經驗來說解。

咱們在學習時,若是隻是拿來別人的代碼一行行看,遇到不會的查閱,而後再下面的,沒一會就頭暈了,相信你們都有過這種經驗,效果很是差,並且做者在寫這些代碼的時候並非逐字逐行的寫的,而是一次次優化改動得來的,經過代碼咱們很難明白做者寫代碼的邏輯和心路歷程,自控力強的多看幾遍屢清楚思路能看明白,自控力稍差的可能就放棄了,下面講解下個人講解思路和學習方法。

學習思路

  • ①首先我會說明HLS下載的實現思路,小夥伴們在看這部分的時候不要把本身當成技術人員,各行各業最有價值的都是解決問題的思想和能力,而不是代碼、文字和各類工具等,因此我儘可能讓一個沒有任何開發技術的人明白HLS下載的邏輯,明白瞭解決問題的邏輯,再看後面的代碼就不至於暈頭轉向了;
  • ②其次我會按照流程逐步講解,在講解每一步流程時,每一步也是一個相對獨立的子流程,我也會大概的描述下每一步子流程的實現思路,小夥伴們理解起來也會更加簡單;
  • ③最後說下小夥伴們在閱讀時的一些注意事項。在對核心功能尚未充分理解的前提下,不要太在乎一些技術細節,好比這裏爲何調用這個方法、這樣作性能不過高等等和核心功能無關的。等小夥伴們對核心功能理解了,再來優化和理解一些小的地方,纔會駕輕就熟。因爲咱們寫這些代碼的時候考慮的也不是很健全,全部會有不少地方寫得不完美,也歡迎小夥伴們留言指出來,絕對知錯就改,感激涕零。

實現思路
實現思路能夠分爲4大步:解碼、下載、打包、播放。

  • 解碼:拿到一個M3U8連接後解析出M3U8索引的具體內容,包括每個TS的下載連接、時長等;
  • 下載:拿到每個TS文件的連接就能夠逐個下載了,下載後存儲到手機裏;
  • 打包:將下載的TS數據按照播放順序打包,供客戶端播放;
  • 播放:數據打包完成,就能夠播放了。

說明: 一、本文借鑑了iOS端M3U8第三方庫的處理流程,因爲這個第三方庫長時間沒有維護和更新,而且採用了ASI做爲網絡請求,直接採用會給項目帶來大量的警告和錯誤,還會致使沒法適配各類架構等問題,處理起來非常繁瑣和棘手,而且即便配置成功,也是沒法直接使用的,仍是須要改動第三方庫的不少地方,因此我這裏模仿M3U8庫的部分處理邏輯,同時網絡請求使用AFN,固然這裏建議你們對AFN作一層封裝後再使用,避免AFN升級換代帶來沒必要要的麻煩。 二、本文封裝了一個名爲「ZYLDecodeTool」的工具類,負責調度每一步。

HLS下載流程

  • 解碼
    解碼這一步就作一件事情,拿到播放連接,讀取M3U8索引文件,解析出每個TS文件的下載地址和時長,封裝到Model中,供後面使用。
    解碼器ZYLM3U8Handler.h文件
#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",主要是用來拼接統一的緩存路徑。

  • 下載
    拿到每個TS的連接就能夠下載了,下載後緩存到本地。
    下載器ZYLVideoDownLoader.h文件
#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

注意:

  • 一、這裏獲取到的M3U8數據包含了不少TS文件,並不會在下載器裏直接下載,而是要對每個TS文件再次封裝,而後每個封裝好的數據模型單獨下載;
  • 二、這裏更新playlist的目的是爲了後續建立.M3U8索引,能夠暫時略過這裏,到了建立索引的地方天然就懂了;
  • 三、這是數據下載成功的代理,因爲本文使用的測試鏈接每個M3U8裏有3個TS文件,因此當第一次3個文件所有下載完成後告訴系在工具類下載完成,後續沒下載完成一個就告訴下載工具類一次;
  • 四、在第一次3個TS文件下載成功和後續每有一個TS下載成功後,都會更新.M3U8索引文件,保證索引文件的更新;
  • 五、這裏要注意,添加了#EXT-X-ENDLIST,代表這個源事HLS的點播源,當播放的時候,HLS會從頭開始播放。

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萬+碼農成長充電第一站,陪有夢想的你一塊兒成長。

相關文章
相關標籤/搜索