Serving HTML5 Video using Nancy
Recently, I have been using OWIN a good deal for developing internal web applications. One of the chief benefits of this is that OWIN offers the ability to host its own HTTP server, which allows me to get out of the business of installing and configuring IIS on windows, which is one of the main points of pain when deploying the products I work on to our customers. Unfortunately, when I first started using OWIN, there was not a version of ASP.NET MVC available that was compatible with OWIN. Most of my previous experience with programming web servers has been based on MVC (except for briefly experiencing WebForms hell), so finding a similar framework that was compatible with OWIN was one of my first priorities.
In my search, I discovered Nancy, a fairly similar MVC-style framework which offered OWIN support. It also was capable of using the same Razor view engine as ASP.NET MVC, with some minor differences, so I was able to convert existing IIS ASP.NET MVC applications to OWIN/Nancy using most of the existing views and front-end code. At some point I plan to write an article illustrating how one would do this type of conversion, but for now, I'm going to examine one particular gotcha I discovered when converting my personal Netflix-type video application to OWIN/Nancy: serving HTML5 video files.
In ASP.NET MVC, serving video files "Just Works" out of the box. You simply need to supply an application-relative url in the <source> tag's src attribute in the <video> element, like so:
<video controls="controls" autoplay="autoplay"> <source src="@Url.Content("~/videos/myvideo.mp4")" type="video/mp4" /> </video>
Boom, HTML5 video on your page, with controls, full-screen toggle, and the ability to seek back and forth through the video file.
Assuming that you store your video files in the Content directory in Nancy, most of this will work the same. However, out-of-the-box with Nancy, you will not be able to seek to different points in the video - the video loads, but you are only able to watch from the beginning. At least, this is the behavior on Chrome - Firefox and IE 11 seem to buffer more of the file, so you can seek a limited amount backwards and forwards. On the face of it, this is a step backwards.
This behavior is a result of the fact that Nancy returns the entirety of a requested resource with a 200 HTTP status code for static content. When seeking in a video file, however, most browsers add a "Range: bytes=X-Y" header, which is meant to instruct the server to only deliver the bytes of the video file between X and Y. So far as I have been able to discover, this is not supported by default in Nancy, so it is necessary to do some extra work to structure our responses correctly for this.
Thankfully, I was able to find an example that had done a fair amount of the legwork, Christopher Probst's answer in this google groups topic. Unfortunately, the posted code doesn't work in all cases. It only seems to handle "Range" headers which specify a beginning index, not those which include both a beginning and end index. However, with some minor modifications I was able to get it working. Note that these are extension methods, so you will need to place them in a static class in your Nancy project.
public static Response FromPartialFile(this IResponseFormatter f, Request req, string path, string contentType) { return f.FromPartialStream(req, new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read), contentType); } public static Response FromPartialStream(this IResponseFormatter f, Request req, Stream stream, string contentType) { // Store the len var len = stream.Length; // Create the response now var res = f.FromStream(stream, contentType). WithHeader("connection", "keep-alive"). WithHeader("accept-ranges", "bytes"); // Use the partial status code res.StatusCode = HttpStatusCode.PartialContent; long startI = 0; foreach (var s in req.Headers["Range"]) { var start = s.Split('=')[1]; var m = Regex.Match(start, @"(\d+)-(\d+)?"); start = m.Groups[1].Value; var end = len-1; if (m.Groups[2] != null && !string.IsNullOrWhiteSpace(m.Groups[2].Value)) { end = Convert.ToInt64(m.Groups[2].Value); } startI = Convert.ToInt64(start); var length = len - startI; res.WithHeader("content-range","bytes " + start + "-" + end + "/" + len); res.WithHeader("content-length", length.ToString(CultureInfo.InvariantCulture)); } stream.Seek(startI, SeekOrigin.Begin); return res; }
The main change from the original code is to use a regex to capture both ends of the requested range. If the end index is not specifies, it uses the full stream length - 1, the same as the original. We also set the content-length header to match the number of requested bytes, otherwise the browser will throw a "Content-Length mismatch" error and refuse to play the video.
Using these extension methods to return the video stream from a Nancy module is pretty simple:
private dynamic Video(dynamic parameters) { var path = Url.Content("~/Videos/myvideo.mp4"); return Response.FromPartialFile(Request, path, "video/mp4"); }
Assuming that you setup your Nancy module to have a GET route like "/Video/myvideo" that used the above action, the HTML markup would then be:
<video controls="controls" autoplay="autoplay"> <source src="@Url.Content("~/Video/myvideo")" type="video/mp4" /> </video>
Voila! Seekable HTML5 video served using Nancy.