開(kāi)發(fā)提取m3u8格式的視頻工具(Golang盟步,Python休玩,JS)

m3u8 文件是 HTTP Live Streaming(縮寫(xiě)為 HLS) 協(xié)議的部分內(nèi)容狂鞋,而 HLS 是一個(gè)由蘋(píng)果公司提出的基于 HTTP流媒體網(wǎng)絡(luò)傳輸協(xié)議片择。

關(guān)于m3u8 格式詳解,可以參考此文:m3u8 文件格式詳解

JS的實(shí)現(xiàn)版本可以參考這位博主的gitHub:m3u8 視頻在線提取工具

在這里骚揍,我先基于python代碼來(lái)說(shuō)解怎么去提取m3u8文件并合并成真正的視頻文件字管。
一個(gè)正常的m3u8文件格式如下:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="https://xxxx/init.mp4"
#EXTINF:3.00000000000000000000,
https://xxxx/0.m4s
#EXTINF:3.00000000000000000000,
https://xxxxx/1.m4s
#EXTINF:3.00000000000000000000,
https://xxxx/2.m4s
#EXTINF:1.00000000000000000000,
https://xxxx/3.m4s
#EXT-X-ENDLIST

最需要注意的關(guān)鍵節(jié)點(diǎn)是:EXT-X-KEY(表示視頻會(huì)經(jīng)過(guò)指定算法加密)啰挪、EXT-X-MAP(最初的視頻分片,不一定存在)嘲叔、EXTINF(按照順序的視頻分片以及這個(gè)分片的播放秒數(shù))

定義解析函數(shù):m3u8_convert

"""
 _url:m3u8 的下載地址
 save_path:待保存的視頻本地路徑
"""
def m3u8_convert(_url, save_path):

    writer = open(save_path, 'wb')
    if not writer:
        print("%s 沒(méi)法成功打開(kāi)" % save_path)
        return

    #先下載文件
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'}
    rs = callAPI(_url)
    list_content = rs.decode(encoding = 'utf-8').split('\n')
    player_list = []

    # key 以后處理加解密的操作
    key = ''
    #進(jìn)行文件格式解析
    for index, line in enumerate(list_content):
        # 判斷視頻是否經(jīng)過(guò)AES-128加密
        tmp_key = checkExtKey(line)
        if len(tmp_key) > 0:
            key = tmp_key
        else:
            next_line = ""
            if index < len(list_content)-1:
                next_line = list_content[index + 1]
            href = checkExtInf(line, next_line)
            if len(href) > 0:
                player_list.append(href)

    #下載每一個(gè)視頻分片亡呵,并保存到本地文件里
    for i, _url in enumerate(player_list):
        print('正在下載文件:%s' % _url)
        _bytes = callAPI(_url)
        print('已下載文件大小:%d' % len(_bytes))
        writer.write(_bytes)

    writer.close()
    print('視頻生成完成')

函數(shù)中硫戈,需要對(duì)該三個(gè)節(jié)點(diǎn)EXT-X-KEY锰什、EXT-X-MAP、EXTINF進(jìn)行處理丁逝,由于找不到比較好的已加密的視頻demo 汁胆,所以我的代碼里暫時(shí)忽略對(duì)EXT-X-KEY處理。

# 檢測(cè)EXT-X-KEY霜幼,提取key
def checkExtKey(line):
    if "#EXT-X-KEY" in line:
        method_pos = line.find("METHOD")
        comma_pos = line.find(",")
        method = line[method_pos:comma_pos].split('=')[1]  # 獲取加密方式
        print("Decode Method:", method)
        uri_pos = line.find("URI")
        quotation_mark_pos = line.rfind('"')
        key_url = line[uri_pos:quotation_mark_pos].split('"')[1]
        key = callAPI(key_url).decode(encoding='utf-8')  # 獲取加密密鑰
        print("key:", key)
        return key
    return ""


