| | |
| | | import cc.mrbird.febs.ai.res.productPoint.ApiProductPointRecommendVo; |
| | | import cc.mrbird.febs.ai.service.AiProductPointService; |
| | | import cc.mrbird.febs.common.entity.FebsResponse; |
| | | import cc.mrbird.febs.common.utils.LoginUserUtil; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import io.swagger.annotations.ApiResponse; |
| | | import io.swagger.annotations.ApiResponses; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.http.HttpStatus; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.InputStream; |
| | | import java.io.OutputStream; |
| | | import java.io.RandomAccessFile; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Path; |
| | | import java.nio.file.Paths; |
| | | |
| | | /** |
| | | * @author Administrator |
| | |
| | | return aiProductPointService.recommend(dto); |
| | | } |
| | | |
| | | |
| | | public static final String baseUploadDir = "/home/javaweb/webresource/ai/file"; |
| | | /** |
| | | * 播放视频文件 |
| | | */ |
| | | @GetMapping("/play/{fileName}") |
| | | public void playVideo(@PathVariable("fileName") String fileName, |
| | | HttpServletRequest request, |
| | | HttpServletResponse response) { |
| | | try { |
| | | String companyId = LoginUserUtil.getLoginUser().getCompanyId(); |
| | | String uploadDir = baseUploadDir + "/" + companyId; |
| | | Path filePath = Paths.get(uploadDir, fileName); |
| | | if (!Files.exists(filePath)) { |
| | | response.setStatus(HttpStatus.NOT_FOUND.value()); |
| | | return; |
| | | } |
| | | |
| | | // 获取文件大小 |
| | | long fileSize = Files.size(filePath); |
| | | |
| | | // 检查是否有Range请求头 |
| | | String rangeHeader = request.getHeader("Range"); |
| | | if (rangeHeader != null) { |
| | | // 解析Range头,格式为"bytes=start-end" |
| | | String[] ranges = rangeHeader.replace("bytes=", "").split("-"); |
| | | long start = 0, end = fileSize - 1; |
| | | |
| | | if (!ranges[0].isEmpty()) { |
| | | start = Long.parseLong(ranges[0]); |
| | | } |
| | | if (ranges.length > 1 && !ranges[1].isEmpty()) { |
| | | end = Long.parseLong(ranges[1]); |
| | | } else { |
| | | end = fileSize - 1; |
| | | } |
| | | |
| | | // 确保范围有效 |
| | | if (start > end || start >= fileSize) { |
| | | response.setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value()); |
| | | return; |
| | | } |
| | | |
| | | // 限制结束位置不超过文件大小 |
| | | if (end >= fileSize) { |
| | | end = fileSize - 1; |
| | | } |
| | | |
| | | // 计算内容长度 |
| | | long contentLength = end - start + 1; |
| | | |
| | | // 设置响应状态和头部信息 |
| | | response.setStatus(HttpStatus.PARTIAL_CONTENT.value()); |
| | | response.setContentType(getContentTypeByExtension(fileName)); |
| | | response.setContentLengthLong(contentLength); |
| | | response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize); |
| | | response.setHeader("Accept-Ranges", "bytes"); |
| | | response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\""); |
| | | |
| | | // 从指定位置开始读取文件 |
| | | try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r"); |
| | | OutputStream outputStream = response.getOutputStream()) { |
| | | |
| | | randomAccessFile.seek(start); |
| | | byte[] buffer = new byte[8192]; |
| | | long remaining = contentLength; |
| | | int bytesRead; |
| | | |
| | | while (remaining > 0 && (bytesRead = randomAccessFile.read(buffer, 0, (int) Math.min(buffer.length, remaining))) != -1) { |
| | | outputStream.write(buffer, 0, bytesRead); |
| | | remaining -= bytesRead; |
| | | } |
| | | } |
| | | } else { |
| | | |
| | | // 没有Range头,返回前30秒内容(如果可能的话) |
| | | // 首先尝试估计前30秒对应的字节数(假设平均比特率为5 Mbps) |
| | | long estimatedFirst30Seconds = (long) (5 * 1024 * 1024 / 8 * 30); // 5 Mbps => bytes for 30 seconds |
| | | long actualEnd = Math.min(estimatedFirst30Seconds, fileSize - 1); |
| | | |
| | | // 设置响应状态和头部信息 |
| | | response.setStatus(HttpStatus.PARTIAL_CONTENT.value()); // 使用206状态码,因为只返回部分内容 |
| | | response.setContentType(getContentTypeByExtension(fileName)); |
| | | response.setContentLengthLong(actualEnd + 1); |
| | | response.setHeader("Content-Range", "bytes 0-" + actualEnd + "/" + fileSize); |
| | | response.setHeader("Accept-Ranges", "bytes"); |
| | | response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\""); |
| | | |
| | | // 流式传输文件的前30秒 |
| | | try (InputStream inputStream = Files.newInputStream(filePath); |
| | | OutputStream outputStream = response.getOutputStream()) { |
| | | byte[] buffer = new byte[8192]; |
| | | long written = 0; |
| | | int bytesRead; |
| | | |
| | | while (written <= actualEnd && (bytesRead = inputStream.read(buffer)) != -1) { |
| | | long toWrite = Math.min(bytesRead, (int)(actualEnd + 1 - written)); |
| | | outputStream.write(buffer, 0, (int)toWrite); |
| | | written += toWrite; |
| | | |
| | | // 如果已经写完前30秒的内容,则停止 |
| | | if (written > actualEnd) { |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据文件扩展名获取MIME类型 |
| | | */ |
| | | private String getContentTypeByExtension(String fileName) { |
| | | String extension = ""; |
| | | int lastDotIndex = fileName.lastIndexOf('.'); |
| | | if (lastDotIndex > 0) { |
| | | extension = fileName.substring(lastDotIndex).toLowerCase(); |
| | | } |
| | | |
| | | switch (extension) { |
| | | case ".mp4": |
| | | return "video/mp4"; |
| | | case ".avi": |
| | | return "video/x-msvideo"; |
| | | case ".mov": |
| | | return "video/quicktime"; |
| | | case ".wmv": |
| | | return "video/x-ms-wmv"; |
| | | case ".flv": |
| | | return "video/x-flv"; |
| | | case ".webm": |
| | | return "video/webm"; |
| | | case ".mkv": |
| | | return "video/x-matroska"; |
| | | case ".m3u8": |
| | | return "application/vnd.apple.mpegurl"; |
| | | case ".ts": |
| | | return "video/MP2T"; |
| | | default: |
| | | return "application/octet-stream"; |
| | | } |
| | | } |
| | | |
| | | } |