@SneakyThrows //合并操作,最终文件不包含结束标识,方便多次合并 private static void mergeM3U8File(String source, String target) { //读取target List<String> sl = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new FileReader(source))) { String line; while ((line = reader.readLine()) != null) { sl.add(line); } } //读取source List<String> tl = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new FileReader(target))) { String line; while ((line = reader.readLine()) != null) { tl.add(line); } } //合并且统一ts文件名 String filename = target.replace(dir,""); filename = filename.replace(".m3u8",""); Long order = 0l; if (tl.size() <= 0) { for (String s : sl) { if(s.startsWith("#")) tl.add(s); else { tl.add(filename + order + ".ts"); log.info("转存文件 {}{} 存在状态 {}", dir, s, Files.exists(Paths.get(dir + s))); Files.copy(Paths.get(dir + s), Paths.get(dir + filename + order + ".ts")); Files.delete(Paths.get(dir + s)); order++; } } } else { //删除结束段落 if(tl.get(tl.size()-1).startsWith("#EXT-X-ENDLIST")) { tl.remove(tl.size() - 1); } //获取文件序号(dir+filename+order.ts) String s = tl.get(tl.size() - 1); s = s.replace(".ts",""); s = s.replace(filename,""); order = Long.parseLong(s); //sl处理 获取头部标识的下标并移除头部 int i = 0; for (i = 0; i < sl.size(); i++) { if (sl.get(i).startsWith("#EXTINF")) break; } for (int j = 0; j < i; j++) { sl.remove(0); } sl.remove(sl.size() - 1); tl.add("#EXT-X-DISCONTINUITY"); //sl文件内容写入tl for (int j = 0; j < sl.size(); j++) { String s1 = sl.get(j); if(s1.startsWith("#EXTINF")) //#EXTINF tl.add(s1); else//按规则写入文件索引 { order++; tl.add(filename + order + ".ts"); log.info("转存文件 {}{} 存在状态 {}",dir,s1,Files.exists(Paths.get(dir + s1))); Files.copy(Paths.get(dir + s1), Paths.get(dir + filename + order + ".ts")); Files.delete(Paths.get(dir + s1)); } } } //生成新的buffer StringBuffer buffer = new StringBuffer(); for (String t : tl) { buffer.append(t).append(System.lineSeparator()); } //写入文件 try (BufferedWriter writer = new BufferedWriter(new FileWriter(target))) { writer.write(buffer.toString()); } //清除source Files.delete(Paths.get(source)); } @SneakyThrows //给m3u8文件添加结束标识 private static void endM3u8(String m3u8) { List<String> sl = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new FileReader(m3u8))) { String line; while ((line = reader.readLine()) != null) { sl.add(line); } } sl.add("#EXT-X-ENDLIST"); //生成新的buffer StringBuffer buffer = new StringBuffer(); for (String t : sl) { buffer.append(t).append(System.lineSeparator()); } //写入文件 try (BufferedWriter writer = new BufferedWriter(new FileWriter(m3u8))) { writer.write(buffer.toString()); } }
关于m3u8文件的说明:
- #EXTM3U:文件头,标识这是一个M3U8文件。
- #EXT-X-VERSION:表示M3U8的版本号。
- #EXT-X-TARGETDURATION:表示每个分段的最长时间(以秒为单位)。
- #EXT-X-MEDIA-SEQUENCE:表示播放列表中的第一个分段的编号。
- #EXTINF:表示当前分段的播放时间长度(以秒为单位)和URI。
- #EXT-X-ENDLIST:表示播放列表结束。
- #EXT-X-STREAM-INF:表示变换码率视频流中的音视频属性。
- #EXT-X-DISCONTINUITY:表示两个分段之间的不连续性。
- #EXT-X-PROGRAM-DATE-TIME:表示当前分段的播放时间点。
- #EXT-X-BYTERANGE:表示分段的字节范围。
合并代码中的m3u8文件的格式:
* #EXTM3U
* #EXT-X-VERSION:3
* #EXT-X-TARGETDURATION:2
* #EXT-X-MEDIA-SEQUENCE:0
* #EXT-X-PLAYLIST-TYPE:EVENT
* #EXTINF:1.441000,
* 2052636967-12640237988249000.ts
* #EXTINF:1.848000,
* 2052636967-12640237988249001.ts
* #EXT-X-ENDLIST
关于代码中的一些说明:
1、由于m3u8文件中的ts文件索引规则一般是 m3u8文件名+索引号,因此在合并文件之前需要先统一索引文件名;
2、由于m3u8文件之间是相互独立的,所以要在写入外部m3u8索引文件之前加入 #EXT-X-DISCONTINUITY 标识,否则播放器会在播放完第一部分的m3u8文件之后停止播放;
3、关于m3u8文件的结束行 #EXT-X-ENDLIST 问题,大部分播放器在未识别到结束标识且已经读取到文件最后一行索引时,会不断的请求m3u8文件,获取新资源,可以通过这个特新来实现直播效果;
合并后的文件内容:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:EVENT
# EXTINF: 1.441000.
2052636967-12640237988249000.ts
# EXTINF: 1.848000.
2052636967-12640237988249001.ts
#EXT-X-DISCONTINUITY
# EXTINF: 1.741000.
2052636967-12640237988249002.ts
# EXTINF: 1.148000.
2052636967-12640237988249003.ts
#EXT-X-ENDLIST