Twitter Facebook RSS Feed

domingo, 29 de septiembre de 2019 a las 21:17hs por Gustavo Cantero (The Wolf)

En las cabeceras HTTP/1.1 de las peticiones que recibe nuestra aplicación puede llegarnos el parámetro «range», el cual nos indica que el dispositivo que hace la llamada sólo necesita una parte del archivo que estamos enviando. Una caso muy normal de esta práctica, es cuando desde un dispositivo iOS necesitamos descargar un vídeo para visualizarlo en una página. Lo primero que recibiremos, en ese caso, es una petición solicitando los primeros bytes del archivo, y luego nos pedirá el resto del archivo, como diría Jack, «por partes». Esto se llama «HTTP Byte Range«, y está especificado en el RFC 7233.
Por ejemplo, esta es la primer llamada que realiza un iPhone cuando necesita un archivo de vídeo:

20:38:50.123456 IP 192.168.1.1.49186 > 70.164.240.67.80: tcp 80
GET /video.mp4 HTTP/1.1
Host: programandoamedianoche.com
Range: bytes=0-1
X-Playback-Session-Id: E4C46EAC-17EC-4444-8888-4FBF4B7BE12B
Accept-Encoding: identity
Accept: */*
Accept-Language: es-ar
Connection: keep-alive
User-Agent: AppleCoreMedia/1.0.0.10B146 (iPhone; U; CPU OS 6_1_2 like Mac OS X; en_us)

Luego de esta llamada, donde el servidor le devuelve los dos primeros bytes del archivo, realiza otra llamada pidiendo algunos bytes mas:

20:38:51.123456 IP 192.168.1.1.49186 > 70.164.240.67.80: tcp 80
GET /video.mp4 HTTP/1.1
Host: programandoamedianoche.com
Range: bytes=0-4372372
X-Playback-Session-Id: E4C46EAC-17EC-4444-8888-4FBF4B7BE12B
Accept-Encoding: identity
Accept: */*
Accept-Language: es-ar
Connection: keep-alive
User-Agent: AppleCoreMedia/1.0.0.10B146 (iPhone; U; CPU OS 6_1_2 like Mac OS X; en_us)

Y después, cuando el usuario haya terminado de ver los bytes descargados, se van a pedir más bytes:

20:38:52.123456 IP 192.168.1.1.49186 > 70.164.240.67.80: tcp 80
GET /video.mp4 HTTP/1.1
Host: programandoamedianoche.com
Range: bytes=4325376-4372372
X-Playback-Session-Id: E4C46EAC-17EC-4444-8888-4FBF4B7BE12B
Accept-Encoding: identity
Accept: */*
Accept-Language: es-ar
Connection: keep-alive
User-Agent: AppleCoreMedia/1.0.0.10B146 (iPhone; U; CPU OS 6_1_2 like Mac OS X; en_us)

Todo esto lo maneja automáticamente el IIS, pero supongamos que nuestra aplicación tiene vídeos, o archivos, que están protegidos por el rol del usuario, o al menos es necesario que el usuario esté autenticado, en ese caso no podemos hacer que estos archivos estén en una carpeta pública, por lo cual, necesitaremos hacer un método en un controlador que valide los permisos y que devuelva el archivo, pero si devolvemos el clásico HttpResponseMessage con todo el contenido del archivo, el iPhone nos va a dar error porque no le estamos devolviendo el rango pedido.
Para hacer bien las cosas, deberíamos devolver un HttpResponseMessage con un ByteRangeStreamContent de contenido, especificando el rango de bytes devuelto, y con los bytes correspondientes.
A continuación les dejo una parte del código que nosotros utilizamos en un proyecto para realizar este envío parcial del archivo, con lo cual pudimos hacer que la respuesta la puedan leer los dispositivos iOS sin problema.

/// <summary>
/// Clase para el acceso a los archivos
/// </summary>
public class FileController : ApiController
{
    /// <summary>
    /// Devuelve un vídeo
    /// </summary>
    [HttpGet()]
    [Route("api/file/video/{id:guid}")]
    public HttpResponseMessage Video(Guid id)
    {
        if (!Helper.ExistsVideoFile(id)) //Valido que el archivo exista en el disco
            return new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound };

        // Si me piden un rango de bytes (dispositivos Apple y otros), devuelo lo necesario
        if (Request.Headers.Range != null)
        {
            var objStream = new FileStream(Helper.GetVideoFile(id), FileMode.Open, FileAccess.Read);
            try
            {
                HttpResponseMessage objPartialResponse = Request.CreateResponse(HttpStatusCode.PartialContent);
                objPartialResponse.Content = new ByteRangeStreamContent(objStream, Request.Headers.Range, new MediaTypeHeaderValue("video/mp4"));
                return objPartialResponse;
            }
            catch (InvalidByteRangeException invalidByteRangeException)
            {
                try { objStream.Close(); }
                catch { }
                return Request.CreateErrorResponse(invalidByteRangeException);
            }
        }
        else
        {
            //Si no pide un rango, devuelvo el archivo completo
            using (var objStream = new FileStream(Helper.GetVideoFile(id), FileMode.Open, FileAccess.Read))
            {
                var objResult = new HttpResponseMessage()
                {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StreamContent(new FileStream(Helper.GetVideoFile(id), FileMode.Open, FileAccess.Read))
                };
                objResult.Content.Headers.ContentType = new MediaTypeHeaderValue("video/mp4");
                return objResult;
            }
        }
    }
}

El método Helper.ExistsVideoFile nos confirma si existe el archivo en el disco, y el método Helper.GetVideoFile(id) nos devuelve la ruta completa del archivo a devolver.
Con este código podremos hacer que nuestra aplicación devuelva vídeos u otros archivos en partes, haciendo que sea compatible con dispositivos Apple, como iPhone y iPad.
Espero les sea de utilidad.
¡Suerte!

0 comentarios »

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.