流式传输大型视频文件 .net

13

我正在尝试在WebForms中从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
@vcsjones- 我不确定Async如何工作,但我快速搜索了一下,我不认为它会解决问题。这似乎只是释放一个线程以返回给客户端进行异步从磁盘读取,但由于它仍在从磁盘读取,所以是否会返回内容? - Jake
这个问题帮了我大忙,谢谢! - Sadjad Khazaie
3个回答

18

我曾经遇到过类似的问题,需要完全下载视频才能播放。

我看得出你想要流式播放视频,更具体地说,你需要注意编码(确保它可以被流式传输),不要仅仅依赖于扩展名,因为创建文件的人可能会以奇怪的方式构建视频,但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();
        }
    }

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


请确保httpRuntime具有足够大的maxRequestLength以发送文件。Mediainfo仅用于查看视频的详细信息(编解码器和比特率)。 - Maxad
可能存在其他限制导致了这个问题。 - Jake
@Maxad - 感谢您发布这个! 这段代码有效,我刚刚使用它来改进我的videojs实现,从http处理程序提供视频数据。无法在流中搜索,但是在将我的.WriteFile替换为此后,现在可以搜索了。 - agrath
@Maxad- 这跟我发布的代码有什么不同吗?看起来结果应该是一样的。 - Jake
这是目前为止唯一有效的示例。谢谢! - CodeToad
显示剩余4条评论

3
这里真正重要的是“范围”标头。虽然现有答案是正确的,但它没有解释。
当您发送未指定范围的请求时,整个文件会被流式传输。视频播放器会自动使用与视频播放器位置相符合的起始字节指定“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://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) {}

    }

}

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

1

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的查询。 - Rob None
Seek是一个System.IO函数。你是否包含了正确的版本? - Sadjad Khazaie
1
我找到了问题,你的答案救了我。非常感谢! - Rob None

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接