Created
March 16, 2012 12:26
-
-
Save dph01/2049865 to your computer and use it in GitHub Desktop.
media streaming
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def serve(reqdFile: String): Box[LiftResponse] = { | |
debug("request headers: " + S.request.map(_.headers.map(h => h._1 + "\t\t" + h._2 + "\n").toString).openOr("")) | |
User.currentUser match { | |
case Full(user) => { | |
try { | |
debug("starting in Media Server for logical file: " + reqdFile + ".....") | |
// e.g. turn 1092345.mp4 into (1092345,mp4) | |
var (baseName, ext) = Media.splitExtension(reqdFile) | |
// should be only one in the database | |
val mediaList = Media.findAll(By(Media.baseNamePhysical, baseName), By(Media.fileExtension, ext)) | |
val media = mediaList.length match { | |
case n: Int if n > 1 => | |
throw new Exception("Found more than one entry in the database for logical file: " + reqdFile) | |
case n: Int if n == 0 => | |
throw new Exception("Could not find media entry for logical file: " + reqdFile) | |
case _ => mediaList.head | |
} | |
debug("about to serve media: " + media) | |
// check to see if the user has rights to see this file | |
if (!user.media.contains(media)) | |
throw new Exception("User does not have access rights to: " + reqdFile) | |
// val filePath = Media.mediaHomeLog + filename | |
debug("opening file: " + media.fileNamePhysical) | |
val file: java.io.File = media.file | |
// see: http://balusc.blogspot.com/2009/02/fileservlet-supporting-resume-and.html#matches | |
// Prepare some variables. The ETag is an unique identifier of the file. | |
val eTag = media.baseNamePhysical + media.fileExtension + "_" + media.length + "_" + file.lastModified | |
val req = S.request.open_! // bound to have an request here | |
// Validate request headers for caching --------------------------------------------------- | |
// If-None-Match header should contain "*" or ETag. If not, then return 304. | |
var ifNoneMatch: Box[String] = req.header("If-None-Match") | |
ifNoneMatch.filterNot(matches(_, "ETag")).foreach(x => { | |
debug("returning NotModifiedResponse because If-None-Match is present but does not contain * or ETag") | |
return Full(new NotModifiedResponse(x)) | |
}) | |
// If-Modified-Since header should be greater than LastModified. If not, then return 304. | |
// This header is ignored if any If-None-Match header is specified. | |
var ifModifiedSince: Box[Long] = req.header("If-Modified-Since") | |
.flatMap(DefaultDateTimeConverter.parseDate(_).map(_.getTime)) | |
ifModifiedSince.foreach(x => | |
if (ifNoneMatch.isEmpty && x + 1000 > file.lastModified) { | |
debug("returning NotModifiedResponse because If-Modified-Since is present, If-None-Match isn't and" + | |
" If-Modified-Since date > lastModified: " + x + " > " + file.lastModified) | |
return Full(new NotModifiedResponse(eTag)) | |
} | |
) | |
// Validate request headers for resume ---------------------------------------------------- | |
// If-Match header should contain "*" or ETag. If not, then return 412. | |
req.header("If-Match").filterNot(matches(_, eTag)).foreach { x => | |
debug("returning PreConditionFailed as If-Match does not contain * or ETag (" + eTag + "). It contains: " | |
+ x) | |
return Full(new PreConditionFailedResponse()) | |
} | |
// If-Unmodified-Since header should be greater than LastModified. If not, then return 412. | |
for ( | |
ifUnModifiedSince <- req.header("If-Unmodified-Since"); | |
date <- DefaultDateTimeConverter.parseDate(ifUnModifiedSince); | |
if date.getTime + 1000L <= file.lastModified | |
) { | |
debug("returning PreConditionFailed as If-Unmodified-Since <= LastModified: " + | |
date.getTime + " <= " + file.lastModified) | |
return Full(new PreConditionFailedResponse()) | |
} | |
/* Validate and process range ------------------------------------------------------------- */ | |
// Prepare some variables. The full Range represents the complete file. | |
val fullRange = ReqRange(0, file.length - 1, file.length); | |
// Validate and process Range and If-Range headers. | |
debug("Range header is: " + req.header("Range").openOr("Not Set")) | |
var ranges: List[ReqRange] = req.header("Range").map(range => { | |
// Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416. | |
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) { | |
error("invalid format for Range header: " + range + ". Returning RequestedRangeNotSatisfiableResponse.") | |
return Full(new RequestedRangeNotSatisfiableResponse(file.length)); | |
} | |
// the following code looks for reasons to force a return of the full file | |
// If-Range header should either match ETag or be greater then LastModified. If not, | |
// then return full file. | |
debug("If-Range = " + req.header("If-Range").openOr("Not Set")) | |
val returnFull_? = req.header("If-Range").filter(_ != eTag).map(ifRange => { | |
// if we're here ifRange does not match eTag, so try to parse as a date | |
DefaultDateTimeConverter.parseDate(ifRange).map(date => { | |
if (date.getTime + 1000 < file.lastModified) { | |
// If-Range is set as a date, but it is less than lastModified | |
debug("If-Range is set as a date (" + date.getTime + | |
", but it is less than lastModified (" + file.lastModified + ". Returning a full range") | |
true | |
} else { | |
// ifRange is set as a date and the date is greater than the last modified date | |
// so OK to return a range | |
debug("ifRange is set as a date and the date is greater than the last modified date so OK to return a range") | |
false | |
} | |
}).openOr({ | |
// date If-Range doesn't parse as a date either, so return full file | |
error("If-Range doesn't either match ETag or parse as a date so ignoring: " + | |
ifRange) | |
false | |
}) | |
}).openOr({ | |
debug("If-Range is either not set, or matches eTag, so carry on to return the requested range.") | |
false // If-Range either not set, or matches eTag, so return full file | |
}) | |
// this whole block returns a List[ReqRange] | |
if (returnFull_? == true) { | |
debug("returning a full range: " + fullRange) | |
List(fullRange) | |
} else { | |
// Range is of format "bytes=n-n,n-n,n-n..." | |
var ranges = range.stripPrefix("bytes=").split(",").map(part => { | |
// Assuming a file with length of 100, the following examples returns bytes at: | |
// 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100). | |
debug("Range part: " + part) | |
var splitPoint = part.indexOf("-") | |
var a = part.take(splitPoint) | |
var b = part.takeRight(part.length - splitPoint - 1) | |
var lastChar = part.length - 1 // need to do the calc here as for some reason we can't do it in the | |
// match | |
var (from, to) = if (splitPoint == 0) { | |
(file.length - b.toLong, file.length - 1) // e.g. -20 means last 20 | |
} else if (splitPoint == lastChar) { | |
(a.toLong, file.length - 1) // e.g. 20- means from 20 to end | |
} else { | |
(a.toLong, scala.math.max(b.toLong, file.length - 1)) // e.g. 10-20 | |
} | |
debug("From: " + from + ", to: " + to) | |
ReqRange(from, to, file.length) | |
}) | |
debug("found ranges: " + ranges.mkString(";")) | |
// Check if Range is syntactically valid. If not, then return 416. | |
ranges.foreach(range => { | |
if (range.start > range.end) { | |
debug("returning a RequestedRangeNotSatisfiableReponse as range.start > range.end (" + | |
range.start + " > " + range.end) | |
return (Full(new RequestedRangeNotSatisfiableResponse(file.length))) | |
} | |
}) | |
debug("Ranges OK") | |
ranges.toList | |
} | |
}).openOr({ | |
debug("Range header not set; returning full range") | |
List(fullRange) | |
}) | |
val headers = ListBuffer[(String, String)]() | |
headers += ("Accept-Ranges" -> "bytes"); | |
headers += ("Content-type" -> media.mimeType.is) | |
//headers += ("ETag" -> eTag); | |
headers += ("Last-Modified" -> Helpers.toInternetDate(file.lastModified)) | |
// headers += ("Expires" -> Helpers.toInternetDate( System.currentTimeMillis() + 31449600000L) ) // 1 year | |
// headers += ("Cache-Control" -> "private; max-age=10800; pre-check=10800") | |
// headers += ("Pragma" -> "private") | |
headers += ("Content-disposition" -> (S.param("download").filter(_ == "true") | |
.map(_ => "attachment").openOr("inline") + "; filname=" + media.displayName)) | |
// S.param("download").filter(_=="true").foreach(x => | |
// headers += ("Content-disposition" -> ("attachment; filname=" + media.displayName))) | |
// debug("headers: " + headers) | |
val inputStream = new java.io.RandomAccessFile(file, "r") | |
val (statusCode, outStreamFunc) = ranges.length match { | |
case 1 => { | |
val range = ranges.head | |
debug("range is : " + range + ", is full: " + range.full_?) | |
headers += ("Content-Range" -> ("bytes " + range.start + "-" + range.end + "/" + range.total)) | |
headers += ("Content-Length" -> range.length.toString) | |
// return a partially applied function which is going to be used to write to the output stream | |
// later | |
(if (range.full_?) 200 else 206, copy(inputStream, _: OutputStream, range.start, range.length)) | |
} | |
case _ => throw new Exception("cannotohandle multiple ranges yet") | |
} | |
val resp = OutputStreamResponse(outStreamFunc, | |
ranges.foldLeft(0L)(_ + _.length), | |
headers.toList, | |
Nil, // cookies | |
statusCode) | |
debug("... finishing in Media Server for file: " + reqdFile + " with response headers: " | |
+ headers.toList.map(h => h._1 + "\t\t" + h._2 + "\n").toString) | |
Full(resp) | |
} catch { | |
case e => { | |
// TODO -decide how best to handle an exception here | |
error("exception caught: " + e.getMessage) | |
Full(new NotFoundResponse(e.getMessage)) | |
} | |
} | |
} | |
case _ => { | |
S.error("Login to stream your video.") | |
val uri = S.uriAndQueryString | |
S.redirectTo(User.loginPageURL, () => { User.loginRedirect.set(uri) }) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment