Java下载加密视频

有的时候我们需要下载加密的流媒体,而市面上很多下载器是不支持下载加密的流媒体的。所以我通过Java的方式来下载这种流媒体并解密。 例如以下的一个例子:网页

网页示例

NDM下载示例

准备工作

  1. 具有加密的流媒体的网页 例如:以上示例的网页
  2. JDK 文章使用Graal JDK 17.0.11
  3. IDE

获取加密媒体的m3u8 URL

先打开具有加密流媒体的网页,然后按F12打开开发人员工具,转到“网络”一栏,然后刷新网页或按下Ctrl+R。

在刷新的列表中尝试找到对应的m3u8文件,然后记下它的URL。

开发人员工具-寻找m3u8文件
当然,也可以在这里选择预览m3u8文件的内容。
m3u8预览

我们需要的内容只有里面所有的链接(https://xxx.xxx/xxx.ts )以及文件最上面的#EXT-X-MEDIA-SEQUENCE和#EXT-X-KEY两行的内容。链接由于太多了,我们选择使用Java来帮我们获取,而#EXT-X-MEDIA-SEQUENCE和#EXT-X-KEY的内容可以选择在这里直接记下来,也可以使用Java来获取。

仅当m3u8文件中不存在`IV=xxx…`时才使用#EXT-X-MEDIA-SEQUENCE的值来充当IV值。如果IV值本身存在,则后面不需要#EXT-X-MEDIA-SEQUENCE的值。
IV值必须为16个数字,如果不符合请取前16位(如果多于16位)或在后面补0(如果少于16位)

获取m3u8文件内容

简单来说,只需要请求刚才获取的URL,就能返回我们所需要的m3u8文件内容。一个示例如下:

import java.net.HttpURLConnection;
import java.net.URL;

public void get_m3u8_content() {
    //这里改成目标m3u8的URL.
    URL m3u8_url = new URL("https://v.gsuus.com/play/7ax4RjEa/index.m3u8");
    //获取的文件内容保存在这里(按行保存).也可以使用其他方式保存,这里为了下一步筛选文件内容方便,使用List.
    ArrayList m3u8_contents = new ArrayList();
    HttpURLConnection m3u8_con = (HttpURLConnection) m3u8_url.openConnection();
    m3u8_con.setRequestMethod("GET");
    BufferedReader in = new BufferedReader(new InputStreamReader(m3u8_con.getInputStream()));
    String inputLine;
    while ((inputLine = in.readLine()) != null) m3u8_contents.add(inputLine);
    in.close();
    m3u8_con.disconnect();
}

如果网络连接需要使用代理,以上示例中的m3u8_con对象可以这么创建。

import java.net.HttpURLConnection;
import java.net.URL;
import java.net.Proxy;

Proxy proxy = new Proxy("localhost", 7890); //这里根据你的代理设置.
URL m3u8_url = new URL("https://v.gsuus.com/play/7ax4RjEa/index.m3u8");
HttpURLConnection m3u8_con = (HttpURLConnection) m3u8_url.openConnetion(proxy);

获取到文件内容之后,我们开始筛选有用信息。首先观察文件内容,会发现除了链接之外,所有的内容前面都有个#EXT。既然如此,我们就可以筛选出所有链接了。以下为通过遍历筛选的示例:

import java.net.URL;
import java.util.ArrayList;

public void getURL() {
    //此对象来源于前面的示例,里面每一个元素就是m3u8文件的一行内容.
    ArrayList m3u8_contents = new ArrayList();
    //这个对象用来保存筛选后的URL.
    ArrayList ts_URL = new ArrayList();
    for (int i = 0; i < m3u8_contents.size(); i++) {
        if (m3u8_contents.get(i).startsWith("#EXT")) {
            ts_URL.add(new URL(m3u8_contents.get(i)));
        }
    }
}

类似地,我们也能筛选出#EXT-X-MEDIA-SEQUENCE和#EXT-X-KEY的内容,这里就不加示例了(下面的最终示例包含此部分)。

获取key

在#EXT-X-KEY一行中通常有一个URI="xxx.key"的内容,根据它来获取key文件。你可以在刚才的开发人员工具里找到浏览器获取的这个文件并记下文件内容,也可以使用Java。使用Java获取key的方法与获取m3u8文件的方法类似,故不贴出示例。

以下为key文件的预览。

预览key文件

多线程下载ts文件

这里我们为了加快下载速度,可以使用多线程并发下载文件,以下为一个例子(CPU为英特尔志强E5-2660(2颗),并发线程数请根据自己情况调整)。

import java.io.File;
import java.net.URLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public void download() {
    //创建线程池,线程数请根据自身情况调整.
    ExecutorService pool = Executors.newFixedThreadPool(512);
    //此File对象表示下载存放目录.
    File download_cache_dir = new File("D:\\temp");
    //此对象来源于前面的示例,包含所有ts文件的链接.
    ArrayList ts_URL = new ArrayList();
    for (int i = 0; i < ts_URL.size(); i++) {
        //Iambda表达式,注意使用的版本是否支持.finalI变量也是因为表达式的限制无法直接访问i而创建的.
        int finalI = i;
        pool.submit(() -> {
            //如果需要代理,可根据前面的方法设置.
            URLConnection con = ts_URL.get(finalI).openConnection();
            con.connect();
            BufferedInputStream bin = new BufferedInputStream(con.getInputStream());
            File outFile = new File(download_cache_dir.getPath() + File.separator + finalI + ".ts");
            int size;
            byte[] buf = new byte[2048];
            try (OutputStream out = new FileOutputStream(outFile)) {
                while ((size = bin.read(buf)) != -1) out.write(buf, 0, size);
            }
            bin.close();
        });
    }
    //任务提交后即可关闭线程池.它会等到所有提交的线程运行结束后自动关闭.
    pool.shutdown();
}

如果下载的文件较多或较大,我们可以适当地设置一些提示,如计时器以及下载进度。

import java.net.URL;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public void download() {
    //这两个对象来源于以上示例,作为多线程下载文件的线程池以及下载的URL来源.
    ExecutorService pool = Executors.newFixedThreadPool(512);
    ArrayList ts_URL = new ArrayList();
    //记录下当前时间.
    long startTime = System.currentTimeMillis();
    //计数器,每一个任务完成后都让它加1.
    final int[] counter = {0};
    //这里根据以上示例为pool提交任务,这里把除了pool.submit()之外的部分省略掉.
    pool.submit(() -> {
        //下载文件的部分请参照上面的示例,这里省略掉.执行完成后使计数器加1.
        counter[0]++;
    });
    pool.shutdown();
    //在线程池关闭之前一直循环.
    while (!pool.isTerminated()) {
        Thread.sleep(5000); //每5秒发送一次提示.
        System.out.println("下载进度:" + counter[0] + "/" + ts_URL.size() + "(" + String.format("%.2f",(double) counter[0] * 100 / ts_URL.size()) + "%)");
    }
    System.out.println("完成!花费了" + ((double) (System.currentTimeMillis() - startTime) / 1000) + "秒.");
}

解密及合并文件

下载了所有的ts文件之后,就可以尝试解密及合并文件了。示例中的加密流媒体使用的是AES-128加密(在#EXT-X-KEY一行查询),故这里仅展示AES-128的解密过程。

import java.io.File;
import java.nio.file.Files;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.Cipher;

/*
  通过AES-128解密文件.注意需要修改key和IV值.
  ts文件可选择先解密再合并,也可先合并再解密,二者效果相同.
  input:这个文件是待解密的文件,即刚才下载的ts文件.
 */
public byte[] decrypt(File input) {
    //这两个字符串为key和IV.内容应为之前记下的内容.
    String key,IV;
    byte[] cryptedBytes = Files.readAllBytes(input.toPath());
    //创建AES解密器.
    Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
    cipher.init(Cipher.DECRYPT_MODE,new SecretKeySpec(key.getBytes(),"AES"),new IvParameterSpec(IV.getBytes()));
    //解密文件内容.
    return cipher.doFinal(encryptedBytes);
}

根据ts文件的特殊性,我们可以直接把所有ts文件的内容加到一个文件,就能实现合并ts文件了。以下为一个解密及合并的综合性参考:

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Comparator;

public void finalStep() {
    //这个对象来源于上面的示例,表示下载的ts文件的存放目录.
    File download_cache_dir = new File();
    //这个对象为输出文件,请补充相应的构造方法.
    File output = new File();

    File[] inputs = download_cache_dir.listFiles();
    if (inputs != null) {
        //按文件名顺序整理排序inputs数组.
        Arrays.sort(inputs, Comparator.comparing(File::getName));
        try (OutputStream out = new FileOutputStream(output)) {
            for (File input:inputs) {
                if (input.getName().endsWith(".ts")) {
                    //这个方法来源于上面的示例.
                    byte[] decrypted_bytes = decrypt(input);
                    out.write(decrypted_bytes);
                }
            }
        }
    }
}

最终示例

提供最终代码(所有步骤融合在一起)如下:

运行以上示例所需环境在“准备工作”一节已讲述。以下为运行结果:

实测下载时能跑到约100MB/s(千兆宽带),最终的output.mp4文件约1.2G。

预览-文件夹
预览-下载视频

最后

此文章仅用于学习交流!如果有问题欢迎在评论区指出,感谢不尽!

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