iOS流媒體開發(fā)之三:HLS直播(M3U8)回看和下載功能的實(shí)現(xiàn)

尊重知識(shí),轉(zhuǎn)發(fā)請(qǐng)注明出處:iOS流媒體開發(fā)之三:HLS直播(M3U8)回看和下載功能的實(shí)現(xiàn)


概要

流媒體開發(fā)第一篇文章就說要把這些不是隨便就可以百度到的知識(shí)獻(xiàn)給“簡(jiǎn)書”脱盲,拖了一個(gè)多月了乾蛤,總算弄完了每界,深深松了口氣,萬幸沒有食言家卖,否則對(duì)不起小伙伴們眨层。

流媒體始終是大眾生活?yuàn)蕵纷顬橹匾囊粋€(gè)部分,同時(shí)也是技術(shù)開發(fā)中比較有難度的上荡,尤其是直播趴樱,不僅功能是點(diǎn)播無法替代的,開發(fā)難度也要比點(diǎn)播大酪捡,里約奧運(yùn)會(huì)等重大體育賽事大家只能通過直播觀看比賽叁征,體會(huì)現(xiàn)場(chǎng)觀看的緊張和刺激,點(diǎn)播是無法做到的逛薇。
如今我們也會(huì)有直播回看和下載的需求捺疼,一些APP包括我們自己的項(xiàng)目也已經(jīng)實(shí)現(xiàn)了這些功能,網(wǎng)上講解這部分技術(shù)的知識(shí)相對(duì)較少永罚,而且有很多都不是很靠譜啤呼,我這里拋磚引玉,給大家提供一種思路呢袱,僅供參考官扣。所以建議大家理解我的思路,盡量不要直接拿來用在項(xiàng)目里产捞,后面我會(huì)詳細(xì)講解有哪些地方在應(yīng)用到項(xiàng)目中需要額外的處理醇锚。
注意: 1、本文不適合初級(jí)iOS開發(fā)者,需要有一定的開發(fā)經(jīng)驗(yàn)焊唬,和對(duì)流媒體技術(shù)的基本概念和開發(fā)技術(shù)的了解恋昼,例如本文不會(huì)講解什么是TS、AAC和M3U8等概念赶促,這些知識(shí)網(wǎng)上很多液肌,大家可以自行查閱理解,這里就贅述了鸥滨; 2嗦哆、直播的回看和下載相對(duì)于音視頻的播放開發(fā)難度要大一些,數(shù)據(jù)處理的思路也比較復(fù)雜婿滓,所以為了大家能更快的理解和接受老速,本文著重核心功能的講解,以免過多的代碼對(duì)理解產(chǎn)生干擾凸主,比如我們拿到一個(gè)M3U8鏈接橘券,我們要判斷這個(gè)鏈接是否是http或者h(yuǎn)ttps的,其次要去除鏈接中的空白字符卿吐,注意空白字符不一定是空格旁舰,還有可能是回車、TAB等其他的空白字符嗡官,處理起來也比較繁瑣箭窜,本文不對(duì)這些做過多處理,默認(rèn)M3U8鏈接是有效的衍腥,小伙伴們?cè)趯?shí)際項(xiàng)目中要對(duì)這些地方做處理磺樱,避免因此出現(xiàn)bug; 3婆咸、鑒于HLS直播的回看和下載網(wǎng)上可參考的資料太少坊罢,如果觀看本文的小伙伴有更好的實(shí)現(xiàn)方案,歡迎留言擅耽,對(duì)本文的實(shí)現(xiàn)方案提出建議活孩,感激不盡。

回看

