Merge pull request #1123 from eharris/seekable-transcodes
Change back to allow transcodes to allow seeking
This commit is contained in:
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user