I need to support resume on Jersey REST, I'm trying to do it this way:
@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. 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. You can find the sample project on my Github repository which has more details. Chrome and Safari nicely leverages the range headers to stream media and you can see it in the request/response trace.
@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.
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.
This is the ContainerResponseFilter
which will use encapsulate the output stream in a RangedOutputStream
(see below):
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
:
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