白季飞龙的个人主页

HLS大杂烩

HLS是什么

HLS(HTTP Live Streaming)是苹果公司主导的基于HTTP的流媒体协议。它将视频切分为数秒一个的TS文件,客户端需要轮询视频文件的索引地址,将一小段一小段的视频组装起来

HLS索引文件示例

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

部分字段解释:

注意,萤石云的HLS协议不够规范,比如视频时长用浮点数,两个视频片段的索引间隔跟单个视频时长不一致,不知是何用意。真实时长以下载到视频文件的时长为准。

用Java解析HLS

Java的HLS解析库主要有以下两个:

  1. hlsparserj
  2. open-m3u8

然而,这两个库兼容性太差,也可能是因为这个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


漫漫路,莫论逍遥;潜心修,只为悟道