【问题标题】:Serve video file to iPhone from ASP.NET MVC2从 ASP.NET MVC2 向 iPhone 提供视频文件
【发布时间】:2010-12-08 13:07:56
【问题描述】:

我正在尝试将视频文件从 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

有什么想法吗?谢谢。

【问题讨论】:

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


    【解决方案1】:

    更新:现在是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 内容、修改日期等。我只是暂时没有时间。

    编码愉快!

    【讨论】:

    • 感谢您提供的所有出色帮助。我会在早上测试这个!
    • 你该死,伙计,这就像它应该的那样工作。供大家参考,我在 iPhone 流式传输视频时观看了请求标头。随着视频的进行,它会发出许多后续请求,要求提供小的、特定的字节范围。我认为它会在飞行中将它们拼凑在一起。感谢 Erik 的所有帮助。
    • 另外,忘记提及这一点,但我不得不将“公共类 FileStreamResult : FileStreamResult”改回“公共类 ContentRangeResult : FileStreamResult”。它不喜欢我扩展同名的课程!只是您编辑的一个小问题。
    • 这是一个错字。对不起。在我完成所有工作并使其正常工作后,我在一个新环境中干净利落地重新实现了它,并在其上发表了一篇博客文章,并选择了不同的名称。我在发布更新的源代码时手动编辑了名称,但已经晚了,我显然搞砸了!很高兴它对你有用。我写的博文:vikingerik.wordpress.com/2010/11/09/…
    • 我不确定您是否仍然感兴趣,但我已经改进了解决方案并将其放在 CodePlex 上。我认为它更简洁一些,并且可以更轻松地在您的操作中使用它。有两个项目,库和将媒体流式传输到 iOS 设备的演示 MVC 应用程序。 mediastreamingmvc.codeplex.com
    【解决方案2】:

    我尝试寻找现有的扩展,但没有立即找到(可能我的 search-fu 很弱。)

    我的直接想法是你需要创建两个新课程。

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

    其次,创建一个继承自ActionResultFileResult 的类,并覆盖ExecuteResult 方法以添加您为要返回的字节范围确定的标头。像返回一个 JSON 对象一样返回它,其中包含字节范围开始、结束、总大小的参数,以便它可以正确生成响应标头。

    查看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);
    

    【讨论】:

    • 我试图为“Accept-Requests: bytes”附加一个标头,但 .NET 要么拒绝添加它,要么忽略它。当我输出我的回复时,我无法让它显示出来。这是一个关键的部分,你有如何强制它的想法吗?
    • 我刚刚发布了一些从反射器中提取的代码 sn-ps。第二个在上下文中使用 AddHeader 方法。这是你的做法吗?如果您没有修改上下文的响应副本,它可能不会持续存在,这就是为什么我建议在这里创建您自己的返回类型(而不是尝试将标头添加到另一个返回类型,如 FileContentResult)。
    • 查看您提供的链接,看起来应该像这样运行:装饰您的下载操作以查看是否请求了字节范围。检查请求对象以获取响应期间请求的范围。在响应中,添加您接受字节的标头,无论客户端是否请求它。还包括有关所提供范围的标题(希望基于所请求的内容。)查看 MVC 2 框架中的现有对象,这应该都可以工作。在您的响应类型中,您使用的是 AddHeader 吗?它会抛出错误吗?标头是否真的出现在响应中?
    • 您可能不会关心他们是否在选择您的控制器方法执行之前发送范围请求。如果不是,请不要从 ActionMethodSelectorAttribute 继承,而是实现 IActionFilter 或从 ActionFilter 继承,这将为您提供 ActionExecutingContext ,您可以在其中检查您的请求并查看是否请求了范围。您可以将值作为参数传递到控制器方法中,就好像它是从表单提交一样。
    • 如果你还没有得到它的工作,让我知道。我从 YouTube 上抓取了一个视频,我可以在我的 iPad 上观看,将一些东西放在一起,然后我就可以使用 MVC 成功地将它流式传输到我的 iPad。
    【解决方案3】:

    您可以移出 MVC 吗?这是一个系统抽象让你措手不及的情况,但简单的 jane IHttpHandler 应该有更多的选择。

    话虽如此,在您实施自己的流媒体服务器之前,您最好还是购买或租用一台。 . .

    【讨论】:

    • 我们有一个流媒体服务器......我们的管理员永远不会在我需要完成之前设置它。我现在坚持使用 MVC。
    【解决方案4】:

    有效的标题将 Content-type 设置为 text/plain,这是正确的还是错字? 任何人,您都可以尝试在 Action 上设置此标头:

    Response.Headers.Add(...)
    

    【讨论】:

    • Response.Headers.Add 由于我上面引用的错误,在 IIS6 中不起作用。我认为它只是忽略了测试/简单,这就是它仍然有效的原因。
    • 在 Reflector 中查看,Reponse 中似乎有一个名为 AddHeader 的特定方法 - 如果标头是不可变的,这对我来说似乎很奇怪。此外,其他响应类型修改了内容类型和编码,它们在 IIS 6 上工作(尽管它们访问特定功能而不是直接修改 Headers 属性)。
    • 我相信 Response.AppendHeader(string,string) 有效。不过,我对此没有任何其他控制权。
    猜你喜欢
    • 2013-08-22
    • 1970-01-01
    • 2017-10-16
    • 1970-01-01
    • 2012-09-02
    • 1970-01-01
    • 2017-06-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多