Merge pull request #1123 from eharris/seekable-transcodes

Change back to allow transcodes to allow seeking
master
François-Xavier Thomas 4 years ago committed by GitHub
commit 70de4c8f25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 91
      airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java
  2. 71
      airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java
  3. 4
      integration-test/src/test/resources/blobs/stream/dance/responses/1.dat
  4. 4
      integration-test/src/test/resources/blobs/stream/dead/responses/1.dat
  5. 2
      integration-test/src/test/resources/features/api/stream-flac-as-mp3.feature
  6. 2
      integration-test/src/test/resources/features/api/stream-m4a-as-mp3.feature

@ -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()) {
// Support ranges as long as we're not transcoding blindly; video is always assumed to transcode
if (file.isVideo() || ! parameters.isRangeAllowed()) {
// 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
// Partial content permitted because either know or expect to be able to predict 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,37 @@ 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 {
if (fileLengthExpected != null && bytesWritten <= fileLengthExpected
&& bytesWritten + n > fileLengthExpected) {
LOG.warn("Stream output exceeded expected length of {}. It is likely that "
+ "the transcoder is not adhering to the bitrate limit or the media "
+ "source is corrupted or has grown larger", fileLengthExpected);
}
out.write(buf, 0, n);
bytesWritten += n;
}
}
}
@ -321,30 +335,8 @@ public class StreamController {
return null;
}
private long getFileLength(TranscodingService.Parameters parameters) {
MediaFile file = parameters.getMediaFile();
if (!parameters.isDownsample() && !parameters.isTranscode()) {
return file.getFileSize();
}
Integer duration = file.getDurationSeconds();
Integer maxBitRate = parameters.getMaxBitRate();
if (duration == null) {
LOG.warn("Unknown duration for " + file + ". Unable to estimate transcoded size.");
return file.getFileSize();
}
if (maxBitRate == null) {
LOG.error("Unknown bit rate for " + file + ". Unable to estimate transcoded size.");
return file.getFileSize();
}
return duration * (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 +346,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 +445,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();
}
}

@ -216,6 +216,8 @@ public class TranscodingService {
}
parameters.setMaxBitRate(maxBitRate);
parameters.setExpectedLength(getExpectedLength(parameters));
parameters.setRangeAllowed(isRangeAllowed(parameters));
return parameters;
}
@ -487,6 +489,57 @@ public class TranscodingService {
return matches != null && matches.length > 0;
}
/**
* Returns the length (or predicted/expected length) of a (possibly padded) media stream
*/
private Long getExpectedLength(Parameters parameters) {
MediaFile file = parameters.getMediaFile();
if (!parameters.isDownsample() && !parameters.isTranscode()) {
return file.getFileSize();
}
Integer duration = file.getDurationSeconds();
Integer maxBitRate = parameters.getMaxBitRate();
if (duration == null) {
LOG.warn("Unknown duration for " + file + ". Unable to estimate transcoded size.");
return null;
}
if (maxBitRate == null) {
LOG.error("Unknown bit rate for " + file + ". Unable to estimate transcoded size.");
return null;
}
// 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;
}
private boolean isRangeAllowed(Parameters parameters) {
Transcoding transcoding = parameters.getTranscoding();
List<String> steps = Arrays.asList();
if (transcoding != null) {
steps = Arrays.asList(transcoding.getStep3(), transcoding.getStep2(), transcoding.getStep1());
} else if (parameters.isDownsample()) {
steps = Arrays.asList(settingsService.getDownsamplingCommand());
} else {
return true; // neither transcoding nor downsampling
}
// Verify that were able to predict the length
if (parameters.getExpectedLength() == null) {
return false;
}
// Check if last configured step uses the bitrate, if so, range should be pretty safe
for (String step : steps) {
if (step != null) {
return step.contains("%b");
}
}
return false;
}
/**
* Returns the directory in which all transcoders are installed.
*/
@ -517,6 +570,8 @@ public class TranscodingService {
public static class Parameters {
private boolean downsample;
private Long expectedLength;
private boolean rangeAllowed;
private final MediaFile mediaFile;
private final VideoTranscodingSettings videoTranscodingSettings;
private Integer maxBitRate;
@ -543,6 +598,22 @@ public class TranscodingService {
return transcoding != null;
}
public boolean isRangeAllowed() {
return this.rangeAllowed;
}
public void setRangeAllowed(boolean rangeAllowed) {
this.rangeAllowed = rangeAllowed;
}
public Long getExpectedLength() {
return this.expectedLength;
}
public void setExpectedLength(Long expectedLength) {
this.expectedLength = expectedLength;
}
public void setTranscoding(Transcoding transcoding) {
this.transcoding = transcoding;
}

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d197053e46e2ddf49f0230885d6fbc224a9d634be6462d965be256729e7fbdd2
size 10238118
oid sha256:608519910657cb59f64d904605794f9df97bf92a725d420f002548f825ec4524
size 10272000

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:685a5981d4098ff310c6a9b15587cbcb3878194014699d7f9b60013385ef9aed
size 5951995
oid sha256:6e67621051eba3527bed4d5a8b30cf676089837feaf1ce6919ce84068feb5789
size 6000000

@ -9,5 +9,5 @@ Feature: Stream API for FLAC
When A stream is consumed
Then Print debug output
Then The response bytes are equal
Then The length headers are absent
Then The length headers are correct

@ -9,5 +9,5 @@ Feature: Stream API for VBR M4A
When A stream is consumed
Then Print debug output
Then The response bytes are equal
Then The length headers are absent
Then The length headers are correct
Loading…
Cancel
Save