开发提取m3u8格式的视频工具(Golang,Python,JS)

m3u8 文件是 HTTP Live Streaming(缩写为 HLS) 协议的部分内容,而 HLS 是一个由苹果公司提出的基于 HTTP流媒体网络传输协议

关于m3u8 格式详解,可以参考此文:m3u8 文件格式详解

JS的实现版本可以参考这位博主的gitHub:m3u8 视频在线提取工具

在这里,我先基于python代码来说解怎么去提取m3u8文件并合并成真正的视频文件。
一个正常的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

最需要注意的关键节点是:EXT-X-KEY(表示视频会经过指定算法加密)、EXT-X-MAP(最初的视频分片,不一定存在)、EXTINF(按照顺序的视频分片以及这个分片的播放秒数)

定义解析函数:m3u8_convert

"""
 _url:m3u8 的下载地址
 save_path:待保存的视频本地路径
"""
def m3u8_convert(_url, save_path):

    writer = open(save_path, 'wb')
    if not writer:
        print("%s 没法成功打开" % 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 = ''
    #进行文件格式解析
    for index, line in enumerate(list_content):
        # 判断视频是否经过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)

    #下载每一个视频分片,并保存到本地文件里
    for i, _url in enumerate(player_list):
        print('正在下载文件:%s' % _url)
        _bytes = callAPI(_url)
        print('已下载文件大小:%d' % len(_bytes))
        writer.write(_bytes)

    writer.close()
    print('视频生成完成')

函数中,需要对该三个节点EXT-X-KEY、EXT-X-MAP、EXTINF进行处理,由于找不到比较好的已加密的视频demo ,所以我的代码里暂时忽略对EXT-X-KEY处理。

# 检测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 ""


# 检测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


# 检测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 ""


# 检测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 没法成功打开" % 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 = ''
    # 进行文件格式解析
    for index, line in enumerate(list_content):
        # 判断视频是否经过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)

    # 下载每一个视频分片,并保存到本地文件里
    for i, _url in enumerate(player_list):
        print('正在下载文件:%s' % _url)
        _bytes = callAPI(_url)
        print('已下载文件大小:%d' % len(_bytes))
        writer.write(_bytes)

    writer.close()
    print('视频生成完成')

大功告成后,马上找一个可以测试的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 + " 下载地址出错")
    }
    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
}

// 检测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 ""
}

// 检测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)

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容