HLS(HTTP Live Streaming)是苹果公司主导的基于HTTP的流媒体协议。它将视频切分为数秒一个的TS文件,客户端需要轮询视频文件的索引地址,将一小段一小段的视频组装起来
HLS地址: http://hls.open.ys7.com/openlive/f01018a141094b7fa138b9d0b856507b.hd.m3u8
某一时段的响应:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-ALLOW-CACHE:NO #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:15 #EXT-TS-OFFSET-BEGIN:45 #EXTINF:3.997, http://hzhls01.ys7.com:7888/openlivedata/203751922_1_1/8883db61d09440008b06f7062cdadb31-15.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a #EXT-TS-OFFSET-BEGIN:48 #EXTINF:3.997, http://hzhls01.ys7.com:7888/openlivedata/203751922_1_1/8883db61d09440008b06f7062cdadb31-16.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a #EXT-TS-OFFSET-BEGIN:51 #EXTINF:3.998, http://hzhls01.ys7.com:7888/openlivedata/203751922_1_1/8883db61d09440008b06f7062cdadb31-17.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a
部分字段解释:
#EXT-X-TARGETDURATION:4 #EXT-TS-OFFSET-BEGIN:45 #EXTINF:3.997 http://...
注意,萤石云的HLS协议不够规范,比如视频时长用浮点数,两个视频片段的索引间隔跟单个视频时长不一致,不知是何用意。真实时长以下载到视频文件的时长为准。
Java的HLS解析库主要有以下两个:
然而,这两个库兼容性太差,也可能是因为这个HLS报文不够标准吧,总之两个库没有一个库能解析出来。所以只能手动解析了。
package bj; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.encoder.PatternLayoutEncoder; import ch.qos.logback.core.ConsoleAppender; import ch.qos.logback.core.Context; import io.vavr.control.Try; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.tuple.Triple; import org.junit.Test; import org.slf4j.LoggerFactory; import org.springframework.web.client.RestTemplate; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Created by BaiJiFeiLong@gmail.com at 2018/12/1 上午10:17 */ @Slf4j public class HlsTest { private RestTemplate restTemplate = new RestTemplate(); /** * 从HLS链接下载一个20秒的视频片段 * * @throws IOException . */ @Test public void testAlpha() throws IOException { // Logback配置日志级别为INFO ((Logger) (LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME))).setLevel(Level.INFO); // 自定义控制台日志格式 // noinspection unchecked ((ConsoleAppender) ((Logger) (LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME))).getAppender("console")).setEncoder(new PatternLayoutEncoder() { { setContext((Context) LoggerFactory.getILoggerFactory()); setPattern("[%date] %highlight([%level]) [%logger{10} %file:%line] [%thread] %msg%n"); start(); } }); // 视频分片列表 List<Triple<Integer, Integer, String>> videos = new ArrayList<>(); // HLS索引URL String playlistUrl = "http://hls.open.ys7.com/openlive/f01018a141094b7fa138b9d0b856507b.hd.m3u8"; // 当前下载的秒数 int seconds = 0; // 总共下载的秒数 int needSeconds = 20; loop: while (true) { log.info("Requesting playlist: {}", playlistUrl); // 下载索引文件 String text = restTemplate.getForObject(playlistUrl, String.class); assert text != null; /// 获取视频片段时长 Matcher targetDurationMatcher = Pattern.compile("#EXT-X-TARGETDURATION:(//d+)", Pattern.DOTALL).matcher(text); assert targetDurationMatcher.find(); int targetDuration = Integer.parseInt(targetDurationMatcher.group(1)); /// 获取视频片段 Pattern pattern = Pattern.compile("#EXT-TS-OFFSET-BEGIN:(?<offset>//d+)[/r/n]+#EXTINF:(?<duration>[//d.]+).+?(?<url>http:////S+)", Pattern.DOTALL); Matcher matcher = pattern.matcher(text); while (matcher.find()) { // 视频片段Offset int offset = Integer.parseInt(matcher.group("offset")); // 视频片段Duration int duration = Math.round(Float.parseFloat(matcher.group("duration"))); // 视频片段URL String url = matcher.group("url"); Triple<Integer, Integer, String> triple = Triple.of(offset, duration, url); // 不下载重复文件 if (!videos.contains(triple)) { log.info("Downloading: {}", url); // 下载视频片段 FileUtils.copyURLToFile(new URL(url), new File((seconds / targetDuration) + ".ts")); videos.add(triple); // 更新下载进度 seconds += targetDuration; // 下载完毕,退出循环 if (seconds >= needSeconds) { break loop; } } else { log.info("Ignored: {}", url); } } // 等待一个视频片段的时长,等待索引文件更新 log.info("Sleeping..."); Try.run(() -> Thread.sleep(targetDuration * 1000)).get(); } videos.forEach(System.out::println); } }
[2018-12-04 16:56:48,894] [INFO] [bj.HlsTest HlsTest.java:58] [main] Requesting playlist: http://hls.open.ys7.com/openlive/f01018a141094b7fa138b9d0b856507b.hd.m3u8 [2018-12-04 16:56:49,387] [INFO] [bj.HlsTest HlsTest.java:84] [main] Downloading: http://hzhls05.ys7.com:7894/openlivedata/203751922_1_1/dc6267715c6243d4a224da0903be9408-1953.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a [2018-12-04 16:56:49,821] [INFO] [bj.HlsTest HlsTest.java:84] [main] Downloading: http://hzhls05.ys7.com:7894/openlivedata/203751922_1_1/dc6267715c6243d4a224da0903be9408-1954.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a [2018-12-04 16:56:50,249] [INFO] [bj.HlsTest HlsTest.java:84] [main] Downloading: http://hzhls05.ys7.com:7894/openlivedata/203751922_1_1/dc6267715c6243d4a224da0903be9408-1955.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a [2018-12-04 16:56:50,451] [INFO] [bj.HlsTest HlsTest.java:104] [main] Sleeping... [2018-12-04 16:56:54,524] [INFO] [bj.HlsTest HlsTest.java:58] [main] Requesting playlist: http://hls.open.ys7.com/openlive/f01018a141094b7fa138b9d0b856507b.hd.m3u8 [2018-12-04 16:56:54,631] [INFO] [bj.HlsTest HlsTest.java:99] [main] Ignored: http://hzhls05.ys7.com:7894/openlivedata/203751922_1_1/dc6267715c6243d4a224da0903be9408-1955.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a [2018-12-04 16:56:54,632] [INFO] [bj.HlsTest HlsTest.java:84] [main] Downloading: http://hzhls05.ys7.com:7894/openlivedata/203751922_1_1/dc6267715c6243d4a224da0903be9408-1956.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a [2018-12-04 16:56:54,978] [INFO] [bj.HlsTest HlsTest.java:84] [main] Downloading: http://hzhls05.ys7.com:7894/openlivedata/203751922_1_1/dc6267715c6243d4a224da0903be9408-1957.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a (5859,4,http://hzhls05.ys7.com:7894/openlivedata/203751922_1_1/dc6267715c6243d4a224da0903be9408-1953.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a) (5862,4,http://hzhls05.ys7.com:7894/openlivedata/203751922_1_1/dc6267715c6243d4a224da0903be9408-1954.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a) (5865,4,http://hzhls05.ys7.com:7894/openlivedata/203751922_1_1/dc6267715c6243d4a224da0903be9408-1955.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a) (5868,4,http://hzhls05.ys7.com:7894/openlivedata/203751922_1_1/dc6267715c6243d4a224da0903be9408-1956.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a) (5871,4,http://hzhls05.ys7.com:7894/openlivedata/203751922_1_1/dc6267715c6243d4a224da0903be9408-1957.ts?Usr=c1cbc1d4e86d49a0981f54beea95280a)
文章首发: https://baijifeilong.github.io