为Spring Boot实现字节服务

4

我希望在Angular中使用Spring Boot Rest API实现视频播放器。我可以播放视频,但无法进行视频寻址。每次我在Chrome或Edge中使用时,视频都会一遍又一遍地开始。

我尝试过这个端点:

@RequestMapping(value = "/play_video/{video_id}", method = RequestMethod.GET)
    @ResponseBody public ResponseEntity<byte[]> getPreview1(@PathVariable("video_id") String video_id, HttpServletResponse response) {
        ResponseEntity<byte[]> result = null;
        try {
            String file = "/opt/videos/" + video_id + ".mp4";
            Path path = Paths.get(file);
            byte[] image = Files.readAllBytes(path);

            response.setStatus(HttpStatus.OK.value());
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.setContentLength(image.length);
            result = new ResponseEntity<byte[]>(image, headers, HttpStatus.OK);
        } catch (java.nio.file.NoSuchFileException e) {
            response.setStatus(HttpStatus.NOT_FOUND.value());
        } catch (Exception e) {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        }
        return result;
    }

我找到了一篇文章,其中提供了一些想法:如何在Spring MVC中实现HTTP字节范围请求。但目前它无法正常工作。每当我尝试移动位置时,视频就会重新开始播放。
我使用这个播放器:https://github.com/smnbbrv/ngx-plyr。我已经按照以下方式进行了配置:
<div class="media">
        <div
          class="class-video mr-3 mb-1"
          plyr
          [plyrPlaysInline]="true"
          [plyrSources]="gymClass.video"
          (plyrInit)="player = $event"
          (plyrPlay)="played($event)">
        </div>
        <div class="media-body">
          {{ gymClass.description }}
        </div>
      </div>

你知道我该如何解决这个问题吗?


角度代码是什么样子的? - Zack
2个回答

11

第一种解决方案:使用 FileSystemResource

FileSystemResource 内部处理字节范围头支持,读取和写入适当的头信息。

使用此方法有两个问题:

  1. 它在内部使用 FileInputStream 读取文件。对于小文件来说这没问题,但对于通过字节范围请求提供的大文件不适用。FileInputStream 会从开始处读取文件并丢弃不需要的内容,直到达到所请求的起始偏移量。这会导致大文件速度变慢。

  2. 它将 "Content-Type" 响应头设置为 "application/json"。因此,需要提供自己的 "Content-Type" 头信息。请参见此主题

import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class Stream3 {
    @GetMapping(value = "/play_video/{video_id}")
    @ResponseBody
    public ResponseEntity<FileSystemResource> stream(@PathVariable("video_id") String video_id) {
        String filePathString = "/opt/videos/" + video_id + ".mp4";        
        final HttpHeaders responseHeaders = new HttpHeaders();
        responseHeaders.add("Content-Type", "video/mp4");
        return new ResponseEntity<>(new FileSystemResource(filePathString), responseHeaders, HttpStatus.OK);
    }
}

第二解决方案:使用HttpServletResponseRandomAccessFile

借助RandomAccessFile,您可以实现对字节范围请求的支持。与FileInputStream相比,它的优势在于不需要每次有新的范围请求时从开头读取文件,这使得此方法也适用于较大的文件。RandomAccessFile有一个名为seek(long)的方法,它调用C方法fseek(),直接将文件指针移动到所请求的偏移量。

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class Stream {
    @GetMapping(value = "/play_video/{video_id}")
    @ResponseBody
    public void stream(        
            @PathVariable("video_id") String video_id,
            @RequestHeader(value = "Range", required = false) String rangeHeader,
            HttpServletResponse response) {

        try {
            OutputStream os = response.getOutputStream();
            long rangeStart = 0;
            long rangeEnd;
            String filePathString = "/opt/videos/" + video_id + ".mp4";
            Path filePath = Paths.get(filePathString);
            Long fileSize = Files.size(filePath);
            byte[] buffer = new byte[1024];
            RandomAccessFile file = new RandomAccessFile(filePathString, "r");
            try (file) {
                if (rangeHeader == null) {
                    response.setHeader("Content-Type", "video/mp4");
                    response.setHeader("Content-Length", fileSize.toString());
                    response.setStatus(HttpStatus.OK.value());
                    long pos = rangeStart;
                    file.seek(pos);
                    while (pos < fileSize - 1) {                        
                        file.read(buffer);
                        os.write(buffer);
                        pos += buffer.length;
                    }
                    os.flush();
                    return;
                }

                String[] ranges = rangeHeader.split("-");
                rangeStart = Long.parseLong(ranges[0].substring(6));
                if (ranges.length > 1) {
                    rangeEnd = Long.parseLong(ranges[1]);
                } else {
                    rangeEnd = fileSize - 1;
                }
                if (fileSize < rangeEnd) {
                    rangeEnd = fileSize - 1;
                }

                String contentLength = String.valueOf((rangeEnd - rangeStart) + 1);
                response.setHeader("Content-Type", "video/mp4");
                response.setHeader("Content-Length", contentLength);
                response.setHeader("Accept-Ranges", "bytes");
                response.setHeader("Content-Range", "bytes" + " " + rangeStart + "-" + rangeEnd + "/" + fileSize);
                response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
                long pos = rangeStart;
                file.seek(pos);
                while (pos < rangeEnd) {                    
                    file.read(buffer);
                    os.write(buffer);
                    pos += buffer.length;
                }
                os.flush();

            }

        } catch (FileNotFoundException e) {
            response.setStatus(HttpStatus.NOT_FOUND.value());
        } catch (IOException e) {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        }

    }

}

