最近在做視頻播放器,發(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
? ??????????????????????????????????????????????????????
? 如果覺得有用的朋友,望不吝賜賞,小弟將感激不盡 ??,并祝你合家歡樂,新年快樂,萬事如意。
? ??????????????????????????????????????????????????????