Skip to content

使用 HTTP Range 请求提供视频服务

原文链接:https://smoores.dev/post/http_range_requests/ 发布时间:2025年5月4日

最近我发现自己在为朋友和家人构建一个简单的照片分享应用。我们有一些生活事件,大家都想保持更新,但我不太想依赖云服务来实现这个功能。

相反,我决定用 Next.js 制作一个小型的渐进式 Web 应用。我在几个小时内就完成了第一个版本——只是一个基本的自托管应用,可以指向一个照片目录,并将它们渲染为网格中的缩略图和阴影框中的全尺寸图像。我甚至设置了推送通知,当新照片添加到目录时会收到通知。

然后,就在我准备收工并将整个项目标记为巨大成功时,我想起了一件事:我完全忘记了视频!"好吧,没问题,"我想,"我只需要使用 <video> 标签,对吧?"

<video> 元素

<video> 元素支持许多属性,允许你配置显示哪些控件、视频是否应该自动播放等。像 <img> 元素一样,<video> 元素可以提供一个 src 属性,该属性指定要嵌入的视频的 URL。

由于并非所有浏览器都支持所有视频格式,<video> 元素通常使用一系列 <source> 元素作为子元素创建,而不是单个 src 属性。浏览器会自动遍历这个源列表,直到找到第一个它们支持的源类型。因此,你可以为支持它的用户提供超清晰、超小的 AV1 视频,并为其他人回退到更广泛支持的 WEBM 或 MP4 编码。

以下是 MDN 文档中的一个示例:

html
<video controls width="250">
  <source
    src="/shared-assets/videos/flower.webm"
    type="video/webm"
  />
  <source
    src="/shared-assets/videos/flower.mp4"
    type="video/mp4"
  />
</video>

查看浏览器开发工具的网络选项卡,看看这里发生了什么。根据你使用的浏览器,你会看到不同的请求。我在写这篇文章时使用的是 Zen/Firefox,它支持 WEBM,所以只请求 WEBM 视频(这对所有主要浏览器的最新版本都应该是正确的)。如果我包含了 AV1 源,较旧的 iPhone 和 macOS 设备会跳过它并回退到 WEBM 文件。

Range 请求

如果你观察得很仔细,你可能会注意到网络选项卡中的一些其他有趣的事情。首先,flower.webm 请求的响应代码是 206,而不是标准的 200 成功/OK 响应。根据你的浏览器,你实际上可能会看到对视频文件的多个请求,所有这些都响应 206。还有一些额外的头部,如 Accept-Ranges: bytesContent-Range: bytes 0-554057/554058

这些共同构成了 HTTP range 请求。顾名思义,range 请求允许客户端从服务器请求特定范围的字节,而不是整个资源。这对视频特别有用,因为视频可以任意长(因此很大),所以客户端几乎总是分块加载它们。

最初,我没有打算在我的小照片分享应用中支持 range 请求。它们有明显的用途,但 Next.js 没有内置支持,我不想自己编写实现。另外,我计划分享的所有视频都相对较小,最多只有一分钟长。它们需要几秒钟来加载,但没有什么不能忍受的。我添加了一个直接以 200 响应代码提供视频文件的端点,提交了代码,并为完成我的小副项目而拍了拍自己的背。

javascript
import { FileHandle, open } from "node:fs/promises"
import { join } from "node:path"
import { createReadableStreamFromReadable } from "@remix-run/node"

interface Props {
  params: Promise<{ album: string; file: string }>
}

export async function GET(request: Request, props: Props) {
  const { album, file: filename } = await props.params
  const rootDir = process.env.ROOT_DIR!
  const filepath = join(rootDir, album, filename)
  
  let file: FileHandle
  try {
    file = await open(filepath)
  } catch {
    return new Response(null, { status: 404 })
  }
  
  return new Response(
    createReadableStreamFromReadable(file.createReadStream()),
    {
      headers: { "Content-Type": "video/mp4" },
    },
  )
}

然后我在手机上测试了这个应用。

Safari 说不

事实证明,Safari 实际上要求视频源支持 HTTP range 请求。它发送对前两个字节的请求,如果没有得到带有正确 HTTP range 头部的响应,它就会移动到下一个源。如果它遍历整个列表而没有得到适当的 range 响应,它根本不会渲染视频!

通常,像图像和视频这样的静态资产是从 CDN 提供的,或者至少使用适当的静态文件服务器。这些都支持 range 请求,所以开发人员很少需要实际思考它们是如何工作的。但是,由于我正在构建一个专门用于在我自己的硬件上自托管的应用,我不能依赖 CDN 或单独的静态文件服务器。

实现 Range 请求支持

为了解决这个问题,我需要在我的 Next.js API 路由中实现 HTTP Range 请求支持。这涉及到:

  1. 解析 Range 头部:从请求中提取客户端想要的字节范围
  2. 验证范围:确保请求的范围是有效的
  3. 返回部分内容:使用 206 状态码和适当的头部返回请求的字节范围
  4. 处理多范围请求:虽然不常见,但规范允许多个范围

这个实现确保了视频可以在所有现代浏览器中正常工作,包括对 range 请求有严格要求的 Safari。

总结

HTTP Range 请求是现代 Web 视频流的重要组成部分。虽然大多数开发人员不需要直接实现它们(因为 CDN 和静态文件服务器通常处理这些),但了解它们的工作原理对于构建自托管解决方案或调试视频播放问题非常有价值。

特别是在构建需要在各种设备和浏览器上工作的应用时,确保正确的 Range 请求支持可以避免许多兼容性问题,特别是与 Safari 相关的问题。


💡 提示:如果你在实现视频流服务时遇到问题,记住检查浏览器的网络选项卡,查看是否有 Range 请求相关的错误。这通常是 Safari 视频播放问题的根本原因。

Last updated: