2010-11-08 12 views
9

Estoy intentando servir archivos de video desde ASP.NET MVC a clientes de iPhone. El video está formateado correctamente, y si lo tengo en un directorio web de acceso público funciona bien.Servir archivo de video a iPhone desde ASP.NET MVC2

El problema principal de lo que he leído es que el iPhone requiere que tengas un entorno de descarga listo para reanudar que te permite filtrar tus rangos de bytes a través de encabezados HTTP. Supongo que esto es para que los usuarios puedan saltar hacia adelante a través de los videos.

Al servir archivos con MVC, estos encabezados no existen. Intenté emularlo, pero sin suerte. Tenemos IIS6 aquí y no puedo hacer muchas manipulaciones de encabezado. ASP.NET se quejará a mí diciendo "Esta operación requiere modo canalización integrada de IIS."

La actualización no es una opción, y no se me permite mover los archivos a un recurso compartido de web público. Me siento limitado por nuestro entorno, pero estoy buscando soluciones, no obstante.

Aquí hay un código de ejemplo de lo que estoy tratando de hacer en fin ...

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

Sé que tengo que hacer algo como esto, sin embargo soy incapaz de hacerlo en. NET MVC. http://dotnetslackers.com/articles/aspnet/Range-Specific-Requests-in-ASP-NET.aspx

Aquí es un ejemplo de un encabezado de respuesta HTTP que funciona:

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 

Y aquí es un ejemplo de uno que no lo hace (esto es de .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 

Cualquier ideas? Gracias.

Respuesta

20

ACTUALIZACIÓN: Este es ahora un project on CodePlex.

Bien, lo tengo trabajando en mi estación local de pruebas y puedo transmitir videos a mi iPad. Está un poco sucio porque fue un poco más difícil de lo que esperaba y ahora que está funcionando no tengo tiempo para limpiarlo en este momento. Las partes fundamentales:

acción de filtro:

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

de Resultados personalizado basado en 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; 
      } 
     } 
    }  
} 

Mi acción 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); 
} 

Realmente espero que esto ayude. ¡Pasé MUCHO tiempo en esto! Una cosa que quizás quieras probar es quitar las piezas hasta que se rompa nuevamente. Sería bueno ver si las cosas ETag, la fecha de modificación, etc. podrían eliminarse. Simplemente no tengo el tiempo en este momento.

Happy coding!

+0

Gracias por toda su increíble ayuda. Voy a probar esto en la mañana! – jocull

+0

Usted patea el culo, hombre, esto funciona como debería. Para la referencia de todos, vi los encabezados de solicitud de iPhone mientras transmitía el video. Hace que muchas solicitudes posteriores soliciten rangos de bytes pequeños y específicos a medida que avanza a través del video. Supongo que los une todos juntos sobre la marcha. Gracias por toda su ayuda Erik. – jocull

+0

Además, olvidé mencionar esto, pero tuve que cambiar "clase pública FileStreamResult: FileStreamResult" de nuevo a "publicResultationRange class class: FileStreamResult". ¡No me gustó que extendiera una clase con el mismo nombre! Solo un pequeño problema para su edición. – jocull

-1

El encabezado que funciona tiene el tipo de contenido configurado en texto/normal, ¿es correcto o es un error tipográfico? Cualquier persona, se puede tratar de establecer este cabeceras en la acción con:

Response.Headers.Add(...) 
+0

Response.Headers.Add no funciona en IIS6 debido al error que he citado anteriormente. Creo que simplemente ignora la prueba/plain por lo que todavía funciona. – jocull

+0

Mirando en Reflector parece que hay un método específico en respuesta llamado AddHeader - me parece extraño que esto esté allí si los encabezados fueran inmutables. Además, otros tipos de respuestas modifican el tipo de contenido y la codificación, y funcionan en IIS 6 (aunque acceden a funciones específicas en lugar de modificar directamente la propiedad Headers). –

+0

Creo que Response.AppendHeader (string, string) funciona. Sin embargo, no tengo ningún otro control sobre eso. – jocull

2

He intentado buscar una extensión existente pero no se encontró inmediatamente una

(tal vez mi búsqueda-fu es débil.)

Mi pensamiento inmediato es que tendrá que hacer dos clases nuevas.

Primero, crea una clase que hereda de ActionMethodSelectorAttribute. Esta es la misma clase base para HttpGet, HttpPost, etc. En esta clase anulará IsValidForRequest. En ese método, examine los encabezados para ver si se solicitó un rango. Ahora puede usar este atributo para decorar un método en su controlador que se llamará cuando se solicite a alguien parte de una transmisión (iOS, Silverlight, etc.)

En segundo lugar, cree una clase heredando ya sea ActionResult o quizás FileResult y anula el método ExecuteResult para agregar los encabezados que identificó para el rango de bytes que va a devolver. Regrésalo como lo harías con un objeto JSON con parámetros para el rango de bytes de inicio, fin y tamaño total, para que pueda generar los encabezados de respuesta correctamente.

Eche un vistazo a la manera en que se implementa FileContentResult para ver cómo accede al objeto HttpResponse del contexto para alterar los encabezados.

Observe cómo implementa el cheque IsValidForRequest. La fuente está disponible en CodePlex o puedes usar Reflector como acabo de hacerlo.

Puede utilizar esta información para hacer un poco más de búsqueda y ver si alguien ya ha creado esta costumbre ActionResult ya.

Para referencia, aquí es lo que los AcceptVerbs atribuyen el siguiente aspecto:

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

Y aquí es lo FileResult se parece. Observe el uso de 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); 
} 

Acabo de reconstruir esto. No sé si se adaptará a sus necesidades (o trabajos).

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

utilizar de esta manera:

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

Intenté agregar un encabezado para "Aceptar solicitudes: bytes", pero .NET se niega a agregarlo o ignorarlo. No puedo mostrarlo cuando genero mi respuesta. Es una pieza crítica, ¿tienes ideas sobre cómo forzarla? – jocull

+0

Acabo de publicar algunos fragmentos de código extraídos del reflector. El segundo usa el método AddHeader en el contexto. ¿Es así como lo estás haciendo? Si no se modifica la copia de la respuesta del contexto, probablemente no persista, por lo que recomiendo crear aquí su propio tipo de devolución (en lugar de intentar agregar los encabezados a otro tipo de devolución como FileContentResult). –

+0

Al mirar el enlace que proporcionó, parece que las cosas deberían seguir así: decore su acción de descarga para ver si se solicitó un rango de bytes. Examine el objeto de solicitud para obtener el rango solicitado durante su respuesta. En la respuesta, agregue el encabezado que acepta bytes independientemente de si el cliente lo ha solicitado. También incluya encabezados sobre el rango provisto (con suerte basado en lo que se solicitó). Al observar los objetos existentes en el marco de MVC 2, todo debería funcionar. En su tipo de respuesta, ¿usa AddHeader? ¿Lanza un error? ¿El encabezado realmente aparece en la respuesta? –

0

¿Puede moverse fuera de MVC? Este es un caso en el que las abstracciones del sistema te están disparando en el pie, pero un simple IHttpHandler de jane debería tener muchas más opciones.

Dicho todo esto, antes de implementar su propio servidor de transmisión, es mejor que compre o alquile uno. . .

+0

Tenemos un servidor de transmisión ... Nuestros administradores nunca lo configurarán justo antes de que lo necesite. Estoy atrapado con MVC por ahora. – jocull

Cuestiones relacionadas