This turned out to be an iOS specific issue. The range header is required to be implemented, and if you try to return the entire file content for the response of a range request, iOS will not make additional requests.
The following is the code I used:
try {
def rangeValue = request.getHeader("range")
log.debug("rangeValue: ${rangeValue}")
if (rangeValue != null) {
// Get start and end string, substring(6) removes "bytes="
def (start, end) = rangeValue.substring(6).split("-")
def startInt = start.toLong()
def endInt = end.toLong()
def fileSize = file.length()
response.reset()
response.setStatus(206)
response.setHeader("Accept-Ranges", "bytes")
// WARNING: Do not sent Content-length, as it appears to prevent videos from working in iOS
response.setHeader("Content-range", "bytes ${start}-${end}/"+Long.toString(fileSize))
response.setContentType("video/quicktime")
def bytes = new byte[endInt-startInt+1]
def inputStream = file.newInputStream()
// Skip to the point in the inputStream that the range is requesting
inputStream.skip(startInt)
// Read a chunk of the input stream into the bytes array
inputStream.read(bytes, 0, bytes.length)
response.outputStream << bytes
}
else {
response.outputStream << file.newInputStream()
}
} catch (ClientAbortException e) {
log.error("User aborted download")
}
There are a few important notes:
- If the
Content-length
header is returned in the response, iOS will not play the video. Seems like this could be related to content being gzipped - https://stackoverflow.com/a/2359184/2601060 - When using the
inputStream.read()
function, it will always start reading at the beginning of the stream, so make sure toskip()
to the proper position in the file (the startInt) - A response can be
reset()
to make sure that anything that has already been written is not included (this may not be required, but prevents automatic grails actions from providing default behavior)