HLS直播的回看功能有2種實(shí)現(xiàn)方案乖仇,2種方案都需要借助服務(wù)器憾儒。
1、第一種方案是服務(wù)器將實(shí)時(shí)獲取的TS(AAC音頻處理流程一樣乃沙,后面不贅述)文件片段存儲(chǔ)到指定的路徑下起趾,當(dāng)客戶端請(qǐng)求某一時(shí)間段的回看節(jié)目時(shí),服務(wù)器取出相對(duì)應(yīng)的TS警儒,打包這些TS片段生成.M3U8索引文件和播放鏈接训裆,返回給客戶端眶根,這是客戶端拿到的播放鏈接和直播的鏈接是一樣的,播放的處理流程也是一樣的边琉,只不過這時(shí)的直播只能播放一段時(shí)間属百。
2、第二種方案是服務(wù)器將制定節(jié)目的直播內(nèi)容使用FFMPEG轉(zhuǎn)碼成MP4和3GP等點(diǎn)播源变姨,生成播放連接返回給客戶端播放就可以了族扰。
注意: 由于回看要借助服務(wù)器實(shí)現(xiàn),這里就不附上實(shí)現(xiàn)的代碼了定欧,客戶端的實(shí)現(xiàn)比較簡(jiǎn)單渔呵,拿到播放源直接播放就可以了,后面要講的下載和回看的第一種方案是一樣的砍鸠,都是將TS片段下載下來扩氢,可以參考后面的內(nèi)容。
3爷辱、兩中方案的優(yōu)缺點(diǎn)分析:
①第一種方案對(duì)于服務(wù)器來說處理比較簡(jiǎn)單类茂,只需要將TS存儲(chǔ)并打包即可。對(duì)于客戶端來說播放很簡(jiǎn)單托嚣,同時(shí)HLS的傳輸效率也要更高一些,播放速度會(huì)很快厚骗,但是涉及到調(diào)整視頻進(jìn)度示启、截取視頻某一幀圖片,監(jiān)聽視頻播放狀態(tài)這些就比較麻煩了领舰》蛏ぃ回看的內(nèi)容雖然也是直播的內(nèi)容,但是在用戶看來無所謂點(diǎn)播和直播冲秽,這些已經(jīng)是播放過的節(jié)目舍咖,自然可以調(diào)整進(jìn)度。這里給出一種調(diào)整進(jìn)度的方案锉桑,根據(jù)客戶端的時(shí)間戳向服務(wù)器獲取相應(yīng)的TS片段排霉。例如下面這個(gè)鏈接:

self.playerUrl = @"http://cctv2.vtime.cntv.wscdns.com:8000/live/no/204_/seg0/index.m3u8?begintime=1469509516000";

這個(gè)鏈接有一個(gè)參數(shù):begintime,從命名我們可以看出是要傳輸一個(gè)播放源從哪里開始播放的時(shí)間戳民轴,服務(wù)器拿到這個(gè)參數(shù)后會(huì)生成對(duì)應(yīng)的數(shù)據(jù)返回給客戶端播放攻柠,這里就可以實(shí)現(xiàn)精準(zhǔn)的進(jìn)度控制了。
②第二種方案對(duì)于服務(wù)器來說要繁瑣些后裸,多了一步制作點(diǎn)播源的步驟瑰钮。對(duì)于客戶端,第二種方案的好處是直接拿到的是點(diǎn)播的播放源微驶,無論是進(jìn)度調(diào)整浪谴、獲取幀率圖和播放狀態(tài)的控制都很簡(jiǎn)單,雖然播放速度相對(duì)與HLS來說會(huì)慢一點(diǎn),但影響并不大苟耻。同時(shí)由于服務(wù)器已經(jīng)將每一個(gè)節(jié)目轉(zhuǎn)碼成功篇恒,如果用戶要下載這些節(jié)目觀看,客戶端的實(shí)現(xiàn)也比較簡(jiǎn)單梁呈。這種方案的缺點(diǎn)是不夠靈活婚度,用戶只能以節(jié)目為時(shí)間單位進(jìn)行回看,無法像第一種方案一樣官卡,以時(shí)間戳為單位回看蝗茁,精細(xì)度不夠。
總結(jié) 兩種回看方案并沒有優(yōu)略之分寻咒,具體采用哪一種哮翘,要看具體項(xiàng)目的需求,小伙伴們?cè)陂_發(fā)過程中要注意和服務(wù)器的聯(lián)調(diào)測(cè)試毛秘,尤其是第一種方案饭寺,M3U8的各種tag設(shè)置的不準(zhǔn)確也會(huì)造成各種播放錯(cuò)誤,并沒有那么容易實(shí)現(xiàn)叫挟,當(dāng)然服務(wù)器那邊也會(huì)有一些第三方庫可以直接用艰匙,所以對(duì)于有些開發(fā)經(jīng)驗(yàn)的服務(wù)器工程師還是比較容易實(shí)現(xiàn)的。

