Streaming large video files .net
Asked Answered
U

3

13

I am trying to stream a large file in webforms from an HttpHandler. It doesn't seem to work because its not streaming the file. Instead its reading the file into memory then sends it back to the client. I look all over for a solution and the solution are telling me that they stream the file when they are doing the same thing. My solution that stream is this:

using (Stream fileStream = File.OpenRead(path))
{
    context.Response.Cache.SetExpires(DateTime.UtcNow.AddMinutes(360.0));
    context.Response.Cache.SetCacheability(HttpCacheability.Public);
    context.Response.AppendHeader("Content-Type", "video/mp4");
    context.Response.AppendHeader("content-length", file.Length);
    byte[] buffer = new byte[1024];
    while (true)
    {
      if (context.Response.IsClientConnected)
     {
       int bytesRead = fileStream.Read(buffer, 0, buffer.Length);
       if (bytesRead == 0) break;
       context.Response.OutputStream.Write(buffer, 0, bytesRead);
       context.Response.Flush();
     }
     else
     {
       break;
     }

   }
   context.Response.End();
}

What is happening is for small files if I debug the code, it will play the video but not until it reaches the context.Respond.End() line. But for large files this will not work because it is storing the whole file in memory which will bring issues.

Unlicensed answered 31/5, 2013 at 17:30 Comment(3)
You probably want to implement this all asynchronously with IHttpAsyncHandler and reading from the disk asynchronously.Archibold
@vcsjones- I am not sure how the Async works, but just did a quick google and i dont see how that would solve the problem. That seems to free up a thread to return to the client to async read from disk but would that return the content since its still reading from disk?Unlicensed
This question saved me,Thank you!Optometrist
O
18

I had a similar issue, where the video had to be downloaded completely before playing.

I can see you want to stream videos, to be more specific. You have to be careful about the encoding (make sure it is streamable), don't rely on the extension only, because the person who created the file could have build the video in a wierd way, but 99% of the time you should be good. I use mediainfo. In your case should be H.264.

It also depends on browser and what you use to stream (other than backend code). For my case, I used Chrome/Html5 and .webm (VP8/Ogg Vorbis). It is working for files over 1G. Didn't test for bigger than 4G...

The code I use for download of the video:

    public void Video(string folder, string name) {
        string filepath = Server.MapPath(String.Format("{0}{1}", HttpUtility.UrlDecode(folder), name));
        string filename = name;

        System.IO.Stream iStream = null;
        byte[] buffer = new Byte[4096];
        int length;
        long dataToRead;

        try {
            // Open the file.
            iStream = new System.IO.FileStream(filepath, System.IO.FileMode.Open,
                        System.IO.FileAccess.Read, System.IO.FileShare.Read);


            // Total bytes to read:
            dataToRead = iStream.Length;

            Response.AddHeader("Accept-Ranges", "bytes");
            Response.ContentType = MimeType.GetMIMEType(name);

            int startbyte = 0;

            if (!String.IsNullOrEmpty(Request.Headers["Range"])) {
                string[] range = Request.Headers["Range"].Split(new char[] { '=', '-' });
                startbyte = Int32.Parse(range[1]);
                iStream.Seek(startbyte, SeekOrigin.Begin);

                Response.StatusCode = 206;
                Response.AddHeader("Content-Range", String.Format(" bytes {0}-{1}/{2}", startbyte, dataToRead - 1, dataToRead));
            }

            while (dataToRead > 0) {
                // Verify that the client is connected.
                if (Response.IsClientConnected) {
                    // Read the data in buffer.
                    length = iStream.Read(buffer, 0, buffer.Length);

                    // Write the data to the current output stream.
                    Response.OutputStream.Write(buffer, 0, buffer.Length);
                    // Flush the data to the HTML output.
                    Response.Flush();

                    buffer = new Byte[buffer.Length];
                    dataToRead = dataToRead - buffer.Length;
                } else {
                    //prevent infinite loop if user disconnects
                    dataToRead = -1;
                }
            }
        } catch (Exception ex) {
            // Trap the error, if any.
            Response.Write("Error : " + ex.Message);
        } finally {
            if (iStream != null) {
                //Close the file.
                iStream.Close();
            }
            Response.Close();
        }
    }

Make sure your response header contains everything you need.

Olethea answered 31/5, 2013 at 18:26 Comment(9)
the file is encoded easily with no problem. I can play the file in the browser if I go to it directly. What is mediainfo and how is it beneficial towards this question. I am using html 5 as well, and it still doesn't seem to play large files. the file I am testing is about 150 MB and it sends back to the browser through a handler to the source of the html5 tag but never plays, but if I send the smaller file 6 MB it plays no problemUnlicensed
Make sure that httpRuntime has maxRequestLength big enough to send the file. Mediainfo is just to see the details of the video (codecs and bitrate)Olethea
I know the details of the video. I encoded it myself with ffmpeg. it plays fine in the browser if I go to it directly and the maxRequestLength is 2000000000 and the file is only 150MBUnlicensed
maybe there is some other limitation that is causing the issueUnlicensed
@Olethea - Thanks for posting this! This code works, just used it to improve my videojs implementaton serving the video data from an http handler. Couldn't seek in the streams but after replacing my .WriteFile with this, seeking now worksCollegium
@Maxad- How is this any different from the code I posted. It looks like it would be the same resultsUnlicensed
This is the only example that has worked for me so far. Thanks!Racine
I'd just toss Response.IsClientConnected into the while loop. Cleaner IMO than mucking up that block with an extra layer of indentation for the if.Kerbstone
@Maxad: Thank you so mutch ,This is perfect answer.Thanx. I also made some changes for .Net Core API version.Optometrist
B
3

