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

2023-11-20

总览

本文简要介绍了 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 );
							}
							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 ) 注:代码只是为了演示功能,文件下载比较简单粗暴,请勿直接照搬。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

用 JAVA 编写一个 M3U8 视频下载器 的相关文章

随机推荐

  • [LDUoj 倍增] 题解

    星星之火 可以燎原 细节的地方慢慢补充 欢迎提出问题 私聊 留言均可 A 跳跳棋 较难 B 聚会 板子题 C 祖孙询问 板子题 D Dis 板子题 E 次小生成树 严格次小生成树 难 F 异象石 难度适中 G 暗的连锁 难度适中 H 点的距
  • 3D游戏编程实践——Priests and Devils

    编程实践 Priests and Devils github链接 https github com ctlchild SYSU unity3d learning tree master hw2 Priests and Devils is a
  • 给Protobuf中的repeated类型变量添加子项

    Protobuf为repeated类型变量生成的自动代码 不提供通常的类似add item item 的添加子项的成员函数 Protobuf的做法是 UserDocChangesResp changes DocChangeInfo chan
  • Linux shell 编程之 - 合并两个文件

    两个文件a1 b1 内容分别如下 a1 1 2 3 b1 a b c 如何把它们合在一起内容如下的 1 a 2 b 3 c paste d a1 a2 SUN的Solaris只能合并12个文件 sco5 5下ksh只能合并6个文件 在aix
  • Allegro PCB封装焊盘介绍(一)

    PCB封装焊盘结构 焊盘结构如图 1所示 图 1焊盘结构 锡膏层 SMT刷锡膏贴片用 一般贴片焊盘要选 跟焊盘等大 阻焊层 把焊盘裸露出来 不开的话 焊盘会被油墨盖住 这样无法焊接哦 一般比焊盘大0 1mm 顶层 底层焊盘 实际焊盘大小 电
  • tensorRT 与 torchserve-GPU性能对比

    实验对比 前端时间搭建了TensorRT Torchserve GPU 最近抽时间将这两种方案做一个简单的实验对比 实验数据 Cuda11 0 Xeon 6242 3 1 80 RTX3090 24G Resnet50 TensorRT T
  • nosql练习

    1 string类型数据的命令操作 1 设置键值 2 读取键值 3 数值类型自增1 4 数值类型自减1 5 查看值的长度 2 list类型数据的命令操作 1 对列表city插入元素 Shanghai Suzhou Hangzhou 2 将列
  • Qt中代码添加背景图

    第一步 选择一张背景图下到本地 第二步 在qt中点击添加新文件选择图中位置 随便起个名字 点击下一步 这时项目中多出一个目录 选择打开资源编辑器 底部添加前缀 注意该前缀是在内部使用图的路径 点击添加 gt 添加前缀 我这里直接使用的 作为
  • STM32F4实现SD卡读写

    更多交流欢迎关注作者抖音号 81849645041 目的 熟悉SD卡和SDIO工作原理 掌握SD卡的读写 原理 大多单片机系统都需要大容量存储设备 以存储数据 目前常用的有 U 盘 FLASH 芯片 SD 卡等 他们各有优点 综合比较 最适
  • 2020网易笔试编程题(一)

    题目 在一次聚会中 教授们被要求写下自己认可哪位教授的学术成果 也可以写自己 且可能出现重复 已知 如果教授A认可教授B 且教授B认可教授C 那么即可视为教授A也认可教授C 现在我们想知道多少对教授是两两互相认可的 输入举例 输入教授人数
  • oracle中replace怎么用,oraclereplace函数怎么用

    1 REPLACE函数怎么用 REPLACE 参数1 参数2 参数3 参数4 参数1 是要替换其部分字符的文本 参数2 是要用参数4替换的参数1中字符的起始位置 参数3 是希望REPLACE用参数4替换参数1中从参数2开始算起的字符个数 参
  • js 将一维数组转为二维数组并分组

    let arr a W b W01 a W b W02 a WC b WC01 a WC b WC02 a WC b WC02 a WC b WC02 let map arr forEach item gt if map item a ma
  • 理解Spring定时任务@Scheduled的两个属性fixedRate和fixedDelay

    fixedRate和fixedDelay都是表示任务执行的间隔时间 fixedRate和fixedDelay的区别 fixedDelay非常好理解 它的间隔时间是根据上次的任务结束的时候开始计时的 比如一个方法上设置了fixedDelay
  • js 手机、邮箱、身份证格式验证

  • 使用Transformer与无监督学习,OpenAI提出可迁移至多种NLP任务的通用模型

    OpenAI 最近通过一个与任务无关的可扩展系统在一系列语言任务中获得了当前最优的性能 目前他们已经发布了该系统 OpenAI 表示他们的方法主要结合了两个已存的研究 即 Transformer 和无监督预训练 实验结果提供了非常令人信服的
  • 不相交集类(并查集)

    并查集 就是只有合并和 查找操作的一种数据结构 很简单 主要判断一个元素是否在一个集合里 主要应用在最小生成树 Kruskal算法 看到图的时候会将实现代码贴上 package chapter8 类名 DisjSets 说明 实现并查集 按
  • Siddhi

    1 Siddhi是什么 Siddhi是一个开源的流处理和复杂事件处理引擎 由WSO2公司开发 它提供了一个强大而灵活的框架 用于处理实时流数据和复杂事件 官网 2 Siddhi特点和功能 Siddhi具有以下特点和功能 1 实时流处理 Si
  • TypeScript之元组、数组以及 as const

    一 元组 数组 在 TS 中 元组表示 这个数组有不同的类型 简单的一句话来表述 如果类型相同的一组数据就是数组 反之就是元组 数组的 api 对于元组来讲也是通用的 push pop等 只是类型不同 1 数组的定义 定义数组的方式 let
  • 个人喜欢的网站http://www.w3school.com.cn

    http www w3school com cn 这个网站很好 能帮助很多人
  • 用 JAVA 编写一个 M3U8 视频下载器

    总览 本文简要介绍了 M3U8 视频文件格式 并且用代码实现下载一个 M3U8 文件的视频资源 背景 前段时间在做视频真实地址解析下载时候发现很多视频网站用了 CKplayer 播放的时候传过来的参数是一个 M3U8 文件的链接 和普通的视