天天看點

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個線程測試效果 

java下載下傳m3u8視訊,解密并合并ts(三)編寫代碼
java下載下傳m3u8視訊,解密并合并ts(三)編寫代碼
git位址:https://github.com/qq494257084/m3u8Download

上一篇 java下載下傳m3u8視訊,解密并合并ts(二)——擷取m3u8連結