feat: 增加视频流式播放与批量文件查询功能
Some checks failed
test/timeline-server/pipeline/head There was a failure building this commit

1. 在 `FileController` 中新增 `/video/{instanceId}` 接口,支持 HTTP Range 请求以实现视频分段加载和流式播放。
2. 在 `FileService` 和 `ImageInfoMapper` 中新增 `getBatchFileInfo` 方法,支持通过实例 ID 列表批量获取文件元数据。
3. 优化 `getVideoUrl` 逻辑,改为返回服务内部代理路径而非 MinIO 直接签名地址。
4. 完善相关 DAO 层代码,增加 `selectListByInstanceIds` 查询语句。
This commit is contained in:
2026-02-13 11:14:25 +08:00
parent 1b1e1f4f87
commit d645164daa
5 changed files with 123 additions and 3 deletions

View File

@@ -63,10 +63,53 @@ public class FileController {
@GetMapping("/get-video-url/{instanceId}")
public ResponseEntity<String> getVideoUrl(@PathVariable String instanceId) throws Throwable {
String videoUrl = fileService.generateVideoUrl(instanceId);
// Return proxy URL instead of direct MinIO URL
// Assuming the frontend can access this service via /api/file
String videoUrl = "/api/file/video/" + instanceId;
return ResponseEntity.success(videoUrl);
}
@GetMapping("/video/{instanceId}")
public void fetchVideo(@PathVariable String instanceId,
@RequestHeader(value = "Range", required = false) String rangeHeader,
HttpServletResponse response) throws Throwable {
long fileSize = fileService.getVideoSize(instanceId);
long start = 0;
long end = fileSize - 1;
if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
String[] ranges = rangeHeader.substring(6).split("-");
try {
start = Long.parseLong(ranges[0]);
if (ranges.length > 1 && !ranges[1].isEmpty()) {
end = Long.parseLong(ranges[1]);
}
} catch (NumberFormatException e) {
// Ignore invalid range
}
}
if (end >= fileSize) {
end = fileSize - 1;
}
long length = end - start + 1;
response.setContentType("video/mp4");
response.setHeader("Accept-Ranges", "bytes");
if (rangeHeader != null) {
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
response.setHeader("Content-Length", String.valueOf(length));
} else {
response.setHeader("Content-Length", String.valueOf(fileSize));
}
InputStream inputStream = fileService.fetchVideo(instanceId, start, length);
IOUtils.copy(inputStream, response.getOutputStream());
}
@PostMapping("/uploaded")
public ResponseEntity<String> uploaded(@RequestBody ImageInfoVo imageInfoVo) throws Throwable {
String instanceId = fileService.saveFileMetadata(imageInfoVo);
@@ -79,6 +122,12 @@ public class FileController {
return ResponseEntity.success(objectKey);
}
@PostMapping("/batch-info")
public ResponseEntity<List<ImageInfoVo>> getBatchFileInfo(@RequestBody List<String> instanceIds) {
List<ImageInfoVo> list = fileService.getBatchFileInfo(instanceIds);
return ResponseEntity.success(list);
}
@RequestMapping(value = "/image/{instanceId}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
public void fetchImage(@PathVariable String instanceId, HttpServletResponse response) throws Throwable {
InputStream inputStream = fileService.fetchImage(instanceId);

View File

@@ -3,6 +3,7 @@ package com.timeline.file.dao;
import com.timeline.file.entity.ImageInfo;
import com.timeline.file.vo.ImageInfoVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
@@ -10,9 +11,16 @@ import java.util.Map;
@Mapper
public interface ImageInfoMapper {
void insert(ImageInfo imageInfo);
void update(ImageInfo imageInfo);
List<ImageInfo> selectListByInstanceIds(@Param("ids") List<String> ids);
String selectObjectKeyById(String objectKey);
void delete(String objectKey);
List<ImageInfo> selectListByOwnerId(Map map);
ImageInfo selectByInstanceId(String instanceId);
}

View File

@@ -36,11 +36,17 @@ public interface FileService {
String uploadImage(MultipartFile cover) throws Throwable;
InputStream fetchImage(String coverKey) throws Throwable;
InputStream fetchImage(String instanceId) throws Throwable;
InputStream fetchImageLowRes(String instanceId) throws Throwable;
InputStream fetchVideo(String instanceId, long offset, long length) throws Throwable;
long getVideoSize(String instanceId);
String generateVideoUrl(String instanceId) throws Throwable;
Map<String, Object> getImagesListByOwnerId(ImageInfoVo imageInfoVo);
List<ImageInfoVo> getBatchFileInfo(List<String> instanceIds);
}

View File

@@ -28,7 +28,13 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@@ -386,6 +392,50 @@ public class FileServiceImpl implements FileService {
}
}
@Override
public InputStream fetchVideo(String instanceId, long offset, long length) throws Throwable {
ImageInfo imageInfo = imageInfoMapper.selectByInstanceId(instanceId);
if (imageInfo == null) {
throw new CustomException(ResponseEnum.NOT_FOUND_ERROR);
}
String bucket = userBucket(imageInfo.getUserId());
return minioClient.getObject(GetObjectArgs.builder()
.bucket(bucket)
.object(imageInfo.getObjectKey())
.offset(offset)
.length(length)
.build());
}
@Override
public long getVideoSize(String instanceId) {
ImageInfo imageInfo = imageInfoMapper.selectByInstanceId(instanceId);
if (imageInfo == null) {
throw new CustomException(ResponseEnum.NOT_FOUND_ERROR);
}
return imageInfo.getSize();
}
@Override
public List<ImageInfoVo> getBatchFileInfo(List<String> instanceIds) {
if (instanceIds == null || instanceIds.isEmpty()) {
return new ArrayList<>();
}
List<ImageInfo> imageInfos = imageInfoMapper.selectListByInstanceIds(instanceIds);
if (imageInfos == null) {
return new ArrayList<>();
}
return imageInfos.stream().map(info -> {
ImageInfoVo vo = new ImageInfoVo();
vo.setInstanceId(info.getInstanceId());
vo.setImageName(info.getImageName());
vo.setThumbnailInstanceId(info.getThumbnailInstanceId());
vo.setContentType(info.getContentType());
vo.setDuration(info.getDuration());
return vo;
}).collect(Collectors.toList());
}
@Override
public Map<String, Object> getImagesListByOwnerId(ImageInfoVo imageInfoVo) {
HashMap<String, String> map = new HashMap<>();

View File

@@ -43,4 +43,11 @@
is_deleted = #{isDeleted}
WHERE instance_id = #{instanceId}
</update>
<select id="selectListByInstanceIds" resultType="com.timeline.file.entity.ImageInfo">
SELECT * FROM image_info WHERE instance_id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND is_deleted = 0
</select>
</mapper>