m3u8流媒體下載 swift

最近在做視頻播放器,發(fā)現(xiàn)目前主流的視頻播放都是流媒體,以前的MP4 大文件播放時(shí)代已經(jīng)過去了。之前做的一個(gè)播放器:

https://github.com/yangxina/NicooPlayer

在此之前沒有支持m3u8流媒體播放,現(xiàn)在也已經(jīng)兼容了m3u8流媒體播放。 不過當(dāng)前正在處理流媒體的斷點(diǎn)續(xù)傳。邊下邊播。

- 首先,邊下邊播的好處就不多說了,比起從服務(wù)器直接拉取數(shù)據(jù)流播放,顯得更加流暢,用戶體驗(yàn)更加爽。要做邊下邊播,那肯定要先玩會(huì) 流媒體下載。 NicooM3u8Downloader這個(gè)組件就是針對(duì)流媒體下載封裝的一個(gè)組件。

- 封裝思路

(1) 在線解析m3u8文件內(nèi)容,把里面的ts對(duì)應(yīng)連接的資源下載本地的Document文件下。

(2) 把下載下來的資源使用本地路徑重新拼接成一個(gè)新的本地m3u8文件。

(3) 然后在開啟一個(gè)本地http服務(wù)端,把m3u8共享成連接地址,讓播放器播放。

- (1).m3u8的解析:

? 在做m3u8文件在線解析之前,必須要對(duì)m3u8文件格式,文件規(guī)則,文本鍵值對(duì)作用,意義有一定的了解。 這里就不對(duì) m3u8 做過多解釋,需?

? 要了解的同學(xué)請(qǐng)查看博客: https://blog.csdn.net/blueboyhi/article/details/40107683

? 本人在做這一塊時(shí),也是先去研讀了這篇博客。給博客作者點(diǎn)個(gè)??。 另外還要了解一下 .ts 后綴的視頻片段文件,.ts文件就是你播放的視頻文件,不過每一個(gè)ts?

? 視頻文件的長度都很短,一般就只有幾秒鐘,長點(diǎn)的也就幾十秒。 這里不做多解釋,可以自行去查資料。

? 了解完了m3u8之后,就知道,m3u8 有可能是一層,或者兩層。(不會(huì)再多,游戲規(guī)則說的是最多包一層,也就是最多兩層)。

? m3u8一層:

? ?如果m3u8已有一層,那么第一此解析出來就會(huì)帶有 xxx.ts流路徑的m3u8文件內(nèi)容。 例如我們將一個(gè)視頻url:如(http://xxx/yyy/zzz/sss.m3u8)

? 解析處理的文件內(nèi)容。

? 如下:

? ? ? #EXTM3U

? ? ? #EXT-X-VERSION:3

? ? ? #EXT-X-MEDIA-SEQUENCE:0

? ? ? #EXT-X-ALLOW-CACHE:YES

? ? ? #EXT-X-TARGETDURATION:21

? ? ? #EXTINF:19.263833,

? ? ? d104cd51ca787c02b4ceaf084801ace4_free_0000.ts

? ? ? #EXTINF:8.000000,

? ? ? d104cd51ca787c02b4ceaf084801ace4_free_0001.ts

? ? ? #EXTINF:3.260867,

? ? ? d104cd51ca787c02b4ceaf084801ace4_free_0002.ts

? ? ? #EXTINF:20.043478,

? ? ? d104cd51ca787c02b4ceaf084801ace4_free_0003.ts

? ? ? #EXTINF:2.782611,

? ? ? d104cd51ca787c02b4ceaf084801ace4_free_0004.ts

? ? ? ....(- 中間省略95行 - )

? ? ? #EXTINF:10.869567,

? ? ? d104cd51ca787c02b4ceaf084801ace4_free_0100.ts

? ? ? #EXT-X-ENDLIST

? 可以看到,這里解析出來的 xxx.ts : d104cd51ca787c02b4ceaf084801ace4_free_0002.ts, 不是一個(gè)可以直接下載的全路徑。

? 這時(shí)候,需要將這個(gè)ts下載下來,就需要拼接一個(gè)正確的下載地址。這時(shí)候就需要對(duì)拿來解析的視頻url進(jìn)行路徑切片。比如: http://xxx/yyy/zzz/sss.m3u8

? 這個(gè)url。

? 我們需要把它切成:

? ? [ http://xxx,

? ? ? http://xxx/yyy,

? ? ? http://xxx/yyy/zzz? ]

? 這樣3個(gè)路徑,然后將我們解析出來的xxx.ts,分別拼接到3個(gè)路徑后,生成3

? 個(gè)ts文件下載路徑.(其中只有一個(gè)是有效的url), 我們需要從這3個(gè)(有可能是N個(gè))中找到那個(gè)可以下載ts的有效url. 在組件內(nèi),我是直接每一個(gè)都拼接文件中

? 第一個(gè)ts,然后分別拿去做一次下載,下載到的第一個(gè)ts數(shù)據(jù)不為空,就表示當(dāng)前這個(gè)url是有效的。當(dāng)我們拿到了有效的ts下載路徑,我們只需要?jiǎng)?chuàng)建下載任務(wù)去下

