Easy Handling of Http Range Requests in ASP.NET

I was recently working on adding video playing capabilities to a client’s existing ASP.NET application. They had video files that had been uploaded by users, and they wanted to allow the users to be able to preview the files on the web. I found MediaElement.js to be a excellent solution for this: As long as there were WEBM and H.264 encoded versions of the video to be played, it would handle playing the video on most browsers/devices and would fall back to a Flash video player if html video was not supported.

I did up a sample page, encoded some sample videos and everything worked great. However when I finally implemented it on the ASP.NET application only the Flash fallback player would play the videos. The HTML 5 player would not play the video.  I finally narrowed down the culprit to the way the video files were returned to the browser. Chrome and the iPhone/iPad require Http Range Requests to be supported for HTML5 video support.  Range requests allow the browser to only request a portion of the file from the server. So for videos, the user could drag the seek slider to the middle of the video and the browser would request the video from that point on, instead of downloading the full file from the server.

IIS 7 supports range requests natively, so if you are simply serving the video files directly from IIS you won’t have any problems. In this case, all file requests were being served by an ASPX page – it looked up information in a database as well as did some authorization checking before returning the file.  Unfortunately range requests are not natively supported from aspx responses – they have to be handled manually.

Scott Mitchell wrote a great article and HTTP Handler to handle this exact scenario, and it looks like a great solution. It’s very detailed and looks like it handles pretty much every edge case. Unfortunately it would require a lot of changes to fit into my client’s existing solution (and I didn’t want to rewrite the client’s solution – as it worked well for every other case), so I searched for a simpler solution.

After a lot of searching I finally found the VideoStreamer project at CodePlex by cipto0382. It includes a great little function that handles Http Range Requests.  I was able to include it in my project with very few modifications and it worked great. Chrome, Iphone and the IPad now played the video served by the ASP.Net Application.

I’ve included the function for reference below. It checks if the HTTP Request is a range request and returns a partial file with the necessary headers if it is. If it’s not a range request, it simply returns the complete file:

RangeDownload by cipto0382


private void RangeDownload(string fullpath,HttpContext context)
{
	long size,start,end,length,fp=0;
	using (StreamReader reader = new StreamReader(fullpath))
	{

		size = reader.BaseStream.Length;
		start = 0;
		end = size - 1;
		length = size;
		// Now that we've gotten so far without errors we send the accept range header
		/* At the moment we only support single ranges.
		 * Multiple ranges requires some more work to ensure it works correctly
		 * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2
		 *
		 * Multirange support annouces itself with:
		 * header('Accept-Ranges: bytes');
		 *
		 * Multirange content must be sent with multipart/byteranges mediatype,
		 * (mediatype = mimetype)
		 * as well as a boundry header to indicate the various chunks of data.
		 */
		context.Response.AddHeader("Accept-Ranges", "0-" + size);
		// header('Accept-Ranges: bytes');
		// multipart/byteranges
		// http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2

		if (!String.IsNullOrEmpty(context.Request.ServerVariables["HTTP_RANGE"]))
		{
			long anotherStart = start;
			long anotherEnd = end;
			string[] arr_split = context.Request.ServerVariables["HTTP_RANGE"].Split(new char[] { Convert.ToChar("=") });
			string range = arr_split[1];

			// Make sure the client hasn't sent us a multibyte range
			if (range.IndexOf(",") > -1)
			{
				// (?) Shoud this be issued here, or should the first
				// range be used? Or should the header be ignored and
				// we output the whole content?
				context.Response.AddHeader("Content-Range", "bytes " + start + "-" + end + "/" + size);
				throw new HttpException(416, "Requested Range Not Satisfiable");

			}

			// If the range starts with an '-' we start from the beginning
			// If not, we forward the file pointer
			// And make sure to get the end byte if spesified
			if (range.StartsWith("-"))
			{
				// The n-number of the last bytes is requested
				anotherStart = size - Convert.ToInt64(range.Substring(1));
			}
			else
			{
				arr_split = range.Split(new char[] { Convert.ToChar("-") });
				anotherStart = Convert.ToInt64(arr_split[0]);
				long temp = 0;
				anotherEnd = (arr_split.Length > 1 && Int64.TryParse(arr_split[1].ToString(), out temp)) ? Convert.ToInt64(arr_split[1]) : size;
			}
			/* Check the range and make sure it's treated according to the specs.
			 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
			 */
			// End bytes can not be larger than $end.
			anotherEnd = (anotherEnd > end) ? end : anotherEnd;
			// Validate the requested range and return an error if it's not correct.
			if (anotherStart > anotherEnd || anotherStart > size - 1 || anotherEnd >= size)
			{

				context.Response.AddHeader("Content-Range", "bytes " + start + "-" + end + "/" + size);
				throw new HttpException(416, "Requested Range Not Satisfiable");
			}
			start = anotherStart;
			end = anotherEnd;

			length = end - start + 1; // Calculate new content length
			fp = reader.BaseStream.Seek(start, SeekOrigin.Begin);
			context.Response.StatusCode = 206;
		}
	}
	// Notify the client the byte range we'll be outputting
	context.Response.AddHeader("Content-Range", "bytes " + start + "-" + end + "/" + size);
	context.Response.AddHeader("Content-Length", length.ToString());
	// Start buffered download
	context.Response.WriteFile(fullpath, fp, length);
	context.Response.End();

}

Comments

32 responses to “Easy Handling of Http Range Requests in ASP.NET”

  1. Don Nell Avatar
    Don Nell

    Thanks you (and Scott) for sharing this.

    I was stuck for almost 3 days in a similar situation having to support video content on both desktop and mobile devices. No issues on any browsers except the IOS devices. Found this article, implemented the function above unchanged and all is well.

    Thanks again.

  2. James Lovett Avatar
    James Lovett

    Hi Chris, thank you for this brilliant post. And thanks to Scott too! This has had me banging my head against a brick wall for weeks!

  3. Thanks Avatar
    Thanks

    Thank you, this helped me immensly, very useful.

  4. Chris Avatar
    Chris

    How do you handle large files. If I use this code on a file larger than int32 I get:

    “The size parameter must be between zero and the maximum Int32 value.
    Parameter name: size
    Actual value was 3413381708.”

    is there away to make it work with large video files?

    Thanks,
    Chris

    1. Chris Coulson Avatar

      I assume it’s the Response.WriteFile that’s causing the error? You’ll have to add some logic that says if the length is greater than Int32.MaxValue, then set the length to Int32.MaxValue and adjust the ‘end’ variable accordingly.

  5. Karthic Avatar

    Excellent article. Thanks for the code and references.

  6. Helge Avatar
    Helge

    Thank you! Thank you! Thank you! Easy to implement and works right away. It took me 30 seconds! Thank you!

  7. Fredrik Avatar
    Fredrik

    Thank you for sharing this! You saved me from hours of work, and also thanks for writing good, self-contained code that really worked out-of-the box! Thanks again!

    1. abhishek Avatar
      abhishek

      Hey Fredrick,
      can you please have a thought about my problem.

  8. abhishek Avatar
    abhishek

    Hi there
    I am trying to add this handler in my visual studio 2013 solution with .Net 4.5.
    The solution works fantastic against large file upto 500 mb in visual studio 2008 but as soon as i move to VS2013 with latest .Net4.5. I get error:
    Not enough storage is available to process this command. (Exception from HRESULT: 0x80070008)

    I did registry fix mentioned by Microsoft , i added response.flush etc. It helps a bit as it played once or twice but then again fall back to that error.

    Code is replica on both VS2013 and VS2008, i even tried same .Net3.5 version and serving same 360Mb file. Works like a charm in Vs2008 but failed in Vs2013.

    Any help is appreciated

    1. Chris Coulson Avatar

      I can’t say i’ve run into this before. I am thinking though that maybe it doesn’t have to do with Visual Studio as much as what’s used to host your solution. Are you using the visual studio built in web server to test? I believe they’re different between VS 2008 and VS 2013 (2013 uses IIS Express).

      I would try hosting the app using full IIS to see if it works properly there.

      1. abhishek Avatar
        abhishek

        Hi
        actually its working on production environment :), sorry i was bit lazy to test it on that.

        btw IE does not do any range request for any file, either big or small, via http handler or even a direct hosted link of mp4 file. IT just tries to download the whole file first. Do you know why or can we change that behavior.

        I don’t want to web optimize my file using ffmpeg or something.

        1. Chris Coulson Avatar

          If this is for viewing online on the site itself, I suggest you use an html5 video player like http://mediaelementjs.com/. But you likely will have to use ffmpeg to convert the video, depending on it’s format. Alternatively, you could upload them to YouTube and use YouTube’s player on your site – assuming you want them available to the general public.

      2. Abhishek Saxena Avatar
        Abhishek Saxena

        Hi Chris
        thanks for bearing with me
        i think i found the way
        i did compared headers of all the initial requests done by chrome and IE when playing a file using direct link as well as our http handler.
        What i found is that : IE is not making range request proactively like chrome and firefox if served by http handler.
        our handler when does not detect “Range” in the request header coming from IE, it goes to else part of this function to process handler request, which in turn pushes the whole file out.
        I looked at header that plays a direct link to file in IE, it got this in it
        “Accept-Ranges:bytes”
        Then IE comes to know that it can do range request. So i added that header below in else part:

        if (!String.IsNullOrEmpty(context.Request.ServerVariables[“HTTP_RANGE”]))
        {
        //request for chunk
        RangeDownload(fullpath, context);
        }
        else
        {
        //ask for all
        long fileLength = File.OpenRead(fullpath).Length;
        context.Response.AddHeader(“Content-Length”, fileLength.ToString());
        context.Response.AddHeader(“Accept-Ranges”, “bytes”);
        context.Response.WriteFile(fullpath);
        }

        and wohoo!! it starts playing in IE.

        1. Chris Coulson Avatar

          Glad to hear that you got it working!

  9. Lasith Avatar
    Lasith

    Hello,
    Thanks for tutorial.
    I was following whole tutorial and wrote simple MVC application to stream large video files.

    When I streaming large capacity(around 700MB) video in a network using above code the starting of video is very slow (around 1-2 minutes). In this stage I checked the network requests and it seems browser ask for video and waiting for a response from server. This is really annoying for the user.

    Then once it started video is playing smoothly (It is a 720P resolution video and as my network connection is good video is playing very smoothly).

    But when I do a seek with controls on html video player, then same issue happens and I have to wait another 1-2 minutes till response completed.

    I am using IIS7 (MVC4). If I play the same video which located inside of IIS dir then I can play it without mentioned delay. Also if the video is located outside of IIS folder but if it within the same machine that hosted IIS then also no issues.

    I am having this issue when I have video in a network location which is a different machine that IIS hosted.

    So conclusion is, This is not because of user browser trying to load large video in to browser. It is something between video share machine vs IIS.

    Any idea about resolving this?

    1. Chris Coulson Avatar

      Is the file coming from a local windows network? It could be that reader.BaseStream.Length is not supported to get the file length from the network location you are using, so it is having to read the full file over the network to determine it’s size. Instead of reading the file length, maybe try hardcoding the length to see if that is the issue with the slowdown. If that’s not it, it could just be that the network the file is on is just REALLY slow.

      1. Lasith Avatar
        Lasith

        Hello,

        Thanks for response.
        Yes after hard-coding the length value also it got delay. Anyway following request is the slow one.

        ———–
        GET /myapp/MyVideo HTTP/1.1
        Host: 10.2.2.91
        Connection: keep-alive
        Cache-Control: max-age=0
        Accept-Encoding: identity;q=1, *;q=0
        User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36
        Accept: */*
        Referer: http://10.2.2.91/myapp/Sample
        Accept-Language: en-US,en;q=0.8
        Range: bytes=0-
        ——————–

        I am not sure why it is asking for full range here (bytes=0-). And it take more than 1 minute to get response. If I do the same request with Range: bytes=0-6653270 (this is 1/00 of full file size) that request can do in 1 second.

        My html code is,

          1. Lasith Avatar
            Lasith

            Hello,

            Finally I was able to do following change and resolve the issue. So as I mention in a previous post issue is with the request that asking range from start to end of file.

            So I did following change.

            long sizeThreshold = size / 100;
            if (end – start > sizeThreshold)
            {
            context.Response.AddHeader(“Content-Length”, sizeThreshold.ToString());
            context.Response.WriteFile(fullpath, fp, sizeThreshold);
            }
            else
            {
            //Existing logic
            }

  10. Kirk Avatar
    Kirk

    I think I might have found a small bug in the code. If you look at this particular code

    // If the range starts with an ‘-‘ we start from the beginning
    // If not, we forward the file pointer
    // And make sure to get the end byte if spesified
    if (range.StartsWith(“-“))
    {
    // The n-number of the last bytes is requested
    anotherStart = size – Convert.ToInt64(range.Substring(1));
    }

    I believe you want to set anotherEnd here. From my testing I never saw a request start started with -, but I wanted to point it out since it could cause a problem in some specific case.

  11. xhaxha Avatar
    xhaxha

    Hi, can you please help me by providing an example on how to call this function? I don’t know what is the CONTEXT that I must provide. I am a newbie in this. Thank you very much. Your response will be highly appreciated.

    1. Chris Coulson Avatar

      The HttpContext is a variable that’s available when your ASP.NET application is handling a web request. Check the project on Github, there is a sample application showing it’s use:

      http://videostreamer.codeplex.com/SourceControl/latest#SampleWebApplication/VideoStreamer.ashx

  12. LeKuchen Avatar
    LeKuchen

    Hi Chris,

    I use your code for delivering large files. Thanks for the code! Sometimes I do receive an error in the IIS log: not enough storage is available to proccess this command.

    Wouldn’t it make sense to use
    context.Response.TransmitFile(fullpath, fp, sizeThreshold);
    instead of
    context.Response.WriteFile(fullpath, fp, sizeThreshold);
    to release memory and not buffering the file before writing the file to the output stream?

    1. Chris Coulson Avatar

      I haven’t tested it, but that does sound like a good enhancement – I would think it would work.

  13. Serious.Netter Avatar
    Serious.Netter

    this code snippet has saved me lots of time. there is just one thing i thought i should point out.

    When using this code to load videos that are a couple Gigs in size.

    the second last line

    —————
    context.Response.WriteFile(fullpath, fp, length);
    —————

    would throw an error saying length can only be between 0 and Int32.MaxValue. in that case, a file of 2.2Gb would trigger this error.

    I have worked around this by sending the data in blocks of 2Mb ( you can customize the block size). The following is my modification. Use it over the line above.

    —————
    long blockSize = 1024 * 1024 * 2; //2mb
    while(length > 0)
    {
    if(length > blockSize)
    {
    context.Response.WriteFile(fullpath, fp, blockSize);
    context.Response.Flush();
    fp += blockSize;
    length -= blockSize;
    }
    else
    {
    context.Response.WriteFile(fullpath, fp, length);
    context.Response.Flush();
    fp += length;
    length -= length;
    }
    Thread.Sleep(10);
    }
    —————

  14. Ali Sheikhpour Avatar
    Ali Sheikhpour

    It seem there is some object or streamer which is not closed or Disposed proprly. The files remain in use with w3wp.exe even after the video is was finished.

  15. Ali Sheikhpour Avatar
    Ali Sheikhpour

    Sorry . I had replaced WriteFile with TransmitFile (to handle large files) And seems it is lokcing the file.

  16. Susantha Soysa Avatar
    Susantha Soysa

    You are a life saver. Thanks so much for sharing your code. With a small tweak, I managed to get it working for my scenario.

    context.Response.WriteFile(fullpath, fp, length);
    context.Response.Flush();
    context.Response.Close();

    Without Flush, my code was throwing an error.

  17. […] And another helpful post from Chris Coulson which provided the implementation in C# for the solution! http://blogs.visigo.com/chriscoulson/easy-handling-of-http-range-requests-in-asp-net/ […]

Leave a Reply to Kirk Cancel reply

Your email address will not be published. Required fields are marked *