[英]Jersey REST support resume/media streaming
I need to support resume on Jersey REST, I'm trying to do it this way: 我需要支持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;
}
}
I want to be able stream mp3 so the browser can seek the music, but in safari it still not working. 我希望能够流式播放MP3,以便浏览器可以搜索音乐,但在Safari中它仍然无法正常工作。 any ideas? 有任何想法吗?
Here is my take based on the solution provided here . 以下是基于此处提供的解决方案的内容。 It works correctly on different browsers. 它在不同的浏览器上正常工作。 I am able to seek the music just fine in Safari and other browsers as well. 我也可以在Safari和其他浏览器中搜索音乐。 You can find the sample project on my Github repository which has more details. 您可以在我的Github 存储库中找到具有更多详细信息的示例项目。 Chrome and Safari nicely leverages the range headers to stream media and you can see it in the request/response trace. 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();
}
Here is the MediaStreamer implementation, which is used to stream the output in your resource method. 这是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;
}
}
Since I was facing the same problema, I tried a more general solution[1] with a ContainerResponseFilter
which will trigger when a Range
header is present in the request and will work seamlessly with any media-type, entity and resource methods. 由于我遇到了同样的问题,我尝试了一个更通用的解决方案[1],其中包含一个ContainerResponseFilter
,它将在请求中出现Range
标头时触发,并且可以与任何媒体类型,实体和资源方法无缝地协作。
This is the ContainerResponseFilter
which will use encapsulate the output stream in a RangedOutputStream
(see below): 这是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);
}
}
}
And here's the 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 [1] https://github.com/heruan/jersey-range-filter
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.