? 載這些ts文件,存放到本地一個(gè)文件夾內(nèi)。

? m3u8兩層:

? 兩層的m3u8解析,其實(shí)也就是在一層的基礎(chǔ)上,多做一次解析,當(dāng)然我們要判斷第一次解析沒有解析出來ts列表,才會(huì)做第二層解析。這里拿個(gè)例子來說:

? 比如我們要解析: http://youku.com-www-163.com/20180506/576_bf997390/index.m3u8 這個(gè)視頻地址。 第一層解析出來內(nèi)容如下:

? ? "#EXTM3U\n#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=800000,RESOLUTION=1080x608\n1000k/hls/index.m3u8"

? 什么?? 解析出來沒有ts路徑,莫慌。雖然沒看到.ts 文件路徑,但是我們看到 ”#EXT-X-STREAM-INF:“ 這個(gè),看到這玩意兒,表示我們還需要在解析一次。?

? 那么,問題又來了:

? 第二次解析,我們解析哪個(gè)url?

? 第一次解析出來的內(nèi)容里面沒有啊?。。?/p>

? 不, 是有的。

? 我們看到最后有個(gè): RESOLUTION=1080x608\n1000k/hls/index.m3u8 ,我們需要把這個(gè)帶.m3u8后綴的東西,以 "\n" (換行符) 切開。?

? 拿到帶有.m3u8后綴的一段。也就是: 1000k/hls/index.m3u8

? 但是,我們拿到的只是后綴,前面的路徑是什么?

? 我也不知道!

? 那怎么辦?

? 一個(gè)一個(gè)試唄! (當(dāng)然這里的試不是手動(dòng)試,是寫程序一個(gè)一個(gè)去請(qǐng)求試。)

? 這里我們就會(huì)用到一層解析,拼接有效ts路徑的思路。? 這里我們需要,將 http://youku.com-www-163.com/20180506/576_bf997390/index.m3u8

? 視頻url切片成:

? ? [ http://youku.com-www-163.com,

? ? http://youku.com-www-163.com/20180506,

? ? http://youku.com-www-163.com/20180506/576_bf997390 ]

? 這樣的一個(gè)數(shù)組,然后拼接? 1000k/hls/index.m3u8 到他們后面,得到

? ? [ http://youku.com-www-163.com/1000k/hls/index.m3u8,

? ? ? http://youku.com-www-163.com/20180506/1000k/hls/index.m3u8,

? ? ? http://youku.com-www-163.com/20180506/576_bf997390/1000k/hls/index.m3u8? ]

? 三個(gè)里層url,當(dāng)然這里面也只有一個(gè)是正確的,能夠解析到.ts列表的地址。 這樣我們只需要依次去解析每一個(gè)。 如果誰能解析出來,誰就是真的。

拿到了那個(gè)正確的解析地址,就可以直接解析出來帶.ts的文件。 (因?yàn)檫@是第二層解析,最多兩層)

?這里判斷是否解析出來.ts列表,可以驗(yàn)證解析出來的內(nèi)容 是否有包含 “#EXTINF:” 或者是否包含 ”.ts“, 個(gè)人建議用第一個(gè)來判斷。

?解析到了.ts路徑列表,那么,我們就可以直接創(chuàng)建任務(wù)去下載了。

?別急,沒那個(gè)簡(jiǎn)單。

?上面說的只是沒有加密的.m3u8解析。 一般情況下,我們會(huì)在里面加一些騷操作的。(加密)

?一般我們會(huì)在m3u8文件中植入加密,常用的 AES-128,對(duì)稱加密。 這種最常見。 別的還沒碰到。碰到了在更新組件。如果是加過密的M3u8,不對(duì)其處理,下載下來的.ts文件很有可能是播放不了的。 不,是百分之100播放不了。這時(shí),我們需要對(duì)用于播放的本地m3u8文件做一些處理。

? 一般加密的m3u8解析出來長這樣:

? ? ? #EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:11\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-KEY:METHOD=AES-128,URI=\"http://xinyuan.zeikcdn.com/20180616/LQfzeEFU/800kb/hls/key.key\"\n#EXTINF:6.839,\nhttp://xinyuan.zeikcdn.com/20180616/LQfzeEFU/800kb/hls/PJy4h6573000.jpg\n#EXTINF:8.59,\nhttp://xinyuan.zeikcdn.com/20180616/LQfzeEFU/800kb/hls/PJy4h6573001.jpg\n#EXTINF:6.547,\nhttp://xinyuan.zeikcdn.com/20180616/LQfzeEFU/800kb/hls/PJy4h6573002.jpg\n

? 我們看到里面有這樣一段: #EXT-X-KEY:METHOD=AES-128,URI=\"http://xinyuan.zeikcdn.com/20180616/LQfzeEFU/800kb/hls/key.key\"

? 我們可以看出使用了 AES-128 對(duì)稱加密。 既然加了密,我們?cè)趺崔k?

