为 Spring Boot 实现字节服务
Posted
技术标签:
【中文标题】为 Spring Boot 实现字节服务【英文标题】:Implement Byte serving for Spring Boot 【发布时间】:2021-04-06 01:52:50 【问题描述】:我想使用 Spring Boot Rest API 在 Angular 中实现视频播放器。我可以播放视频,但我无法进行视频搜索。每次我使用 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;
我发现这篇文章提供了一些想法:How to Implement HTTP byte-range requests in Spring MVC
但目前它不起作用。当我尝试移动位置时,视频再次从头开始播放。
我用这个播放器: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>
你知道我该如何解决这个问题吗?
【问题讨论】:
角码是什么样子的? 【参考方案1】:第一个解决方案:使用FileSystemResource
FileSystemResource 在内部处理字节范围标头支持,读取和写入适当的标头。
这种方法有两个问题。
它在内部使用FileInputStream
来读取文件。这适用于小文件,但不适用于通过字节范围请求提供的大文件。 FileInputStream
将从头开始读取文件并丢弃不需要的内容,直到它重新达到请求的起始偏移量。这可能会导致文件较大时速度变慢。
它将"application/json"
设置为"Content-Type"
响应标头。所以,我提供了我自己的 "Content-Type"
标头。 See this thread
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);
第二种解决方案:使用HttpServletResponse
和RandomAccessFile
使用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);
在您的 component.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控制器都在工作,我也可以跳过视频。 如果我们有数百个客户端,哪一种解决方案会消耗更少的资源? 2.或 3. 您还可以查看 spring 反应式。 Spring 反应式允许您以异步方式处理函数执行。这种方法适用于AsynchronousFileChannel
,它使用本机函数readFile(long handle, long address, int len, long offset, long overlapped)
。这里的偏移量是正在读取的文件中的位置。 FileInputStream
不是这种情况,它使用readBytes(byte b[], int off, int len)
,其中偏移量是目标缓冲区中的位置。
在 azure-core 中已经有一个桥接反应器(spring 反应使用的反应库)和AsynchronousFileChannel
的实现。所以如果你想这样做,你可以在这里找到文档azuresdkdocs.blob.core.windows.net/$web/java/azure-core/1.1.0/…【参考方案2】:
如果您在 Chrome 中使用 <video />
元素,则仅当端点通过使用 Range header 响应请求并以 206 Partial Content 响应响应来实现部分内容请求时,搜索才有效。
【讨论】:
我用这个播放器:github.com/smnbbrv/ngx-plyr以上是关于为 Spring Boot 实现字节服务的主要内容,如果未能解决你的问题,请参考以下文章
企业分布式微服务云SpringCloud SpringBoot mybatis (十七)Spring Boot中的事务管理
spring boot 系列之三:spring boot 整合JdbcTemplate
Spring Boot学习笔记——Spring Boot与ActiveMQ的集成
Spring Boot + Spring Security解决UsernameNotFoundException无法被捕获的问题