From e691c807741b304ff8df750d4898d3abf3a83ea7 Mon Sep 17 00:00:00 2001 From: Evan Harris Date: Tue, 11 Jun 2019 05:32:10 -0500 Subject: [PATCH] Change back to allow transcodes to allow seeking This switches back to allowing range rather than chunked requests so that seeking works in the web player, but does so by safer means than previous solutions, by slightly over-estimating the transcoded size, then sending dummy bytes to the client to fill any gap. Fixes #1117. Addresses #685. --- .../player/controller/StreamController.java | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java index 3207af01..b0070fb7 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java @@ -128,6 +128,7 @@ public class StreamController { MediaFile file = getSingleFile(request); boolean isSingleFile = file != null; HttpRange range = null; + Long fileLengthExpected = null; if (isSingleFile) { @@ -153,39 +154,36 @@ public class StreamController { TranscodingService.Parameters parameters = transcodingService.getParameters(file, player, maxBitRate, preferredTargetFormat, null); - boolean isConversion = parameters.isDownsample() || parameters.isTranscode(); - boolean estimateContentLength = ServletRequestUtils.getBooleanParameter(request, - "estimateContentLength", false); boolean isHls = ServletRequestUtils.getBooleanParameter(request, "hls", false); + fileLengthExpected = parameters.getExpectedLength(); // Wrangle response length and ranges. // // Support ranges as long as we're not transcoding; video is always assumed to transcode - if (isConversion || file.isVideo()) { + if (file.isVideo()) { // Use chunked transfer; do not accept range requests response.setStatus(HttpServletResponse.SC_OK); response.setHeader("Accept-Ranges", "none"); } else { // Not transcoding, partial content permitted because we know the final size long contentLength; - // If range was requested, respond in kind - range = getRange(request, file); + range = getRange(request, file.getDurationSeconds(), fileLengthExpected); if (range != null) { response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); response.setHeader("Accept-Ranges", "bytes"); // Both ends are inclusive long startByte = range.getFirstBytePos(); - long endByte = range.isClosed() ? range.getLastBytePos() : file.getFileSize() - 1; + long endByte = range.isClosed() ? range.getLastBytePos() : fileLengthExpected - 1; response.setHeader("Content-Range", - String.format("bytes %d-%d/%d", startByte, endByte, file.getFileSize())); + String.format("bytes %d-%d/%d", startByte, endByte, fileLengthExpected)); contentLength = endByte + 1 - startByte; } else { // No range was requested, give back the whole file response.setStatus(HttpServletResponse.SC_OK); - contentLength = file.getFileSize(); + contentLength = fileLengthExpected; } response.setIntHeader("ETag", file.getId()); @@ -212,6 +210,10 @@ public class StreamController { return; } + if (fileLengthExpected != null) { + LOG.info("Streaming request for [{}] with range [{}]", file.getPath(), response.getHeader("Content-Range")); + } + // Terminate any other streams to this player. if (!isPodcast && !isSingleFile) { for (TransferStatus streamStatus : statusService.getStreamStatusesForPlayer(player)) { @@ -230,25 +232,31 @@ public class StreamController { ) { final int BUFFER_SIZE = 2048; byte[] buf = new byte[BUFFER_SIZE]; + long bytesWritten = 0; while (!status.terminated()) { if (player.getPlayQueue().getStatus() == PlayQueue.Status.STOPPED) { if (isPodcast || isSingleFile) { break; } else { - sendDummy(buf, out); + sendDummyDelayed(buf, out); } } else { int n = in.read(buf); if (n == -1) { if (isPodcast || isSingleFile) { + // Pad the output if needed to avoid content length errors on transcodes + if (fileLengthExpected != null && bytesWritten < fileLengthExpected) { + sendDummy(buf, out, fileLengthExpected - bytesWritten); + } break; } else { - sendDummy(buf, out); + sendDummyDelayed(buf, out); } } else { out.write(buf, 0, n); + bytesWritten += n; } } } @@ -340,11 +348,12 @@ public class StreamController { return file.getFileSize(); } - return duration * (long)maxBitRate * 1000L / 8L; + // Over-estimate size a bit (2 seconds) so don't cut off early in case of small calculation differences + return (duration + 2) * (long)maxBitRate * 1000L / 8L; } @Nullable - private HttpRange getRange(HttpServletRequest request, MediaFile file) { + private HttpRange getRange(HttpServletRequest request, Integer fileDuration, Long fileSize) { // First, look for "Range" HTTP header. HttpRange range = HttpRange.valueOf(request.getHeader("Range")); @@ -354,27 +363,25 @@ public class StreamController { // Second, look for "offsetSeconds" request parameter. String offsetSeconds = request.getParameter("offsetSeconds"); - range = parseAndConvertOffsetSeconds(offsetSeconds, file); + range = parseAndConvertOffsetSeconds(offsetSeconds, fileDuration, fileSize); return range; } @Nullable - private HttpRange parseAndConvertOffsetSeconds(String offsetSeconds, MediaFile file) { + private HttpRange parseAndConvertOffsetSeconds(String offsetSeconds, Integer fileDuration, Long fileSize) { if (offsetSeconds == null) { return null; } try { - Integer duration = file.getDurationSeconds(); - Long fileSize = file.getFileSize(); - if (duration == null || fileSize == null) { + if (fileDuration == null || fileSize == null) { return null; } float offset = Float.parseFloat(offsetSeconds); // Convert from time offset to byte offset. - long byteOffset = (long) (fileSize * (offset / duration)); + long byteOffset = (long) (fileSize * (offset / fileDuration)); return new HttpRange(byteOffset, null); } catch (Exception x) { @@ -455,17 +462,28 @@ public class StreamController { return size + (size % 2); } + private void sendDummy(byte[] buf, OutputStream out, long len) throws IOException { + long bytesWritten = 0; + int n; + + Arrays.fill(buf, (byte) 0xFF); + while (bytesWritten < len) { + n = (int) Math.min(buf.length, len - bytesWritten); + out.write(buf, 0, n); + bytesWritten += n; + } + } + /** * Feed the other end with some dummy data to keep it from reconnecting. */ - private void sendDummy(byte[] buf, OutputStream out) throws IOException { + private void sendDummyDelayed(byte[] buf, OutputStream out) throws IOException { try { Thread.sleep(2000); } catch (InterruptedException x) { LOG.warn("Interrupted in sleep.", x); } - Arrays.fill(buf, (byte) 0xFF); - out.write(buf); + sendDummy(buf, out, buf.length); out.flush(); } }