Jersey REST支持恢复/媒体流传输

18
我需要在Jersey REST上支持简历,我正在尝试这样做:
@Path("/helloworld")
public class RestServer {

@GET

@Path("say")
@Produces("audio/mp3")
public Response getMessage(@HeaderParam("Range") String r ) throws IOException{
    String str="/Users/dima/Music/crazy_town_-_butterfly.mp3";

    System.out.println(r);
    RandomAccessFile f=new RandomAccessFile(str, "r");

    int off=0;
    int to=(int)f.length();
    byte[] data ;
    if(r!=null){
        String from=r.split("=")[1].split("-")[0];
        String t=r.split("=")[1].split("-")[1];
        off=Integer.parseInt(from);
        to=Integer.parseInt(t);

    }
    data= new byte[to-off];
    f.readFully(data, off, to-off);

    ResponseBuilder res=Response.ok(data)
            .header("Accept-Ranges","bytes")
            .header("Content-Range:", "bytes "+off+"-"+to+"/"+data.length)
            .header("Pragma", "no-cache");;

            if(r==null){
                res=res.header("Content-Length", data.length);
            }
            f.close();

            Response ans=res.build();

            return ans;


}
}

我希望能够流式传输mp3,以便浏览器可以寻找音乐,但在Safari中仍然无法正常工作。有什么想法吗?

2个回答

25

基于提供这里的解决方案,这是我的看法。在不同浏览器上都能正常工作。我可以在Safari和其他浏览器中很好地寻找音乐。你可以在我的Github仓库中找到样例项目以获取更多细节。Chrome和Safari很好地利用了范围标头来流媒体传输,并且可以在请求/响应跟踪中看到它。

    @GET
    @Produces("audio/mp3")
    public Response streamAudio(@HeaderParam("Range") String range) throws Exception {
        return buildStream(audio, range);
    }

    private Response buildStream(final File asset, final String range) throws Exception {
        // range not requested : Firefox, Opera, IE do not send range headers
        if (range == null) {
            StreamingOutput streamer = new StreamingOutput() {
                @Override
                public void write(final OutputStream output) throws IOException, WebApplicationException {

                    final FileChannel inputChannel = new FileInputStream(asset).getChannel();
                    final WritableByteChannel outputChannel = Channels.newChannel(output);
                    try {
                        inputChannel.transferTo(0, inputChannel.size(), outputChannel);
                    } finally {
                        // closing the channels
                        inputChannel.close();
                        outputChannel.close();
                    }
                }
            };
            return Response.ok(streamer).header(HttpHeaders.CONTENT_LENGTH, asset.length()).build();
        }

        String[] ranges = range.split("=")[1].split("-");
        final int from = Integer.parseInt(ranges[0]);
        /**
         * Chunk media if the range upper bound is unspecified. Chrome sends "bytes=0-"
         */
        int to = chunk_size + from;
        if (to >= asset.length()) {
            to = (int) (asset.length() - 1);
        }
        if (ranges.length == 2) {
            to = Integer.parseInt(ranges[1]);
        }

        final String responseRange = String.format("bytes %d-%d/%d", from, to, asset.length());
        final RandomAccessFile raf = new RandomAccessFile(asset, "r");
        raf.seek(from);

        final int len = to - from + 1;
        final MediaStreamer streamer = new MediaStreamer(len, raf);
        Response.ResponseBuilder res = Response.status(Status.PARTIAL_CONTENT).entity(streamer)
                .header("Accept-Ranges", "bytes")
                .header("Content-Range", responseRange)
                .header(HttpHeaders.CONTENT_LENGTH, streamer.getLenth())
                .header(HttpHeaders.LAST_MODIFIED, new Date(asset.lastModified()));
        return res.build();
    }

这里是MediaStreamer实现,它用于在资源方法中流式传输输出。

public class MediaStreamer implements StreamingOutput {

    private int length;
    private RandomAccessFile raf;
    final byte[] buf = new byte[4096];

    public MediaStreamer(int length, RandomAccessFile raf) {
        this.length = length;
        this.raf = raf;
    }

    @Override
    public void write(OutputStream outputStream) throws IOException, WebApplicationException {
        try {
            while( length != 0) {
                int read = raf.read(buf, 0, buf.length > length ? length : buf.length);
                outputStream.write(buf, 0, read);
                length -= read;
            }
        } finally {
            raf.close();
        }
    }

    public int getLenth() {
        return length;
    }
}

2
顺便问一下,chunk_size应该设置为什么值? - Dima
1
我在这里的示例中使用了1MB的块 https://github.com/aruld/jersey-streaming/blob/master/src/main/java/com/aruld/jersey/streaming/MediaResource.java , 当客户端没有发送范围上限时,我们决定将媒体分成几个块进行传输。 - Arul Dhesiaseelan
天啊,我希望我能在Jetty上用这个。我不知道如何转换它。 - dessalines
1
请查看使用Jetty的jersey2 分支 - Arul Dhesiaseelan
@ArulDhesiaseelan:我在这里有一个类似的问题:http://stackoverflow.com/questions/31070137/how-to-do-server-side-responses-for-http-ranges?。如果您能分享一下您的想法,那就太好了,因为我也在使用Jersey。 - carlspring

5

我曾遇到同样的问题,尝试了一个更通用的解决方案[1],使用ContainerResponseFilter。当请求中有Range头时,该过滤器将触发,并且可以无缝地与任何媒体类型、实体和资源方法配合使用。

这是ContainerResponseFilter,它将使用RangedOutputStream(见下文)封装输出流:

