Serve video file to iPhone from ASP.NET MVC2

若如初见. 提交于 2019-12-03 06:33:37

UPDATE: This is now a project on CodePlex.

Okay, I got it working on my local testing station and I can stream videos to my iPad. It's a bit dirty because it was a little more difficult than I expected and now that it's working I don't have the time to clean it up at the moment. Key parts:

Action Filter:

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 headerKeys = filterContext.RequestContext.HttpContext.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 = filterContext.RequestContext.HttpContext.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)
    {
    }
}

Custom Result based on FileStreamResult:

public class ContentRangeResult : FileStreamResult
{
    public int StartIndex { get; set; }
    public int EndIndex { get; set; }
    public long TotalSize { get; set; }
    public DateTime LastModified { get; set; }

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

    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;
            }
        }
    }      
}

My MVC action:

[ByteRangeRequest("StartByte", "EndByte")]
public FileStreamResult NextSegment(int? StartByte, int? EndByte)
{
    FileStream contentFileStream = System.IO.File.OpenRead(@"C:\temp\Gets.mp4");
    var time = System.IO.File.GetLastWriteTime(@"C:\temp\Gets.mp4");
    if (StartByte.HasValue && EndByte.HasValue)
        return new ContentRangeResult(StartByte.Value, EndByte.Value, contentFileStream.Length, time, "video/x-m4v", contentFileStream);

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

I really hope this helps. I spent a LOT of time on this! One thing you might want to try is removing pieces until it breaks again. It would be nice to see if the ETag stuff, modified date, etc. could be removed. I just don't have the time at the moment.

Happy coding!

I tried looking for an existing extension but I didn't immediately find one (maybe my search-fu is weak.)

My immediate thought is that you'll need to make two new classes.

First, create a class inheriting from ActionMethodSelectorAttribute. This is the same base class for HttpGet, HttpPost, etc. In this class you'll override IsValidForRequest. In that method, examine the headers to see if a range was requested. You can now use this attribute to decorate a method in your controller which will get called when someone is requested part of a stream (iOS, Silverlight, etc.)

Second, create a class inheriting from either ActionResult or maybe FileResult and override the ExecuteResult method to add the headers you identified for the byte range that you'll be returning. Return it like you would a JSON object with parameters for the byte range start, end, total size so it can generate the response headers correctly.

Take a look at the way FileContentResult is implemented to see how you access the context's HttpResponse object to alter the headers.

Take a look at HttpGet to see how it implements the check for IsValidForRequest. The source is available on CodePlex or you can use Reflector like I just did.

You might use this info to do a little more searching and see if anyone has already created this custom ActionResult already.

For reference, here is what the AcceptVerbs attribute looks like:

public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
    if (controllerContext == null)
    {
        throw new ArgumentNullException("controllerContext");
    }
    string httpMethodOverride = controllerContext.HttpContext.Request.GetHttpMethodOverride();
    return this.Verbs.Contains<string>(httpMethodOverride, StringComparer.OrdinalIgnoreCase);
}

And here is what FileResult looks like. Notice the use of AddHeader:

public override void ExecuteResult(ControllerContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException("context");
    }
    HttpResponseBase response = context.HttpContext.Response;
    response.ContentType = this.ContentType;
    if (!string.IsNullOrEmpty(this.FileDownloadName))
    {
        string headerValue = ContentDispositionUtil.GetHeaderValue(this.FileDownloadName);
        context.HttpContext.Response.AddHeader("Content-Disposition", headerValue);
    }
    this.WriteFile(response);
}

I just pieced this together. I don't know if it will suit your needs (or works).

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

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

    public ContentRangeResult(int startIndex, int endIndex, string contentType, string fileDownloadName, Stream fileStream)
        : base(fileStream, contentType)
    {
        StartIndex = startIndex;
        EndIndex = endIndex;
        TotalSize = endIndex - startIndex;
        FileDownloadName = fileDownloadName;
    }

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

        HttpResponseBase response = context.HttpContext.Response;
        if (!string.IsNullOrEmpty(this.FileDownloadName))
        {
            System.Net.Mime.ContentDisposition cd = new System.Net.Mime.ContentDisposition() { FileName = FileDownloadName };
            context.HttpContext.Response.AddHeader("Content-Disposition", cd.ToString());
        }

        context.HttpContext.Response.AddHeader("Accept-Ranges", "bytes");
        context.HttpContext.Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", StartIndex, EndIndex, TotalSize));
        //Any other headers?


        this.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;

            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;
            }
        }
    }
}

Use it like this:

return new ContentRangeResult(50, 100, "video/x-m4v", "SomeOptionalFileName", contentFileStream);

Can you move outside of MVC? This is a case where the system abstractions are shooting you in the foot, but a plain jane IHttpHandler should have alot more options.

All that said, before you implement your own streaming server, you are probably better off buying or renting one . . .

The header that work have the Content-type set to text/plain, is that correct or is a typo?. Anyone, you can try to set this headers on the Action with:

Response.Headers.Add(...)
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!