下載

下載的流程比較復(fù)雜抹恳,為了讓小伙伴更容易理解员凝,我不會(huì)按照我的代碼一步步講解,這樣只會(huì)讓人頭暈?zāi)X脹奋献,意義不大健霹。我這里按照我在學(xué)習(xí)新知識(shí)時(shí)比較容易理解知識(shí)的經(jīng)驗(yàn)來講解。
我們?cè)趯W(xué)習(xí)時(shí)瓶蚂,如果只是拿來別人的代碼一行行看糖埋,遇到不會(huì)的查閱,然后再下面的窃这,沒一會(huì)就頭暈了瞳别,相信大家都有過這種經(jīng)驗(yàn),效果非常差杭攻,而且作者在寫這些代碼的時(shí)候并不是逐字逐行的寫的洒试,而是一次次優(yōu)化改動(dòng)得來的,通過代碼我們很難明白作者寫代碼的邏輯和心路歷程朴上,自控力強(qiáng)的多看幾遍屢清楚思路能看明白垒棋,自控力稍差的可能就放棄了,下面講解下我的講解思路和學(xué)習(xí)方法痪宰。

*學(xué)習(xí)思路

①首先我會(huì)說明HLS下載的實(shí)現(xiàn)思路叼架,小伙伴們?cè)诳催@部分的時(shí)候不要把自己當(dāng)成技術(shù)人員畔裕,各行各業(yè)最有價(jià)值的都是解決問題的思想和能力,而不是代碼乖订、文字和各種工具等扮饶,所以我盡量讓一個(gè)沒有任何開發(fā)技術(shù)的人明白HLS下載的邏輯,明白了解決問題的邏輯乍构,再看后面的代碼就不至于暈頭轉(zhuǎn)向了甜无;
②其次我會(huì)按照流程逐步講解,在講解每一步流程時(shí)哥遮,每一步也是一個(gè)相對(duì)獨(dú)立的子流程岂丘,我也會(huì)大概的描述下每一步子流程的實(shí)現(xiàn)思路,小伙伴們理解起來也會(huì)更加簡(jiǎn)單眠饮;
③最后說下小伙伴們?cè)陂喿x時(shí)的一些注意事項(xiàng)奥帘。在對(duì)核心功能還沒有充分理解的前提下,不要太在意一些技術(shù)細(xì)節(jié)仪召,比如這里為什么調(diào)用這個(gè)方法寨蹋、這樣做性能不太高等等和核心功能無關(guān)的。等小伙伴們對(duì)核心功能理解了扔茅,再來優(yōu)化和理解一些小的地方已旧,才會(huì)得心應(yīng)手。由于我們寫這些代碼的時(shí)候考慮的也不是很健全召娜,所有會(huì)有很多地方寫得不完美运褪,也歡迎小伙伴們留言指出來,絕對(duì)知錯(cuò)就改萤晴,感激不盡。

*實(shí)現(xiàn)思路

實(shí)現(xiàn)思路可以分為4大步:解碼胁后、下載店读、打包、播放攀芯。
解碼:拿到一個(gè)M3U8鏈接后解析出M3U8索引的具體內(nèi)容屯断,包括每一個(gè)TS的下載鏈接、時(shí)長(zhǎng)等侣诺;
下載:拿到每一個(gè)TS文件的鏈接就可以逐個(gè)下載了殖演,下載后存儲(chǔ)到手機(jī)里;
打包:將下載的TS數(shù)據(jù)按照播放順序打包年鸳,供客戶端播放趴久;
播放:數(shù)據(jù)打包完成,就可以播放了搔确。
說明: 1彼棍、本文借鑒了iOS端M3U8第三方庫的處理流程灭忠,由于這個(gè)第三方庫長(zhǎng)時(shí)間沒有維護(hù)和更新,并且采用了ASI作為網(wǎng)絡(luò)請(qǐng)求座硕,直接采用會(huì)給項(xiàng)目帶來大量的警告和錯(cuò)誤弛作,還會(huì)導(dǎo)致無法適配各種架構(gòu)等問題,處理起來很是繁瑣和棘手华匾,并且即使配置成功映琳,也是無法直接使用的,還是需要改動(dòng)第三方庫的很多地方蜘拉,所以我這里模仿M3U8庫的部分處理邏輯萨西,同時(shí)網(wǎng)絡(luò)請(qǐng)求使用AFN,當(dāng)然這里建議大家對(duì)AFN做一層封裝后再使用诸尽,避免AFN升級(jí)換代帶來不必要的麻煩原杂。 2、本文封裝了一個(gè)名為“ZYLDecodeTool”的工具類您机,負(fù)責(zé)調(diào)度每一步穿肄。

HLS下載流程
*解碼

解碼這一步就做一件事情,拿到播放鏈接际看,讀取M3U8索引文件咸产,解析出每一個(gè)TS文件的下載地址和時(shí)長(zhǎng),封裝到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;

/**
 * 存儲(chǔ)TS片段的數(shù)組
 */
@property (strong, nonatomic) NSMutableArray *segmentArray;

/**
 * 打包獲取的TS片段
 */
@property (strong, nonatomic) M3U8Playlist *playList;

/**
 * 存儲(chǔ)原始的M3U8數(shù)據(jù)
 */
@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];//這一步是耗時(shí)操作,要在子線程中進(jìn)行
self.oriM3U8Str = m3u8Str;
    /*注意1赖欣、請(qǐng)看代碼下方注意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];
}

    //逐個(gè)解析TS文件屑彻,并存儲(chǔ)
    while (segmentRange.location != NSNotFound) {
        //聲明一個(gè)model存儲(chǔ)TS文件鏈接和時(shí)長(zhǎng)的model
        M3U8SegmentModel *model = [[M3U8SegmentModel alloc] init];
        //讀取TS片段時(shí)長(zhǎng)
        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下載鏈接,這需要根據(jù)具體的M3U8獲取鏈接,可以根據(jù)自己公司的需求
        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顶吮、請(qǐng)看代碼下方注意2*/
    //已經(jīng)獲取了所有TS片段社牲,繼續(xù)打包數(shù)據(jù)
    [self.playList initWithSegmentArray:self.segmentArray];
    self.playList.uuid = @"moive1";
    
    //到此數(shù)據(jù)TS解析成功,通過代理發(fā)送成功消息
    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

注意:
1悴了、下面就是解析出來的M3U8索引數(shù)據(jù)搏恤,#EXTINF:10表示的是這段TS的時(shí)長(zhǎng)是10秒,57b3f432.ts這里表示的是每一個(gè)TS的文件名湃交,有的M3U8這里直接是一個(gè)完成的http鏈接熟空。前面說到我們要拼接處每一個(gè)TS文件的下載鏈接,這里應(yīng)該如何拼接呢搞莺,在一開始做這里的時(shí)候息罗,我也費(fèi)解了一段時(shí)間,查閱了一些資料和博文都不靠譜才沧,所以不建議大家根據(jù)這些不靠譜的信息拼接鏈接阱当,我這里總結(jié)出來的經(jīng)驗(yàn)是俏扩,TS文件一般都存儲(chǔ)在.M3U8索引文件所在的路徑,只需要將TS文件名替換到.M3U8索引即可弊添,當(dāng)然最靠譜的做法和你們的服務(wù)器小伙伴協(xié)商好下載路徑录淡。
#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

2、M3U8Playlist是一個(gè)存儲(chǔ)一個(gè)M3U8數(shù)據(jù)的Model油坝,存儲(chǔ)的是TS下載鏈接數(shù)組嫉戚,數(shù)組的數(shù)量。uuid設(shè)置為固定的"moive1"澈圈,主要是用來拼接統(tǒng)一的緩存路徑彬檀。

