Grails (iOS specific): Returning video (mp4) file gives Broken Pipe exception (getOutputStream() has already been called for this response)

StackOverflow https://stackoverflow.com/questions/23071164

  •  03-07-2023
  •  | 
  •  

Domanda

I'm trying to return an mp4 file from my Grails controller so that it can be played in the browser. The following is the simplest version of what I have:

def file = new File(<path to mp4 file>)
response.outputStream << file.newInputStream()

The strange thing is that this works when hitting it from a desktop (Chrome on my MacBook), works on an Android phone, but does not work on an iPad Air.

The one header that's different in the iOS request is for "range" of "0-1", but it looks like that might not be causing a problem (tested by adding that request on my laptop).

The exception says:

ERROR errors.GrailsExceptionResolver - SocketException occurred when processing request: [GET]

and further down it says

getOutputStream() has already been called for this response.

I've found many others with similar errors, but they talk about webRequest.setRenderView(false), flushing and closing the outputstream, and many other options. I've tried all of those, but nothing seems to work.

The part that really gets me is that it works on everything except iOS.

Any thoughts would be greatly appreciated. Thanks in advance!

UPDATE 1

Per Graeme's answer below, the accept header from Chrome is:

accept -> text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

And iOS produces multiple requests, which have the following accept headers:

accept -> text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
accept -> */*

The second accept header, */* is the what occurs during the exception.

I have also created a JIRA issue for Grails: http://jira.grails.org/browse/GRAILS-11325

È stato utile?

Soluzione 2

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:

  1. 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
  2. When using the inputStream.read() function, it will always start reading at the beginning of the stream, so make sure to skip() to the proper position in the file (the startInt)
  3. 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)

Altri suggerimenti

Might be related to the Accept header that gets sent, as Grails has some parsing depending on the Accept header. If you could post an example in a JIRA with steps to reproduce that would help.

http://jira.grails.org/browse/GRAILS

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top