用 JAVA 编写一个 M3U8 视频下载器

来源:互联网 发布:stl文件编辑软件 编辑:程序博客网 时间:2024/04/28 08:02

总览

本文简要介绍了 M3U8 视频文件格式,并且用代码实现下载一个 M3U8 文件的视频资源。

背景

前段时间在做视频真实地址解析下载时候发现很多视频网站用了 CKplayer,播放的时候传过来的参数是一个 M3U8 文件的链接,和普通的视频文件不一样,M3U8 文件并不是真正的视频,它一般只有几 kb 左右,当时没想太多,遇到 M3U8 的格式就都没搞了,最近突发奇想研究了下 M3U8,发现其实下载 M3U8 的资源也挺简单的。

M3U8介绍

首先我们找到一个测试地址
http://playertest.longtailvideo.com/adaptive/bipbop/gear4/prog_index.m3u8
浏览器打开下载可以得到一个 prog_index.m3u8 文件,打开内容如下:

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10, no desc
fileSequence0.ts
#EXTINF:10, no desc
fileSequence1.ts
#EXTINF:10, no desc
fileSequence2.ts
#EXTINF:10, no desc
fileSequence3.ts
#EXTINF:10, no desc
fileSequence4.ts
#EXTINF:10, no desc
fileSequence5.ts
#EXTINF:10, no desc
fileSequence6.ts
#EXTINF:10, no desc
fileSequence7.ts
#EXTINF:10, no desc
fileSequence8.ts
#EXTINF:10, no desc
fileSequence9.ts
#EXT-X-ENDLIST

可以看到 M3U8 文件一般以 #EXTM3U 开头,接着包含几行类似 #EXT-X-TARGETDURATION:10 这样的信息行,M3U8 文件具体格式稍微有点多,不在本篇介绍范围内,感兴趣的读者可以看 m3u8文件信息总结 这篇介绍,我们看有关下载的重点,M3U8 文件包含着许多视频切片的地址,这些切片资源组合起来实际就是真实的视频了,我们看到接下来有 #EXTINF:10, no descfileSequence0.ts 这两行信息,前一行包含了时间,后一行包含了此段切片视频真实地址,不过此地址是相对的,是相对 M3U8文件的路径,有的 M3U8 文件里面的切片是完整的路径,而我们只要解析 M3U8 文件获取每段切片地址,下载到本地,然后按顺序拼接成一个完整的 ts 文件即可。

代码

首先我们编写一个 M3U8 的实体类
M3U8.java

