大家好,欢迎来到IT知识分享网。
总览
本文简要介绍了 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 desc 和 fileSequence0.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);
}
}
}
IT知识分享网
我们就利用 http://playertest.longtailvideo.com/adaptive/bipbop/gear4/prog_index.m3u8 地址做测试,随便新建一个测试类,将下面代码写进去,导好该导的包,即可测试了。
IT知识分享网 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 );
}
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 个切片文件,并且在项目根目录下也已经合并成一个整体的文件了。(这里要注意的是有些系统 File.listfiles 方法得到的文件顺序并不是按照文件创建时间排序的,所以这里可能需要自行排序)。
总结
有些M3U8格式可能不完全和本次测试的格式一样,比如有的切片是完整路径,有的里面还嵌套了一层 M3U8,有的 ts 切片甚至还加密了,但万变不离其宗,也就多几步操作而已,好了,祝大家都能下载自己想要的片吧。
( 2022-6-16 ) 注:代码只是为了演示功能,文件下载比较简单粗暴,请勿直接照搬。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/11914.html