代码之家  ›  专栏  ›  技术社区  ›  jocull

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

  •  9
  • jocull  · 技术社区  · 14 年前

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

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

    当用MVC服务文件时,这些标题不存在。我试过模仿它,但没有运气。我们这里有IIS6,我根本不能做很多头球操作。ASP.NET会抱怨我说 此操作需要IIS集成管道模式。 "

    升级不是一个选项,我不允许将文件移动到公共web共享。我觉得我们的环境有限,但我仍在寻找解决办法。

    这里是一些我试图做的简单代码。。。

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

    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 回复  |  直到 14 年前
        1
  •  21
  •   Erik Noren    14 年前

    更新:现在是 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)
        {
        }
    }
    

    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的内容、修改日期等是否可以删除。我只是现在没有时间。

        2
  •  2
  •   Erik Noren    14 年前

    我试图寻找一个现有的扩展,但我没有立即找到一个(也许我的搜索FU是弱的。)

    我现在的想法是你需要上两门新课。

    首先,创建一个继承自 ActionMethodSelectorAttribute HttpGet , HttpPost ,等等。在这个类中,您将重写 IsValidForRequest . 在该方法中,检查头以查看是否请求了范围。现在可以使用此属性修饰控制器中的方法,当某人被请求成为流的一部分(iOS、Silverlight等)时,将调用该方法

    ActionResult 或者也许 FileResult 并覆盖 ExecuteResult

    看一看路 FileContentResult 实现以查看如何访问上下文的 HttpResponse 对象来更改标题。

    看一看 看看它是如何实现对 . 源代码可以在CodePlex上找到,也可以像我刚才那样使用Reflector。

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

    作为参考,这里是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);
    
        3
  •  0
  •   Wyatt Barnett    14 年前

    你能搬出MVC吗?这是一个系统抽象让你大吃一惊的例子,但是一个普通的jane ihttfandler应该有更多的选择。

    尽管如此,在您实现自己的流媒体服务器之前,您可能最好购买或租赁一台流媒体服务器。. .

        4
  •  -1
  •   David Martinez    14 年前

    工作的标题将内容类型设置为text/plain,这是正确的还是打字错误?.

    Response.Headers.Add(...)