m3u8解析代码,亲测可用

VideoSpider.java

## 使用方法
下载代码,修改C#或Java代码的这四个参数,改成对应的自己要下载的地址、key和保存路径,然后运行即可
    
    string DRMKey = "11, 22, 33, 44, 55, 66, 77, 88, 99, 00, 111, 111, 111, 111, 111, 111";        //DRMKey
    string m3u8Url = "https://XXXXXXX/123.m3u8";     //m3u8在线地址
    string savePath = "D:\\VIDEO\\";        //保存的本地路径
    string saveFileName = "VIDEO_FILE_NAME";        //保存的文件(夹)名称,如果为空 则使用默认m3u8文件名
   
C#可以创建个控制台应用,代码全部复制粘贴,修改以上参数就可以运行了。Java的也可以直接下载运行,我的JDK版本1.8
本来想做个桌面应用程序的,结果嫌麻烦,费时间就没做了。哪位看官要是有时间可以做个桌面程序方便操作,另外可以加上多线程去下载试试。

> **DRMKey参数说明**:解密视频需要key和IV, 我们可以看到 IV在m3u8文件里有,每一个.ts文件都有一个对应的IV,#EXT-X-KEY:后面的 **IV=xxxxxx** 就是我们需要用到的 IV了, 可是key却没有,需要在网页上找找了,有一点基础的基本都能找到。下面是我找到key的过程,仅供参考:

> 打开控制台后,重新加载页面,发现一个 **qiniu-web-player.js** 在控制台输出了一些配置信息和日志记录,其中的 hls.DRMKey 引起了我的注意
数组长度也是16位,刚好加解密用到的key的长度也是16位,所以这个应该就是AES加解密要用到的key了。不过需要先转换一下。。

