【问题标题】:Streaming large video files .net流式传输大型视频文件 .net
【发布时间】:2013-05-27 14:23:16
【问题描述】:

我正在尝试从 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() 行。但是对于大文件,这将不起作用,因为它将整个文件存储在内存中会带来问题。

【问题讨论】:

  • 您可能希望使用IHttpAsyncHandler 异步实现这一切并异步从磁盘读取。
  • @vcsjones- 我不确定异步是如何工作的,但只是快速谷歌了一下,我不明白这将如何解决问题。这似乎释放了一个线程以返回客户端以从磁盘异步读取,但由于它仍在从磁盘读取内容,这会返回内容吗?
  • 这个问题救了我,谢谢!

标签: c# asp.net video-streaming webforms ihttphandler


【解决方案1】:

我遇到了类似的问题,视频必须在播放前完全下载。

我可以看到您想要流式传输视频,更具体地说。 你必须小心编码(确保它是可流式的),不要只依赖扩展名,因为创建文件的人可能会以一种奇怪的方式构建视频,但 99% 的时间你应该做个好人。我使用mediainfo。 在你的情况下应该是 H.264。

这还取决于浏览器和您用于流式传输的内容(后端代码除外)。就我而言,我使用了 Chrome/Html5 和 .webm (VP8/Ogg Vorbis)。它适用于超过 1G 的文件。没有测试大于 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();
        }
    }

确保您的响应标头包含您需要的所有内容。

【讨论】:

  • 文件很容易编码,没有问题。如果我直接访问它,我可以在浏览器中播放该文件。什么是 mediainfo 以及它对这个问题有何益处。我也在使用 html 5,但它似乎仍然无法播放大文件。我正在测试的文件大约 150 MB,它通过处理程序将 html5 标记的源发送回浏览器,但从不播放,但如果我发送 6 MB 的较小文件,它播放没有问题
  • 确保 httpRuntime 的 maxRequestLength 足够大以发送文件。 mediainfo 只是查看视频的详细信息(编解码器和比特率)
  • 我知道视频的细节。我用ffmpeg自己编码。如果我直接访问它,它在浏览器中可以正常播放并且 maxRequestLength 是 2000000000 并且文件只有 150MB
  • 可能还有其他一些限制导致了这个问题
  • @Maxad - 感谢您发布此信息!此代码有效,只是用它来改进我的 videojs 实现,从 http 处理程序提供视频数据。无法在流中寻找,但在用这个替换我的 .WriteFile 之后,现在寻找工作
【解决方案2】:

这里真正重要的是“范围”标题。尽管现有答案是正确的,但它没有任何解释。

当您在未指定范围的情况下发出请求时,整个文件都会被流式传输。视频播放器会根据播放器在视频中的位置自动指定带有起始字节的“范围”标头。

由于这本质上是 HTTP 的一部分,因此在 RFC 7233. 中有很好的记录

“Accept-Range: bytes”标头告诉客户端我们希望接受范围标头作为字节数。状态码“206”告诉客户端我们发送了部分内容,也就是整个文件的一部分。 “Content-Range: start-end/total”标头告诉客户端我们在当前请求中发回的信息范围。

这是一个功能齐全的sn-p:

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

    }

}

请注意,我们可以简单地检查文件流的“位置”(以字节为单位),而不是跟踪我们总共发送了多少字节。

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 我尝试了您的代码,但在“iStream.Seek(startbyte, SeekOrigin.Begin”) 中出现错误。例外是“不支持指定的方法”。我的流来自查询在 mongodb 上。
    • Seek 是一个 System.IO 函数。您是否包含正确的版本?
    • 我找到了问题,你的回答救了我。非常感谢!
    猜你喜欢
    • 2010-10-25
    • 2015-08-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-28
    • 1970-01-01
    • 2020-09-20
    相关资源
    最近更新 更多