用 JAVA 编写一个 M3U8 视频下载器「建议收藏」

用 JAVA 编写一个 M3U8 视频下载器「建议收藏」本文简要介绍了M3U8视频文件格式,并且用代码实现下载一个M3U8文件的视频资源。

大家好,欢迎来到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 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);
		}
	}
}

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

(0)
上一篇 2023-02-27 15:00
下一篇 2023-02-28 18:00

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信