![DRMKey](https://github.com/Myron1024/m3u8_download/blob/master/screenshot/DRMKey.png?raw=true)

> 经过一番搜索得知转换步骤为:把数组里每个元素转换成16进制字符串,然后把16进制字符串转为ASCII码,这16个ASCII字符最终拼接出来的结果就是AES的key了。
> 不过**此处DRMKey的参数值只需要配置成数组的字符串格式即可(不包括前后中括号)**
 

C# 代码如下:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

namespace VideoDownload
{
    class Program
    {
        private static List error_arr = new List();

        static void Main(string[] args)
        {
            string DRMKey = "50, 101, 54, 52, 54, 55, 49, 48, 100, 56, 97, 54, 56, 57, 55, 102";		//DRMKey
            string m3u8Url = "https://xxxxxxxxxxx/video_m3u8_drm_1e352f596a55eaaedb2c0a0742ef83e8.m3u8?pm3u8&e=1698347593&token=MQZe3HEuDyokCVRSKOiKUv7FEHqhkZDDy2bqB9lV:EFxe81GER99XZLO2ExcxOkQKthk=&id=723"; 	//m3u8在线地址
            string savePath = "D:\\VIDEO\\";				//保存的本地路径
            string saveFileName = "VIDEO_FILE_NAME";    	//保存的文件(夹)名称,如果为空 则使用默认m3u8文件名

            try
            {
                // 创建本地保存目录
                int index = m3u8Url.LastIndexOf("/");
                string dirName = string.IsNullOrEmpty(saveFileName) ? m3u8Url.Substring(index + 1) : saveFileName;
                string finalSavePath = savePath + dirName + "\\";
                if (!Directory.Exists(finalSavePath))
                {
                    Directory.CreateDirectory(finalSavePath);
                }

                // 读取m3u8文件内容
                string m3u8Content = HttpGet(m3u8Url);
                //string m3u8Content = File.ReadAllText("D:/test.m3u8");

                string aesKey = getAESKey(DRMKey);
                //Console.WriteLine("aesKey:" + aesKey);

                Uri uri = new Uri(m3u8Url);
                string domain = uri.Scheme + "://" + uri.Authority;
                //Console.WriteLine("m3u8域名为:" + domain);

                List tsList = Regex.Matches(m3u8Content, @"\n(.*?.ts)").Select(m => m.Value).ToList();
                List ivList = Regex.Matches(m3u8Content, @"IV=(.*?)\n").Select(m => m.Value).ToList();
                if (tsList.Count != ivList.Count || tsList.Count == 0)
                {
                    Console.WriteLine("m3u8Content 解析失败");
                }
                else
                {
                    Console.WriteLine("m3u8Content 解析完成,共有 " + ivList.Count + " 个ts文件");

                    for (int i = 0; i < tsList.Count; i++)
                    {
                        string ts = tsList[i].Replace("\n", "");
                        string iv = ivList[i].Replace("\n", "");
                        iv = iv.Replace("IV=0x", "");
                        iv = iv.Substring(0, 16);   //去除前缀,取IV前16位

                        int idx = ts.LastIndexOf("/");
                        string tsFileName = ts.Substring(idx + 1);

                        try
                        {
                            string saveFilepath = finalSavePath + tsFileName;
                            if (!File.Exists(saveFilepath))
                            {
                                Console.WriteLine("开始下载ts: " + domain + ts);
                                byte[] encByte = HttpGetByte(domain + ts);
                                if (encByte != null)
                                {
                                    Console.WriteLine("开始解密, IV -> " + iv);
                                    byte[] decByte = null;
                                    try
                                    {
                                        decByte = AESDecrypt(encByte, aesKey, iv);
                                    }
                                    catch (Exception e1)
                                    {
                                        error_arr.Add(tsFileName);
                                        Console.WriteLine("解密ts文件异常。" + e1.Message);
                                    }
                                    if (decByte != null)
                                    {
                                        //保存视频文件
                                        File.WriteAllBytes(saveFilepath, decByte);
                                        Console.WriteLine(tsFileName + " 下载完成");
                                    }
                                }
                                else
                                {
                                    error_arr.Add(tsFileName);
                                    Console.WriteLine("HttpGetByte 结果返回null");
                                }
                            }
                            else
                            {
                                Console.WriteLine($"文件 {saveFilepath} 已存在");
                            }
                        }
                        catch (Exception ee)
                        {
                            error_arr.Add(tsFileName);
                            Console.WriteLine("发生异常。" + ee);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("发生异常。" + ex);
            }

            Console.WriteLine("所有操作已完成. 保存目录 " + savePath);
            if (error_arr.Count > 0)
            {
                List list = error_arr.Distinct().ToList();
                Console.WriteLine($"其中 共有{error_arr.Count}个文件下载失败:");
                list.ForEach(x =>
                {
                    Console.WriteLine(x);
                });
            }
            Console.ReadKey();
        }


        private static string getAESKey(string key)
        {
            string[] arr = key.Split(",");
            string aesKey = "";
            for (int i = 0; i < arr.Length; i++)
            {
                string tmp = int.Parse(arr[i].Trim()).ToString("X");     //10进制转16进制
                tmp = HexStringToASCII(tmp);
                aesKey += tmp;
            }
            return aesKey;
        }

        /// 
        /// 十六进制字符串转换为ASCII
        /// 
        /// 一条十六进制字符串
        /// 返回一条ASCII码
        public static string HexStringToASCII(string hexstring)
        {
            byte[] bt = HexStringToBinary(hexstring);
            string lin = "";
            for (int i = 0; i < bt.Length; i++)
            {
                lin = lin + bt[i] + " ";
            }
            string[] ss = lin.Trim().Split(new char[] { ' ' });
            char[] c = new char[ss.Length];
            int a;
            for (int i = 0; i < c.Length; i++)
            {
                a = Convert.ToInt32(ss[i]);
                c[i] = Convert.ToChar(a);
            }
            string b = new string(c);
            return b;
        }

        /// 
        /// 16进制字符串转换为二进制数组
        /// 
        /// 用空格切割字符串
        /// 返回一个二进制字符串
        public static byte[] HexStringToBinary(string hexstring)
        {
            string[] tmpary = hexstring.Trim().Split(' ');
            byte[] buff = new byte[tmpary.Length];
            for (int i = 0; i < buff.Length; i++)
            {
                buff[i] = Convert.ToByte(tmpary[i], 16);
            }
            return buff;
        }

        /// 
        /// AES解密
        /// 
        /// 
        /// 
        /// 
        /// 
        public static byte[] AESDecrypt(byte[] cipherText, string Key, string IV)
        {
            // Check arguments.
            if (cipherText == null || cipherText.Length <= 0)
                throw new ArgumentNullException("cipherText");
            if (Key == null || Key.Length <= 0)
                throw new ArgumentNullException("Key");
            if (IV == null || IV.Length <= 0)
                throw new ArgumentNullException("IV");

            // Declare the string used to hold
            // the decrypted text.
            byte[] res = null;

            // Create an AesManaged object
            // with the specified key and IV.
            using (AesManaged aesAlg = new AesManaged())
            {
                aesAlg.Key = Encoding.ASCII.GetBytes(Key);
                aesAlg.IV = Encoding.ASCII.GetBytes(IV);
                aesAlg.Mode = CipherMode.CBC;
                aesAlg.Padding = PaddingMode.PKCS7;

                // Create a decrytor to perform the stream transform.
                ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

                // Create the streams used for decryption.
                using (MemoryStream msDecrypt = new MemoryStream(cipherText))
                {
                    using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                    {
                        byte[] tmp = new byte[cipherText.Length + 32];
                        int len = csDecrypt.Read(tmp, 0, cipherText.Length + 32);
                        byte[] ret = new byte[len];
                        Array.Copy(tmp, 0, ret, 0, len);
                        res = ret;
                    }
                }
            }
            return res;
        }


        public static string HttpGet(string url)
        {
            try
            {
                HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
                request.Timeout = 20000;
                var response = (HttpWebResponse)request.GetResponse();
                using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
                {
                    return reader.ReadToEnd();
                }
            }
            catch (Exception ex)
            {
                Console.Write("HttpGet 异常," + ex.Message);
                Console.Write(ex);
                return "";
            }
        }

        public static byte[] HttpGetByte(string url)
        {
            try
            {
                byte[] arraryByte = null;

                HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
                request.Timeout = 20000;
                request.Method = "GET";
                using (WebResponse wr = request.GetResponse())
                {
                    int length = (int)wr.ContentLength;
                    using (StreamReader reader = new StreamReader(wr.GetResponseStream(), Encoding.UTF8))
                    {
                        HttpWebResponse response = wr as HttpWebResponse;
                        Stream stream = response.GetResponseStream();
                        //读取到内存
                        MemoryStream stmMemory = new MemoryStream();
                        byte[] buffer1 = new byte[length];
                        int i;
                        //将字节逐个放入到Byte 中
                        while ((i = stream.Read(buffer1, 0, buffer1.Length)) > 0)
                        {
                            stmMemory.Write(buffer1, 0, i);
                        }
                        arraryByte = stmMemory.ToArray();
                        stmMemory.Close();
                    }
                }
                return arraryByte;
            }
            catch (Exception ex)
            {
                Console.Write("HttpGetByte 异常," + ex.Message);
                Console.Write(ex);
                return null;
            }
        }
    }
}

java  VideoDownload代码如下:

public class VideoDownload {
    public static void main(String[] args) {

        String DRMKey = "50, 101, 54, 52, 54, 55, 49, 48, 100, 56, 97, 54, 56, 57, 55, 102";
        String m3u8Url = "https://xxxxxxxxxm/video_m3u8_drm_1e352f596a55eaaedb2c0a0742ef83e8.m3u8?pm3u8&e=1698347593&token=MQZe3HEuDyokCVRSKOiKUv7FEHqhkZDDy2bqB9lV:EFxe81GER99XZLO2ExcxOkQKthk=&id=723";	//m3u8在线地址
        String savePath = "D:\\VIDEO\\";				//保存的本地路径
        String saveFileName = "VIDEO_FILE_NAME";    	//保存的文件(夹)名称,如果为空 则使用默认m3u8文件名

        VideoSpider.run(DRMKey, m3u8Url, savePath, saveFileName);
    }
}

java CommonUtils 工具类 代码如下:

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CommonUtils {

    public static String getAESKey(String key) {
        String[] arr = key.split(",");
        String aesKey = "";
        for (int i = 0; i < arr.length; i++) {
            String tmp = intTohex(Integer.valueOf(arr[i].trim()));     //10进制转16进制
            tmp = convertHexToString(tmp);
            aesKey += tmp;
        }
        return aesKey;
    }

    public static byte[] decryptAESByKey(byte[] source, SecretKey key, String iv) throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");//"算法/模式/补码方式"
        //使用CBC模式,需要一个向量iv,可增加加密算法的强度
        IvParameterSpec ips = new IvParameterSpec(iv.getBytes());
        cipher.init(Cipher.DECRYPT_MODE, key, ips);
        return cipher.doFinal(source);
    }

    //将Base64编码后的AES秘钥转换成SecretKey对象
    public static SecretKey loadKeyAES(String base64Key) throws Exception{
        byte[] bytes = base64Key.getBytes("ASCII");
        SecretKeySpec key = new SecretKeySpec(bytes, "AES");
        return key;
    }

    /**
     * int型转换成16进制
     * @param n
     * @return
     */
    public static String intTohex(int n) {
        StringBuffer s = new StringBuffer();
        String a;
        char[] b = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        while (n != 0) {
            s = s.append(b[n % 16]);
            n = n / 16;
        }
        a = s.reverse().toString();
        if ("".equals(a)) {
            a = "00";
        }
        if (a.length() == 1) {
            a = "0" + a;
        }
        return a;
    }

    /**
     * 16进制转Ascii
     * @param hex
     * @return
     */
    public static String convertHexToString(String hex){
        StringBuilder sb = new StringBuilder();
        StringBuilder temp = new StringBuilder();
        //49204c6f7665204a617661 split into two characters 49, 20, 4c...
        for( int i=0; i List> zip(List... lists) {
        List> zipped = new ArrayList>();
        for (List list : lists) {
            for (int i = 0, listSize = list.size(); i < listSize; i++) {
                List list2;
                if (i >= zipped.size())
                    zipped.add(list2 = new ArrayList());
                else
                    list2 = zipped.get(i);

                list2.add(list.get(i));
            }
        }
        return zipped;
    }

    public static List getMatchers(String regex, String source){
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(source);
        System.out.println("matcher 地址:" +matcher);
        List list = new ArrayList<>();
        while (matcher.find()) {
            list.add(matcher.group());
        }
        return list;
    }

    /**
     * 执行cmd命令,并获取返回结果
     * @param commandStr
     */
    public static void exeCmd(String commandStr) {
        BufferedReader br = null;
        try {
            //执行cmd命令
            Process p = Runtime.getRuntime().exec(commandStr);
            //返回值是流,以便读取。
            br = new BufferedReader(new InputStreamReader(p.getInputStream(), Charset.forName("GBK")));
            String line = null;
            StringBuilder sb = new StringBuilder();
            while ((line = br.readLine()) != null) {
                sb.append(line + "\n");
            }
            System.out.println(sb.toString());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (br != null){
                try {
                    br.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 VideoSpider.java 代码如下:

import javax.crypto.SecretKey;
import java.awt.*;
import java.io.File;
import java.io.FileOutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

public class VideoSpider {

    private static List error_arr = new ArrayList<>();

    /**
     * 下载视频
     * @param DRMKey key
     * @param m3u8Url m3u8在线地址
     * @param savePath 视频保存目录
     * @param saveFileName 保存的文件(夹)名
     */
    public static void run (String DRMKey, String m3u8Url, String savePath, String saveFileName) {
        try {
            String StartDate = new SimpleDateFormat("HH:mm:ss").format(new Date());
            System.out.println(StartDate + " ---- 开始下载 ----");

            // 根据保存的文件名或m3u8文件名,创建本地保存目录
            int index = m3u8Url.lastIndexOf("/");
            String dirName = "".equals(saveFileName) ? m3u8Url.substring(index + 1) : saveFileName;
            String finalSavePath = savePath;
            File dir = new File(finalSavePath);
            if (!dir.exists()) {
                dir.mkdir();
            }

            // ----------------  读取线上m3u8文件内容  ----------------
            System.out.println(" ---- 地址 ----"+ m3u8Url);
            String m3u8Content = CommonUtils.doGet(m3u8Url);

            // ----------------  读取本地m3u8文件测试 Start ----------------
//            StringBuilder m3u8Content = new StringBuilder();
//            String filePath = "D:\\test2.m3u8";
//            File file = new File(filePath);
//            try {
//                FileReader reader = new FileReader(file);
//                BufferedReader bufferedReader = new BufferedReader(reader);
//                String tempString = null;
//                while ((tempString = bufferedReader.readLine()) != null) {
//                    m3u8Content.append(tempString);
//                    m3u8Content.append("\r\n");
//                }
//                bufferedReader.close();
//            } catch (FileNotFoundException e) {
//                e.printStackTrace();
//            } catch (IOException e) {
//                e.printStackTrace();
//            }
            // ----------------  读取本地m3u8文件测试 End ----------------

            String aesKey = CommonUtils.getAESKey(DRMKey);
            System.out.println("m3u8Content aesKey"+aesKey);
            SecretKey skey = CommonUtils.loadKeyAES(aesKey);

            // 根据在线m3u8地址 获取域名,如果操作本地m3u8文件,则直接手动设置 domain
            URL url = new URL(m3u8Url);
            String domain = url.getProtocol() + "://" + url.getAuthority();

            List tsList = CommonUtils.getMatchers("(/ts.*?)\r\n#", m3u8Content.toString());
            List ivList = CommonUtils.getMatchers("IV=(.*?)\r\n", m3u8Content.toString());
            if (tsList.size() != ivList.size()) {
                System.out.println("m3u8Content 解析失败");
            } else {
                System.out.println("m3u8Content 解析完成,共有 " + ivList.size() + " 个ts文件");

                List> listTuple = CommonUtils.zip(tsList, ivList);
                listTuple.forEach(x -> {
                    String ts = x.get(0).replace("\r\n#", "");
                    System.out.println("m3u8Content 解析完成,ts " +ts);
                    String iv = x.get(1).replace("\r\n", "");
                    iv = iv.replace("IV=0x", "");
                    iv = iv.substring(0, 16);   //去除前缀,取IV前16位

                    int idx = ts.lastIndexOf("/");
                    String tsFileName = ts.substring(idx + 1);

                    try {
                        String saveFilepath ="D:\\VIDEO\\"+ iv+".ts";

                        File saveFile  = new File(saveFilepath);
                        if (!saveFile.exists()) {
                            System.out.println("开始下载ts: " + domain + ts);
                            byte[] encByte = CommonUtils.doGetByteArr(domain + ts);
                            if (encByte != null) {
                                System.out.println("开始解密, IV -> " + iv);
                                byte[] decByte = null;     //解密视频流
                                try {
                                    decByte = CommonUtils.decryptAESByKey(encByte, skey, iv);
                                } catch (Exception e) {
                                    error_arr.add(tsFileName);
                                    System.out.println("解密ts文件["+tsFileName+"]异常。" + e.getMessage());
                                    e.printStackTrace();
                                }
                                if (decByte != null) {
                                    //保存视频文件

                                    FileOutputStream fos = new FileOutputStream(saveFile);
                                    fos.write(decByte,0,decByte.length);
                                    fos.flush();
                                    fos.close();

                                    Integer ii = listTuple.indexOf(x);
                                    System.out.println(tsFileName + " 下载完成. " + (ii + 1) + "/" + ivList.size());
                                }
                            } else {
                                error_arr.add(tsFileName);
                                System.out.println("doGetByteArr 结果返回null");
                            }
                        } else {
                            System.out.println("文件 " + saveFilepath + " 已存在");
                        }
                    } catch (Exception e) {
                        error_arr.add(tsFileName);
                        e.printStackTrace();
                    }
                });
                System.out.println("所有操作已完成. 保存目录 " + finalSavePath);
                if (error_arr.size() > 0) {
                    List list = error_arr.stream().distinct().collect(Collectors.toList());
                    System.out.println("其中 共有" + list.size() + "个文件下载失败:");
                    list.forEach(x -> {
                        System.out.println(x);
                    });
                } else {
                    // 文件全部下载成功,调用 cmd的 copy /b命令合并  eg: CommonUtils.exeCmd("cmd /c copy /b D:\\cmdtest\\*.ts D:\\cmdtest\\newfile.ts");
                    CommonUtils.exeCmd("cmd /c copy /b " + finalSavePath + "*.ts " + finalSavePath + dirName + ".ts");
                }

                String endDate = new SimpleDateFormat("HH:mm:ss").format(new Date());
                System.out.println(endDate + " ---- 下载完成 ----");
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

原文:点击跳转

你可能感兴趣的:(python,开发语言)