? 別急,m3u8 想播放器能播放,肯定會(huì)有對(duì)應(yīng)的操作的; 我們這里看到一個(gè) ”URI=“ 后面是一個(gè) http://xxx/yyy/sss.key 的路徑,很奇怪。 這讓我們聯(lián)想 ? 到 密鑰。 對(duì)。沒錯(cuò)。 就是它。 這個(gè)路徑就是密鑰的下載地址。 我們需要將它下載下來存放到本地。 記錄下來它的本地路徑。 最好和下載的 .ts文件一個(gè)目 錄。

?? 上面說的加密還沒有涉及到 IV。

? 什么? IV又是什么鬼?

? 別慌,IV的處理很簡(jiǎn)單,有些密鑰中沒有,有些有: 有IV的key長這樣:

? ? #EXT-X-KEY:METHOD=AES-128,URI=\"http://xinyuan.zeikcdn.com/20180616/LQfzeEFU/800kb/hls/key.key\",IV=b66cb67a9bfd78ed

?? URI后面多了一個(gè)東西。 只需要將這一串 b66cb67a9bfd78ed 摳出來。在后面編寫本地.m3u8文件時(shí)填入,就行了。

? 到這里,m3u8的解析就完了。

? 解析完了,拿到了ts的下載路徑,那么ts流視頻的下載也就簡(jiǎn)單了,開一堆下載任務(wù),異步去執(zhí)行ts文件的下載。這里就不做講解了。就是下載到一個(gè)本地文件夾里面。下面我要講的是:

- (2)本地m3u8文件創(chuàng)建?

1.本地m3u8文件創(chuàng)建。 2.本地服務(wù)器的搭建。

? 1.本地m3u8文件創(chuàng)建。

? ? 創(chuàng)建本地文件:

? ? /// 創(chuàng)建本地M3u8文件,播放要用

