Play Framework 2.3.x ByteChunks MP3 streaming has

2019-04-02 11:14发布

问题:

Using Play Framework (version 2.3.x) (Java style), I am trying to serve an .mp3 file to the browser. Since it is a 'large' file I have decided to go with Play's ByteChunks Object, as follows.

@With(MP3Headers.class)
public static Result test() {

    Chunks<byte[]> chunks = new ByteChunks() {
        public void onReady(Chunks.Out<byte[]> out) {
            try {
                byte[] song = Files.readAllBytes(Paths.get("public/mp3/song.mp3"));
                out.write(song);
            } catch(Exception e) {
                e.printStackTrace();
            } finally {
                out.close();
            }
        }
    };

    return ok(chunks);
}

For clarification, my Mp3Headers file, which is responsable for setting the headers so that the browser knows what type the payload has:

public class MP3Headers extends Action.Simple {

    public Promise<Result> call(Http.Context ctx) throws Throwable {
        ctx.response().setContentType("audio/mpeg");
        return delegate.call(ctx);
    }
}

For completion, my routes file:

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                           controllers.Application.index()

GET     /test                       controllers.Application.test()

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.at(path="/public", file) 

As is to be expected, navigating to localhost:9000/test renders to a nice HTML5 audio player (see picture).

The problem I have is that 'scrolling' in the audio player does not work. If I do scroll, the music pauses, and when I let go (when I 'chose' a position in time), it continues where it first paused.

I hope that I make sense, and I hope that you guys know something more about this. Thanks in advance.

回答1:

You will need to tell your browser that your server support range requests and implement the ranges responses (ie just provide the part of the music the browser needs). You can get an overview of the request/response cycle in this answer.

@With(MP3Headers.class)
public static Result test() {
    final int begin, end;
    final boolean isRangeReq;
    response().setHeader("Accept-Ranges", "bytes");
    if (request().hasHeader("RANGE")) {
        isRangeReq = true;
        String[] range = request().getHeader("RANGE").split("=")[1].split("-");
        begin = Integer.parseInt(range[0]);
        if (range.length > 1) {
            end = Integer.parseInt(range[1]);
        } else {
            end = song.length-1;
        }
        response().setHeader("Content-Range", String.format("bytes %d-%d/%d", begin, end, song.length));
    } else {
        isRangeReq = false;
        begin = 0;
        end = song.length - 1;
    }

    Chunks<byte[]> chunks = new ByteChunks() {
        public void onReady(Chunks.Out<byte[]> out) {
            if(isRangeReq) {
                out.write(Arrays.copyOfRange(song, begin, end));
            } else {
                out.write(song);
            }
            out.close();
        }
    };
    response().setHeader("Content-Length", (end - begin + 1) + "");
    if (isRangeReq) {
        return status(206, chunks);
    } else {
        return status(200, chunks);
    }
}

Note that in this code the song was already loaded in song. Also the parsing of the RANGE header is very dirty (you can get values like RANGE:)



回答2:

I Found this code very easy implementation.

Put the below action and its private helper method in your controller.

Controller Action

public static Result file(Long id, String filename) throws IOException {

Item item = Item.fetch(id);
File file = item.getFile();

if(file== null || !file.exists()) {
  Logger.error("File no longer exist item"+id+" filename:"+filename);           
  return notFound();
}

String rangeheader = request().getHeader(RANGE);
if(rangeheader != null) {

  String[] split = rangeheader.substring("bytes=".length()).split("-");
  if(Logger.isDebugEnabled()) { Logger.debug("Range header is:"+rangeheader); }

  if(split.length == 1) {
    long start = Long.parseLong(split[0]);
    long length = file.length()-1l;             
    return stream(start, length, file);                             
  } else {
    long start = Long.parseLong(split[0]);
    long length = Long.parseLong(split[1]);
    return stream(start, length, file);
  }
}

// if no streaming is required we simply return the file as a 200 OK
if(Play.isProd()) {
  response().setHeader("Cache-Control", "max-age=3600, must-revalidate");           
}
return ok(file);
}

Stream Helper method

private static Result stream(long start, long length, File file) throws IOException {

FileInputStream fis = new FileInputStream(file);
fis.skip(start);
response().setContentType(MimeTypes.forExtension("mp4").get());
response().setHeader(CONTENT_LENGTH, ((length - start) +1l)+"");
response().setHeader(CONTENT_RANGE, String.format("bytes %d-%d/%d", start, length,file.length()));

response().setHeader(ACCEPT_RANGES, "bytes");
response().setHeader(CONNECTION, "keep-alive");
return status(PARTIAL_CONTENT, fis);
}   

Complete example link is here Byte range requests in Play 2 Java Controllers