视频
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
package com.hotwj.platform.video.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频分片上传与转码配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "video.upload")
|
||||||
|
public class VideoUploadProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作目录,包含分片、源文件和 HLS 输出目录。
|
||||||
|
*/
|
||||||
|
private String workDir = System.getProperty("java.io.tmpdir") + File.separator + "hot-video-upload";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对外访问的 HLS 路径前缀。
|
||||||
|
*/
|
||||||
|
private String publicUrlPrefix = "/video-hls";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对外访问的基础域名,可为空。
|
||||||
|
*/
|
||||||
|
private String publicBaseUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FFmpeg 可执行命令。
|
||||||
|
*/
|
||||||
|
private String ffmpegCommand = "ffmpeg";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分片状态保留时长(小时)。
|
||||||
|
*/
|
||||||
|
private Integer chunkExpireHours = 24;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务状态保留时长(小时)。
|
||||||
|
*/
|
||||||
|
private Integer taskExpireHours = 24;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HLS 切片时长(秒)。
|
||||||
|
*/
|
||||||
|
private Integer hlsSegmentSeconds = 10;
|
||||||
|
|
||||||
|
public Duration chunkExpireDuration() {
|
||||||
|
return Duration.ofHours(chunkExpireHours == null ? 24L : chunkExpireHours.longValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration taskExpireDuration() {
|
||||||
|
return Duration.ofHours(taskExpireHours == null ? 24L : taskExpireHours.longValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String normalizedPublicUrlPrefix() {
|
||||||
|
if (publicUrlPrefix == null || publicUrlPrefix.isBlank()) {
|
||||||
|
return "/video-hls";
|
||||||
|
}
|
||||||
|
String value = publicUrlPrefix.startsWith("/") ? publicUrlPrefix : "/" + publicUrlPrefix;
|
||||||
|
return value.endsWith("/") ? value.substring(0, value.length() - 1) : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.hotwj.platform.video.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaIgnore;
|
||||||
|
import com.hotwj.platform.video.domain.bo.VideoChunkUploadBo;
|
||||||
|
import com.hotwj.platform.video.domain.bo.VideoMergeBo;
|
||||||
|
import com.hotwj.platform.video.domain.vo.VideoTranscodeTaskInfo;
|
||||||
|
import com.hotwj.platform.video.service.VideoChunkUploadService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.dromara.common.core.domain.R;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频分片上传与转码接口
|
||||||
|
*/
|
||||||
|
@SaIgnore
|
||||||
|
@Validated
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Tag(name = "视频分片上传", description = "分片上传、合并和转码任务")
|
||||||
|
public class VideoChunkUploadController {
|
||||||
|
|
||||||
|
private final VideoChunkUploadService videoChunkUploadService;
|
||||||
|
|
||||||
|
@GetMapping("/api/check")
|
||||||
|
@Operation(summary = "检查视频分片上传状态")
|
||||||
|
public R<List<Integer>> check(@RequestParam String fileHash) {
|
||||||
|
return R.ok(videoChunkUploadService.checkUploadedChunks(fileHash));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/api/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
@Operation(summary = "上传视频分片")
|
||||||
|
public R<Void> upload(@Valid VideoChunkUploadBo bo) {
|
||||||
|
videoChunkUploadService.uploadChunk(bo);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/api/merge")
|
||||||
|
@Operation(summary = "合并视频分片并创建转码任务")
|
||||||
|
public R<Map<String, String>> merge(@Valid @RequestBody VideoMergeBo bo) {
|
||||||
|
return R.ok(videoChunkUploadService.mergeAndCreateTask(bo));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/api/task/{taskId}")
|
||||||
|
@Operation(summary = "查询视频转码任务状态")
|
||||||
|
public R<VideoTranscodeTaskInfo> task(@PathVariable String taskId) {
|
||||||
|
return R.ok(videoChunkUploadService.getTaskInfo(taskId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.hotwj.platform.video.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaIgnore;
|
||||||
|
import com.hotwj.platform.video.config.VideoUploadProperties;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.MediaTypeFactory;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地 HLS 资源公开访问接口
|
||||||
|
*/
|
||||||
|
@SaIgnore
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VideoHlsController {
|
||||||
|
|
||||||
|
private final VideoUploadProperties properties;
|
||||||
|
|
||||||
|
@GetMapping("${video.upload.public-url-prefix:/video-hls}/{fileHash}/{fileName:.+}")
|
||||||
|
public void stream(@PathVariable String fileHash, @PathVariable String fileName, HttpServletResponse response) throws IOException {
|
||||||
|
File file = new File(properties.getWorkDir(),
|
||||||
|
"hls" + File.separator + fileHash + File.separator + fileName);
|
||||||
|
if (!file.exists() || !file.isFile()) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fileName.contains("..")) {
|
||||||
|
throw new ServiceException("非法文件路径");
|
||||||
|
}
|
||||||
|
MediaType mediaType = MediaTypeFactory.getMediaType(file.getName()).orElse(MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
response.setContentType(mediaType.toString());
|
||||||
|
response.setHeader("Cache-Control", "public, max-age=60");
|
||||||
|
FileCopyUtils.copy(new FileSystemResource(file).getInputStream(), response.getOutputStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.hotwj.platform.video.domain.bo;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频分片上传参数
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class VideoChunkUploadBo {
|
||||||
|
|
||||||
|
@NotNull(message = "分片文件不能为空")
|
||||||
|
private MultipartFile chunk;
|
||||||
|
|
||||||
|
@NotBlank(message = "文件哈希不能为空")
|
||||||
|
private String fileHash;
|
||||||
|
|
||||||
|
@NotNull(message = "分片索引不能为空")
|
||||||
|
private Integer chunkIndex;
|
||||||
|
|
||||||
|
@NotNull(message = "总分片数不能为空")
|
||||||
|
private Integer totalChunks;
|
||||||
|
|
||||||
|
@NotBlank(message = "文件名不能为空")
|
||||||
|
private String fileName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.hotwj.platform.video.domain.bo;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频分片合并参数
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class VideoMergeBo {
|
||||||
|
|
||||||
|
@NotBlank(message = "文件哈希不能为空")
|
||||||
|
private String fileHash;
|
||||||
|
|
||||||
|
@NotBlank(message = "文件名不能为空")
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
@NotNull(message = "总分片数不能为空")
|
||||||
|
private Integer totalChunks;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.hotwj.platform.video.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频转码任务状态
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public class VideoTranscodeTaskInfo implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private String taskId;
|
||||||
|
|
||||||
|
private String fileHash;
|
||||||
|
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* waiting / processing / completed / failed
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
private String m3u8Url;
|
||||||
|
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.hotwj.platform.video.service;
|
||||||
|
|
||||||
|
import com.hotwj.platform.video.domain.bo.VideoChunkUploadBo;
|
||||||
|
import com.hotwj.platform.video.domain.bo.VideoMergeBo;
|
||||||
|
import com.hotwj.platform.video.domain.vo.VideoTranscodeTaskInfo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface VideoChunkUploadService {
|
||||||
|
|
||||||
|
List<Integer> checkUploadedChunks(String fileHash);
|
||||||
|
|
||||||
|
void uploadChunk(VideoChunkUploadBo bo);
|
||||||
|
|
||||||
|
Map<String, String> mergeAndCreateTask(VideoMergeBo bo);
|
||||||
|
|
||||||
|
VideoTranscodeTaskInfo getTaskInfo(String taskId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package com.hotwj.platform.video.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
|
import com.hotwj.platform.video.config.VideoUploadProperties;
|
||||||
|
import com.hotwj.platform.video.domain.bo.VideoChunkUploadBo;
|
||||||
|
import com.hotwj.platform.video.domain.bo.VideoMergeBo;
|
||||||
|
import com.hotwj.platform.video.domain.vo.VideoTranscodeTaskInfo;
|
||||||
|
import com.hotwj.platform.video.service.VideoChunkUploadService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
|
import org.redisson.api.RSet;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频分片上传、合并与异步转码服务
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VideoChunkUploadServiceImpl implements VideoChunkUploadService {
|
||||||
|
|
||||||
|
private static final String CHUNK_KEY_PREFIX = "video:chunk:";
|
||||||
|
private static final String TASK_KEY_PREFIX = "video:task:";
|
||||||
|
|
||||||
|
private final VideoUploadProperties properties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> checkUploadedChunks(String fileHash) {
|
||||||
|
RSet<Integer> rSet = RedisUtils.getClient().getSet(CHUNK_KEY_PREFIX + fileHash);
|
||||||
|
Set<Integer> uploaded = rSet.readAll();
|
||||||
|
List<Integer> result = new ArrayList<>(uploaded);
|
||||||
|
result.sort(Integer::compareTo);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void uploadChunk(VideoChunkUploadBo bo) {
|
||||||
|
validateChunkRequest(bo);
|
||||||
|
File chunkDir = getChunkDir(bo.getFileHash());
|
||||||
|
FileUtil.mkdir(chunkDir);
|
||||||
|
File chunkFile = new File(chunkDir, bo.getChunkIndex().toString());
|
||||||
|
try {
|
||||||
|
bo.getChunk().transferTo(chunkFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ServiceException("分片上传失败");
|
||||||
|
}
|
||||||
|
RSet<Integer> uploadedSet = RedisUtils.getClient().getSet(CHUNK_KEY_PREFIX + bo.getFileHash());
|
||||||
|
uploadedSet.add(bo.getChunkIndex());
|
||||||
|
uploadedSet.expire(properties.chunkExpireDuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> mergeAndCreateTask(VideoMergeBo bo) {
|
||||||
|
validateMergeRequest(bo);
|
||||||
|
RSet<Integer> uploadedSet = RedisUtils.getClient().getSet(CHUNK_KEY_PREFIX + bo.getFileHash());
|
||||||
|
Set<Integer> uploaded = uploadedSet.readAll();
|
||||||
|
for (int i = 0; i < bo.getTotalChunks(); i++) {
|
||||||
|
if (!uploaded.contains(i)) {
|
||||||
|
throw new ServiceException("存在未上传完成的分片,无法合并");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File sourceDir = getSourceDir(bo.getFileHash());
|
||||||
|
FileUtil.mkdir(sourceDir);
|
||||||
|
File mergedFile = new File(sourceDir, sanitizeFileName(bo.getFileName()));
|
||||||
|
mergeChunksToFile(bo.getFileHash(), bo.getTotalChunks(), mergedFile.toPath());
|
||||||
|
|
||||||
|
String taskId = "task_" + IdUtil.fastSimpleUUID();
|
||||||
|
VideoTranscodeTaskInfo taskInfo = new VideoTranscodeTaskInfo()
|
||||||
|
.setTaskId(taskId)
|
||||||
|
.setFileHash(bo.getFileHash())
|
||||||
|
.setFileName(bo.getFileName())
|
||||||
|
.setStatus("waiting")
|
||||||
|
.setCreateTime(LocalDateTime.now())
|
||||||
|
.setUpdateTime(LocalDateTime.now());
|
||||||
|
saveTask(taskInfo);
|
||||||
|
SpringUtils.getAopProxy(this).startTranscodeAsync(taskId, bo.getFileHash(), mergedFile.toPath());
|
||||||
|
return Collections.singletonMap("taskId", taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VideoTranscodeTaskInfo getTaskInfo(String taskId) {
|
||||||
|
VideoTranscodeTaskInfo taskInfo = RedisUtils.getCacheObject(TASK_KEY_PREFIX + taskId);
|
||||||
|
if (taskInfo == null) {
|
||||||
|
throw new ServiceException("任务不存在或已过期");
|
||||||
|
}
|
||||||
|
return taskInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void startTranscodeAsync(String taskId, String fileHash, Path sourceFile) {
|
||||||
|
VideoTranscodeTaskInfo taskInfo = getTaskInfo(taskId);
|
||||||
|
try {
|
||||||
|
taskInfo.setStatus("processing").setUpdateTime(LocalDateTime.now()).setErrorMessage(null);
|
||||||
|
saveTask(taskInfo);
|
||||||
|
|
||||||
|
Path outputDir = getHlsDir(fileHash).toPath();
|
||||||
|
FileUtil.mkdir(outputDir.toFile());
|
||||||
|
cleanDirectory(outputDir);
|
||||||
|
|
||||||
|
Path playlistPath = outputDir.resolve("index.m3u8");
|
||||||
|
runFfmpeg(sourceFile, outputDir, playlistPath);
|
||||||
|
|
||||||
|
taskInfo.setStatus("completed")
|
||||||
|
.setM3u8Url(buildPublicM3u8Url(fileHash))
|
||||||
|
.setUpdateTime(LocalDateTime.now())
|
||||||
|
.setErrorMessage(null);
|
||||||
|
saveTask(taskInfo);
|
||||||
|
|
||||||
|
FileUtil.del(getChunkDir(fileHash));
|
||||||
|
log.info("视频转码完成: taskId={}, fileHash={}, playlist={}", taskId, fileHash, playlistPath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("视频转码失败: taskId={}, fileHash={}, msg={}", taskId, fileHash, e.getMessage(), e);
|
||||||
|
taskInfo.setStatus("failed")
|
||||||
|
.setErrorMessage(StringUtils.substring(e.getMessage(), 0, 500))
|
||||||
|
.setUpdateTime(LocalDateTime.now());
|
||||||
|
saveTask(taskInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateChunkRequest(VideoChunkUploadBo bo) {
|
||||||
|
if (bo.getChunkIndex() < 0) {
|
||||||
|
throw new ServiceException("分片索引不能小于0");
|
||||||
|
}
|
||||||
|
if (bo.getTotalChunks() <= 0) {
|
||||||
|
throw new ServiceException("总分片数必须大于0");
|
||||||
|
}
|
||||||
|
if (bo.getChunkIndex() >= bo.getTotalChunks()) {
|
||||||
|
throw new ServiceException("分片索引超出范围");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateMergeRequest(VideoMergeBo bo) {
|
||||||
|
if (bo.getTotalChunks() <= 0) {
|
||||||
|
throw new ServiceException("总分片数必须大于0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeChunksToFile(String fileHash, int totalChunks, Path targetFile) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(targetFile);
|
||||||
|
Files.createDirectories(targetFile.getParent());
|
||||||
|
try (OutputStream outputStream = Files.newOutputStream(targetFile)) {
|
||||||
|
for (int i = 0; i < totalChunks; i++) {
|
||||||
|
Path chunkPath = getChunkDir(fileHash).toPath().resolve(String.valueOf(i));
|
||||||
|
if (!Files.exists(chunkPath)) {
|
||||||
|
throw new ServiceException("缺少分片: " + i);
|
||||||
|
}
|
||||||
|
try (InputStream inputStream = Files.newInputStream(chunkPath)) {
|
||||||
|
inputStream.transferTo(outputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ServiceException("合并分片失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runFfmpeg(Path sourceFile, Path outputDir, Path playlistPath) throws IOException, InterruptedException {
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add(properties.getFfmpegCommand());
|
||||||
|
command.add("-y");
|
||||||
|
command.add("-i");
|
||||||
|
command.add(sourceFile.toAbsolutePath().toString());
|
||||||
|
command.add("-profile:v");
|
||||||
|
command.add("baseline");
|
||||||
|
command.add("-level");
|
||||||
|
command.add("3.0");
|
||||||
|
command.add("-start_number");
|
||||||
|
command.add("0");
|
||||||
|
command.add("-hls_time");
|
||||||
|
command.add(String.valueOf(properties.getHlsSegmentSeconds() == null ? 10 : properties.getHlsSegmentSeconds()));
|
||||||
|
command.add("-hls_list_size");
|
||||||
|
command.add("0");
|
||||||
|
command.add("-hls_segment_filename");
|
||||||
|
command.add(outputDir.resolve("segment_%05d.ts").toAbsolutePath().toString());
|
||||||
|
command.add("-f");
|
||||||
|
command.add("hls");
|
||||||
|
command.add(playlistPath.toAbsolutePath().toString());
|
||||||
|
|
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||||
|
processBuilder.redirectErrorStream(true);
|
||||||
|
Process process = processBuilder.start();
|
||||||
|
String output;
|
||||||
|
try (InputStream inputStream = process.getInputStream()) {
|
||||||
|
output = new String(inputStream.readAllBytes());
|
||||||
|
}
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
if (exitCode != 0 || !Files.exists(playlistPath)) {
|
||||||
|
throw new ServiceException("FFmpeg 转码失败: " + abbreviate(output, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveTask(VideoTranscodeTaskInfo taskInfo) {
|
||||||
|
RedisUtils.setCacheObject(TASK_KEY_PREFIX + taskInfo.getTaskId(), taskInfo, properties.taskExpireDuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildPublicM3u8Url(String fileHash) {
|
||||||
|
String relativePath = properties.normalizedPublicUrlPrefix() + "/" + fileHash + "/index.m3u8";
|
||||||
|
if (StringUtils.isBlank(properties.getPublicBaseUrl())) {
|
||||||
|
return relativePath;
|
||||||
|
}
|
||||||
|
String baseUrl = properties.getPublicBaseUrl().trim();
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
|
||||||
|
}
|
||||||
|
return baseUrl + relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanDirectory(Path directory) throws IOException {
|
||||||
|
if (!Files.exists(directory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (var stream = Files.list(directory)) {
|
||||||
|
stream.forEach(path -> FileUtil.del(path.toFile()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getChunkDir(String fileHash) {
|
||||||
|
return new File(properties.getWorkDir(), "chunks" + File.separator + fileHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getSourceDir(String fileHash) {
|
||||||
|
return new File(properties.getWorkDir(), "source" + File.separator + fileHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getHlsDir(String fileHash) {
|
||||||
|
return new File(properties.getWorkDir(), "hls" + File.separator + fileHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sanitizeFileName(String fileName) {
|
||||||
|
return fileName.replace("\\", "_").replace("/", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String abbreviate(String text, int maxLength) {
|
||||||
|
if (text == null || text.length() <= maxLength) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return text.substring(0, maxLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user