? ? ? func createLocalM3U8file() {

? ? ? ? ? NicooDownLoadHelper.checkOrCreatedM3u8Directory(tsPlaylist.identifier)

? ? ? ? let filePath =? ? ? ? ? NicooDownLoadHelper.getDocumentsDirectory().appendingPathComponent(NicooDownLoadHelper.downloadFile).appendingPathComponent(tsPlaylist.identifier).appendingPathComponent("\(tsPlaylist.identifier).m3u8")

? ? ? ? /// 解密的key 所在的路徑和ts視頻片段在同一文件目錄下,所以這里直接用相對(duì)路徑,如果不在一個(gè)文件夾下,需要拼接絕對(duì)路徑

? ? ? ? let keyPath = "key"

?? ? ? ?///絕對(duì)路徑

? ? ? ? let keyPathAll = NicooDownLoadHelper.getDocumentsDirectory().appendingPathComponent(NicooDownLoadHelper.downloadFile).appendingPathComponent(tsPlaylist.identifier).appendingPathComponent("key")

? ? ? ? var header = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:60\n"

? ? ? ? if m3u8Data.contains("#EXT-X-KEY:") && FileManager.default.fileExists(atPath: keyPathAll.path) {

? ? ? ? ? ? var keyStringPath = String(format: "#EXT-X-KEY:METHOD=AES-128,URI=\"%@\"", keyPath)

? ? ? ? ? ? if getIV() != nil {

? ? ? ? ? ? ? ? keyStringPath = String(format: "#EXT-X-KEY:METHOD=AES-128,URI=\"%@\",IV=%@", keyPath,getIV()!) ? ? ? ??

? ?}

? ? ? ? ? ? header = String(format: "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:60\n%@\n", keyStringPath)

? ? ? ? }

? ? ? ? var content = ""

? ? ? ? for i in 0 ..< tsPlaylist.tsModelArray.count {

? ? ? ? ? ? let segmentModel = tsPlaylist.tsModelArray[i]

? ? ? ? ? ? let length = "#EXTINF:\(segmentModel.duration),\n"

? ? ? ? ? ? let fileName = "\(segmentModel.index).ts\n"

? ? ? ? ? ? content += (length + fileName)

? ? ? ? }

? ? ? ? header.append(content)

? ? ? ? header.append("#EXT-X-ENDLIST\n")


? ? ? ? let writeData: Data = header.data(using: .utf8)!

? ? ? ? try! writeData.write(to: filePath)


? 這里不好文字描述,就上代碼。 反正記住兩個(gè)東西: 1. 解析下載的密鑰文件的相對(duì)路徑,一定要寫入文件, 2. 有IV的要將IV也拼接到后面。

? 代碼如下:

? ? /// 解密的key 所在的路徑和ts視頻片段在同一文件目錄下,所以這里直接用相對(duì)路徑,如果不在一個(gè)文件夾下,需要拼接絕對(duì)路徑

? ? ? ? let keyPath = "key"

? ? ? ? ///絕對(duì)路徑

? ? ? ? let keyPathAll = NicooDownLoadHelper.getDocumentsDirectory().appendingPathComponent(NicooDownLoadHelper.downloadFile).appendingPathComponent(tsPlaylist.identifier).appendingPathComponent("key")

? ? ? ? var header = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:60\n"

? ? ? ? if m3u8Data.contains("#EXT-X-KEY:") && FileManager.default.fileExists(atPath: keyPathAll.path) {

? ? ? ? ? ? var keyStringPath = String(format: "#EXT-X-KEY:METHOD=AES-128,URI=\"%@\"", keyPath)

? ? ? ? ? ? if getIV() != nil {

? ? ? ? ? ? ? ? keyStringPath = String(format: "#EXT-X-KEY:METHOD=AES-128,URI=\"%@\",IV=%@", keyPath,getIV()!)

? ? ? ? ? ? }

? ? ? ? ? ? header = String(format: "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:60\n%@\n", keyStringPath)

? ? ? ? }

? 這樣就只需要等異步下載的ts下載完成了。

? --- warning:(這里需要提一下的是: 組件中只是針對(duì)下載,所以在下載之前就將本地.m3u8文件創(chuàng)建好了。

? 如果是要做播放器的斷點(diǎn)續(xù)傳。 這里需要每下載完一個(gè) .ts 文件,就更新一次本地 .m3u8 文件。而且沒有下載完成的ts文件,不能寫入.m3u8 文件中。 只能開

? 定時(shí)器,每多少秒去復(fù)制本地文件夾一次,供給播放器使用。)

- (3) 本地服務(wù)器搭建,播放本地視頻。

這個(gè)我使用了 CocoaHTTPServer 這個(gè)框架來搭建本地服務(wù)器。 播放器使用自己的播放器: NicooPlayer

代碼:

? private func playLocalVideo() {

? server = HTTPServer()

? server.setType("_http.tcp") ? ? ? ?

server.setDocumentRoot(NicooDownLoadHelper.getDocumentsDirectory().appendingPathComponent(NicooDownLoadHelper.downloadFile).appendingPathComponent(videoName).path)

? ? print("localFilePath = \(NicooDownLoadHelper.getDocumentsDirectory().appendingPathComponent(NicooDownLoadHelper.downloadFile).path)")

?server.setPort(UInt16(port))

? ? ? ? do {

? ? ? ? ? ? try server.start()

? ? ? ? }catch{

? ? ? ? ? ? print("本地服務(wù)器啟動(dòng)失敗")

? ? ? ? }

? ? ? ? let videoLocalUrl = "\(getLocalServerBaseUrl()):\(port)/\(videoName).m3u8"

? ? ? ? videoPlayer.playLocalVideoInFullscreen(videoLocalUrl, "localFile", view, sinceTime: 0)

? ? ? ? videoPlayer.playLocalFileVideoCloseCallBack = { [weak self] (playValue) in

? ? ? ? ? ? // 退出時(shí),關(guān)閉本地服務(wù)器

? ? ? ? ? ? self?.server.stop()

? ? ? ? ? ? self?.navigationController?.popViewController(animated: false)

? ? ? ? }

? ? }

? 到這里,m3u8流視頻下載和本地播放,就做完了。

? demo下載地址:NicooM3u8Downloader

? ??????????????????????????????????????????????????????

? 如果覺得有用的朋友,望不吝賜賞,小弟將感激不盡 ??,并祝你合家歡樂,新年快樂,萬事如意。

? ??????????????????????????????????????????????????????

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容