feat: 增加视频流式播放与批量文件查询功能
Some checks failed
test/timeline-server/pipeline/head There was a failure building this commit
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:
@@ -63,10 +63,53 @@ public class FileController {
|
|||||||
|
|
||||||
@GetMapping("/get-video-url/{instanceId}")
|
@GetMapping("/get-video-url/{instanceId}")
|
||||||
public ResponseEntity<String> getVideoUrl(@PathVariable String instanceId) throws Throwable {
|
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);
|
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")
|
@PostMapping("/uploaded")
|
||||||
public ResponseEntity<String> uploaded(@RequestBody ImageInfoVo imageInfoVo) throws Throwable {
|
public ResponseEntity<String> uploaded(@RequestBody ImageInfoVo imageInfoVo) throws Throwable {
|
||||||
String instanceId = fileService.saveFileMetadata(imageInfoVo);
|
String instanceId = fileService.saveFileMetadata(imageInfoVo);
|
||||||
@@ -79,6 +122,12 @@ public class FileController {
|
|||||||
return ResponseEntity.success(objectKey);
|
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)
|
@RequestMapping(value = "/image/{instanceId}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
|
||||||
public void fetchImage(@PathVariable String instanceId, HttpServletResponse response) throws Throwable {
|
public void fetchImage(@PathVariable String instanceId, HttpServletResponse response) throws Throwable {
|
||||||
InputStream inputStream = fileService.fetchImage(instanceId);
|
InputStream inputStream = fileService.fetchImage(instanceId);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.timeline.file.dao;
|
|||||||
import com.timeline.file.entity.ImageInfo;
|
import com.timeline.file.entity.ImageInfo;
|
||||||
import com.timeline.file.vo.ImageInfoVo;
|
import com.timeline.file.vo.ImageInfoVo;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -10,9 +11,16 @@ import java.util.Map;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface ImageInfoMapper {
|
public interface ImageInfoMapper {
|
||||||
void insert(ImageInfo imageInfo);
|
void insert(ImageInfo imageInfo);
|
||||||
|
|
||||||
void update(ImageInfo imageInfo);
|
void update(ImageInfo imageInfo);
|
||||||
|
|
||||||
|
List<ImageInfo> selectListByInstanceIds(@Param("ids") List<String> ids);
|
||||||
|
|
||||||
String selectObjectKeyById(String objectKey);
|
String selectObjectKeyById(String objectKey);
|
||||||
|
|
||||||
void delete(String objectKey);
|
void delete(String objectKey);
|
||||||
|
|
||||||
List<ImageInfo> selectListByOwnerId(Map map);
|
List<ImageInfo> selectListByOwnerId(Map map);
|
||||||
|
|
||||||
ImageInfo selectByInstanceId(String instanceId);
|
ImageInfo selectByInstanceId(String instanceId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,17 @@ public interface FileService {
|
|||||||
|
|
||||||
String uploadImage(MultipartFile cover) throws Throwable;
|
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 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;
|
String generateVideoUrl(String instanceId) throws Throwable;
|
||||||
|
|
||||||
Map<String, Object> getImagesListByOwnerId(ImageInfoVo imageInfoVo);
|
Map<String, Object> getImagesListByOwnerId(ImageInfoVo imageInfoVo);
|
||||||
|
|
||||||
|
List<ImageInfoVo> getBatchFileInfo(List<String> instanceIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,13 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.time.LocalDateTime;
|
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
|
@Slf4j
|
||||||
@Service
|
@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
|
@Override
|
||||||
public Map<String, Object> getImagesListByOwnerId(ImageInfoVo imageInfoVo) {
|
public Map<String, Object> getImagesListByOwnerId(ImageInfoVo imageInfoVo) {
|
||||||
HashMap<String, String> map = new HashMap<>();
|
HashMap<String, String> map = new HashMap<>();
|
||||||
|
|||||||
@@ -43,4 +43,11 @@
|
|||||||
is_deleted = #{isDeleted}
|
is_deleted = #{isDeleted}
|
||||||
WHERE instance_id = #{instanceId}
|
WHERE instance_id = #{instanceId}
|
||||||
</update>
|
</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>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user