后端返回视频流 断点续传

1. 返回stream的所有数据

  • 从本地读取视频文件,返回前端视频流。
  • 前端拼接请求地址,放在例如video标签的src属性里;或者浏览器键入也可以直接播放视频。
  • 这种方式只能请求所有数据,如果中途断开,不能从断开处重新获取数据;并且有可能出现作为视频流时,拖动进度条无法正常播放的情况。
    @GetMapping("/stream")
    public void stream(@RequestParam Long key, HttpServletResponse response) {
        LearningFile storage = learningService.getById(key);
        if (ObjectUtil.isNull(storage)) {
            throw new ServiceException(ErrorCode.FILE_NOT_EXIST);
        }

        String filePath = storage.getAbsolutePath();
        File file = FileUtil.file(filePath);
        if (!FileUtil.exist(file)) {
            throw new ServiceException(ErrorCode.FILE_NOT_EXIST);
        }

        try (BufferedInputStream is = FileUtil.getInputStream(file); ServletOutputStream os = response.getOutputStream()) {
            response.reset();
            response.addHeader("Content-Length", "" + file.length());
            response.setContentType("video/mp4;charset=UTF-8");

            IOUtils.copy(is, os);
            os.flush();
            response.flushBuffer();
        } catch (IOException e) {
            throw new ServiceException(ErrorCode.FILE_DOWNLOAD_FAIL);
        }
    }

2. 断点续传,请求部分stream数据

即使线路中断,恢复时继续上传,视频也能拖动进度条了。

2.1 Http Header参数

  • Range
    请求头中参数,表明请求数据的起始和末尾,一般格式

    Range: bytes=0-499 表示第 0-499 字节范围的内容 
    Range: bytes=500-999 表示第 500-999 字节范围的内容 
    Range: bytes=-500 表示最后 500 字节的内容 
    Range: bytes=500- 表示从第 500 字节开始到文件结束部分的内容 
    Range: bytes=0-0,-1 表示第一个和最后一个字节 
    Range: bytes=500-600,601-999 同时指定几个范围
    
  • Content-Range
    用于响应头中,在发出带 Range 的请求后,服务器会在 Content-Range 头部返回当前接受的范围和文件总大小。一般格式:

    Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]
    

    例如:

    Content-Range: bytes 0-499/22400
    

    0-499 是指当前发送的数据的范围,而 22400 则是文件的总大小。

    而在响应完成后,返回的响应头内容也不同:

    HTTP/1.1 200 Ok(不使用断点续传方式) 
    HTTP/1.1 206 Partial Content(使用断点续传方式)
    
    @GetMapping("/stream")
      public void stream(@RequestParam Long key, HttpServletResponse response, HttpServletRequest request) {
          LearningFile storage = learningService.getById(key);
          if (ObjectUtil.isNull(storage)) {
              throw new ServiceException(ErrorCode.FILE_NOT_EXIST);
          }
    
          String filePath = storage.getAbsolutePath();
          File file = FileUtil.file(filePath);
    
          // 文件不存在
          if (!FileUtil.exist(file)) {
              throw new ServiceException(ErrorCode.FILE_NOT_EXIST);
          }
    
          // 判断文件类型
          String type = FileTypeUtil.getType(file);
          boolean isVideo = com.nbport.answer.domain.util.FileUtil.isVideoType(type);
    
          response.reset();
    
          if (isVideo) {
              // 处理视频文件
              // 请求头中range, 不存在则退出
              String rangeString = request.getHeader("Range");
              if (!StringUtils.hasText(rangeString)) {
                  return;
              }
              try (RandomAccessFile accessFile = new RandomAccessFile(file, "r"); ServletOutputStream os = response.getOutputStream()) {
                  // 获取range的起始 末尾
                  long rangeStart = 0, rangeEnd = 0;
                  String[] ranges = rangeString.split("=");
                  if (ranges.length > 1) {
                      String[] rangeDatas = ranges[1].split("-");
                      rangeStart = Integer.parseInt(rangeDatas[0]);
                      if (rangeDatas.length > 1) {
                          rangeEnd = Integer.parseInt(rangeDatas[1]);
                      }
                  }
    
                  long contentLength = rangeEnd > 0 ? rangeEnd - rangeStart + 1 : accessFile.length();
                  rangeEnd = rangeEnd > 0 ? rangeEnd : file.length() - 1;
    
                  response.addHeader(HttpHeaders.CONTENT_LENGTH, "" + contentLength);
                  response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
                  response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + rangeStart + "-" + rangeEnd + "/" + accessFile.length());
                  response.setContentType("video/" + type + ";charset=UTF-8");
                  response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
    
                  accessFile.seek(rangeStart);
    
                  byte[] cache = new byte[4096];
                  while (contentLength > 0) {
                      int len = accessFile.read(cache);
                      if (contentLength < cache.length) {
                          os.write(cache, 0, (int) contentLength);
                      } else {
                          os.write(cache, 0, len);
                          if (len < cache.length) {
                              break;
                          }
                      }
                      contentLength -= cache.length;
                  }
    
                  os.flush();
              } catch (IOException e) {
                  log.error("异常读取", e);
              }
          } else {
              // 非视频文件
              try (BufferedInputStream is = FileUtil.getInputStream(file); ServletOutputStream os = response.getOutputStream()) {
                  IOUtils.copy(is, os);
                  response.setHeader(HttpHeaders.CONTENT_LENGTH, file.length() + "");
                  response.setHeader(HttpHeaders.CONTENT_TYPE, "application/octet-stream");
                  os.flush();
              } catch (Exception e) {
                  log.error("处理文件异常, {}", e);
              }
          }
    
      }
    
posted @ 2022-11-10 10:19  OraCat  阅读(454)  评论(0编辑  收藏  举报