import java.util.ArrayList;import java.util.Collections;import java.util.List;public class M3U8 {    private String basepath;    private List<Ts> tsList = new ArrayList<>();    private long startTime;// 开始时间    private long endTime;// 结束时间    private long startDownloadTime;// 开始下载时间    private long endDownloadTime;// 结束下载时间    public String getBasepath() {        return basepath;    }    public void setBasepath(String basepath) {        this.basepath = basepath;    }    public List<Ts> getTsList() {        return tsList;    }    public void setTsList(List<Ts> tsList) {        this.tsList = tsList;    }    public void addTs(Ts ts) {        this.tsList.add(ts);    }    public long getStartDownloadTime() {        return startDownloadTime;    }    public void setStartDownloadTime(long startDownloadTime) {        this.startDownloadTime = startDownloadTime;    }    public long getEndDownloadTime() {        return endDownloadTime;    }    public void setEndDownloadTime(long endDownloadTime) {        this.endDownloadTime = endDownloadTime;    }    /**     * 获取开始时间     *     * @return     */    public long getStartTime() {        if (tsList.size() > 0) {            Collections.sort(tsList);            startTime = tsList.get(0).getLongDate();            return startTime;        }        return 0;    }    /**     * 获取结束时间(加上了最后一段时间的持续时间)     *     * @return     */    public long getEndTime() {        if (tsList.size() > 0) {            Ts m3U8Ts = tsList.get(tsList.size() - 1);            endTime = m3U8Ts.getLongDate() + (long) (m3U8Ts.getSeconds() * 1000);            return endTime;        }        return 0;    }    @Override    public String toString() {        StringBuilder sb = new StringBuilder();        sb.append("basepath: " + basepath);        for (Ts ts : tsList) {            sb.append("\nts_file_name = " + ts);        }        sb.append("\n\nstartTime = " + startTime);        sb.append("\n\nendTime = " + endTime);        sb.append("\n\nstartDownloadTime = " + startDownloadTime);        sb.append("\n\nendDownloadTime = " + endDownloadTime);        return sb.toString();    }    public static class Ts implements Comparable<Ts> {        private String file;        private float seconds;        public Ts(String file, float seconds) {            this.file = file;            this.seconds = seconds;        }        public String getFile() {            return file;        }        public void setFile(String file) {            this.file = file;        }        public float getSeconds() {            return seconds;        }        public void setSeconds(float seconds) {            this.seconds = seconds;        }        @Override        public String toString() {            return file + " (" + seconds + "sec)";        }        /**         * 获取时间         */        public long getLongDate() {            try {                return Long.parseLong(file.substring(0, file.lastIndexOf(".")));            } catch (Exception e) {                return 0;            }        }        @Override        public int compareTo(Ts o) {            return file.compareTo(o.file);        }    }}

我们就利用 http://playertest.longtailvideo.com/adaptive/bipbop/gear4/prog_index.m3u8 地址做测试,随便新建一个测试类,将下面代码写进去,导好该导的包,即可测试了。

    public static String TEMP_DIR = "temp";    public static int connTimeout = 30 * 60 * 1000;    public static int readTimeout = 30 * 60 * 1000;    public static String s1 = "http://playertest.longtailvideo.com/adaptive/bipbop/gear4/prog_index.m3u8";    public static void main(String[] args) {        File tfile = new File(TEMP_DIR);        if (!tfile.exists()) {            tfile.mkdirs();        }        M3U8 m3u8ByURL = getM3U8ByURL(s1);        String basePath = m3u8ByURL.getBasepath();        m3u8ByURL.getTsList().stream().parallel().forEach(m3U8Ts -> {            File file = new File(TEMP_DIR + File.separator + m3U8Ts.getFile());            if (!file.exists()) {// 下载过的就不管了                FileOutputStream fos = null;                InputStream inputStream = null;                try {                    URL url = new URL(basePath + m3U8Ts.getFile());                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();                    conn.setConnectTimeout(connTimeout);                    conn.setReadTimeout(readTimeout);                    if (conn.getResponseCode() == 200) {                        inputStream = conn.getInputStream();                        fos = new FileOutputStream(file);// 会自动创建文件                        int len = 0;                        byte[] buf = new byte[1024];                        while ((len = inputStream.read(buf)) != -1) {                            fos.write(buf, 0, len);// 写入流中                        }                    }                } catch (Exception e) {                    e.printStackTrace();                } finally {// 关流                    try {                        if (inputStream != null) {                            inputStream.close();                        }                        if (fos != null) {                            fos.close();                        }                    } catch (IOException e) {e.printStackTrace();}                }            }        });        System.out.println("文件下载完毕!");        mergeFiles(tfile.listFiles(), "test.ts");    }    public static M3U8 getM3U8ByURL(String m3u8URL) {        try {            HttpURLConnection conn = (HttpURLConnection) new URL(m3u8URL).openConnection();            if (conn.getResponseCode() == 200) {                String realUrl = conn.getURL().toString();                BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));                String basepath = realUrl.substring(0, realUrl.lastIndexOf("/") + 1);                M3U8 ret = new M3U8();                ret.setBasepath(basepath);                String line;                float seconds = 0;                int mIndex;                while ((line = reader.readLine()) != null) {                    if (line.startsWith("#")) {                        if (line.startsWith("#EXTINF:")) {                            line = line.substring(8);                            if ((mIndex = line.indexOf(",")) != -1) {                                line = line.substring(0, mIndex + 1);                            }                            try {                                seconds = Float.parseFloat(line);                            } catch (Exception e) {                                seconds = 0;                            }                        }                        continue;                    }                    if (line.endsWith("m3u8")) {                        return getM3U8ByURL(basepath + line);                    }                    ret.addTs(new M3U8.Ts(line, seconds));                    seconds = 0;                }                reader.close();                return ret;            }        } catch (IOException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }        return null;    }    public static boolean mergeFiles(File[] fpaths, String resultPath) {        if (fpaths == null || fpaths.length < 1) {            return false;        }        if (fpaths.length == 1) {            return fpaths[0].renameTo(new File(resultPath));        }        for (int i = 0; i < fpaths.length; i++) {            if (!fpaths[i].exists() || !fpaths[i].isFile()) {                return false;            }        }        File resultFile = new File(resultPath);        try {            FileOutputStream fs = new FileOutputStream(resultFile, true);            FileChannel resultFileChannel = fs.getChannel();            FileInputStream tfs;            for (int i = 0; i < fpaths.length; i++) {                tfs = new FileInputStream(fpaths[i]);                FileChannel blk = tfs.getChannel();                resultFileChannel.transferFrom(blk, resultFileChannel.size(), blk.size());                tfs.close();                blk.close();            }            fs.close();            resultFileChannel.close();        } catch (Exception e) {            e.printStackTrace();            return false;        }        // for (int i = 0; i < fpaths.length; i ++) {        // fpaths[i].delete();        // }        return true;    }

结果

我们可以看到,在我的 temp 目录下一句下载好了 10 个切片文件,并且在项目根目录下也已经合并成一个整体的文件了,
这里写图片描述

总结

有些M3U8格式可能不完全和本次测试的格式一样,比如有的切片是完整路径,有的里面还嵌套了一层 M3U8,有的 ts 切片甚至还加密了,但万变不离其宗,也就多几步操作而已,好了,祝大家都能下载自己想要的片吧。

原创粉丝点击