Потоковое воспроизведение больших видеофайлов .net

Я пытаюсь передать большой файл в веб-формах из HttpHandler. Кажется, это не работает, потому что это не потоковая передача файла. Вместо этого он читает файл в память, а затем отправляет его обратно клиенту. Я ищу решение повсюду, и решение сообщает мне, что они транслируют файл, когда делают то же самое. Мое решение этого потока таково:

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

Что происходит с небольшими файлами, если я отлаживаю код, он будет воспроизводить видео, но не до тех пор, пока не достигнет строки context.Respond.End (). Но для больших файлов это не сработает, потому что при сохранении всего файла в памяти возникнут проблемы.


person Jake    schedule 31.05.2013    source источник
comment
Вероятно, вы захотите реализовать все это асинхронно с помощью IHttpAsyncHandler и чтение с диска асинхронно.   -  person vcsjones    schedule 31.05.2013
comment
@ vcsjones - я не уверен, как работает Async, но просто быстро погуглил, и я не вижу, как это решит проблему. Кажется, это освобождает поток, чтобы вернуться к клиенту для асинхронного чтения с диска, но вернет ли это содержимое, поскольку оно все еще читается с диска?   -  person Jake    schedule 31.05.2013
comment
Этот вопрос меня спас, Спасибо!   -  person Sadjad Khazaie    schedule 20.10.2020


Ответы (3)


У меня была аналогичная проблема, когда видео нужно было полностью загрузить перед воспроизведением.

Я вижу, вы хотите транслировать видео, чтобы быть более конкретным. Вы должны быть осторожны с кодировкой (убедитесь, что она транслируема), не полагайтесь только на расширение, потому что человек, создавший файл, мог построить видео странным способом, но в 99% случаев вам следует будь хорошим. Я использую mediainfo. В вашем случае должен быть H.264.

Это также зависит от браузера и того, что вы используете для потоковой передачи (кроме внутреннего кода). В моем случае я использовал Chrome / Html5 и .webm (VP8 / Ogg Vorbis). Он работает для файлов размером более 1 ГБ. Больше 4G не тестировал ...

Код, который я использую для загрузки видео:

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

Убедитесь, что заголовок вашего ответа содержит все, что вам нужно.

person Maxad    schedule 31.05.2013
comment
файл кодируется легко и без проблем. Я могу воспроизвести файл в браузере, если перейду к нему напрямую. Что такое mediainfo и чем он полезен в ответ на этот вопрос. Я также использую html 5, и он по-прежнему не воспроизводит большие файлы. файл, который я тестирую, составляет около 150 МБ, и он отправляет обратно в браузер через обработчик в источник тега html5, но никогда не воспроизводится, но если я отправлю файл меньшего размера 6 МБ, он не будет играть - person Jake; 31.05.2013
comment
Убедитесь, что httpRuntime имеет достаточно большой maxRequestLength для отправки файла. Mediainfo предназначена только для просмотра деталей видео (кодеки и битрейт) - person Maxad; 31.05.2013
comment
Я знаю подробности этого видео. Я сам закодировал его с помощью ffmpeg. он отлично работает в браузере, если я перейду к нему напрямую, а maxRequestLength - 2000000000, а файл - всего 150 МБ - person Jake; 31.05.2013
comment
возможно, есть какое-то другое ограничение, которое вызывает проблему - person Jake; 01.06.2013
comment
@Maxad - Спасибо, что разместили это! Этот код работает, просто использовал его для улучшения моей реализации videojs, обслуживающей видеоданные из обработчика http. Не удалось выполнить поиск в потоках, но после замены моего .WriteFile этим поиск теперь работает - person agrath; 14.06.2013
comment
@ Maxad - Чем это отличается от кода, который я опубликовал. Похоже, будут те же результаты - person Jake; 14.06.2013
comment
Это единственный пример, который до сих пор работал у меня. Спасибо! - person CodeToad; 13.12.2018
comment
Я бы просто бросил Response.IsClientConnected в while цикл. Более чистый IMO, чем убирать этот блок с дополнительным слоем отступа для if. - person Broots Waymb; 25.06.2019
comment
@Maxad: Большое спасибо, это отличный ответ. Спасибо. Я также внес некоторые изменения в версию API .Net Core. - person Sadjad Khazaie; 20.10.2020

Что действительно важно, так это заголовок Range. Хотя существующий ответ правильный, он не содержит пояснений.

Когда вы делаете запрос без указания диапазона, весь файл передается в потоковом режиме. Видеопроигрыватели автоматически определяют заголовок «диапазона» с начальным байтом, который соответствует положению проигрывателя в видео.

Поскольку это неотъемлемая часть HTTP, это очень хорошо задокументировано в RFC 7233.

Заголовок Accept-Range: bytes сообщает клиенту, что мы хотим принять заголовок диапазона как количество байтов. Код состояния «206» сообщает клиенту, что мы отправили частичное содержимое, то есть только часть всего файла. Заголовок Content-Range: start-end / total сообщает клиенту диапазон информации, которую мы отправляем обратно в текущем запросе.

Вот полностью функциональный фрагмент:

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://tools.ietf.org/html/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) {}

    }

}

Обратите внимание, что мы можем просто проверять «Позицию» файлового потока (в байтах) вместо того, чтобы отслеживать, сколько байтов мы уже отправили в целом.

person Justin G    schedule 30.04.2019

Ответ Maxad - идеальный ответ. Я также внес некоторые изменения в версию .Net Core:

<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();
        }
    }
person Sadjad Khazaie    schedule 19.10.2020
comment
Я пробовал ваш код, но получаю ошибку в iStream.Seek (startbyte, SeekOrigin.Begin. Исключение: указанный метод не поддерживается. Мой поток исходит из запроса на mongodb. - person Rob None; 09.04.2021
comment
Поиск - это функция System.IO. Вы включили правильную версию? - person Sadjad Khazaie; 10.04.2021
comment
Я нашел проблему, ваш ответ меня спас. Большое спасибо! - person Rob None; 12.04.2021