public class RangeResponseFilter implements ContainerResponseFilter {

    private static final String RANGE = "Range";

    private static final String ACCEPT_RANGES = "Accept-Ranges";

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
            throws IOException {
        if (requestContext.getHeaders().containsKey(RANGE)) {
            String rangeHeader = requestContext.getHeaderString(RANGE);
            String contentType = responseContext.getMediaType().toString();
            OutputStream originOutputStream = responseContext.getEntityStream();
            RangedOutputStream rangedOutputStream = new RangedOutputStream(originOutputStream, rangeHeader, contentType, responseContext.getHeaders());
            responseContext.setStatus(Status.PARTIAL_CONTENT.getStatusCode());
            responseContext.getHeaders().putSingle(ACCEPT_RANGES, rangedOutputStream.getAcceptRanges());
            responseContext.setEntityStream(rangedOutputStream);
        }
    }

}

这里是RangedOutputStream

public class RangedOutputStream extends OutputStream {

    public class Range extends OutputStream {

        private ByteArrayOutputStream outputStream;

        private Integer from;

        private Integer to;

        public Range(Integer from, Integer to) {
            this.outputStream = new ByteArrayOutputStream();
            this.from = from;
            this.to = to;
        }

        public boolean contains(Integer i) {
            if (this.to == null) {
                return (this.from <= i);
            }
            return (this.from <= i && i <= this.to);
        }

        public byte[] getBytes() {
            return this.outputStream.toByteArray();
        }

        public Integer getFrom() {
            return this.from;
        }

        public Integer getTo(Integer ifNull) {
            return this.to == null ? ifNull : this.to;
        }

        @Override
        public void write(int b) throws IOException {
            this.outputStream.write(b);
        }

    }

    private final static char[] MULTIPART_CHARS = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
            .toCharArray();

    private static final String BOUNDARY_LINE_FORMAT = "--%s";

    private static final String CONTENT_TYPE_LINE_FORMAT = "Content-Type: %s";

    private static final String CONTENT_RANGE_FORMAT = "%s %d-%d/%d";

    private static final String CONTENT_RANGE_LINE_FORMAT = "Content-Range: " + CONTENT_RANGE_FORMAT;

    private static final String EMPTY_LINE = "\r\n";

    private OutputStream outputStream;

    private String boundary;

    private String accept;

    private String contentType;

    private boolean multipart;

    private boolean flushed = false;

    private int pos = 0;

    List<Range> ranges;

    MultivaluedMap<String, Object> headers;

    public RangedOutputStream(OutputStream outputStream, String ranges, String contentType, MultivaluedMap<String, Object> headers) {
        this.outputStream = outputStream;
        this.ranges = new ArrayList<>();
        String[] acceptRanges = ranges.split("=");
        this.accept = acceptRanges[0];
        for (String range : acceptRanges[1].split(",")) {
            String[] bounds = range.split("-");
            this.ranges.add(new Range(Integer.valueOf(bounds[0]), bounds.length == 2 ? Integer.valueOf(bounds[1]) : null ));
        }
        this.headers = headers;
        this.contentType = contentType;
        this.multipart = this.ranges.size() > 1;
        this.boundary = this.generateBoundary();
    }

    private String generateBoundary() {
        StringBuilder buffer = new StringBuilder();
        Random rand = new Random();
        int count = rand.nextInt(11) + 30;
        for (int i = 0; i < count; i++) {
            buffer.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
        }
        return buffer.toString();
    }

    public boolean isMultipart() {
        return this.multipart;
    }

    public String getBoundary() {
        return this.boundary;
    }

    public String getAcceptRanges() {
        return this.accept;
    }

    public String getContentRange(int index) {
        Range range = this.ranges.get(index);
        return String.format(CONTENT_RANGE_LINE_FORMAT, this.accept, range.getFrom(), range.getTo(this.pos), this.pos);
    }

    @Override
    public void write(int b) throws IOException {
        for (Range range : this.ranges) {
            if (range.contains(this.pos)) {
                range.write(b);
            }
        }
        this.pos++;
    }

    @Override
    public void flush() throws IOException {
        if (this.flushed) {
            return;
        }
        if (this.multipart) {
            this.headers.putSingle(HttpHeaders.CONTENT_TYPE, String.format("multipart/byteranges; boundary=%s", this.boundary));
            for (Range range : this.ranges) {
                this.outputStream.write(String.format(BOUNDARY_LINE_FORMAT + EMPTY_LINE, this.boundary).getBytes());
                this.outputStream.write(String.format(CONTENT_TYPE_LINE_FORMAT + EMPTY_LINE, this.contentType).getBytes());
                this.outputStream.write(
                        String.format(CONTENT_RANGE_LINE_FORMAT + EMPTY_LINE, this.accept, range.getFrom(), range.getTo(this.pos), this.pos)
                                .getBytes());
                this.outputStream.write(EMPTY_LINE.getBytes());
                this.outputStream.write(range.getBytes());
                this.outputStream.write(EMPTY_LINE.getBytes());
            }
            this.outputStream.write(String.format(BOUNDARY_LINE_FORMAT, this.boundary + "--").getBytes());
        } else {
            Range range = this.ranges.get(0);
            this.headers.putSingle("Content-Range", String.format(CONTENT_RANGE_FORMAT, this.accept, range.getFrom(), range.getTo(this.pos), this.pos));
            this.outputStream.write(range.getBytes());
        }
        this.flushed = true;
    }

}

[1] https://github.com/heruan/jersey-range-filter


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