有的时候我们需要下载加密的流媒体,而市面上很多下载器是不支持下载加密的流媒体的。所以我通过Java的方式来下载这种流媒体并解密。 例如以下的一个例子:网页
准备工作
- 具有加密的流媒体的网页 例如:以上示例的网页
- JDK 文章使用Graal JDK 17.0.11
- IDE
获取加密媒体的m3u8 URL
先打开具有加密流媒体的网页,然后按F12打开开发人员工具,转到“网络”一栏,然后刷新网页或按下Ctrl+R。
在刷新的列表中尝试找到对应的m3u8文件,然后记下它的URL。
当然,也可以在这里选择预览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文件的预览。
多线程下载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);
}
}
}
}
}
最终示例
提供最终代码(所有步骤融合在一起)如下:
最终代码
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.*;
import java.nio.file.Files;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 下载加密的m3u8流媒体.下载时没有超时时间,如果下载失败请尝试在参数中添加代理.
* @author Denvo Zonis
* @version 0.0.1
*/
public class Main {
public static void main(String[] args) {
/*
请先调整参数.
m3u8_url: m3u8文件的URL,需加http(s)前缀.
hasProxy: 如果有网络代理,请设置为true.
proxy: 网络代理设置.
threads: 设置多线程下载ts文件时使用的线程数.
save_dir: 文件的保存路径(文件夹路径,文件夹必须为空),下载的文件名请自行在下载后修改.
以下为示例参数:
m3u8_url: https://www.example.com/play/abc123/index.m3u8
hasProxy: true
proxy: localhost,7890
download_threads: 512
save_dir: D:\folder1folder2
*/
URL m3u8_url;
boolean hasProxy;
Proxy proxy;
int threads;
File save_dir;
try {
m3u8_url = new URL("https://v.gsuus.com/play/7ax4RjEa/index.m3u8");
hasProxy = true;
proxy = new Proxy(Proxy.Type.HTTP,new InetSocketAddress("localhost",7897));
threads = 512;
save_dir = new File("D:\user-temp\test\getMedia");
} catch (MalformedURLException e) {
System.out.println("参数错误!");
throw new RuntimeException(e);
}
System.out.println("[步骤1]检查参数...");
//每一步都设置了计时器,可以统计每一步花费的时间.
long startTime = System.currentTimeMillis();
if (!save_dir.exists()) {
System.out.println("输出目录不存在!自动创建一个.");
if (!save_dir.mkdirs()) {
System.out.println("无法创建目录!请确保程序拥有读写权限!");
System.exit(0);
}
} else if (!save_dir.isDirectory()) {
System.out.println("参数设置的输出目录为一个文件!请修改save_dir参数!");
System.exit(0);
} else if (save_dir.listFiles().length != 0) {
System.out.println("指定的输出目录内存在文件!请指定一个空文件夹!");
System.exit(0);
}
System.out.println("[步骤2]获取m3u8文件...");
long step2startTime = System.currentTimeMillis();
ArrayList m3u8_contents = new ArrayList();
HttpURLConnection m3u8_con;
try {
if (hasProxy) {
m3u8_con = (HttpURLConnection) m3u8_url.openConnection(proxy);
} else {
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();
} catch (IOException e) {
System.out.println("在获取m3u8文件的URL时发生错误!");
throw new RuntimeException(e);
}
System.out.println("完成!花费了" + ((double) (System.currentTimeMillis() - step2startTime) / 1000) + "秒.");
System.out.println("[步骤3]从m3u8文件获取数据...");
ArrayList ts_URL = new ArrayList();
String EncryptMethod = "",IV = "",key_URI = "",media_sequence = "";
for (int i = 0; i < m3u8_contents.size(); i++) {
if (!m3u8_contents.get(i).startsWith("#EXT")) {
try {
ts_URL.add(new URL(m3u8_contents.get(i)));
} catch (MalformedURLException e) {
System.out.println("获取ts文件的URL失败!");
throw new RuntimeException(e);
}
} else if (m3u8_contents.get(i).contains("#EXT-X-KEY")) {
String[] keyLineArgs = m3u8_contents.get(i).substring(11).split(",");
for (int j = 0; j < keyLineArgs.length; j++) {
String[] key_value = keyLineArgs[j].split("=");
if ("METHOD".equals(key_value[0])) {
EncryptMethod = key_value[1];
} else if ("URI".equals(key_value[0])) {
key_URI = key_value[1].substring(1,key_value[1].length() - 1);
} else if ("IV".equals(key_value[0])) {
IV = key_value[1].substring(2,18);
} else {
System.out.println("解析到m3u8文件内有多余的参数:" + key_value[0] + "=" + key_value[1]);
}
}
} else if (m3u8_contents.get(i).startsWith("#EXT-X-MEDIA-SEQUENCE")) {
media_sequence = m3u8_contents.get(i).substring(22);
}
}
if ("".equals(IV)) {
if (media_sequence.length() < 16) {
StringBuilder builder = new StringBuilder();
builder.append(media_sequence);
for (int i = 0; i < 16 - media_sequence.length(); i++) {
builder.append(0);
}
media_sequence = builder.toString();
} else if (media_sequence.length() > 16) {
media_sequence = media_sequence.substring(0,16);
}
IV = media_sequence;
System.out.println("m3u8文件不存在IV参数!使用'#EXT-X-MEDIA-SEQUENCE'的值作为IV.");
}
System.out.println("加密参数:加密方法:" + EncryptMethod + ",IV:" + IV);
System.out.println("[步骤4]获取key...");
URL key_url;
try {
if (key_URI.startsWith("http")) {
key_url = new URL(key_URI);
} else {
String m3u8_path = m3u8_url.toString();
key_url = new URL(m3u8_path.substring(0,m3u8_path.lastIndexOf("/") + 1) + key_URI);
}
} catch (MalformedURLException e) {
System.out.println("无法获取key!");
throw new RuntimeException(e);
}
String key;
long step4startTime = System.currentTimeMillis();
try {
HttpURLConnection key_con;
if (hasProxy) {
key_con = (HttpURLConnection) key_url.openConnection(proxy);
} else {
key_con = (HttpURLConnection) key_url.openConnection();
}
key_con.setRequestMethod("GET");
BufferedReader in = new BufferedReader(new InputStreamReader(key_con.getInputStream()));
key = in.readLine();
in.close();
key_con.disconnect();
} catch (IOException e) {
System.out.println("获取key时出现问题!");
throw new RuntimeException(e);
}
System.out.println("完成!花费了" + ((double) (System.currentTimeMillis() - step4startTime) / 1000) + "秒.");
System.out.println("key:" + key);
System.out.println("[步骤5]多线程下载ts文件...");
File download_cache_dir = new File(save_dir.getPath() + File.separator +"downloadCache");
download_cache_dir.mkdir();
long step5startTime = System.currentTimeMillis();
ExecutorService download_pool = Executors.newFixedThreadPool(threads);
final int[] counter = {0};
for (int i = 0; i < ts_URL.size(); i++) {
int finalI = i;
download_pool.submit(() -> {
try {
URLConnection ts_con;
if (hasProxy) {
ts_con = ts_URL.get(finalI).openConnection(proxy);
} else {
ts_con = ts_URL.get(finalI).openConnection();
}
ts_con.connect();
BufferedInputStream bin = new BufferedInputStream(ts_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();
counter[0]++;
} catch (IOException e) {
System.out.println("下载:" + ts_URL.get(finalI) + "出现问题!");
throw new RuntimeException(e);
}
});
}
download_pool.shutdown();
double bar;
while (!download_pool.isTerminated()) {
try {
Thread.sleep(5000);
bar =(double) counter[0] * 100 / ts_URL.size();
System.out.println("下载进度:" + counter[0] + "/" + ts_URL.size() + "(" + String.format("%.2f",bar) + "%)");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("完成!花费了" + ((double) (System.currentTimeMillis() - step5startTime) / 1000) + "秒.");
System.out.println("[步骤6]尝试解密并合并文件...");
File output_file = new File(save_dir.getPath() + File.separator + "output.mp4");
File[] inputs = download_cache_dir.listFiles();
if (inputs != null) {
Arrays.sort(inputs,Comparator.comparing(File::getName));
long step6startTime = System.currentTimeMillis();
try (OutputStream out = new FileOutputStream(output_file)) {
for (File inputFile:inputs) {
if (inputFile.getName().endsWith(".ts")) {
byte[] encryptedBytes = Files.readAllBytes(inputFile.toPath());
//TODO 这里只处理了AES-128加密,其他加密方式的解密请自行处理.
//创建AES密钥和初始化向量.
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(),"AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes());
// 创建AES加密解密器
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.DECRYPT_MODE,secretKeySpec,ivParameterSpec);
// 解密文件内容
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
// 写入解密后的内容到新文件
out.write(decryptedBytes);
}
}
} catch (IOException | InvalidAlgorithmParameterException | NoSuchPaddingException |
IllegalBlockSizeException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException e) {
System.out.println("无法保存输出!");
throw new RuntimeException(e);
}
System.out.println("完成!花费了" + ((double) (System.currentTimeMillis() - step6startTime) / 1000) + "秒.");
} else {
System.out.println("上一步中下载的文件不存在!请检查!");
System.exit(0);
}
System.out.println("程序运行总用时:" + ((double) (System.currentTimeMillis() - startTime) / 1000) + "秒.");
}
}
运行以上示例所需环境在“准备工作”一节已讲述。以下为运行结果:
运行结果
[步骤1]检查参数...
[步骤2]获取m3u8文件...
完成!花费了1.687秒.
[步骤3]从m3u8文件获取数据...
加密参数:加密方法:AES-128,IV:0000000000000000
[步骤4]获取key...
完成!花费了0.265秒.
key:po6uJRFkNulZsfxB
[步骤5]多线程下载ts文件...
下载进度:205/844(24.29%)
下载进度:754/844(89.34%)
下载进度:844/844(100.00%)
完成!花费了16.663秒.
[步骤6]尝试解密并合并文件...
完成!花费了14.368秒.
程序运行总用时:33.033秒.
实测下载时能跑到约100MB/s(千兆宽带),最终的output.mp4文件约1.2G。
最后
此文章仅用于学习交流!如果有问题欢迎在评论区指出,感谢不尽!