4

I'm attempting to create a controller method that serves a video file backed by some CMS-like database entry. My controller method looks like this:

def getVideo(id: Int) = DBAction { request => implicit dbSession =>
  { for {
      dbFile <- fetchDBFile(id)
      fsFile <- fetchFilesystemFile(dbFile)
      rangeOpt <- request.headers.get(RANGE).map(_.replaceAll("bytes=", "").split("-").toList match {
                                case rangeStart :: rangeEnd :: Nil => Some(rangeStart.toLong, rangeEnd.toLong)
                                case rangeStart :: Nil => Some(rangeStart.toLong, fsFile.length())
                                case _ => None
                              })
      (rangeStart, rangeEnd) <- rangeOpt
    } yield SimpleResult(
            header = ResponseHeader(
              status = PARTIAL_CONTENT,
              headers = Map(
                CONTENT_TYPE -> MimeTypes.forExtension("mp4").get,
                ACCEPT_RANGES -> "bytes",
                DATE -> new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz").format(new Date()),
                CONTENT_LENGTH -> fsFile.length.toString,
                CONTENT_RANGE -> s"bytes $rangeStart-$rangeEnd/${fsFile.length}",
                CONNECTION -> "keep-alive"
              )
            ),
            body = Enumerator.fromStream(new FileInputStream(fsFile))
          )
  } getOrElse {
    NotFound
  }
}

It's mostly based on two sources to implement the logic to handle the specific byte range request necessary to serve videos.

When using either Chrome or Safari on OS X to access this controller method, the developer tools report that the request is cancelled - no response, be it a 200 or 404, is received. I have confirmed that the SimpleResponse is actually returned by this controller action on requests I expect it to deliver a good response, but either Play won't finish the response or my browsers won't accept it. Am I doing something wrong here in response, or have I stumbled upon a bug in the framework?

My Play version is 2.1.3.

Schleichardt
  • 7,502
  • 1
  • 27
  • 37
DCKing
  • 4,253
  • 2
  • 28
  • 43

1 Answers1

3

Reasons why Chrome cancels requests.

The sources you used are more complepete. I show you with code what I spotted:

The response code is not always 206 PARTIAL_CONTENT:

val responseCode = if (rangeStart != 0 || rangeEnd != fileLength - 1) 206 else 200

Skipping the bytes is missing:

val stream = new FileInputStream(file)
stream.skip(rangeStart) # range starts defaults to 0

The content range can be the full file and partial:

val contentLength = if (responseCode == 206) (rangeEnd - rangeStart + 1) else fileLength

So you should not send on every case send the Content-Range header.

Community
  • 1
  • 1
Schleichardt
  • 7,502
  • 1
  • 27
  • 37