RangeFileContentResult and Video streaming with Ranged Requests
Asked Answered
S

3

14

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?

Salliesallow answered 24/10, 2013 at 7:27 Comment(3)
Hi. I'm the Lib.Web.Mvc author and I will be happy to dig in this particular issue. We had similar one with Safari on iPad which was caused by it making the request without upper bound just to get the info about the file. The browser was then canceling the request without waiting for the data to be transmitted (in general the protocol was being violated by the browser). This might be something similar. Please send me an email with details of your environment so I can recreate the issue. You can also find entire sourced code of Lib.Web.Mvc and some other stuff here: http:\\tpeczek.codeplex.com.Bimbo
@Bimbo Hi Thomasz, thanks for that, can you let me know the best address to contact you on. The majorly strange thing I'm seeing is inconsistency between loading from the same build. e.g. For user 1 it works, for user 2 it doesn't. Then user 1 refreshes and it works, but user 2 refreshes and it doesn't. Let me know the best address and I'll pop you some details.Salliesallow
The one from my profile: [email protected]. Please provide as much details as you can (code, video, browsers involved, hosting configuration, exceptions details, any logs you have etc.)Bimbo
B
6

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.

Bimbo answered 27/11, 2013 at 15:1 Comment(3)
This is the correct answer, I was using a DateTime.Now as the modified date as I didn't understand the importance of the parameter. As a side point some documentation around this parameter may be useful as if you're not streaming static files (as we weren't) it's possible you won't store a 'modified date' with each file.Salliesallow
@Salliesallow As you suggested the documentation will be extendedBimbo
Hi Thomasz, Thanks again for your help sorting this one, really appreciate the effort taken.Salliesallow
T
4

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.

Toed answered 5/11, 2013 at 22:41 Comment(1)
Thanks again Anton, I will go through this with a fine tooth comb later and let you know the outcome. As a couple of side points, I've got the latest Lib.Web.MVC from Nuget so I'd hope that covers the need for the files (although I will double check the content to make sure I don't have an old copy). Also, my content types I store when the video is uploaded using the content type from the HttpPostedFileBase. I accept that not all browsers will handle all extensions, but with the test video I'm using if I return a simple FileResult it renders correctly, but just in one chunk with no seeking.Salliesallow
T
3

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!

Toed answered 5/11, 2013 at 12:32 Comment(1)
Hi Anton, thanks for the response. A quick question before I rework the code a bit. Our videos are stored in the db and I return an entity with media.Content as a byte[] and media.MimeType as a string, I've been padding those items as parameters to a 'RangeFileContentResult' which accepts this values, presumably that should work as well? Or do a specifically need to open a file stream and use the RangeFileStreamResult?Salliesallow

© 2022 - 2024 — McMap. All rights reserved.