Category Archives: ASP.NET MVC

ASP.NET MVC Resuming Actions

(Update 2014-12-09: I’ve created a WebApi based library that leverages the System.Net.Http classes to do all the parsing. The code is very simple and much easier to support. I’d recommend trying that first. It’s a lot easier to support.)

This is to announce my new CodePlex project Resuming Action Results for ASP.NET MVC. Or MVC Resuming Actions. Or something else even more clever and catchy. Oh, and it’s already on the NuGet official feed to check out.

The History

Some time ago I started a project called Media Streaming MVC. The project was an early attempt to give ASP.NET MVC developers the ability to easily expose dynamic, routable resources as progressive-download compliant. It was just an ActionFilter and some ActionResults with code to parse HTTP Request Headers and construct the appropriate response.

That project was developed very rapidly as a proof of concept for a StackOverflow question I had tried to answer. Over the past few months I grew increasingly unhappy with the implementation. I knew I wanted to bring it more in line with the way MVC FileResult actions were called and give it a major overhaul. Unfortunately every time I sat down to take a serious look at how I could accomplish these goals, I got overwhelmed and let my attention wander to something less challenging.

Finally I made the decision that there was really no way to clean up the project, make it easier for developers to use and retain any sort of compatibility with existing code. I made the decision to cut the cord and start fresh with the lessons I’ve learned, the feedback I’d gotten and my expanded experience with the ASP.NET MVC platform.

Continue reading

Advertisements

Media Streaming MVC Alt Approach

Now that the bulk of the holiday season is over, I started back in on my side projects. Tonight’s goal: remove some of the lifting required of developers to implement resuming downloads.

While playing around with the Controller class I started digging into the OnActionExecuted method. From here I had access to the Request and Response objects. In essence, I could unify my 2-prong approach (ActionFilter to scan the request and ActionResult to write the response) in one step and eliminate the need for the developer to do anything at all (except change their code to inherit from ResumingController rather than Controller).

What I’m doing is checking for the 3 ActionResult types that are used when a Controller returns a File() result. I take the values I need and replace the object with my custom result.

The upside: controller code stays the same.

The downside: it feels a little dirty to me. I’m explicitly casting and checking the FilterContext.Result type and replacing it with an equivalent. While it works I’m not sure it passes the smell test.

Continue reading

Media Streaming MVC and Preconditions

I’ve added more HTTP precondition support to my Media Streaming / Resuming Downloads project for MVC on CodePlex. In the last version I added a quick bit of code to enable returning a Not Modified (304) status code if a request came in with If-Modified-Since precondition. Today I’ve committed source changes which now support 4 other preconditions: If-Unmodified-Since, If-Range, If-Match and If-None-Match.

So far the only client I have that issues a request other than If-Modified-Since was Firefox. It used the If-Range request once but I can’t replicate it. I would rather not implement my own client to test these request types as I’ve already got a preconceived notion on how it should work and I’m more interested in testing real products that have implemented the protocols according to their understanding.

Continue reading

Media Streaming MVC is now Time Aware!

I was looking at my CodePlex Project to see if I could ease debugging a bit by logging the request and response headers. Before I got into the meat of things I enabled the Trace attribute in the web.config of my test project. This gave me the request headers but not the response. Before I had a chance to really dig more into this, I noticed my iPad was sending If-Modified-Since HTTP header in its request for a media file.

I thought about this for a minute and realized that in some cases I’m aware of the LastModified time for a media file and I should be able to compare the two. I started hacking at the project to get initial support to see how it would work. I ran into a couple things:

Not all timestamps are created equal.

The date and time my iPad sends with the request is GMT. The DateTime on my media file was localized (GMT -5:00). I decided it should be very quick to just convert them both to UTC and compare them. I wrote the code to parse the request headers, pass through the new Precondition in the ResumambleDownloadRequest object, and compare them in the ResumableDownloadResult base class. Unfortunately the comparison always thought the file had been modified more recently than the request and despite re-streaming the file, subsequent requests still contained the same If-Modified-Since timestamp.

Continue reading

My First CodePlex Project: MVC Resumable Downloads

My first CodePlex Project is an attempt to bring iOS streaming and resuming downloads to ASP.NET MVC.

In my previous post I discussed how I’d hacked together a quick solution to bring resuming download capability to dynamic MVC content. Though I got some credit on StackOverflow for coming up with that solution I wasn’t terribly happy with how patched together it was and how little support it had for the fully HTTP protocol specification for Content-Range requests.

I mulled it over for awhile and did some searching and finally came up with more information about the specification, applications that use it and examples of how to solve the problem for static files. The best solution I found was an adaptation of a conversion. Rather than further tweaking the solution and trying to force it to fit in the context of an ASP.NET MVC application framework I decided to do a clean implementation using the framework features.

I thought my initial instinct about the design was correct: use an ActionFilter to examine the request headers for a partial data request and a custom Result to output the data and necessary headers. I kept this structure but expanded on how I parsed the ranges and output different results based on the request. The new design would remove almost all of the effort required for a developer to implement the solution.

In the end the new solution took a little over a day to implement and get working with an iPad streaming a video. After a few more hours of tweaking I think I arrived at a better and cleaner solution that should be easily extensible without requiring any modification in order to be useful for common scenarios. It was also more dynamic and more capable than the original solution by adding support for multiple ranges in a single request and adjusted the output result to suit the request without requiring higher-level intervention.

I think it’s a pretty good solution and it could be helpful to someone. I would like to see it extended and become even more useful. The only way I could realistically see this happening is to make the source publicly available and see how it matures. So with that in mind, I took the plunge and I now have my first CodePlex project.