*下載

拿到每一個(gè)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數(shù)據(jù)
     */
    - (void)startDownloadVideo;
    
    /**
     * 儲(chǔ)存正在下載的數(shù)組
     */
    @property (strong, nonatomic) NSMutableArray *downLoadArray;
    
    /**
     * 下載成功或者失敗的代理
     */
    @property (weak, nonatomic) id <ZYLVideoDownLoaderDelegate> delegate;
    
    /**
     * 創(chuàng)建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;//記錄下載成功的文件的數(shù)量
    
    @end
    
    @implementation ZYLVideoDownLoader
    
    -(instancetype)init {
        self = [super init];
        if (self) {
            self.index = 0;
            self.sIndex = 0;
        }
        return self;
    }
    
#pragma mark - 下載TS數(shù)據(jù)
    - (void)startDownloadVideo {
        //首相檢查是否存在路徑
        [self checkDirectoryIsCreateM3U8:NO];
        
        __weak __typeof(self)weakSelf = self;

/*注意1窍帝,請(qǐng)看下方注意1*/
        //將解析的數(shù)據(jù)打包成一個(gè)個(gè)獨(dú)立的下載器裝進(jìn)數(shù)組
        [self.playList.segmentArray enumerateObjectsUsingBlock:^(M3U8SegmentModel *obj, NSUInteger idx, BOOL * _Nonnull stop) {
            //檢查此下載對(duì)象是否存在
            __block BOOL isE = NO;
            [weakSelf.downloadUrlArray enumerateObjectsUsingBlock:^(NSString *inObj, NSUInteger inIdx, BOOL * _Nonnull inStop) {
                if ([inObj isEqualToString:obj.locationUrl]) {
                    //已經(jīng)存在
                    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,請(qǐng)看下方注意2*/
        //根據(jù)新的數(shù)據(jù)更改新的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 {
        //創(chuàng)建緩存路徑
        NSString *pathPrefix = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0];
        NSString *saveTo = [[pathPrefix stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:self.playList.uuid];
        NSFileManager *fm = [NSFileManager defaultManager];
        //路徑不存在就創(chuàng)建一個(gè)
        BOOL isD = [fm fileExistsAtPath:saveTo];
        if (isD) {
            //存在
            
        } else {
            //不存在
            BOOL isS = [fm createDirectoryAtPath:saveTo withIntermediateDirectories:YES attributes:nil error:nil];
            if (isS) {
                NSLog(@"路徑不存在創(chuàng)建成功");
            } else {
                NSLog(@"路徑不存在創(chuàng)建失敗");
            }
            
        }
    }
    
#pragma mark - SegmentDownloaderDelegate

 /*注意3诽偷,請(qǐng)看下方注意3*/
#pragma mark - 數(shù)據(jù)下載成功
    - (void)segmentDownloadFinished:(SegmentDownloader *)downloader {
        //數(shù)據(jù)下載成功后再數(shù)據(jù)源中移除當(dāng)前下載器
        self.sIndex++;
        if (self.sIndex >= 3) {
            //每次下載完成后都要?jiǎng)?chuàng)建M3U8文件
            [self createLocalM3U8file];
            //證明所有的TS已經(jīng)下載完成
            [self.delegate videoDownloaderFinished:self];
        }
        
    }
    
#pragma mark - 數(shù)據(jù)下載失敗
    - (void)segmentDownloadFailed:(SegmentDownloader *)downloader {
        [self.delegate videoDownloaderFailed:self];
    }
    
#pragma mark - 進(jìn)度更新
    - (void)segmentProgress:(SegmentDownloader *)downloader TotalUnitCount:(int64_t)totalUnitCount completedUnitCount:(int64_t)completedUnitCount {
        //NSLog(@"下載進(jìn)度:%f", completedUnitCount * 1.0 / totalUnitCount * 1.0);
    }
    

 /*注意4坤学,請(qǐng)看下方注意4*/
#pragma mark - 創(chuàng)建M3U8文件
            - (void)createLocalM3U8file {
        
        [self checkDirectoryIsCreateM3U8:YES];
        //創(chuàng)建M3U8的鏈接地址
        NSString *path = [[[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0] stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:self.playList.uuid] stringByAppendingPathComponent:@"movie.m3u8"];
        
        //拼接M3U8鏈接的頭部具體內(nèi)容
        //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數(shù)據(jù)
        __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];
            //文件時(shí)長(zhǎng)
            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,請(qǐng)看下方注意5*/
        header = [header stringByAppendingString:@"#EXT-X-ENDLIST"];
        //拼接完成报慕,存儲(chǔ)到本地
        NSMutableData *writer = [[NSMutableData alloc] init];
        NSFileManager *fm = [NSFileManager defaultManager];
        //判斷m3u8是否存在,已經(jīng)存在的話就不再重新創(chuàng)建
        if ([fm fileExistsAtPath:path isDirectory:nil]) {
            //存在這個(gè)鏈接
            NSLog(@"存在這個(gè)鏈接");
        } else {
            //不存在這個(gè)鏈接
            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(@"創(chuàng)建目錄成功");
            } else {
                NSLog(@"創(chuàng)建目錄失敗");
            }
        }
        [writer appendData:[header dataUsingEncoding:NSUTF8StringEncoding]];
        BOOL bSucc = [writer writeToFile:path atomically:YES];
        if (bSucc) {
            //成功
            NSLog(@"M3U8數(shù)據(jù)保存成功");
        } else {
            //失敗
            NSLog(@"M3U8數(shù)據(jù)保存失敗");
        }
        NSLog(@"新數(shù)據(jù)\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];
        //路徑不存在就創(chuàng)建一個(gè)
        BOOL isD = [fm fileExistsAtPath:saveTo];
        if (isD) {
            //存在
            NSArray *deleteArray = [_downloadUrlArray subarrayWithRange:NSMakeRange(0, _downloadUrlArray.count - 20)];
            //清空當(dāng)前的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

注意:
1深浮、這里獲取到的M3U8數(shù)據(jù)包含了很多TS文件,并不會(huì)在下載器里直接下載眠冈,而是要對(duì)每一個(gè)TS文件再次封裝飞苇,然后每一個(gè)封裝好的數(shù)據(jù)模型單獨(dú)下載;
2蜗顽、這里更新playlist的目的是為了后續(xù)創(chuàng)建.M3U8索引布卡,可以暫時(shí)略過這里,到了創(chuàng)建索引的地方自然就懂了雇盖;
3忿等、這是數(shù)據(jù)下載成功的代理,由于本文使用的測(cè)試連接每一個(gè)M3U8里有3個(gè)TS文件刊懈,所以當(dāng)?shù)谝淮?個(gè)文件全部下載完成后告訴系在工具類下載完成这弧,后續(xù)沒下載完成一個(gè)就告訴下載工具類一次娃闲;
4虚汛、在第一次3個(gè)TS文件下載成功和后續(xù)每有一個(gè)TS下載成功后,都會(huì)更新.M3U8索引文件皇帮,保證索引文件的更新卷哩;
5、這里要注意属拾,添加了#EXT-X-ENDLIST将谊,表明這個(gè)源事HLS的點(diǎn)播源冷溶,當(dāng)播放的時(shí)候,HLS會(huì)從頭開始播放尊浓。

*TS文件下載器

上面的下載器將每一個(gè)TS文件單獨(dú)封裝逞频,單獨(dú)下載,下面我們來看看每一個(gè)TS文件是如何下載的

TS文件下載器 SegmentDownloader.h文件

#import <Foundation/Foundation.h>
@class SegmentDownloader;

@protocol SegmentDownloaderDelegate <NSObject>

/**
 * 下載成功
 */
- (void)segmentDownloadFinished:(SegmentDownloader *)downloader;

/**
 * 下載失敗
 */
- (void)segmentDownloadFailed:(SegmentDownloader *)downloader;

/**
 * 監(jiān)聽進(jìn)度
 */
- (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;

/**
 * 標(biāo)記這個(gè)下載器是否正在下載
 */
@property (assign, nonatomic) BOOL flag;

/**
 * 初始化TS下載器
 */
- (instancetype)initWithUrl:(NSString *)url andFilePath:(NSString *)path andFileName:(NSString *)fileName withDuration:(NSInteger)duration withIndex:(NSInteger)index;

/**
 * 傳遞數(shù)據(jù)下載成功或者失敗的代理
 */
@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 {
    //首先檢查此文件是否已經(jīng)下載
    if ([self checkIsDownload]) {
        //下載了
        [self.delegate segmentDownloadFinished:self];
        return;
    } else {
        //沒下載
        
    }
    
    //首先拼接存儲(chǔ)數(shù)據(jù)的路徑
    __block NSString *path = [[[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0] stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:self.filePath] stringByAppendingPathComponent:self.fileName];
    /*注意1栋齿,請(qǐng)查看下方注意1*/
    //這里使用AFN下載,并將數(shù)據(jù)同時(shí)存儲(chǔ)到沙盒目錄制定的目錄中
    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數(shù)據(jù)存儲(chǔ)的路徑和文件名
        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"];
    }];
    //添加對(duì)進(jìn)度的監(jiān)聽
    [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) {
        //判斷是否已經(jīng)緩存了此文件
        if ([self.fileName isEqualToString:[NSString stringWithFormat:@"%@", obj]]) {
            //已經(jīng)下載
            isE = YES;
            *stop = YES;
        } else {
            //沒有存在
            isE = NO;
        }
    }];
    
    return isE;
}

#pragma mark - 監(jiān)聽進(jìn)度
- (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

注意:
1苗胀、這里使用AFN的AFURLSessionManager下載數(shù)據(jù)并緩存數(shù)據(jù)到本地,同時(shí)可以通過這里獲得下載的進(jìn)度瓦堵;
2基协、由于這里是自己下載TS文件,所有若是我們的項(xiàng)目中有直接操作視頻數(shù)據(jù)的需求菇用,就可以在這里獲取視頻數(shù)據(jù)進(jìn)行處理了澜驮。具體的下載流程,大家參考代碼即可惋鸥。
3杂穷、為了直觀的看到TS文件的下載過程,小伙伴們可以在模擬器上運(yùn)行DEMO揩慕,然后進(jìn)入到沙盒目錄下亭畜,可以看到數(shù)據(jù)的實(shí)時(shí)更新,如下圖:

TS文件下載過程
TS文件下載過程

播放

TS文件下載完成了迎卤,.M3U8索引文件也創(chuàng)建好了拴鸵,那么如何播放呢,看著一段段零散的TS文件蜗搔,我們難道要一段段播放給用戶看嗎劲藐?這樣顯然不合理,這里我們要使用HLS直播播放技術(shù)樟凄,模擬服務(wù)器和客戶端的交互的過程聘芜,所以我們?cè)诒镜亟⒁粋€(gè)http服務(wù)器,讓HLS訪問本地的http服務(wù)器就可以播放了缝龄,下面看看具體的實(shí)現(xiàn)過程

*建立本地的http服務(wù)器

這里我們使用iOS端很有名也很好用的CocoaHTTPServer第三方庫建立http服務(wù)器汰现,可以直接cocoaPods導(dǎo)入工程,導(dǎo)入后創(chuàng)建服務(wù)器叔壤,代碼如下:

- (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(@"服務(wù)器路徑:%@", webPath);
    NSError *error;
    if ([self.httpServer start:&error]) {
        NSLog(@"開啟HTTP服務(wù)器 端口:%hu",[self.httpServer listeningPort]);
    }
    else{
        NSLog(@"服務(wù)器啟動(dòng)失敗錯(cuò)誤為:%@",error);
    }
}

注意:
1瞎饲、[self.httpServer setPort:9479];這里是設(shè)置服務(wù)器端口,端口號(hào)寫一個(gè)不容易重復(fù)的即可炼绘,避免用戶手機(jī)其他APP也建立了端口號(hào)一樣的服務(wù)器嗅战,導(dǎo)致服務(wù)器建立失敗,或者數(shù)據(jù)混亂俺亮,另外用模擬器在本地建立的服務(wù)器驮捍,是直接建立的mac上的疟呐,可以把播放鏈接直接給vlc打開播放;
2东且、[self.httpServer setDocumentRoot:webPath];這一步在給服務(wù)器設(shè)置路徑的時(shí)候启具,一定要注意和緩存TS數(shù)據(jù)的路徑一致;
3珊泳、解碼工具類中使用了一些定時(shí)器富纸,小伙伴們?cè)谑褂玫臅r(shí)候,要記得聲明一個(gè)銷毀解碼工具類的方法旨椒,在這個(gè)方法里銷毀定時(shí)器等晓褪,避免頁面無法銷毀的bug。

*播放

服務(wù)器頁建立好了综慎,那么播放鏈接是什么呢涣仿?懂一些網(wǎng)絡(luò)技術(shù)的小伙伴可能已經(jīng)猜到了,服務(wù)器是建立在本地的示惊,網(wǎng)絡(luò)里127.0.0.1是本地IP地址好港,因此播放連接是:@"http://127.0.0.1:9479/moive1/movie.m3u8", 將這個(gè)連接直接交給AVPlayer就可以播放了米罚,用VLC打開钧汹,不僅可以播放,還可以調(diào)整進(jìn)度录择。當(dāng)下載了一些文件后拔莱,退出APP,即使在沒有網(wǎng)絡(luò)的情況下打開隘竭,也可以正常播放塘秦,如圖:

手機(jī)播放
VLC播放

尾巴

到這里我們已經(jīng)實(shí)現(xiàn)了M3U8直播的回看和下載,DEMO下載地址:Demo动看。
本文為小伙伴們提供了一種思路尊剔,整個(gè)實(shí)現(xiàn)過程還是有些復(fù)雜的,需要小伙伴們反復(fù)理解菱皆,當(dāng)然有一定的音視頻開發(fā)技術(shù)理解起來就簡(jiǎn)單多了须误,本文并沒有對(duì)M3U8做過多技術(shù)講解,這方面的知識(shí)可以查閱蘋果官方文檔:HLS蘋果官方資料<small></small>仇轻,這里只是挑出一些問題講解一下京痢,最終能否理解還要靠小伙伴們自己的努力,若在文中發(fā)現(xiàn)錯(cuò)誤請(qǐng)及時(shí)指正拯田,感激不盡历造。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末甩十,一起剝皮案震驚了整個(gè)濱河市船庇,隨后出現(xiàn)的幾起案子吭产,更是在濱河造成了極大的恐慌,老刑警劉巖鸭轮,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件臣淤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡窃爷,警方通過查閱死者的電腦和手機(jī)邑蒋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來按厘,“玉大人医吊,你說我怎么就攤上這事〈” “怎么了卿堂?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)懒棉。 經(jīng)常有香客問我草描,道長(zhǎng),這世上最難降的妖魔是什么策严? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任穗慕,我火速辦了婚禮,結(jié)果婚禮上妻导,老公的妹妹穿的比我還像新娘逛绵。我一直安慰自己,他們只是感情好倔韭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布暑脆。 她就那樣靜靜地躺著,像睡著了一般狐肢。 火紅的嫁衣襯著肌膚如雪添吗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天份名,我揣著相機(jī)與錄音碟联,去河邊找鬼。 笑死僵腺,一個(gè)胖子當(dāng)著我的面吹牛鲤孵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播辰如,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼普监,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起凯正,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤毙玻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后廊散,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桑滩,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年允睹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了运准。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡缭受,死狀恐怖胁澳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情米者,我是刑警寧澤听哭,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站塘雳,受9級(jí)特大地震影響陆盘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜败明,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一隘马、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧妻顶,春花似錦酸员、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沽甥。三九已至身隐,卻和暖如春其徙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背钝鸽。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工汇恤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拔恰。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓因谎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親颜懊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子财岔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容