第三种解决方案:同样使用RandomAccessFile,但使用StreamingResponseBody代替HttpServletResponse

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

@Controller
public class Stream2 {
    @GetMapping(value = "/play_video/{video_id}")
    @ResponseBody
    public ResponseEntity<StreamingResponseBody> stream(
            @PathVariable("video_id") String video_id,
            @RequestHeader(value = "Range", required = false) String rangeHeader) {        
        try {
            StreamingResponseBody responseStream;
            String filePathString = "/opt/videos/" + video_id + ".mp4";
            Path filePath = Paths.get(filePathString);
            Long fileSize = Files.size(filePath);
            byte[] buffer = new byte[1024];      
            final HttpHeaders responseHeaders = new HttpHeaders();

            if (rangeHeader == null) {
                responseHeaders.add("Content-Type", "video/mp4");
                responseHeaders.add("Content-Length", fileSize.toString());
                responseStream = os -> {
                    RandomAccessFile file = new RandomAccessFile(filePathString, "r");
                    try (file) {
                        long pos = 0;
                        file.seek(pos);
                        while (pos < fileSize - 1) {                            
                            file.read(buffer);
                            os.write(buffer);
                            pos += buffer.length;
                        }
                        os.flush();
                    } catch (Exception e) {}
                };
                return new ResponseEntity<>(responseStream, responseHeaders, HttpStatus.OK);
            }

            String[] ranges = rangeHeader.split("-");
            Long rangeStart = Long.parseLong(ranges[0].substring(6));
            Long rangeEnd;
            if (ranges.length > 1) {
                rangeEnd = Long.parseLong(ranges[1]);
            } else {
                rangeEnd = fileSize - 1;
            }
            if (fileSize < rangeEnd) {
                rangeEnd = fileSize - 1;
            }

            String contentLength = String.valueOf((rangeEnd - rangeStart) + 1);
            responseHeaders.add("Content-Type", "video/mp4");
            responseHeaders.add("Content-Length", contentLength);
            responseHeaders.add("Accept-Ranges", "bytes");
            responseHeaders.add("Content-Range", "bytes" + " " + rangeStart + "-" + rangeEnd + "/" + fileSize);
            final Long _rangeEnd = rangeEnd;
            responseStream = os -> {
                RandomAccessFile file = new RandomAccessFile(filePathString, "r");
                try (file) {
                    long pos = rangeStart;
                    file.seek(pos);
                    while (pos < _rangeEnd) {                        
                        file.read(buffer);
                        os.write(buffer);
                        pos += buffer.length;
                    }
                    os.flush();
                } catch (Exception e) {}
            };
            return new ResponseEntity<>(responseStream, responseHeaders, HttpStatus.PARTIAL_CONTENT);

        } catch (FileNotFoundException e) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        } catch (IOException e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

在你的组件.ts文件中:

你可以通过playVideoFile()函数来改变当前显示的视频。

export class AppComponent implements OnInit {
  videoSources: Plyr.Source[];
  ngOnInit(): void {
    const fileName = 'sample';
    this.playVideoFile(fileName);
  }

  playVideoFile(fileName: string) {
    this.videoSources = [
      {
        src: `http://localhost:8080/play_video/${fileName}`,
      },
    ];
  }
}

并且HTML代码:

<div
  #plyr
  plyr
  [plyrPlaysInline]="false"
  [plyrSources]="videoSources"
></div>


我更新了我的回答。我检查过了,所有三个Java控制器都可以工作,我也可以在视频中跳过。 - Dani
如果我们有数百个客户端,哪种解决方案将消耗更少的资源? - Peter Penzov
  1. 或者 3. 您还可以查看 Spring Reactive。Spring Reactive 可以让您以异步方式处理函数执行。这种方法与 AsynchronousFileChannel 配合得很好,后者使用本机函数 readFile(long handle, long address, int len, long offset, long overlapped)。这里的偏移量是正在读取的文件中的位置。而对于使用 readBytes(byte b[], int off, int len)FileInputStream,偏移量是目标缓冲区中的位置。
- Dani
在azure-core中已经有一个实现将反应式库(spring reactive使用的反应式库)和AsynchronousFileChannel一起桥接的实现。因此,如果您想以这种方式完成它,可以在此处找到文档 https://azuresdkdocs.blob.core.windows.net/$web/java/azure-core/1.1.0/com/azure/core/util/FluxUtil.html - Dani

0
如果您在Chrome中使用<video />元素,仅当终端通过遵守带有范围标头的请求并响应206部分内容响应来实现部分内容请求时,寻找才能正常工作。

我使用这个播放器:https://github.com/smnbbrv/ngx-plyr - Peter Penzov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接