Bootstrap

音视频文件提供流式传输之HTTP Range 请求

在 Web 开发中,正确返回音频和视频流给前端的方式是确保服务器端以流的形式发送媒体文件,而不是将整个文件加载到内存中,然后再传输。这种做法可以提高性能,避免内存溢出,尤其是在处理大文件时。
对于音频和视频流的处理,最常见的技术是 HTTP 流式传输(HTTP Streaming) Range 请求。这些方法允许客户端(浏览器或播放器)按需请求并接收音频和视频的部分内容,而不是一次性加载整个文件。

要在后端处理包含 Range 头的请求,首先需要理解 Range 请求头是如何工作的。客户端通过 Range 请求头向服务器指定请求的字节范围。服务器根据 Range 请求返回相应的数据片段,通常用于视频、音频或大文件的流式传输。

1. Range 请求头的工作原理

客户端请求某个文件的部分内容时,会在 HTTP 请求头中包含 Range,例如:

Range: bytes=0-1023

表示客户端请求文件的第 0 字节到第 1023 字节(共 1024 字节)。

如果客户端想请求多个范围(例如文件的两个不连续部分),则可以使用如下格式:

Range: bytes=0-1023, 2048-3071

2. 后端处理 Range 请求头

在 C# 中,后端需要从请求头中获取 Range 的信息,并且根据该信息返回对应的字节范围。通常可以通过 HttpRequest.Headers 来获取请求头中的 Range

示例代码(ASP.NET Core):
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;

[Route("api/[controller]")]
public class MediaController : ControllerBase
{ 
    [HttpGet("audio")]
    public IActionResult GetAudio(string filename)
    {
        var fileInfo = new FileInfo(filename);
        if (!fileInfo.Exists)
        {
            return NotFound();
        }

        var fileLength = fileInfo.Length;
        var rangeHeader = Request.Headers["Range"].ToString();

        if (string.IsNullOrEmpty(rangeHeader))
        {
            return BadRequest("Invalid Range header.");
        }

        // 解析 Range 请求头
        var rangeMatch = System.Text.RegularExpressions.Regex.Match(rangeHeader, @"bytes=(\d+)-(\d+)?");
        if (!rangeMatch.Success)
        {
            return BadRequest("Invalid Range header.");
        }

        var start = long.Parse(rangeMatch.Groups[1].Value);
        var end = rangeMatch.Groups[2].Success ? long.Parse(rangeMatch.Groups[2].Value) : fileLength - 1;

        // 确保范围合法
        if (start < 0 || end >= fileLength || start > end)
        {
            return BadRequest("Invalid range.");
        }

        // 读取文件的指定范围
        var fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
        var contentRange = new RangeHeaderValue(start, end);
        var contentLength = end - start + 1;

        // 设置响应头
        Response.Headers.Add("Content-Range", $"bytes {start}-{end}/{fileLength}");
        Response.Headers.Add("Accept-Ranges", "bytes");
        Response.StatusCode = StatusCodes.Status206PartialContent; // 206 Partial Content

        fileStream.Seek(start, SeekOrigin.Begin);

        return File(fileStream, "audio/mpeg", enableRangeProcessing: true);
    }
}

解释:

  1. Range 请求头解析

    • Request.Headers["Range"] 获取 Range 请求头的内容。
    • 使用正则表达式 @"bytes=(\d+)-(\d+)?" 来提取请求的开始和结束字节。
    • 如果 end 字段缺失,则将 end 设置为文件的最后一个字节。
  2. 合法性检查

    • 确保 start 和 end 在合法范围内,即它们不能超出文件的总长度,也不能发生 start > end 的情况。
  3. 文件流处理

    • 使用 FileStream 打开文件,并设置 Seek 方法从指定的字节位置开始读取文件内容。
    • 返回部分文件内容时,设置响应头 Content-Range,告知客户端返回的数据范围。
  4. 返回部分内容

    • 设置响应状态码为 206 Partial Content,表示这是一个部分内容的响应。
    • 使用 File 方法返回文件流,并通过 enableRangeProcessing: true 告诉 ASP.NET Core 进行范围处理。

3. 前端使用 <audio> 标签播放音频

在前端使用 <audio> 标签时,浏览器会自动发出 Range 请求,只要服务器支持 Range 请求,并且文件能够分段提供。你不需要特别配置客户端来处理 Range,浏览器会自己处理。

示例:基础 <audio> 标签用法
<html>
    <head>
        <title>audio page</title>
    </head>
    <body>
        <audio controls>
            <source src="http://127.0.0.1:9099/api/media/audio?filename=1.mp3" type="audio/mp3">
        </audio>
    </body>
</html>
;