What really matters here is the 'Range' header. Although the existing answer is correct, it contains no explanation.

When you make a request without specifying a range, the whole entire file is streamed. Video players automatically specify the 'range' header with a starting byte that is in accordance with the players position in the video.

Since this is inherently part of HTTP, it's extremely well documented in RFC 7233.

The 'Accept-Range: bytes' header tells the client that we want to accept the range header as byte counts. The status code '206' tells the client that we sent partial content, aka only a piece of the whole file. The 'Content-Range: start-end/total' header tells the client the range of the information that we're sending back in the current request.

Here is a fully functional snippet:

public static void RespondFile(this HttpListenerContext context, string path, bool download = false) {

    HttpListenerResponse response = context.Response;

    // tell the browser to specify the range in bytes
    response.AddHeader("Accept-Ranges", "bytes");

    response.ContentType = GetMimeType(path);
    response.SendChunked = false;

    // open stream to file we're sending to client
    using(FileStream fs = File.OpenRead(path)) {

        // format: bytes=[start]-[end]
        // documentation: https://www.rfc-editor.org/rfc/rfc7233#section-4
        string range = context.Request.Headers["Range"];
        long bytes_start = 0,
        bytes_end = fs.Length;
        if (range != null) {
            string[] range_info = context.Request.Headers["Range"].Split(new char[] { '=', '-' });
            bytes_start = Convert.ToInt64(range_info[1]);
            if (!string.IsNullOrEmpty(range_info[2])) 
                bytes_end = Convert.ToInt64(range_info[2]);
            response.StatusCode = 206;
            response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", bytes_start, bytes_end - 1, fs.Length));
        }

        // determine how many bytes we'll be sending to the client in total
        response.ContentLength64 = bytes_end - bytes_start;

        // go to the starting point of the response
        fs.Seek(bytes_start, SeekOrigin.Begin);

        // setting this header tells the browser to download the file
        if (download) 
            response.AddHeader("content-disposition", "attachment; filename=" + Path.GetFileName(path));

        // stream video to client
        // note: closed connection during transfer throws exception
        byte[] buffer = new byte[HttpServer.BUFFER_SIZE];
        int bytes_read = 0;
        try {

            while (fs.Position < bytes_end) {
                bytes_read = fs.Read(buffer, 0, buffer.Length);
                response.OutputStream.Write(buffer, 0, bytes_read);
            }

            response.OutputStream.Close();

        } catch(Exception) {}

    }

}

Note that we can simply check the file stream's "Position" (in bytes) rather than keeping track of how many bytes we've already sent in total.

Birkle answered 30/4, 2019 at 21:46 Comment(0)
O
1

Maxad's answer is the perfect answer .I also made some changes for .Net Core version:

<video id="myvideo" height="400" width="600" controls>
    <source src="../api/StreamApi/GetStream" type="video/mp4"/>
</video>

    [Route("api/StreamApi/GetStream")]
    [HttpGet]
    public async Task GetStream()
    {
        string filepath = @"C:\temp\car.mp4";
        string filename = Path.GetFileName(filepath);

        System.IO.Stream iStream = null;
        byte[] buffer = new Byte[4096];
        int length;
        long dataToRead;

        try
        {
            // Open the file.
            iStream = new System.IO.FileStream(filepath, System.IO.FileMode.Open,
                        System.IO.FileAccess.Read, System.IO.FileShare.Read);


            // Total bytes to read:
            dataToRead = iStream.Length;

            Response.Headers["Accept-Ranges"] = "bytes";
            Response.ContentType = "application/octet-stream";

            int startbyte = 0;

            if (!String.IsNullOrEmpty(Request.Headers["Range"]))
            {
                string[] range = Request.Headers["Range"].ToString().Split(new char[] { '=', '-' });
                startbyte = Int32.Parse(range[1]);
                iStream.Seek(startbyte, SeekOrigin.Begin);

                Response.StatusCode = 206;
                Response.Headers["Content-Range"] = String.Format(" bytes {0}-{1}/{2}", startbyte, dataToRead - 1, dataToRead);
            }
            var outputStream = this.Response.Body;
            while (dataToRead > 0)
            {
                // Verify that the client is connected.
                if (HttpContext.RequestAborted.IsCancellationRequested == false)
                {
                    // Read the data in buffer.
                    length = await iStream.ReadAsync(buffer, 0, buffer.Length);

                    // Write the data to the current output stream.
                    await outputStream.WriteAsync(buffer, 0, buffer.Length);
                    // Flush the data to the HTML output.
                    outputStream.Flush();

                    buffer = new Byte[buffer.Length];
                    dataToRead = dataToRead - buffer.Length;
                }
                else
                {
                    //prevent infinite loop if user disconnects
                    dataToRead = -1;
                }
            }
        }
        catch (Exception ex)
        {
            // Trap the error, if any.
          
        }
        finally
        {
            if (iStream != null)
            {
                //Close the file.
                iStream.Close();
            }
            Response.Clear();
        }
    }
Optometrist answered 19/10, 2020 at 21:56 Comment(3)
I tried your code but i'm gettin an error in "iStream.Seek(startbyte, SeekOrigin.Begin". The exception is "Specified method is not supported". My stream is coming from a query on mongodb.Photogenic
Seek is a System.IO function. Did you include the right version?Optometrist
I found the problem, your answer saved me. Thanks a lot!Photogenic

© 2022 - 2024 — McMap. All rights reserved.