Question

I have an application which intended to stream videos back from our local DB. I spent a lot of time yesterday attempting to return the data a either a RangeFileContentResult or RangeFileStreamResult without success.

In short, when I return the file as either of these two results I cannot seem to get a video to stream correctly (or play at all).

The request from the browser gets sent with the following headers:

Range: bytes=0-

And the response comes provided gives these headers as an example:

Accept-Ranges: bytes
Content-Range: bytes 0-5103295/5103296

In terms of network traffic, I get a series of 206's for partial results, then a 200 at the end (according to fiddler) which seems correct. Chrome's network tab disagrees with this and see's an initial request (always 13 bytes which I assume is a handshake) then a couple more requests which have a status of either cancelled or pending. As far as I understand, this is more or less correct, 206 - cancel, 206 - cancel etc. But the video never plays.

If I switch the result from my controller to a FileResult, the video plays and Chrome, IE10 and Firefox and appears to begin playing before the end of the download is completed (which feels a little like it's streaming! although I suspect it's not)

But with the range result I get nothing in chrome or IE and the entire video downloads in one drop in firefox.

As far as I understood, the RangeFileContentResult should handle responding to the client with a range of bytes to download (which mine doesn't seem to do, it just tells it to get the whole file (illustrated by the response above)). And the client should respond to that, which it doesn't seem to do.

Does anyone have any thoughts in this area? Specifically:

a) Should RangeFileContentResult be sending a range of bytes back to the client? b) Is there any way I can explicitly control the range of bytes requested from the client side? c) Is there any reason or anything I'm doing wrong here which would cause browsers not to load the video at all, when requesting a RangeFileContentResult?

EDIT: Added a diagram to help describe what I'm seeing:

RangedRequestImage

EDIT2: Ok, so the plot thickens. Whilst playing around with the RangedFile gubbins we needed to push another system test version out and I left the 'RangeFileContentResult' on my controller action as below:

private ActionResult RetrieveVideo(MediaItem media)
{
    return new RangeFileContentResult(
        media.Content, 
        media.MimeType, 
        media.Id.ToString(), 
        DateTime.Now);            
}

Rather oddly, this now seems to work as expected on our Azure system test environment but still not on my local machine. I wonder if there's something IIS based which works happily on Azures IIS8, but not on my local 7.5 instance?

Was it helpful?

Solution

The reason of the issue described here is the value passed to modificationDate parameter of RangeFileContentResult constructor:

return new RangeFileContentResult(media.Content, media.MimeType, media.Id.ToString(), DateTime.Now); 

This date is used by the RangeFileResult in order to create two headers:

  • ETag - This header is an identifier used by browser and server to make sure that they are speaking about the same entity.
  • Last-Modified - This header informs the browser about the last modification date of the entity.

The fact that a DateTime.Now is being passed every time the browser makes partial request might be a reason for ETag and Last-Modified headers values to change before the client will get the whole entity (usually if the entire process takes longer than one second).

In case described above, the browser is sending If-Range header with the request. This header is telling the server that the entire entity should be resend if the entity tag (or modification date because If-Range can carry either one of those two values) doesn't much. This is what happens in this case.

The fact that modification date is "dynamic" may also cause further issues if client decides to use one of following headers for verification: If-Modified-Since, If-Unmodified-Since, If-Match, If-None-Match.

The solution in this situation is to keep a modification date in database with the file to make sure it is consistent.

There is also a place for optimization here. Instead of grabbing the whole video from DB every time a partial request is being made, one can either cache it or grab only the relevant part (if the database engine which application is using allows such an operation). Such a mechanism can be used in order to create specialized action result by delivering from RangeFileResult and overwriting WriteEntireEntity and WriteEntityRange methods.

OTHER TIPS

Ok So I didn't have enough time to look at RangeFileResult in details, but I have just downloaded the file (RangeFileContentResult) from RangeFileContentResult

and modified my code so it looks like

public ActionResult Movie()
{
    byte[] file = System.IO.File.ReadAllBytes(@"C:\HOME\asp\Java\Java EE. Programming Spring 3.0\01.avi");

    return new RangeFileContentResult(file, "video/x-msvideo", "01.avi", DateTime.Now);
}

and again it works. However, I noticed that when I stop the video I have an exception and it happens in RangeFileResult

if (context.HttpContext.Response.IsClientConnected)
{
    WriteEntityRange(context.HttpContext.Response, RangesStartIndexes[i], RangesEndIndexes[i]);
    if (MultipartRequest)
                context.HttpContext.Response.Write("\r\n");
    context.HttpContext.Response.Flush();
}

So you better modify the code to handle it.In terms when users already disconnected , but you are still trying to send them a response.

Again, technically it's not a big difference whether you pass byte[] or Stream , because even when you pass Stream the code working with it

using (FileStream)
{
    FileStream.Seek(rangeStartIndex, SeekOrigin.Begin);

    int bytesRemaining = Convert.ToInt32(rangeEndIndex - rangeStartIndex) + 1;
    byte[] buffer = new byte[_bufferSize];

    while (bytesRemaining > 0)
    {
        int bytesRead = FileStream.Read(buffer, 0, _bufferSize < bytesRemaining ? _bufferSize : bytesRemaining);
        response.OutputStream.Write(buffer, 0, bytesRead);
        bytesRemaining -= bytesRead;
    }
}

again reads data and puts them into an byte[] array!.... So it's up to you!

BUT... I suggest that you pay attention to a content type that you provide!!! Point is that your browser must be able to handle it!So if you provide something unknown definitely you will have problems.To find your content type string please check mime-types-by-content-type

Again I just gave a quick look and if you have problems I will help you later when come home.

mofiPlease just copy these two files in your mvc project
RangeFileResult
RangeFileStreamResult

public ActionResult Movie()
{
    var path = new FileStream(@"C:\temp\01.avi", FileMode.Open);
    return new RangeFileStreamResult(path, "video/x-msvideo", "01.avi", DateTime.Now);
}

Now run your project and open in chrome (for example: http://youraddress.com:45454/Main/Movie) you should see your file playing using a standard chrome video player. it's streaming and you can see it if you put a breakpoint at

return new RangeFileStreamResult(path, "video/x-msvideo", "01.avi", DateTime.Now);

Again the source is easy to modify to change the buffer size which is used for streaming!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top