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链接

 

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