java下載m3u8視頻,解密併合並ts(三)

上一篇 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鏈接

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章