gpt4 book ai didi

html - 将视频从 Controller 流式传输到 html5 视频播放器

转载 作者:行者123 更新时间:2023-12-02 17:29:58 26 4
gpt4 key购买 nike

我们正在努力做到以下几点

  • 将视频作为流上传到 S3 存储桶中 -- 完成
  • 在 java Controller 中读取流 -- 完成
  • 使用流在 html5 视频播放器中作为视频播放。 --待定

注意:视频尺寸可能很大。因此,我们正在寻找流式传输视频的最佳方式。

当我们在 Controller 中使用以下代码时,它不会作为流工作。相反,它会尝试下载整个视频,然后尝试播放内容。另请注意,这适用于小于 5 Mb 的视频,但不适用于其他视频。是否有任何其他方法可以实现此目的?

final String contentType =
StringUtils.isBlank(obj.getContentType())
? MediaType.APPLICATION_OCTET_STREAM_VALUE
: obj.getContentType();
return ResponseEntity.ok()
.contentLength(obj.getContentLength())
.header("Content-Type", contentType)
.body(new InputStreamResource(obj.getContent()));

最佳答案

我已经用这个实现了,

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* Created by kevin on 10/02/15.
* See full code here : https://github.com/davinkevin/Podcast-Server/blob/d927d9b8cb9ea1268af74316cd20b7192ca92da7/src/main/java/lan/dk/podcastserver/utils/multipart/MultipartFileSender.java
*/
public class MultipartFileSender {

protected final Logger logger = LoggerFactory.getLogger(this.getClass());

private static final int DEFAULT_BUFFER_SIZE = 20480; // ..bytes = 20KB.
private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.
private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";

Path filepath;
HttpServletRequest request;
HttpServletResponse response;

public MultipartFileSender() {
}

public static MultipartFileSender fromPath(Path path) {
return new MultipartFileSender().setFilepath(path);
}

public static MultipartFileSender fromFile(File file) {
return new MultipartFileSender().setFilepath(file.toPath());
}

public static MultipartFileSender fromURIString(String uri) {
return new MultipartFileSender().setFilepath(Paths.get(uri));
}

//** internal setter **//
private MultipartFileSender setFilepath(Path filepath) {
this.filepath = filepath;
return this;
}

public MultipartFileSender with(HttpServletRequest httpRequest) {
request = httpRequest;
return this;
}

public MultipartFileSender with(HttpServletResponse httpResponse) {
response = httpResponse;
return this;
}

public void serveResource() throws Exception {
if (response == null || request == null) {
return;
}

if (!Files.exists(filepath)) {
logger.error("File doesn't exist at URI : {}", filepath.toAbsolutePath().toString());
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}

Long length = Files.size(filepath);
String fileName = filepath.getFileName().toString();
FileTime lastModifiedObj = Files.getLastModifiedTime(filepath);

if (StringUtils.isEmpty(fileName) || lastModifiedObj == null) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
long lastModified = LocalDateTime.ofInstant(lastModifiedObj.toInstant(), ZoneId.of(ZoneOffset.systemDefault().getId())).toEpochSecond(ZoneOffset.UTC);
String contentType = "video/mp4";

// Validate request headers for caching ---------------------------------------------------

// If-None-Match header should contain "*" or ETag. If so, then return 304.
String ifNoneMatch = request.getHeader("If-None-Match");
if (ifNoneMatch != null && HttpUtils.matches(ifNoneMatch, fileName)) {
response.setHeader("ETag", fileName); // Required in 304.
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
return;
}

// If-Modified-Since header should be greater than LastModified. If so, then return 304.
// This header is ignored if any If-None-Match header is specified.
long ifModifiedSince = request.getDateHeader("If-Modified-Since");
if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
response.setHeader("ETag", fileName); // Required in 304.
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
return;
}

// Validate request headers for resume ----------------------------------------------------

// If-Match header should contain "*" or ETag. If not, then return 412.
String ifMatch = request.getHeader("If-Match");
if (ifMatch != null && !HttpUtils.matches(ifMatch, fileName)) {
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return;
}

// If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return;
}

// Validate and process range -------------------------------------------------------------

// Prepare some variables. The full Range represents the complete file.
Range full = new Range(0, length - 1, length);
List<Range> ranges = new ArrayList<>();

// Validate and process Range and If-Range headers.
String range = request.getHeader("Range");
if (range != null) {

// Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return;
}

String ifRange = request.getHeader("If-Range");
if (ifRange != null && !ifRange.equals(fileName)) {
try {
long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
if (ifRangeTime != -1) {
ranges.add(full);
}
} catch (IllegalArgumentException ignore) {
ranges.add(full);
}
}

// If any valid If-Range header, then process each part of byte range.
if (ranges.isEmpty()) {
for (String part : range.substring(6).split(",")) {
// Assuming a file with length of 100, the following examples returns bytes at:
// 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
long start = Range.sublong(part, 0, part.indexOf("-"));
long end = Range.sublong(part, part.indexOf("-") + 1, part.length());

if (start == -1) {
start = length - end;
end = length - 1;
} else if (end == -1 || end > length - 1) {
end = length - 1;
}

// Check if Range is syntactically valid. If not, then return 416.
if (start > end) {
response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return;
}

// Add range.
ranges.add(new Range(start, end, length));
}
}
}

