Подавать видеофайл на iPhone с ASP.NET MVC2 - PullRequest
9 голосов
/ 08 ноября 2010

Я пытаюсь передать видеофайлы из 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 ]

21 голосов
/ 09 ноября 2010

ОБНОВЛЕНИЕ: теперь это проект на 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, дату модификации и т. Д. У меня просто нет времени на данный момент.

Удачного кодирования!

2 голосов
/ 09 ноября 2010

Я пытался найти существующее расширение, но не сразу нашел его (возможно, у меня слабая функция поиска).

Я сразу подумал, что вам нужно создать два новых класса.

Сначала создайте класс, унаследованный от ActionMethodSelectorAttribute.Это тот же базовый класс для HttpGet, HttpPost и т. Д. В этом классе вы переопределите IsValidForRequest.В этом методе проверьте заголовки, чтобы увидеть, был ли запрошен диапазон.Теперь вы можете использовать этот атрибут для украшения метода в вашем контроллере, который будет вызываться, когда кому-либо будет запрошена часть потока (iOS, Silverlight и т. Д.)

Во-вторых, создайте класс, наследующий либо ActionResultили, возможно, FileResult и переопределите метод 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);
0 голосов
/ 09 ноября 2010

Можете ли вы выйти за пределы MVC?Это тот случай, когда системные абстракции стреляют вам в ногу, но у простого джейна IHttpHandler должно быть намного больше опций.или арендовать один.,.

0 голосов
/ 08 ноября 2010

В заголовке, который работает, для Content-type установлено значение text / plain, это правильно или это опечатка? Любой, вы можете попробовать установить эти заголовки в действии с помощью:

Response.Headers.Add(...)
...