Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problems with streaming video for IOS Client (Server developed on ASP.NET WEB API 2)

I have a problem with streaming video. I developed the server on ASP.NET Web API 2 and implemented 2 methods:

The first method:

if (Request.Headers.Range != null)
    {
        try
        {
            var httpResponce = Request.CreateResponse();
            httpResponce.Content =
                new PushStreamContent((Action<Stream, HttpContent, TransportContext>) WriteContentToStream);

            return httpResponce;
        }
        catch (Exception ex)
        {
            return new HttpResponseMessage(HttpStatusCode.InternalServerError);
        }
    }
    else
    {
        return new HttpResponseMessage(HttpStatusCode.RequestedRangeNotSatisfiable);
    }

/*method for streaming*/

private async void WriteContentToStream(Stream outputStream, HttpContent content, TransportContext transportContext)
{
    string relativeFilePath = "~/App_Data/Videos/4.mp4";
    try
    {
        var filePath = System.Web.Hosting.HostingEnvironment.MapPath(relativeFilePath);

        int bufferSize = 1000;
        byte[] buffer = new byte[bufferSize];
        using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            int totalSize = (int)fileStream.Length;
            while (totalSize > 0)
            {
                int count = totalSize > bufferSize ? bufferSize : totalSize;
                int sizeOfReadedBuffer = fileStream.Read(buffer, 0, count);
                await outputStream.WriteAsync(buffer, 0, sizeOfReadedBuffer);
                totalSize -= sizeOfReadedBuffer;
            }
        }
    }
    catch (HttpException ex)
    {
        if (ex.ErrorCode == -2147023667)  
        {
            return;
        }
    }
    finally
    {
        outputStream.Close();
    }
}

2) The second method:

public HttpResponseMessage Test()
{
    if (Request.Headers.Range != null)
    {
        try 
        {
            string relativeFilePath = "~/App_Data/Videos/4.mp4";
            var filePath = System.Web.Hosting.HostingEnvironment.MapPath(relativeFilePath);
            HttpResponseMessage partialResponse = Request.CreateResponse(HttpStatusCode.PartialContent);
            partialResponse.Headers.AcceptRanges.Add("bytes");
            var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            partialResponse.Content = new ByteRangeStreamContent(stream, Request.Headers.Range, new MediaTypeHeaderValue("video/mp4")); 
            return partialResponse;
        }
        catch (Exception)
        {
            return new HttpResponseMessage(HttpStatusCode.InternalServerError);
        }
    }   
    else
    {
        return new HttpResponseMessage(HttpStatusCode.RequestedRangeNotSatisfiable);    
    }
}

Both of these methods worked on Web-client and Android-client, but iOS-client doesn't show video.

I think, that problem may be with codec of video (but I used codecs, which recommend Apple) or http-headers.

like image 415
Violetta Avatar asked Nov 22 '17 08:11

Violetta


1 Answers

I just solved this one, and it was because the Content-Length header had (what iOS considered to be) an invalid value.

My solution was based on method #2 above... Here's the important part of my code that actually worked.

if (!file.Exists) {
    response.StatusCode = HttpStatusCode.NotFound;
    response.ReasonPhrase = "Deleted";
} else {
    var range = Request.Headers.Range?.Ranges?.FirstOrDefault();
    if (range == null) {
        using (var stream = new MemoryStream()) {
            using (var video = file.OpenRead()) await video.CopyToAsync(stream);
            response.Content = new ByteArrayContent(stream.ToArray());
        }
        response.Content.Headers.ContentType = new MediaTypeHeaderValue("video/mp4");
        response.Content.Headers.ContentLength = file.Length;
    } else {
        var stream = new MemoryStream();
        using (var video = file.OpenRead()) await video.CopyToAsync(stream);
        response.Content = new ByteRangeStreamContent(
            stream,
            new RangeHeaderValue(range.From, range.To),
            new MediaTypeHeaderValue("video/mp4")
        );
        //  response.Content.Headers.ContentLength = file.Length;
        // this is what makes iOS work
        response.Content.Headers.ContentLength = (range.To.HasValue ? range.To.Value + 1 : file.Length) - (range.From ?? 0);
    }
    response.StatusCode = HttpStatusCode.OK;
}

I should probably put in an HTTP 206 (partial content) status when dealing with ranges, but I was working on this for nearly two days before coming up with a solution.

The only problem I have yet to fully track down is that from time-to-time, the Application_EndRequest doesn't fire for some of these. I am able to log the response being sent by the endpoint, but it's like iOS disconnects the connection somewhere and the request hangs until it times out internally.

like image 81
Alex Lein Avatar answered Sep 19 '22 22:09

Alex Lein