上一篇 java下載下傳m3u8視訊,解密并合并ts(二)——擷取m3u8連結
編寫代碼
加載jar包
由于java不支援AES/CBC/PKCS7Padding模式解密,是以我們要借助第一篇下載下傳好的jar包
當類加載時,通過靜态代碼塊加載
/**
*
* 解決java不支援AES/CBC/PKCS7Padding模式解密
*
*/
static {
Security.addProvider(new BouncyCastleProvider());
}
所需類字段
//要下載下傳的m3u8連結
private final String DOWNLOADURL;
//線程數
private int threadCount = 1;
//重試次數
private int retryCount = 30;
//連結連接配接逾時時間(機關:毫秒)
private long timeoutMillisecond = 1000L;
//合并後的檔案存儲目錄
private String dir;
//合并後的視訊檔案名稱
private String fileName;
//已完成ts片段個數
private int finishedCount = 0;
//解密算法名稱
private String method;
//密鑰
private String key = "";
//所有ts片段下載下傳連結
private Set<String> tsSet = new LinkedHashSet<>();
//解密後的片段
private Set<File> finishedFiles = new ConcurrentSkipListSet<>(Comparator.comparingInt(o -> Integer.parseInt(o.getName().replace(".xyz", ""))));
//已經下載下傳的檔案大小
private BigDecimal downloadBytes = new BigDecimal(0);
擷取連結内容
模拟HTTP請求,擷取連結相應内容
/**
* 模拟http請求擷取内容
*
* @param urls http連結
* @return 内容
*/
private StringBuilder getUrlContent(String urls) {
int count = 1;
HttpURLConnection httpURLConnection = null;
StringBuilder content = new StringBuilder();
while (count <= retryCount) {
try {
URL url = new URL(urls);
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setConnectTimeout((int) timeoutMillisecond);
httpURLConnection.setReadTimeout((int) timeoutMillisecond);
httpURLConnection.setUseCaches(false);
httpURLConnection.setDoInput(true);
String line;
InputStream inputStream = httpURLConnection.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
while ((line = bufferedReader.readLine()) != null)
content.append(line).append("\n");
bufferedReader.close();
inputStream.close();
System.out.println(content);
break;
} catch (Exception e) {
// System.out.println("第" + count + "擷取連結重試!\t" + urls);
count++;
// e.printStackTrace();
} finally {
if (httpURLConnection != null) {
httpURLConnection.disconnect();
}
}
}
if (count > retryCount)
throw new M3u8Exception("連接配接逾時!");
return content;
}
判斷是否需要解密
首先将m3u8連結内容通過getUrlContent方法擷取到,然後解析,如果内容含有#EXT-X-KEY标簽,則說明這個連結是需要進行ts檔案解密的,然後通過下面的.m3u8的if語句擷取含有密鑰以及ts片段的連結。
如果含有#EXTINF,則說明這個連結就是含有ts視訊片段的連結,沒有第二個m3u8連結了。
之後我們要擷取密鑰的getKey方法,即時不需要密鑰。并把ts片段加進set集合,即tsSet字段。
/**
* 擷取所有的ts片段下載下傳連結
*
* @return 連結是否被加密,null為非加密
*/
private String getTsUrl() {
StringBuilder content = getUrlContent(DOWNLOADURL);
//判斷是否是m3u8連結
if (!content.toString().contains("#EXTM3U"))
throw new M3u8Exception(DOWNLOADURL + "不是m3u8連結!");
String[] split = content.toString().split("\\n");
String keyUrl = "";
boolean isKey = false;
for (String s : split) {
//如果含有此字段,則說明隻有一層m3u8連結
if (s.contains("#EXT-X-KEY") || s.contains("#EXTINF")) {
isKey = true;
keyUrl = DOWNLOADURL;
break;
}
//如果含有此字段,則說明ts片段連結需要從第二個m3u8連結擷取
if (s.contains(".m3u8")) {
if (StringUtils.isUrl(s))
return s;
String relativeUrl = DOWNLOADURL.substring(0, DOWNLOADURL.lastIndexOf("/") + 1);
keyUrl = relativeUrl + s;
break;
}
}
if (StringUtils.isEmpty(keyUrl))
throw new M3u8Exception("未發現key連結!");
//擷取密鑰
String key1 = isKey ? getKey(keyUrl, content) : getKey(keyUrl, null);
if (StringUtils.isNotEmpty(key1))
key = key1;
else key = null;
return key;
}
擷取密鑰
如果參數content不為空,則說明密鑰資訊從此字段取,否則則通路第二個m3u8連結,然後擷取資訊。
也就是說,如果content為空,說明則為樣例一,三的情況,第一個m3u8檔案裡面沒有ts片段資訊,需要從第二個m3u8檔案取。
如果發現不需要解密,此方法将會傳回null。需要解密的話,那麼解密算法将會存在method字段,密鑰将存在key字段。
/**
* 擷取ts解密的密鑰,并把ts片段加入set集合
*
* @param url 密鑰連結,如果無密鑰的m3u8,則此字段可為空
* @param content 内容,如果有密鑰,則此字段可以為空
* @return ts是否需要解密,null為不解密
*/
private String getKey(String url, StringBuilder content) {
StringBuilder urlContent;
if (content == null || StringUtils.isEmpty(content.toString()))
urlContent = getUrlContent(url);
else urlContent = content;
if (!urlContent.toString().contains("#EXTM3U"))
throw new M3u8Exception(DOWNLOADURL + "不是m3u8連結!");
String[] split = urlContent.toString().split("\\n");
for (String s : split) {
//如果含有此字段,則擷取加密算法以及擷取密鑰的連結
if (s.contains("EXT-X-KEY")) {
String[] split1 = s.split(",", 2);
if (split1[0].contains("METHOD"))
method = split1[0].split("=", 2)[1];
if (split1[1].contains("URI"))
key = split1[1].split("=", 2)[1];
}
}
String relativeUrl = url.substring(0, url.lastIndexOf("/") + 1);
//将ts片段連結加入set集合
for (int i = 0; i < split.length; i++) {
String s = split[i];
if (s.contains("#EXTINF"))
tsSet.add(relativeUrl + split[++i]);
}
if (!StringUtils.isEmpty(key)) {
key = key.replace("\"", "");
return getUrlContent(relativeUrl + key).toString().replaceAll("\\s+", "");
}
return null;
}
解密ts片段
目前此程式隻支援AES算法,因為目前我沒有遇到别的。。。
如果你的m3u8發現了EXT-X-KEY标簽,并且後面後IV鍵值對,那麼請new IvParameterSpec(new byte[16]);的參數換成IV後面的值(把字元串通過getBytes換成位元組數組)(git代碼已實作此功能)
/**
* 解密ts
*
* @param sSrc ts檔案位元組數組
* @param sKey 密鑰
* @return 解密後的位元組數組
*/
private static byte[] decrypt(byte[] sSrc, String sKey, String method) {
try {
if (StringUtils.isNotEmpty(method) && !method.contains("AES"))
throw new M3u8Exception("未知的算法!");
// 判斷Key是否正确
if (StringUtils.isEmpty(sKey)) {
return sSrc;
}
// 判斷Key是否為16位
if (sKey.length() != 16) {
System.out.print("Key長度不是16位");
return null;
}
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
SecretKeySpec keySpec = new SecretKeySpec(sKey.getBytes("utf-8"), "AES");
//如果m3u8有IV标簽,那麼IvParameterSpec構造函數就把IV标簽後的内容轉成位元組數組傳進去
AlgorithmParameterSpec paramSpec = new IvParameterSpec(new byte[16]);
cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
return cipher.doFinal(sSrc);
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
啟動線程下載下傳ts片段
代碼中xy字尾檔案是未解密的ts片段,xyz是解密後的ts片段,這兩個字尾起成什麼無所謂。
如果線程數設定的大,那麼占記憶體就會很多,這個是因為代碼中byte1變量沒有進行複用,垃圾回收沒有即時回收引起的,可以自己優化一下。
/**
* 開啟下載下傳線程
*
* @param urls ts片段連結
* @param i ts片段序号
* @return 線程
*/
private Thread getThread(String urls, int i) {
return new Thread(() -> {
int count = 1;
HttpURLConnection httpURLConnection = null;
//xy為未解密的ts片段,如果存在,則删除
File file2 = new File(dir + "\\" + i + ".xy");
if (file2.exists())
file2.delete();
OutputStream outputStream = null;
InputStream inputStream1 = null;
FileOutputStream outputStream1 = null;
//重試次數判斷
while (count <= retryCount) {
try {
//模拟http請求擷取ts片段檔案
URL url = new URL(urls);
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setConnectTimeout((int) timeoutMillisecond);
httpURLConnection.setUseCaches(false);
httpURLConnection.setReadTimeout((int) timeoutMillisecond);
httpURLConnection.setDoInput(true);
InputStream inputStream = httpURLConnection.getInputStream();
try {
outputStream = new FileOutputStream(file2);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
int len;
byte[] bytes = new byte[1024];
//将未解密的ts片段寫入檔案
while ((len = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
synchronized (this) {
downloadBytes = downloadBytes.add(new BigDecimal(len));
}
}
outputStream.flush();
inputStream.close();
inputStream1 = new FileInputStream(file2);
byte[] bytes1 = new byte[inputStream1.available()];
inputStream1.read(bytes1);
File file = new File(dir + "\\" + i + ".xyz");
outputStream1 = new FileOutputStream(file);
//開始解密ts片段,這裡我們把ts字尾改為了xyz,改不改都一樣
outputStream1.write(decrypt(bytes1, key, method));
finishedFiles.add(file);
break;
} catch (Exception e) {
// System.out.println("第" + count + "擷取連結重試!\t" + urls);
count++;
// e.printStackTrace();
} finally {
try {
if (inputStream1 != null)
inputStream1.close();
if (outputStream1 != null)
outputStream1.close();
if (outputStream != null)
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
if (httpURLConnection != null) {
httpURLConnection.disconnect();
}
}
}
if (count > retryCount)
//自定義異常
throw new M3u8Exception("連接配接逾時!");
finishedCount++;
// System.out.println(urls + "下載下傳完畢!\t已完成" + finishedCount + "個,還剩" + (tsSet.size() - finishedCount) + "個!");
});
}
合并以及删除多餘的ts片段
/**
* 合并下載下傳好的ts片段
*/
private void mergeTs() {
try {
File file = new File(dir + "/" + fileName + ".mp4");
if (file.exists())
file.delete();
else file.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] b = new byte[4096];
for (File f : finishedFiles) {
FileInputStream fileInputStream = new FileInputStream(f);
int len;
while ((len = fileInputStream.read(b)) != -1) {
fileOutputStream.write(b, 0, len);
}
fileInputStream.close();
fileOutputStream.flush();
}
fileOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 删除下載下傳好的片段
*/
private void deleteFiles() {
File file = new File(dir);
for (File f : file.listFiles()) {
if (!f.getName().contains(fileName + ".mp4"))
f.deleteOnExit();
}
}
開始多線程下載下傳
這裡有個問題,就是System.out.println("視訊合并完成,歡迎使用!");列印出來了,但是檔案還沒有删除完,當控制台輸出Process finished with exit code 0的時候才說明執行完。
/**
* 下載下傳視訊
*/
private void startDownload() {
//線程池
final ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadCount);
int i = 0;
//如果生成目錄不存在,則建立
File file1 = new File(dir);
if (!file1.exists())
file1.mkdirs();
//執行多線程下載下傳
for (String s : tsSet) {
i++;
fixedThreadPool.execute(getThread(s, i));
}
fixedThreadPool.shutdown();
//下載下傳過程監視
new Thread(() -> {
int consume = 0;
//輪詢是否下載下傳成功
while (!fixedThreadPool.isTerminated()) {
try {
consume++;
BigDecimal bigDecimal = new BigDecimal(downloadBytes.toString());
Thread.sleep(1000L);
System.out.print("已用時" + consume + "秒!\t下載下傳速度:" + StringUtils.convertToDownloadSpeed(new BigDecimal(downloadBytes.toString()).subtract(bigDecimal), 3) + "/s");
System.out.print("\t已完成" + finishedCount + "個,還剩" + (tsSet.size() - finishedCount) + "個!");
System.out.println(new BigDecimal(finishedCount).divide(new BigDecimal(tsSet.size()), 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP) + "%");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("下載下傳完成,正在合并檔案!共" + finishedFiles.size() + "個!" + StringUtils.convertToDownloadSpeed(downloadBytes, 3));
//開始合并視訊
mergeTs();
//删除多餘的ts片段
deleteFiles();
System.out.println("視訊合并完成,歡迎使用!");
}).start();
}
啟動入口
startDownload()方法可以放進getTsUrl()方法裡面。
/**
* 開始下載下傳視訊
*/
public void start() {
checkField();
String tsUrl = getTsUrl();
if(StringUtils.isEmpty(tsUrl))
System.out.println("不需要解密");
startDownload();
}
測試類
public class M3u8Main {
private static final String M3U8URL = "https://XXX/index.m3u8";
public static void main(String[] args) {
M3u8DownloadFactory.M3u8Download m3u8Download = M3u8DownloadFactory.getInstance(M3U8URL);
//設定生成目錄
m3u8Download.setDir("F://m3u8JavaTest");
//設定視訊名稱
m3u8Download.setFileName("test");
//設定線程數
m3u8Download.setThreadCount(100);
//設定重試次數
m3u8Download.setRetryCount(100);
//設定連接配接逾時時間(機關:毫秒)
m3u8Download.setTimeoutMillisecond(10000L);
m3u8Download.start();
}
}
100個線程測試效果

git位址:https://github.com/qq494257084/m3u8Download
上一篇 java下載下傳m3u8視訊,解密并合并ts(二)——擷取m3u8連結