|
@@ -1,5 +1,12 @@
|
|
package com.chuanxia.mcp.sched;
|
|
package com.chuanxia.mcp.sched;
|
|
|
|
|
|
|
|
+import cn.hutool.core.collection.CollectionUtil;
|
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
|
+import com.chuanxia.mcp.bean.model.VideoM3u8;
|
|
|
|
+import com.chuanxia.mcp.bean.model.VideoTs;
|
|
|
|
+import com.chuanxia.mcp.bean.vo.TsArrIsOrderVo;
|
|
|
|
+import com.chuanxia.mcp.exception.ResultException;
|
|
|
|
+import com.chuanxia.mcp.service.VideoM3u8Service;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
@@ -7,9 +14,13 @@ import org.springframework.http.ResponseEntity;
|
|
import org.springframework.stereotype.Component;
|
|
import org.springframework.stereotype.Component;
|
|
import org.springframework.web.client.RestTemplate;
|
|
import org.springframework.web.client.RestTemplate;
|
|
|
|
|
|
-import java.io.*;
|
|
|
|
-import java.nio.file.Files;
|
|
|
|
-import java.util.Objects;
|
|
|
|
|
|
+import java.net.URI;
|
|
|
|
+import java.net.URISyntaxException;
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
+import java.util.Base64;
|
|
|
|
+import java.util.List;
|
|
|
|
+import java.util.regex.Matcher;
|
|
|
|
+import java.util.regex.Pattern;
|
|
|
|
|
|
@Slf4j
|
|
@Slf4j
|
|
@Component
|
|
@Component
|
|
@@ -17,27 +28,329 @@ import java.util.Objects;
|
|
public class M3U8Utils {
|
|
public class M3U8Utils {
|
|
|
|
|
|
private final RestTemplate restTemplate;
|
|
private final RestTemplate restTemplate;
|
|
|
|
+ private final VideoM3u8Service m3u8Service;
|
|
|
|
|
|
@Value("${m3u8.downLoadPath}")
|
|
@Value("${m3u8.downLoadPath}")
|
|
private String downLoadPath;
|
|
private String downLoadPath;
|
|
@Value("${m3u8.tsDownLoadPath}")
|
|
@Value("${m3u8.tsDownLoadPath}")
|
|
private String tsDownLoadPath;
|
|
private String tsDownLoadPath;
|
|
|
|
+ private Pattern pattern = Pattern.compile("\\d+");
|
|
|
|
|
|
/**
|
|
/**
|
|
- * 下载m3u8文件,包含key的下载地址(如果有) 和 ts文件名
|
|
|
|
|
|
+ * 下载m3u8文件,解析ts
|
|
|
|
+ *
|
|
|
|
+ * @param videoId 视频id
|
|
|
|
+ * @return video对象
|
|
*/
|
|
*/
|
|
- public String downloadM3U8(String url, String videoId) {
|
|
|
|
- log.info("-- 开始下载 m3u8 --");
|
|
|
|
- ResponseEntity<byte[]> forEntity = restTemplate.getForEntity(url, byte[].class);
|
|
|
|
- //log.info("m3u8 内容 = {}", new String(forEntity.getBody()));
|
|
|
|
- String m3u8Path = downLoadPath + videoId + ".m3u8";
|
|
|
|
- File file = new File(m3u8Path);
|
|
|
|
|
|
+ public List<VideoM3u8> downloadM3U8(String baseUrl, String videoId) {
|
|
|
|
+ ArrayList<VideoM3u8> result = new ArrayList<>();
|
|
|
|
+ String fileName = "";
|
|
|
|
+ String uri = "";
|
|
|
|
+ String url = "";
|
|
try {
|
|
try {
|
|
- Files.write(file.toPath(), Objects.requireNonNull(forEntity.getBody(), "未获取到文件"));
|
|
|
|
- } catch (IOException e) {
|
|
|
|
- e.printStackTrace();
|
|
|
|
|
|
+ URI baseUri = new URI(baseUrl);
|
|
|
|
+ String scheme = baseUri.getScheme();
|
|
|
|
+ url = scheme + "://" + baseUri.getHost();
|
|
|
|
+ String path = baseUri.getPath();
|
|
|
|
+ uri = path.substring(0, path.lastIndexOf("/") + 1);
|
|
|
|
+ fileName = path.substring(path.lastIndexOf("/") + 1);
|
|
|
|
+ } catch (URISyntaxException e) {
|
|
|
|
+ log.info("解析客户服务器地址失败,原因:{}", e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ String m3u8Str = m3u8ToString(fileName, url + uri);
|
|
|
|
+ String resolutionRatio = "1280x720";
|
|
|
|
+ String bandwidth = "2400000";
|
|
|
|
+ String[] m3u8Arr = m3u8Str.split("\n");
|
|
|
|
+ if (m3u8Arr.length > 0) {
|
|
|
|
+ //判断当前m3U8是单码文件还是多码文件
|
|
|
|
+ String endData = m3u8Arr[m3u8Arr.length - 1];
|
|
|
|
+ log.info("当前m3u8最后结尾内容:{}", endData);
|
|
|
|
+ if (endData.endsWith("#EXT-X-ENDLIST")) {
|
|
|
|
+ //当前m3u8文件是单码文件,直接解析,入库
|
|
|
|
+ VideoM3u8 videoM3u8 = analysisM3u8(url + uri + fileName, m3u8Str, resolutionRatio, bandwidth, videoId, url, uri);
|
|
|
|
+ result.add(videoM3u8);
|
|
|
|
+ } else {
|
|
|
|
+ for (int i = 0; i < m3u8Arr.length; i++) {
|
|
|
|
+ String m3u8StreamStr = m3u8Arr[i];
|
|
|
|
+ //判断当前行是否是分辨率
|
|
|
|
+ if (m3u8StreamStr.contains("#EXT-X-STREAM-INF")) {
|
|
|
|
+ String[] split = m3u8StreamStr.split(",");
|
|
|
|
+ if (split.length >= 2) {
|
|
|
|
+ String resolutionRatioStr = split[1];
|
|
|
|
+ //bandwidth
|
|
|
|
+ if (resolutionRatioStr.contains("RESOLUTION=")) {
|
|
|
|
+ String[] resolutionArr = resolutionRatioStr.split("RESOLUTION=");
|
|
|
|
+ resolutionRatio = resolutionArr[1];
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (split[0].contains("BANDWIDTH")) {
|
|
|
|
+ bandwidth = split[0].split("BANDWIDTH=")[1];
|
|
|
|
+ }
|
|
|
|
+ //是分辨率头文件,获取下一行.m3u8地址,可能是http完整地址,可能是相对路径
|
|
|
|
+ String m3u8RadioUrl = m3u8Arr[i + 1];
|
|
|
|
+ String nextM3u8Url;
|
|
|
|
+ String nextM3u8FileName;
|
|
|
|
+ if (m3u8RadioUrl.startsWith("http")) {
|
|
|
|
+ //完整链接,直接去获取m3u8
|
|
|
|
+ nextM3u8Url = m3u8RadioUrl;
|
|
|
|
+ nextM3u8FileName = nextM3u8Url.substring(nextM3u8Url.lastIndexOf("/") + 1);
|
|
|
|
+ } else {
|
|
|
|
+ nextM3u8Url = url + uri;
|
|
|
|
+ nextM3u8FileName = m3u8RadioUrl;
|
|
|
|
+ }
|
|
|
|
+ String nextM3U8FileStr = m3u8ToString(nextM3u8FileName, nextM3u8Url);
|
|
|
|
+ VideoM3u8 videoM3u8 = analysisM3u8(nextM3u8Url + nextM3u8FileName, nextM3U8FileStr, resolutionRatio, bandwidth, videoId, url, uri);
|
|
|
|
+ result.add(videoM3u8);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 获取m3u8文件内容
|
|
|
|
+ *
|
|
|
|
+ * @param fileName 文件名
|
|
|
|
+ * @param url 地址
|
|
|
|
+ * @return 文件内容
|
|
|
|
+ */
|
|
|
|
+ private String m3u8ToString(String fileName, String url) {
|
|
|
|
+ StringBuilder fileUrl = new StringBuilder();
|
|
|
|
+ fileUrl.append(url);
|
|
|
|
+ fileUrl.append(fileName);
|
|
|
|
+ log.info("开始去下载m3u8文件,当前url:{},文件名:{}", fileUrl.toString(), fileName);
|
|
|
|
+ ResponseEntity<byte[]> forEntity = restTemplate.getForEntity(fileUrl.toString(), byte[].class);
|
|
|
|
+ if (forEntity.getBody() == null) {
|
|
|
|
+ throw new ResultException(500, "没有获取到对应m3u8文件");
|
|
}
|
|
}
|
|
return new String(forEntity.getBody());
|
|
return new String(forEntity.getBody());
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ * 解析当前m3u8文件
|
|
|
|
+ *
|
|
|
|
+ * @param m3u8Url m3u8文件下载地址
|
|
|
|
+ * @param m3u8Content 文件内容
|
|
|
|
+ * @param resolutionRatio 分辨率
|
|
|
|
+ * @param bandwidth 码率
|
|
|
|
+ * @param videoId videoId
|
|
|
|
+ * @param url 请求域名
|
|
|
|
+ * @param uri 相对路径
|
|
|
|
+ * @return m3u8对象
|
|
|
|
+ */
|
|
|
|
+ private VideoM3u8 analysisM3u8(String m3u8Url, String m3u8Content, String resolutionRatio, String bandwidth, String videoId, String url, String uri) {
|
|
|
|
+ //开始解析m3u8文件
|
|
|
|
+ String[] split = m3u8Content.split("\n");
|
|
|
|
+ VideoM3u8 m3u8 = new VideoM3u8();
|
|
|
|
+ m3u8.setVideoId(videoId);
|
|
|
|
+ m3u8.setBandwidth(bandwidth);
|
|
|
|
+ m3u8.setResolution(resolutionRatio);
|
|
|
|
+ m3u8.setSEQUENCE("0");
|
|
|
|
+
|
|
|
|
+ //开始封装组合数据入库
|
|
|
|
+ int i = 0;
|
|
|
|
+ List<VideoTs> tsList = new ArrayList<>(split.length);
|
|
|
|
+ for (String data : split) {
|
|
|
|
+ if (data.contains("#EXT-X-VERSION")) {
|
|
|
|
+ //版本
|
|
|
|
+ m3u8.setVERSION(data.split(":")[1]);
|
|
|
|
+ } else if (data.contains("#EXT-X-TARGETDURATION")) {
|
|
|
|
+ //分片最大时长
|
|
|
|
+ m3u8.setTARGETDURATION(data.split(":")[1]);
|
|
|
|
+ } else if (data.contains("#EXT-X-MEDIA-SEQUENCE")) {
|
|
|
|
+ //播放最开始下标
|
|
|
|
+ m3u8.setSEQUENCE(data.split(":")[1]);
|
|
|
|
+ } else if (data.contains("#EXT-X-PLAYLIST-TYPE:")) {
|
|
|
|
+ //播放类型
|
|
|
|
+ m3u8.setTYPE(data.split(":")[1]);
|
|
|
|
+ } else if (data.contains("#EXT-X-KEY:METHOD")) {
|
|
|
|
+ if (data.contains("URI")) {
|
|
|
|
+ for (String s : data.split(",")) {
|
|
|
|
+ if (s.contains("URI=")) {
|
|
|
|
+ String keyUrl = s.split("URI=")[1].replaceAll("\"", "");
|
|
|
|
+ if (keyUrl.contains("http")) {
|
|
|
|
+ ResponseEntity<byte[]> forEntity = restTemplate.getForEntity(keyUrl, byte[].class);
|
|
|
|
+ String key = Base64.getEncoder().encodeToString(forEntity.getBody());
|
|
|
|
+ m3u8.setKEY(key);
|
|
|
|
+ } else {
|
|
|
|
+ //如果key是相对路径,就拼接地址,获取key地址
|
|
|
|
+ ResponseEntity<byte[]> forEntity = restTemplate.getForEntity(url + uri + keyUrl, byte[].class);
|
|
|
|
+ String key = Base64.getEncoder().encodeToString(forEntity.getBody());
|
|
|
|
+ m3u8.setKEY(key);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ //IV,key
|
|
|
|
+ m3u8.setIV(data.split("IV=")[1]);
|
|
|
|
+ } else if (data.contains("#EXTINF")) {
|
|
|
|
+ //分片时间
|
|
|
|
+ String extinf = data.split(":")[1].replaceAll(",", "");
|
|
|
|
+ //ts
|
|
|
|
+ String tsName = split[i + 1];
|
|
|
|
+ VideoTs videoTs = new VideoTs();
|
|
|
|
+ videoTs.setTime(extinf);
|
|
|
|
+ videoTs.setName(tsName);
|
|
|
|
+ tsList.add(videoTs);
|
|
|
|
+ }
|
|
|
|
+ i++;
|
|
|
|
+ }
|
|
|
|
+ if (CollectionUtil.isNotEmpty(tsList)) {
|
|
|
|
+ int listSize = 2;
|
|
|
|
+ TsArrIsOrderVo orderVo = checkIsOrder(tsList);
|
|
|
|
+ //判断当前ts是绝对路径还是相对路径,如果是绝对路径,则不再设置path
|
|
|
|
+ VideoTs videoTs = tsList.get(0);
|
|
|
|
+ String name = videoTs.getName();
|
|
|
|
+ if (!name.contains("http")) {
|
|
|
|
+ try {
|
|
|
|
+ URI m3u8Uri = new URI(m3u8Url);
|
|
|
|
+ String path = m3u8Uri.getPath();
|
|
|
|
+ path = path.substring(0, path.lastIndexOf("/") + 1);
|
|
|
|
+ m3u8.setPREFIX_PATH(path);
|
|
|
|
+ } catch (URISyntaxException e) {
|
|
|
|
+ log.error("获取当前m3u8所在相对路径失败,原因:{}", e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (tsList.size() >= listSize) {
|
|
|
|
+ if (orderVo.isOrder()) {
|
|
|
|
+ //是自然排序,设置值
|
|
|
|
+ m3u8.setTS_START(orderVo.getTsStart());
|
|
|
|
+ m3u8.setTS_END(orderVo.getTsEnd());
|
|
|
|
+ m3u8.setEXTINF(orderVo.getExtinf());
|
|
|
|
+ m3u8.setTS_LAST_TIME(orderVo.getTsLastTime());
|
|
|
|
+ m3u8.setTS_NAME(orderVo.getTsName());
|
|
|
|
+ } else {
|
|
|
|
+ //不是自然排序,设置arr
|
|
|
|
+ m3u8.setTsListStr(JSON.toJSONString(orderVo.getVideoTs()));
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ //ts数组长度小于2,直接设置入库
|
|
|
|
+ m3u8.setTsListStr(JSON.toJSONString(tsList));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ return m3u8;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 比较两个ts文件是否按照自然排序规则
|
|
|
|
+ *
|
|
|
|
+ * @param tsList ts数组
|
|
|
|
+ * @return true表示按规则排序, false表示无规则
|
|
|
|
+ */
|
|
|
|
+ private TsArrIsOrderVo checkIsOrder(List<VideoTs> tsList) {
|
|
|
|
+
|
|
|
|
+ VideoTs first = tsList.get(0);
|
|
|
|
+ VideoTs last = tsList.get(tsList.size() - 1);
|
|
|
|
+ String firstName = first.getName();
|
|
|
|
+ firstName = firstName.split("\\.")[0];
|
|
|
|
+ String lastName = last.getName();
|
|
|
|
+ lastName = lastName.split("\\.")[0];
|
|
|
|
+
|
|
|
|
+ TsArrIsOrderVo orderVo = new TsArrIsOrderVo();
|
|
|
|
+ //判断里面每一个ts时间是否固定(去除最后一条)
|
|
|
|
+
|
|
|
|
+ //每循环一次,用第一次的时间跟下一次时间做比较
|
|
|
|
+ String lastExtinfTime = "";
|
|
|
|
+ for (int i = 0; i < tsList.size() - 1; i++) {
|
|
|
|
+ VideoTs videoTs = tsList.get(i);
|
|
|
|
+ if (i == 0) {
|
|
|
|
+ lastExtinfTime = videoTs.getTime();
|
|
|
|
+ } else {
|
|
|
|
+ if (!lastExtinfTime.equals(videoTs.getTime())) {
|
|
|
|
+ orderVo.setOrder(false);
|
|
|
|
+ orderVo.setVideoTs(tsList);
|
|
|
|
+ return orderVo;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ int size = tsList.size();
|
|
|
|
+ String numberMatcher = "[0-9]+";
|
|
|
|
+ //开头数字,结尾字符串,直接false
|
|
|
|
+ if (firstName.matches(numberMatcher) && !lastName.matches(numberMatcher)) {
|
|
|
|
+ orderVo.setOrder(false);
|
|
|
|
+ orderVo.setVideoTs(tsList);
|
|
|
|
+ return orderVo;
|
|
|
|
+ }
|
|
|
|
+ //开头字符串,结尾数字,直接false
|
|
|
|
+ if (!firstName.matches(numberMatcher) && lastName.matches(numberMatcher)) {
|
|
|
|
+ orderVo.setOrder(false);
|
|
|
|
+ orderVo.setVideoTs(tsList);
|
|
|
|
+ return orderVo;
|
|
|
|
+ }
|
|
|
|
+ //两边都是数字,看看是不是自然排序
|
|
|
|
+ if (firstName.matches(numberMatcher) && lastName.matches(numberMatcher)) {
|
|
|
|
+ //长度+首数-1=尾数
|
|
|
|
+
|
|
|
|
+ if (Long.parseLong(firstName) + size - 1 != Long.parseLong(lastName)) {
|
|
|
|
+ orderVo.setOrder(false);
|
|
|
|
+ orderVo.setVideoTs(tsList);
|
|
|
|
+ return orderVo;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ // 匹配一个或多个数字
|
|
|
|
+ Matcher firstMatcher = pattern.matcher(firstName);
|
|
|
|
+ Matcher lastMatcher = pattern.matcher(lastName);
|
|
|
|
+ ArrayList<String> firstArr = new ArrayList<>();
|
|
|
|
+ ArrayList<String> lastArr = new ArrayList<>();
|
|
|
|
+ while (firstMatcher.find()) {
|
|
|
|
+ String number = firstMatcher.group();
|
|
|
|
+ firstArr.add(number);
|
|
|
|
+ }
|
|
|
|
+ while (lastMatcher.find()) {
|
|
|
|
+ String number = lastMatcher.group();
|
|
|
|
+ lastArr.add(number);
|
|
|
|
+ }
|
|
|
|
+ if (firstArr.size() != lastArr.size()) {
|
|
|
|
+ //数字切割分组长度不一致,直接false
|
|
|
|
+ orderVo.setOrder(false);
|
|
|
|
+ orderVo.setVideoTs(tsList);
|
|
|
|
+ return orderVo;
|
|
|
|
+ } else {
|
|
|
|
+ //长度一致,判断数字是不是自然排序
|
|
|
|
+ String firstNum = "";
|
|
|
|
+ String lastNum = "";
|
|
|
|
+ int count = 0;
|
|
|
|
+ //记录一下不一致的下标
|
|
|
|
+ int inconsistentIndex = 0;
|
|
|
|
+ for (int i = 0; i < firstArr.size(); i++) {
|
|
|
|
+ if (!firstArr.get(i).equals(lastArr.get(i))) {
|
|
|
|
+ count++;
|
|
|
|
+ firstNum = firstArr.get(i);
|
|
|
|
+ lastNum = lastArr.get(i);
|
|
|
|
+ inconsistentIndex = i;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (count > 1) {
|
|
|
|
+ //出现不相等的数字次数大于1
|
|
|
|
+ orderVo.setOrder(false);
|
|
|
|
+ orderVo.setVideoTs(tsList);
|
|
|
|
+ return orderVo;
|
|
|
|
+ }
|
|
|
|
+ String tsName = firstName.substring(0, firstName.indexOf(firstNum, inconsistentIndex));
|
|
|
|
+ String substring = tsName + (Integer.parseInt(firstNum) + size - 1);
|
|
|
|
+ lastName = lastName.substring(0, lastName.indexOf(lastNum, inconsistentIndex)) + (Integer.parseInt(lastNum));
|
|
|
|
+ if (substring.equals(lastName)) {
|
|
|
|
+ tsName = tsName + "{num}.ts";
|
|
|
|
+ //规则匹配
|
|
|
|
+ orderVo.setOrder(true);
|
|
|
|
+ //分片时间
|
|
|
|
+ orderVo.setExtinf(lastExtinfTime);
|
|
|
|
+ //最后一次分片时间
|
|
|
|
+ orderVo.setTsLastTime(last.getTime());
|
|
|
|
+ orderVo.setTsStart(firstNum);
|
|
|
|
+ orderVo.setTsEnd(lastNum);
|
|
|
|
+ orderVo.setTsName(tsName);
|
|
|
|
+ } else {
|
|
|
|
+ orderVo.setOrder(false);
|
|
|
|
+ orderVo.setVideoTs(tsList);
|
|
|
|
+ return orderVo;
|
|
|
|
+ }
|
|
|
|
+ return orderVo;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
}
|
|
}
|