ios - 请求spring mvc服务器资源在iOS上无法播放mp4,但在Nginx上可以直接访问mp4文件

标签 ios spring-mvc nginx mp4

我在 iOS 设备上播放 mp4 文件时有两种情况。

Nginx 的 MP4 访问正常: 将mp4文件放在/html下,使用如下配置,iOS设备和chrome浏览器就可以正常播放mp4文件了。

server {
  listen 127.0.0.1:80 default_server;
  location ~ ^/storage\/*.mp4 {  
    root html;
  }
}

Tomcat Spring MVC 无法访问 MP4: 当我通过 Spring mvc restful API 请求并返回包含资源对象的 ResponseEntity 时,chrome 浏览器将正确接收和播放 mp4。但是 iOS 设备不工作

@GetMapping(value = "/storage/{filename:.+}")
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> accessStorageFile(HttpServletResponse response, @PathVariable String filename) throws IOException {
    org.springframework.core.io.Resource resource = storageUtil.loadAsResource(filename);
    InputStream inputStream = resource.getInputStream();
    InputStreamResource inputStreamResource = new InputStreamResource(inputStream);
    String contentType = FileTypeMap.getDefaultFileTypeMap().getContentType(resource.getFile());
    contentType = resource.getFilename().contains(".mp4") ? "video/mp4" : contentType;
    response.setContentType(contentType);
    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.valueOf(contentType));
    responseHeaders.setContentLength(resource.getFile().length());
    return new ResponseEntity<>(inputStreamResource, responseHeaders, HttpStatus.OK);
}

最佳答案

import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; 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; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.util.StringUtils;

/** * * @author David 1 */ public class MultipartFileSender {

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)) {
        System.out.println("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";
    }
    System.out.println("Content-Type : {" + contentType + "}");
    // Initialize response.
    response.reset();
    response.setBufferSize(DEFAULT_BUFFER_SIZE);
    response.setHeader("Content-Type", contentType);
    response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
    System.out.println("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.
            System.out.println("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);
            System.out.println("Return 1 part of file : from ({" + r.start + "}) to ({" + 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) {
                System.out.println("Return multi part of file : from ({" + r.start + "}) to ({" + 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;
    }
} }

在你的 Controller 中使用并确保你没有使用@RestController 你应该使用@Controller,并在 void 方法中使用下面的类 @RequestMapping(method = RequestMethod.GET, value = "/getFile") public void getFile(HttpServletRequest httpRequest, HttpServletResponse httpResponse) 抛出异常{ MultipartFileSender.fromPath(Paths.get("D:\8561523973120206.mp4")) .with(http请求) .with(httpResponse) .serveResource();

关于ios - 请求spring mvc服务器资源在iOS上无法播放mp4,但在Nginx上可以直接访问mp4文件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49568111/

相关文章:

javascript - ionic 2 声音在加载音频时停止背景音乐

jquery - 多个表单、一次保存调用和多个同名输入元素

jsp - 如何在 spring mvc 3 中删除缓存(后退按钮和刷新应该不起作用)

tomcat - Nginx 到 tomcat SSL 连接 |没有共同的密码套件

ios - Swift Cocoa 按钮未到位

ios - iTunes Connect - 如何获得大小合适的屏幕截图?

ios - 如何异步绘制InRect

hibernate - 如何处理 Spring Data JPA 中 ID 数组的批量删除?

nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use) on docker

Python、uWSGI、NGINX、FLASK 适用于 WiFi,不适用于蜂窝网络