# 檢測(cè)EXTINF 和 EXT-X-MAP
def checkExtInf(line, next_line):
    href = ""
    if '#EXTINF' in line:
        # 提取下一行的http 鏈接地址
        if 'http' in next_line:
            href = next_line
    elif '#EXT-X-MAP' in line:
        # 提取最初的視頻地址
        uri_pos = line.find("URI=\"")
        if uri_pos > -1:
            href = line[uri_pos + 5:-1]
    return href

完整的代碼如下:

# -*- coding: UTF-8 -*-
import requests
import os
# from Crypto.Cipher import AES

def callAPI(_url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/75.0.3770.100 Safari/537.36'}
    return requests.get(_url, headers=headers).content


# 檢測(cè)EXT-X-KEY嫩码,提取key
def checkExtKey(line):
    if "#EXT-X-KEY" in line:
        method_pos = line.find("METHOD")
        comma_pos = line.find(",")
        method = line[method_pos:comma_pos].split('=')[1]  # 獲取加密方式
        print("Decode Method:", method)
        uri_pos = line.find("URI")
        quotation_mark_pos = line.rfind('"')
        key_url = line[uri_pos:quotation_mark_pos].split('"')[1]
        key = callAPI(key_url).decode(encoding='utf-8')  # 獲取加密密鑰
        print("key:", key)
        return key
    return ""


# 檢測(cè)EXTINF 和 EXT-X-MAP
def checkExtInf(line, next_line):
    href = ""
    if '#EXTINF' in line:
        # 提取下一行的http 鏈接地址
        if 'http' in next_line:
            href = next_line
    elif '#EXT-X-MAP' in line:
        # 提取最初的視頻地址
        uri_pos = line.find("URI=\"")
        if uri_pos > -1:
            href = line[uri_pos + 5:-1]
    return href

"""
 _url:m3u8 的下載地址
 save_path:待保存的視頻本地路徑
"""
def m3u8_convert(_url, save_path):
    writer = open(save_path, 'wb')
    if not writer:
        print("%s 沒(méi)法成功打開(kāi)" % save_path)
        return

    # 先下載文件
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'}
    rs = callAPI(_url).decode(encoding='utf-8')
    list_content = rs.split('\n')
    player_list = []

    # key 以后處理加解密的操作
    key = ''
    # 進(jìn)行文件格式解析
    for index, line in enumerate(list_content):
        # 判斷視頻是否經(jīng)過(guò)AES-128加密
        tmp_key = checkExtKey(line)
        if len(tmp_key) > 0:
            key = tmp_key
        else:
            next_line = ""
            if index < len(list_content) - 1:
                next_line = list_content[index + 1]
            href = checkExtInf(line, next_line)
            if len(href) > 0:
                player_list.append(href)

    # 下載每一個(gè)視頻分片,并保存到本地文件里
    for i, _url in enumerate(player_list):
        print('正在下載文件:%s' % _url)
        _bytes = callAPI(_url)
        print('已下載文件大凶锛取:%d' % len(_bytes))
        writer.write(_bytes)

    writer.close()
    print('視頻生成完成')

大功告成后铸题,馬上找一個(gè)可以測(cè)試的m3u8吧。

    save_data_file = '~/Desktop/player.mp4'
    url = '【m3u8 URL地址】'
    # 下載視頻
    m3u8_convert(url, save_data_file)

下面是golang的代碼:

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
    "os"
    "strings"
)

func callapi(durl string) []byte {
    _, err := url.ParseRequestURI(durl)
    if err != nil {
        panic(durl + " 下載地址出錯(cuò)")
    }
    client := http.DefaultClient
    //client.Timeout = 5000
    resp, err := client.Get(durl)
    if err != nil {
        panic(err)
    }
    raw := resp.Body
    fmt.Println("拿到Body :")
    //  fmt.Println(resp.Body)
    defer raw.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    return body
}