Please be kind. This is my first project. I look forward to see how it’s received and whether other people find it useful!

-Erik

iOS Media Streaming for ASP.NET MVC

UPDATE (6/27/2011): Taking some lessons learned from the project below, I’ve started another simplified project. Take a look, it probably suits the needs more effectively.

I ran across a question on StackOverflow from a developer asking how to get the correct response headers to stream media to iOS devices (iPhone, iPad, etc.) from ASP.NET MVC 2. I had a few quick ideas but they weren’t really gaining traction so I fired up Visual Studio and tried things out.

First I knew I was going to need to make my Action react to requests for byte ranges. This is key to getting iOS devices to stream media. My first thought was to make an ActionFilter to read the request headers and pass the values on to my Action.

public class ByteRangeRequest : FilterAttribute, IActionFilter
{
    protected string RangeStart { get; set; }
    protected string RangeEnd { get; set; }

    public ByteRangeRequest(string RangeStartParameter,
                            string RangeEndParameter)
    {
        RangeStart = RangeStartParameter;
        RangeEnd = RangeEndParameter;
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext == null)
            throw new ArgumentNullException("filterContext");

        if (!filterContext.ActionParameters.ContainsKey(RangeStart))
            filterContext.ActionParameters.Add(RangeStart, null);
        if (!filterContext.ActionParameters.ContainsKey(RangeEnd))
            filterContext.ActionParameters.Add(RangeEnd, null);

        var request = filterContext.RequestContext.HttpContext.Request;
        var headerKeys = request.Headers.AllKeys.Where(key =>
            key.Equals("Range", StringComparison.InvariantCultureIgnoreCase));
        Regex rangeParser = new Regex(@"(\d+)-(\d+)", RegexOptions.Compiled);

        foreach (string headerKey in headerKeys)
        {
            string value = request.Headers[headerKey];
            if (!string.IsNullOrEmpty(value))
            {
                if (rangeParser.IsMatch(value))
                {
                    Match match = rangeParser.Match(value);

                    filterContext.ActionParameters[RangeStart] =
                        int.Parse(match.Groups[1].ToString());
                    filterContext.ActionParameters[RangeEnd] =
                        int.Parse(match.Groups[2].ToString());
                    break;
                }
            }
        }
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
    }
}

I decided to specify the parameter names you want back just for flexibility. I could have chosen obscure names that were unlikely to collide or maybe stick the values in the TempData but I liked this approach better. It seems more concrete and obvious to me. And with these parameters, you could be passing on just the relevant segment of the stream to the output result but I was in a hurry and this was the smallest I could make my code.

With the request headers parsed I needed a way to return the subset of bytes from my media with the proper response headers. It turns out this was easier than I’d expected but not easier than I made it. After many iterations to get it working and a few more removing all the pieces it turned out where unnecessary I ended up with FileStreamRangeResult based on FileStreamResult. Using this as a guide, the other FileResult types could easily be made to work the same way.

public class FileStreamRangeResult : FileStreamResult
{
    public int StartIndex { get; set; }
    public int EndIndex { get; set; }
    public long TotalSize { get; set; }

    public FileStreamRangeResult(int startIndex, int endIndex,
        long totalSize, string contentType, Stream fileStream)
        : base(fileStream, contentType)
    {
        StartIndex = startIndex;
        EndIndex = endIndex;
        TotalSize = totalSize;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        HttpResponseBase response = context.HttpContext.Response;
        response.ContentType = this.ContentType;
        response.AddHeader(
            HttpWorkerRequest.GetKnownResponseHeaderName(
                HttpWorkerRequest.HeaderContentRange),
            string.Format("bytes {0}-{1}/{2}",
                StartIndex, EndIndex, TotalSize));
        response.StatusCode = 206;

        WriteFile(response);
    }

    protected override void WriteFile(HttpResponseBase response)
    {
        Stream outputStream = response.OutputStream;
        using (this.FileStream)
        {
            byte[] buffer = new byte[0x1000];
            int totalToSend = EndIndex - StartIndex;
            int bytesRemaining = totalToSend;
            int count = 0;

            FileStream.Seek(StartIndex, SeekOrigin.Begin);

            while (bytesRemaining > 0)
            {
                if (bytesRemaining <= buffer.Length)
                    count = FileStream.Read(buffer, 0, bytesRemaining);
                else
                    count = FileStream.Read(buffer, 0, buffer.Length);

                outputStream.Write(buffer, 0, count);
                bytesRemaining -= count;
            }
        }
    }
}

With those done all that’s left is to grab a properly formatted video and build my action.

[ByteRangeRequest("StartByte", "EndByte")]
public FileStreamResult StreamContent(int? StartByte, int? EndByte)
{
    string fileName = @"C:\temp\99RedBalloons.mp4";
    FileInfo mediaFile = new FileInfo(fileName);

    FileStream contentFileStream = mediaFile.OpenRead();
    var time = mediaFile.LastWriteTimeUtc;

    if (StartByte.HasValue && EndByte.HasValue)
        return new FileStreamRangeResult(StartByte.Value,
            EndByte.Value, contentFileStream.Length,
            "video/x-m4v", contentFileStream);

    return new FileStreamRangeResult(0,
        (int)contentFileStream.Length-1, contentFileStream.Length,
        "video/x-m4v", contentFileStream);
}

This was a productive night for me. I forgot dinner and missed my workout but now I know I can stream media to all my gadgets from my MVC projects! Keep in mind this was just a proof of concept so don’t be too hard on the rough edges!

-Erik

P.S. – It took me a lot of effort to get that working. If you feel like being kind, please go upvote my answer!