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);
|
MediaFile file = getSingleFile(request);
|
||||||
boolean isSingleFile = file != null;
|
boolean isSingleFile = file != null;
|
||||||
HttpRange range = null;
|
HttpRange range = null;
|
||||||
|
Long fileLengthExpected = null;
|
||||||
|
|
||||||
if (isSingleFile) {
|
if (isSingleFile) {
|
||||||
|
|
||||||
@@ -153,39 +154,36 @@ public class StreamController {
|
|||||||
|
|
||||||
TranscodingService.Parameters parameters = transcodingService.getParameters(file, player, maxBitRate,
|
TranscodingService.Parameters parameters = transcodingService.getParameters(file, player, maxBitRate,
|
||||||
preferredTargetFormat, null);
|
preferredTargetFormat, null);
|
||||||
boolean isConversion = parameters.isDownsample() || parameters.isTranscode();
|
|
||||||
boolean estimateContentLength = ServletRequestUtils.getBooleanParameter(request,
|
|
||||||
"estimateContentLength", false);
|
|
||||||
boolean isHls = ServletRequestUtils.getBooleanParameter(request, "hls", false);
|
boolean isHls = ServletRequestUtils.getBooleanParameter(request, "hls", false);
|
||||||
|
fileLengthExpected = parameters.getExpectedLength();
|
||||||
|
|
||||||
// Wrangle response length and ranges.
|
// Wrangle response length and ranges.
|
||||||
//
|
//
|
||||||
// Support ranges as long as we're not transcoding; video is always assumed to transcode
|
// Support ranges as long as we're not transcoding blindly; video is always assumed to transcode
|
||||||
if (isConversion || file.isVideo()) {
|
if (file.isVideo() || ! parameters.isRangeAllowed()) {
|
||||||
// Use chunked transfer; do not accept range requests
|
// Use chunked transfer; do not accept range requests
|
||||||
response.setStatus(HttpServletResponse.SC_OK);
|
response.setStatus(HttpServletResponse.SC_OK);
|
||||||
response.setHeader("Accept-Ranges", "none");
|
response.setHeader("Accept-Ranges", "none");
|
||||||
} else {
|
} 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;
|
long contentLength;
|
||||||
|
|
||||||
// If range was requested, respond in kind
|
// If range was requested, respond in kind
|
||||||
range = getRange(request, file);
|
range = getRange(request, file.getDurationSeconds(), fileLengthExpected);
|
||||||
if (range != null) {
|
if (range != null) {
|
||||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||||
response.setHeader("Accept-Ranges", "bytes");
|
response.setHeader("Accept-Ranges", "bytes");
|
||||||
|
|
||||||
// Both ends are inclusive
|
// Both ends are inclusive
|
||||||
long startByte = range.getFirstBytePos();
|
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",
|
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;
|
contentLength = endByte + 1 - startByte;
|
||||||
} else {
|
} else {
|
||||||
// No range was requested, give back the whole file
|
// No range was requested, give back the whole file
|
||||||
response.setStatus(HttpServletResponse.SC_OK);
|
response.setStatus(HttpServletResponse.SC_OK);
|
||||||
contentLength = file.getFileSize();
|
contentLength = fileLengthExpected;
|
||||||
}
|
}
|
||||||
|
|
||||||
response.setIntHeader("ETag", file.getId());
|
response.setIntHeader("ETag", file.getId());
|
||||||
@@ -212,6 +210,10 @@ public class StreamController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileLengthExpected != null) {
|
||||||
|
LOG.info("Streaming request for [{}] with range [{}]", file.getPath(), response.getHeader("Content-Range"));
|
||||||
|
}
|
||||||
|
|
||||||
// Terminate any other streams to this player.
|
// Terminate any other streams to this player.
|
||||||
if (!isPodcast && !isSingleFile) {
|
if (!isPodcast && !isSingleFile) {
|
||||||
for (TransferStatus streamStatus : statusService.getStreamStatusesForPlayer(player)) {
|
for (TransferStatus streamStatus : statusService.getStreamStatusesForPlayer(player)) {
|
||||||
@@ -230,25 +232,37 @@ public class StreamController {
|
|||||||
) {
|
) {
|
||||||
final int BUFFER_SIZE = 2048;
|
final int BUFFER_SIZE = 2048;
|
||||||
byte[] buf = new byte[BUFFER_SIZE];
|
byte[] buf = new byte[BUFFER_SIZE];
|
||||||
|
long bytesWritten = 0;
|
||||||
|
|
||||||
while (!status.terminated()) {
|
while (!status.terminated()) {
|
||||||
if (player.getPlayQueue().getStatus() == PlayQueue.Status.STOPPED) {
|
if (player.getPlayQueue().getStatus() == PlayQueue.Status.STOPPED) {
|
||||||
if (isPodcast || isSingleFile) {
|
if (isPodcast || isSingleFile) {
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
sendDummy(buf, out);
|
sendDummyDelayed(buf, out);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
int n = in.read(buf);
|
int n = in.read(buf);
|
||||||
if (n == -1) {
|
if (n == -1) {
|
||||||
if (isPodcast || isSingleFile) {
|
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;
|
break;
|
||||||
} else {
|
} else {
|
||||||
sendDummy(buf, out);
|
sendDummyDelayed(buf, out);
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
out.write(buf, 0, n);
|
||||||
|
bytesWritten += n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,30 +335,8 @@ public class StreamController {
|
|||||||
return null;
|
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
|
@Nullable
|
||||||
private HttpRange getRange(HttpServletRequest request, MediaFile file) {
|
private HttpRange getRange(HttpServletRequest request, Integer fileDuration, Long fileSize) {
|
||||||
|
|
||||||
// First, look for "Range" HTTP header.
|
// First, look for "Range" HTTP header.
|
||||||
HttpRange range = HttpRange.valueOf(request.getHeader("Range"));
|
HttpRange range = HttpRange.valueOf(request.getHeader("Range"));
|
||||||
@@ -354,27 +346,25 @@ public class StreamController {
|
|||||||
|
|
||||||
// Second, look for "offsetSeconds" request parameter.
|
// Second, look for "offsetSeconds" request parameter.
|
||||||
String offsetSeconds = request.getParameter("offsetSeconds");
|
String offsetSeconds = request.getParameter("offsetSeconds");
|
||||||
range = parseAndConvertOffsetSeconds(offsetSeconds, file);
|
range = parseAndConvertOffsetSeconds(offsetSeconds, fileDuration, fileSize);
|
||||||
return range;
|
return range;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private HttpRange parseAndConvertOffsetSeconds(String offsetSeconds, MediaFile file) {
|
private HttpRange parseAndConvertOffsetSeconds(String offsetSeconds, Integer fileDuration, Long fileSize) {
|
||||||
if (offsetSeconds == null) {
|
if (offsetSeconds == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Integer duration = file.getDurationSeconds();
|
if (fileDuration == null || fileSize == null) {
|
||||||
Long fileSize = file.getFileSize();
|
|
||||||
if (duration == null || fileSize == null) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
float offset = Float.parseFloat(offsetSeconds);
|
float offset = Float.parseFloat(offsetSeconds);
|
||||||
|
|
||||||
// Convert from time offset to byte offset.
|
// Convert from time offset to byte offset.
|
||||||
long byteOffset = (long) (fileSize * (offset / duration));
|
long byteOffset = (long) (fileSize * (offset / fileDuration));
|
||||||
return new HttpRange(byteOffset, null);
|
return new HttpRange(byteOffset, null);
|
||||||
|
|
||||||
} catch (Exception x) {
|
} catch (Exception x) {
|
||||||
@@ -455,17 +445,28 @@ public class StreamController {
|
|||||||
return size + (size % 2);
|
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.
|
* 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 {
|
try {
|
||||||
Thread.sleep(2000);
|
Thread.sleep(2000);
|
||||||
} catch (InterruptedException x) {
|
} catch (InterruptedException x) {
|
||||||
LOG.warn("Interrupted in sleep.", x);
|
LOG.warn("Interrupted in sleep.", x);
|
||||||
}
|
}
|
||||||
Arrays.fill(buf, (byte) 0xFF);
|
sendDummy(buf, out, buf.length);
|
||||||
out.write(buf);
|
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,6 +216,8 @@ public class TranscodingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parameters.setMaxBitRate(maxBitRate);
|
parameters.setMaxBitRate(maxBitRate);
|
||||||
|
parameters.setExpectedLength(getExpectedLength(parameters));
|
||||||
|
parameters.setRangeAllowed(isRangeAllowed(parameters));
|
||||||
return parameters;
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +489,57 @@ public class TranscodingService {
|
|||||||
return matches != null && matches.length > 0;
|
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.
|
* Returns the directory in which all transcoders are installed.
|
||||||
*/
|
*/
|
||||||
@@ -517,6 +570,8 @@ public class TranscodingService {
|
|||||||
|
|
||||||
public static class Parameters {
|
public static class Parameters {
|
||||||
private boolean downsample;
|
private boolean downsample;
|
||||||
|
private Long expectedLength;
|
||||||
|
private boolean rangeAllowed;
|
||||||
private final MediaFile mediaFile;
|
private final MediaFile mediaFile;
|
||||||
private final VideoTranscodingSettings videoTranscodingSettings;
|
private final VideoTranscodingSettings videoTranscodingSettings;
|
||||||
private Integer maxBitRate;
|
private Integer maxBitRate;
|
||||||
@@ -543,6 +598,22 @@ public class TranscodingService {
|
|||||||
return transcoding != null;
|
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) {
|
public void setTranscoding(Transcoding transcoding) {
|
||||||
this.transcoding = transcoding;
|
this.transcoding = transcoding;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d197053e46e2ddf49f0230885d6fbc224a9d634be6462d965be256729e7fbdd2
|
oid sha256:608519910657cb59f64d904605794f9df97bf92a725d420f002548f825ec4524
|
||||||
size 10238118
|
size 10272000
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:685a5981d4098ff310c6a9b15587cbcb3878194014699d7f9b60013385ef9aed
|
oid sha256:6e67621051eba3527bed4d5a8b30cf676089837feaf1ce6919ce84068feb5789
|
||||||
size 5951995
|
size 6000000
|
||||||
|
|||||||
+1
-1
@@ -9,5 +9,5 @@ Feature: Stream API for FLAC
|
|||||||
When A stream is consumed
|
When A stream is consumed
|
||||||
Then Print debug output
|
Then Print debug output
|
||||||
Then The response bytes are equal
|
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
|
When A stream is consumed
|
||||||
Then Print debug output
|
Then Print debug output
|
||||||
Then The response bytes are equal
|
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