| | |
| | | |
| | | |
| | | public static final String baseUploadDir = "/home/javaweb/webresource/ai/file"; |
| | | /** |
| | | * 播放视频文件 |
| | | */ |
| | | |
| | | @GetMapping("/play/{fileName}") |
| | | public void playVideo(@PathVariable("fileName") String fileName, |
| | | HttpServletRequest request, |
| | | HttpServletResponse response) { |
| | | public void playVideo(@PathVariable("fileName") String fileName, HttpServletRequest request, HttpServletResponse response) { |
| | | log.info("开始播放视频文件: {}", fileName); |
| | | |
| | | // 配置参数 |
| | | final int BUFFER_SIZE = 1024 * 1024; // 1MB缓冲区,提高I/O性能 |
| | | final long MAX_INITIAL_SEGMENT = 1024 * 1024 * 15; // 最大初始片段大小(15MB),约30秒中等码率视频 |
| | | |
| | | try { |
| | | String companyId = LoginUserUtil.getLoginUser().getCompanyId(); |
| | | String uploadDir = baseUploadDir + "/" + companyId; |
| | | Path filePath = Paths.get(uploadDir, fileName); |
| | | |
| | | // 检查文件是否存在 |
| | | if (!Files.exists(filePath)) { |
| | | log.warn("视频文件不存在: {}", filePath); |
| | | response.setStatus(HttpStatus.NOT_FOUND.value()); |
| | | return; |
| | | } |
| | | |
| | | // 检查文件是否为常规文件 |
| | | if (!Files.isRegularFile(filePath)) { |
| | | log.warn("路径不是常规文件: {}", filePath); |
| | | response.setStatus(HttpStatus.BAD_REQUEST.value()); |
| | | return; |
| | | } |
| | | |
| | | // 获取文件大小 |
| | | long fileSize = Files.size(filePath); |
| | | log.debug("视频文件大小: {} bytes", fileSize); |
| | | |
| | | // 检查是否有Range请求头 |
| | | String rangeHeader = request.getHeader("Range"); |
| | | if (rangeHeader != null) { |
| | | log.debug("收到Range请求: {}", rangeHeader); |
| | | // 解析Range头,格式为"bytes=start-end" |
| | | String[] ranges = rangeHeader.replace("bytes=", "").split("-"); |
| | | long start = 0, end = fileSize - 1; |
| | | |
| | | try { |
| | | 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; |
| | | // 如果没有指定结束位置,返回从开始位置到文件末尾的内容 |
| | | // 但限制最大长度,避免一次性传输过大的内容 |
| | | end = Math.min(start + BUFFER_SIZE * 10, fileSize - 1); |
| | | } |
| | | } catch (NumberFormatException e) { |
| | | log.error("无效的Range头格式: {}", rangeHeader, e); |
| | | response.setStatus(HttpStatus.BAD_REQUEST.value()); |
| | | return; |
| | | } |
| | | |
| | | // 确保范围有效 |
| | | if (start > end || start >= fileSize) { |
| | | log.warn("无效的Range范围: {}-{},文件大小: {}", start, end, fileSize); |
| | | response.setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value()); |
| | | response.setHeader("Content-Range", "bytes */" + fileSize); |
| | | return; |
| | | } |
| | | |
| | |
| | | |
| | | // 计算内容长度 |
| | | long contentLength = end - start + 1; |
| | | log.debug("处理Range请求: {}-{},长度: {}", start, end, contentLength); |
| | | |
| | | // 设置响应状态和头部信息 |
| | | response.setStatus(HttpStatus.PARTIAL_CONTENT.value()); |
| | |
| | | response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize); |
| | | response.setHeader("Accept-Ranges", "bytes"); |
| | | response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\""); |
| | | response.setHeader("Cache-Control", "public, max-age=3600"); // 添加缓存控制 |
| | | |
| | | // 从指定位置开始读取文件 |
| | | // 使用NIO提高读取性能 |
| | | try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r"); |
| | | OutputStream outputStream = response.getOutputStream()) { |
| | | |
| | | randomAccessFile.seek(start); |
| | | byte[] buffer = new byte[8192]; |
| | | byte[] buffer = new byte[BUFFER_SIZE]; |
| | | 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; |
| | | |
| | | // 定期刷新输出流,避免缓冲区溢出 |
| | | if (remaining % (BUFFER_SIZE * 4) == 0) { |
| | | outputStream.flush(); |
| | | } |
| | | } |
| | | |
| | | // 确保所有数据都被写入 |
| | | outputStream.flush(); |
| | | log.debug("Range请求处理完成: {}-{}", start, end); |
| | | } |
| | | } else { |
| | | log.debug("收到完整文件请求,返回初始片段"); |
| | | |
| | | // 没有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); |
| | | // 没有Range头,返回文件大小的10分之一和15MB中较小的那个 |
| | | long oneTenthSize = fileSize / 10; |
| | | // 计算10分之一和15MB的较小值 |
| | | long maxInitialSize = Math.min(oneTenthSize, MAX_INITIAL_SEGMENT); |
| | | // 确保至少返回1字节,且不超过文件大小 |
| | | long actualEnd = Math.min(Math.max(maxInitialSize, 1), fileSize - 1); |
| | | log.debug("返回初始片段: {} bytes (10分之一大小: {} bytes, 15MB限制: {} bytes, 文件总大小: {} bytes)", |
| | | actualEnd + 1, oneTenthSize, MAX_INITIAL_SEGMENT, fileSize); |
| | | |
| | | // 设置响应状态和头部信息 |
| | | response.setStatus(HttpStatus.PARTIAL_CONTENT.value()); // 使用206状态码,因为只返回部分内容 |
| | |
| | | response.setHeader("Content-Range", "bytes 0-" + actualEnd + "/" + fileSize); |
| | | response.setHeader("Accept-Ranges", "bytes"); |
| | | response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\""); |
| | | response.setHeader("Cache-Control", "public, max-age=3600"); // 添加缓存控制 |
| | | |
| | | // 流式传输文件的前30秒 |
| | | // 流式传输文件的初始片段 |
| | | try (InputStream inputStream = Files.newInputStream(filePath); |
| | | OutputStream outputStream = response.getOutputStream()) { |
| | | byte[] buffer = new byte[8192]; |
| | | byte[] buffer = new byte[BUFFER_SIZE]; |
| | | long written = 0; |
| | | int bytesRead; |
| | | |
| | | while (written <= actualEnd && (bytesRead = inputStream.read(buffer)) != -1) { |
| | | long toWrite = Math.min(bytesRead, (int)(actualEnd + 1 - written)); |
| | | long toWrite = Math.min(bytesRead, actualEnd + 1 - written); |
| | | outputStream.write(buffer, 0, (int)toWrite); |
| | | written += toWrite; |
| | | |
| | | // 如果已经写完前30秒的内容,则停止 |
| | | // 定期刷新输出流 |
| | | if (written % (BUFFER_SIZE * 4) == 0) { |
| | | outputStream.flush(); |
| | | } |
| | | |
| | | // 如果已经写完初始片段,则停止 |
| | | if (written > actualEnd) { |
| | | break; |
| | | } |
| | | } |
| | | |
| | | // 确保所有数据都被写入 |
| | | outputStream.flush(); |
| | | log.debug("初始片段传输完成,大小: {} bytes", written); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | log.error("播放视频文件时发生错误: {}", fileName, e); |
| | | try { |
| | | // 确保响应状态被设置 |
| | | if (!response.isCommitted()) { |
| | | response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); |
| | | } |
| | | } catch (Exception ex) { |
| | | log.error("设置响应状态时发生错误", ex); |
| | | } |
| | | } finally { |
| | | log.info("视频播放请求处理完成: {}", fileName); |
| | | } |
| | | } |
| | | |
| | | /** |