// Prepare and initialize response --------------------------------------------------------

// Get content type by file name and set content disposition.
String disposition = "inline";

// If content type is unknown, then set the default value.
// For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
// To add new content types, add new mime-mapping entry in web.xml.
if (contentType == null) {
contentType = "application/octet-stream";
} else if (!contentType.startsWith("image")) {
// Else, expect for images, determine content disposition. If content type is supported by
// the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
String accept = request.getHeader("Accept");
disposition = accept != null && HttpUtils.accepts(accept, contentType) ? "inline" : "attachment";
}
logger.debug("Content-Type : {}", contentType);
// Initialize response.
response.reset();
response.setBufferSize(DEFAULT_BUFFER_SIZE);
response.setHeader("Content-Type", contentType);
response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
logger.debug("Content-Disposition : {}", disposition);
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("ETag", fileName);
response.setDateHeader("Last-Modified", lastModified);
response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME);

// Send requested file (part(s)) to client ------------------------------------------------

// Prepare streams.
try (InputStream input = new BufferedInputStream(Files.newInputStream(filepath));
OutputStream output = response.getOutputStream()) {

if (ranges.isEmpty() || ranges.get(0) == full) {

// Return full file.
logger.info("Return full file");
response.setContentType(contentType);
response.setHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total);
response.setHeader("Content-Length", String.valueOf(full.length));
Range.copy(input, output, length, full.start, full.length);

} else if (ranges.size() == 1) {

// Return single part of file.
Range r = ranges.get(0);
logger.info("Return 1 part of file : from ({}) to ({})", r.start, r.end);
response.setContentType(contentType);
response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
response.setHeader("Content-Length", String.valueOf(r.length));
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

// Copy single part range.
Range.copy(input, output, length, r.start, r.length);

} else {

// Return multiple parts of file.
response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

// Cast back to ServletOutputStream to get the easy println methods.
ServletOutputStream sos = (ServletOutputStream) output;

// Copy multi part range.
for (Range r : ranges) {
logger.info("Return multi part of file : from ({}) to ({})", r.start, r.end);
// Add multipart boundary and header fields for every range.
sos.println();
sos.println("--" + MULTIPART_BOUNDARY);
sos.println("Content-Type: " + contentType);
sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);

// Copy single part range of multi part range.
Range.copy(input, output, length, r.start, r.length);
}

// End with multipart boundary.
sos.println();
sos.println("--" + MULTIPART_BOUNDARY + "--");
}
}

}

private static class Range {
long start;
long end;
long length;
long total;

/**
* Construct a byte range.
* @param start Start of the byte range.
* @param end End of the byte range.
* @param total Total length of the byte source.
*/
public Range(long start, long end, long total) {
this.start = start;
this.end = end;
this.length = end - start + 1;
this.total = total;
}

public static long sublong(String value, int beginIndex, int endIndex) {
String substring = value.substring(beginIndex, endIndex);
return (substring.length() > 0) ? Long.parseLong(substring) : -1;
}

private static void copy(InputStream input, OutputStream output, long inputSize, long start, long length) throws IOException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int read;

if (inputSize == length) {
// Write full range.
while ((read = input.read(buffer)) > 0) {
output.write(buffer, 0, read);
output.flush();
}
} else {
input.skip(start);
long toRead = length;

while ((read = input.read(buffer)) > 0) {
if ((toRead -= read) > 0) {
output.write(buffer, 0, read);
output.flush();
} else {
output.write(buffer, 0, (int) toRead + read);
output.flush();
break;
}
}
}
}
}
private static class HttpUtils {

/**
* Returns true if the given accept header accepts the given value.
* @param acceptHeader The accept header.
* @param toAccept The value to be accepted.
* @return True if the given accept header accepts the given value.
*/
public static boolean accepts(String acceptHeader, String toAccept) {
String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
Arrays.sort(acceptValues);

return Arrays.binarySearch(acceptValues, toAccept) > -1
|| Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
|| Arrays.binarySearch(acceptValues, "*/*") > -1;
}

/**
* Returns true if the given match header matches the given value.
* @param matchHeader The match header.
* @param toMatch The value to be matched.
* @return True if the given match header matches the given value.
*/
public static boolean matches(String matchHeader, String toMatch) {
String[] matchValues = matchHeader.split("\\s*,\\s*");
Arrays.sort(matchValues);
return Arrays.binarySearch(matchValues, toMatch) > -1
|| Arrays.binarySearch(matchValues, "*") > -1;
}
}
}

像这样在 spring controller 中使用这个实现,

    @RequestMapping(value = "preview", method = RequestMethod.GET)
public void getPreview3(@RequestParam(value="name") String name, HttpServletResponse response,HttpServletRequest request) {
String path = "D://video/"+name;
try {
MultipartFileSender.fromFile(new File(path))
.with(request)
.with(response)
.serveResource();
} catch (Exception e) {
// TODO Auto-generated catch block
//e.printStackTrace();
}
}

在 Html 中使用这个,

<video controls preload="auto" poster="poster.jpg" >
<source src="./preview?name=test.mp4" type="video/mp4" />
</video>

关于html - 将视频从 Controller 流式传输到 html5 视频播放器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34825827/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com