// 檢測(cè)EXT-X-KEY琢感,提取key
func checkExtKey(line string) string {
    if strings.Contains(line, "#EXT-X-KEY") {
        method_pos := strings.Index(line, "METHOD")
        comma_pos := strings.Index(line, ",")
        method := strings.Split(line[method_pos:comma_pos], "=")[1]
        fmt.Println("Decode Method:%s", method)

        uri_pos := strings.Index(line, "URI")
        quotation_mark_pos := strings.LastIndex(line, "\"")
        key_url := strings.Split(line[uri_pos:quotation_mark_pos], "\"")[1]
        key := string(callapi(key_url))
        fmt.Println("Decode Key:%s", key)
        return key
    }
    return ""
}

// 檢測(cè)EXTINF 和 EXT-X-MAP
func checkExtInf(line string, next_line string) string {
    href := ""
    if strings.Contains(line, "#EXTINF") {
        if strings.Contains(next_line, "http") {
            href = next_line
        }
    } else if strings.Contains(line, "#EXT-X-MAP") {
        uri_pos := strings.Index(line, "URI=\"")
        if uri_pos > -1 {
            href = line[uri_pos+5 : len(line)-1]
        }
    }
    return href
}

func m3u8_convert(_url string, save_file string) {

    body := string(callapi(_url))

    list_contents := strings.Split(body, "\n")

    var player_list []string
    //key := ""
    for index, line := range list_contents {
        tmp_key := checkExtKey(line)
        if len(tmp_key) > 0 {
            //key = tmp_key
        } else {
            next_line := ""
            if index < len(list_contents)-1 {
                next_line = list_contents[index+1]
            }
            href := checkExtInf(line, next_line)
            if len(href) > 0 {
                player_list = append(player_list, href)
            }
        }
    }

    saveDataWithPlayList(player_list, save_file)

    fmt.Println("視頻生成完成")

}

func saveDataWithPlayList(play_list []string, save_file string) {
    writer, err := os.Create(save_file)
    if err != nil {
        panic("生成視頻文件失敗")
    }

    defer writer.Close()

    for _, item := range play_list {
        fmt.Println("The item is :", item)
        bytes := callapi(item)
        writer.Write(bytes)
    }

}

func main() {

    play_list_url := "【m3u8 URL地址】"

    save_file := "~/Desktop/player.mp4"
        
    m3u8_convert(play_list_url, save_file)

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末丢间,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子猩谊,更是在濱河造成了極大的恐慌千劈,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件牌捷,死亡現(xiàn)場(chǎng)離奇詭異墙牌,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)暗甥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)喜滨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人撤防,你說(shuō)我怎么就攤上這事虽风。” “怎么了寄月?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵辜膝,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我漾肮,道長(zhǎng)厂抖,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任克懊,我火速辦了婚禮忱辅,結(jié)果婚禮上七蜘,老公的妹妹穿的比我還像新娘。我一直安慰自己墙懂,他們只是感情好橡卤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著损搬,像睡著了一般碧库。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上场躯,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天谈为,我揣著相機(jī)與錄音,去河邊找鬼踢关。 笑死伞鲫,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的签舞。 我是一名探鬼主播秕脓,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼儒搭!你這毒婦竟也來(lái)了吠架?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤搂鲫,失蹤者是張志新(化名)和其女友劉穎傍药,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體魂仍,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拐辽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了擦酌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片俱诸。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖赊舶,靈堂內(nèi)的尸體忽然破棺而出睁搭,到底是詐尸還是另有隱情,我是刑警寧澤笼平,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布园骆,位于F島的核電站,受9級(jí)特大地震影響寓调,放射性物質(zhì)發(fā)生泄漏遇伞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一捶牢、第九天 我趴在偏房一處隱蔽的房頂上張望鸠珠。 院中可真熱鬧,春花似錦秋麸、人聲如沸渐排。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)驯耻。三九已至,卻和暖如春炒考,著一層夾襖步出監(jiān)牢的瞬間可缚,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工斋枢, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留帘靡,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓瓤帚,卻偏偏與公主長(zhǎng)得像描姚,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子戈次,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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