从ASP.NET MVC2向iPhone提供视频文件

时间:2010-11-08 20:27:22

标签: iphone asp.net-mvc video-streaming

我正在尝试将视频文件从ASP.NET MVC提供给iPhone客户端。视频格式正确,如果我在一个可公开访问的网络目录中,它可以正常工作。

我所读到的核心问题是,iPhone要求您拥有一个可随时使用的简历下载环境,该环境允许您通过HTTP标头过滤字节范围。我认为这是为了让用户可以跳过视频。

使用MVC提供文件时,这些标头不存在。我试图模仿它,但没有运气。我们这里有IIS6,我根本无法做很多标题操作。 ASP.NET会抱怨我说“ 此操作需要IIS集成管道模式。

升级不是一种选择,我不允许将文件移动到公共网络共享。我觉得受到环境的限制,但我仍在寻找解决方案。

以下是我正在尝试做的一些示例代码......

public ActionResult Mobile(string guid = "x")
{
    guid = Path.GetFileNameWithoutExtension(guid);
    apMedia media = DB.apMedia_GetMediaByFilename(guid);
    string mediaPath = Path.Combine(Transcode.Swap_MobileDirectory, guid + ".m4v");

    if (!Directory.Exists(Transcode.Swap_MobileDirectory)) //Make sure it's there...
        Directory.CreateDirectory(Transcode.Swap_MobileDirectory);

    if(System.IO.File.Exists(mediaPath))
        return base.File(mediaPath, "video/x-m4v");

    return Redirect("~/Error/404");
}

我知道我需要做这样的事情,但是我无法在.NET MVC中做到这一点。 http://dotnetslackers.com/articles/aspnet/Range-Specific-Requests-in-ASP-NET.aspx

以下是一个有效的HTTP响应标头示例:

Date    Mon, 08 Nov 2010 17:02:38 GMT
Server  Apache
Last-Modified   Mon, 08 Nov 2010 17:02:13 GMT
Etag    "14e78b2-295eff-4cd82d15"
Accept-Ranges   bytes
Content-Length  2711295
Content-Range   bytes 0-2711294/2711295
Keep-Alive  timeout=15, max=100
Connection  Keep-Alive
Content-Type    text/plain

这是一个没有的例子(这是来自.NET)

Server  ASP.NET Development Server/10.0.0.0
Date    Mon, 08 Nov 2010 18:26:17 GMT
X-AspNet-Version    4.0.30319
X-AspNetMvc-Version 2.0
Content-Range   bytes 0-2711294/2711295
Cache-Control   private
Content-Type    video/x-m4v
Content-Length  2711295
Connection  Close

有什么想法吗?谢谢。

4 个答案:

答案 0 :(得分:21)

更新:现在是project on CodePlex

好的,我让它在我当地的测试站工作,我可以将视频流式传输到我的iPad上。它有点脏,因为它比我预期的要困难一些,现在它正在工作,我现在没有时间清理它。关键部分:

动作过滤器:

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)
    {
    }
}

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

我的MVC行动:

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

我真的希望这会有所帮助。我花了很多时间在这上面!您可能想要尝试的一件事是移除碎片直到它再次破裂。很高兴看到ETag的东西,修改日期等是否可以被删除。我现在没有时间。

快乐的编码!

答案 1 :(得分:2)

我尝试寻找现有的扩展程序,但我没有立即找到一个(也许我的搜索功能很弱。)

我的想法是你需要制作两个新课程。

首先,创建一个继承自ActionMethodSelectorAttribute的类。这与HttpGetHttpPost等基类相同。在此课程中,您将覆盖IsValidForRequest。在该方法中,检查标头以查看是否请求了范围。您现在可以使用此属性来装饰控制器中的某个方法,当某人被请求成为流的一部分(iOS,Silverlight等)时,该方法将被调用。

其次,创建一个继承自ActionResultFileResult的类,并覆盖ExecuteResult方法,以添加为要返回的字节范围标识的标题。像JSON对象一样返回它,其中包含字节范围start,end,total size的参数,因此它可以正确生成响应头。

查看FileContentResult的实现方式,了解如何访问上下文的HttpResponse对象以更改标题。

查看HttpGet,了解它如何实现对IsValidForRequest的检查。 CodePlex上提供了源代码,或者您可以像我刚才那样使用Reflector。

您可以使用此信息进行更多搜索,看看是否有人已经创建了此自定义ActionResult

供参考,这是AcceptVerbs属性的样子:

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

这就是FileResult的样子。注意使用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);
}

我把它拼凑在一起。我不知道它是否适合你的需要(或有效)。

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

像这样使用:

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

答案 2 :(得分:0)

你可以搬出MVC吗?这是一个系统抽象射击你的脚的情况,但一个简单的jane IHttpHandler应该有更多的选择。

所有这一切,在您实施自己的流媒体服务器之前,您最好购买或租用一台。 。

答案 3 :(得分:-1)

有效内容类型设置为text / plain的标题是正确的还是拼写错误? 任何人,您可以尝试在Action上设置此标题:

Response.Headers.Add(...)