HTTP Range 请求
一种通过指定文件字节范围加载部分数据的技术,广泛用于断点续传、流媒体播放、分布式文件系统的数据分片加载等场景。
请求格式-在请求头中使用 Range
字段指定所需的字节范围
javascript
Range: bytes=0-1023
// bytes=0-1023:表示请求文件的第 0 到第 1023 字节。
// bytes=1024-:表示从第 1024 字节开始到文件末尾。
// bytes=-1024:表示请求最后 1024 字节。
服务器响应-服务器会返回 206 Partial Content 状态码,并包含 Content-Range
响应头
javascript
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/1234567
// bytes 0-1023/1234567:表示当前返回的范围和文件的总大小(单位:字节)
前端实现文件分片加载
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<!-- 创建一个带有控制条和静音属性的视频播放器 -->
<video id="video" controls muted></video>
<script>
const video=document.querySelector('video'); // 获取页面中的 video 元素
// 视频的 URL 可通过浏览器直接访问【检查视频路径正确无误】;服务器支持 HTTP Range 请求【在服务器配置中启用 Accept-Ranges: bytes 支持】,如果不支持,fetch 请求会失败
const assetURL='xxxxxxx.mp4'; // 定义视频文件的 URL
const mimeCodec='video/mp4; codecs="avc1.64001F, mp4a.40.2"'; // 定义视频的 MIME 类型和编解码器
if('MediaSource' in window&&MediaSource.isTypeSupported(mimeCodec)) { // 检查浏览器是否支持 MediaSource API 和指定的 MIME 类型
const mediaSource=new MediaSource(); // 创建一个新的 MediaSource 对象
video.src=URL.createObjectURL(mediaSource); // 将 MediaSource 对象绑定到 video 元素
mediaSource.addEventListener('sourceopen',() => { // 监听 sourceopen 事件,表示 MediaSource 已经准备好
const sourceBuffer=mediaSource.addSourceBuffer(mimeCodec); // 添加一个 SourceBuffer 来处理指定 MIME 类型的数据
let loadedBytes=0; // 记录已加载的字节数
const segmentSize=1024*1024; // 每段大小 1MB // 定义每次请求的分段大小为 1MB
let totalSize=0; // 在此处声明并初始化 totalSize // 初始化视频总大小为 0
function fetchSegment (start) { // 定义一个函数用于按分段下载视频数据
const end=start+segmentSize-1; // 计算当前分段的结束位置
fetch(assetURL,{
headers: {Range: `bytes=${start}-${end}`}, // 发送 HTTP 请求,指定请求的字节范围
})
.then(response => {
if(response.ok) {
// 提取总大小(仅在第一次加载时获取)
if(!totalSize&&response.headers.get('Content-Range')) { // 如果还没有获取到总大小,则从响应头中提取
const contentRange=response.headers.get('Content-Range');
totalSize=parseInt(contentRange.split('/')[1],10); // 解析 Content-Range 头以获取总大小
}
return response.arrayBuffer(); // 将响应体转换为 ArrayBuffer 格式
}
throw new Error(`Failed to fetch segment: ${response.status}`); // 如果请求失败则抛出错误
})
.then(data => {
sourceBuffer.appendBuffer(data); // 将下载的数据追加到 SourceBuffer 中
loadedBytes+=data.byteLength; // 更新已加载的字节数
sourceBuffer.addEventListener('updateend',() => { // 监听 updateend 事件,表示数据已经成功追加
if(loadedBytes<totalSize) {
fetchSegment(loadedBytes); // 如果还有未加载的数据,则继续加载下一个分段
} else {
mediaSource.endOfStream(); // 如果所有数据都已加载完毕,则结束流
video.play(); // 开始播放视频
}
},{once: true}); // 确保该事件只触发一次
})
.catch(console.error); // 捕获并打印任何错误
}
fetchSegment(0); // 开始加载第一个片段 // 调用 fetchSegment 函数开始加载第一个分段
});
} else {
console.error('Unsupported MIME type or codec: ',mimeCodec); // 如果不支持指定的 MIME 类型或编解码器,则输出错误信息
}
function sourceOpen (_) { // 定义 sourceOpen 回调函数
const mediaSource=this; // 将 this 绑定到 mediaSource 变量
const sourceBuffer=mediaSource.addSourceBuffer(mimeCodec); // 添加一个 SourceBuffer 来处理指定 MIME 类型的数据
let loadedBytes=0; // 已加载的字节数 // 初始化已加载的字节数
let totalSize=null; // 视频总大小 // 初始化视频总大小
const segmentSize=4096; // 每段大小 (假设 4KB) // 定义每次请求的分段大小为 4KB
let isAppending=false; // 标记是否正在追加数据
sourceBuffer.addEventListener('updateend',() => { // 监听 updateend 事件,表示数据已经成功追加
isAppending=false; // 标记不再追加数据
if(loadedBytes<totalSize) {
const nextStart=loadedBytes; // 计算下一个分段的起始位置
const nextEnd=Math.min(nextStart+segmentSize-1,totalSize-1); // 计算下一个分段的结束位置
fetchAndAppend(nextStart,nextEnd); // 加载并追加下一个分段的数据
} else {
mediaSource.endOfStream(); // 如果所有数据都已加载完毕,则结束流
video.play(); // 开始播放视频
}
});
function fetchAndAppend (start,end) { // 定义一个函数用于按分段下载视频数据
if(isAppending) return; // 避免重复加载 // 如果正在追加数据,则直接返回
isAppending=true; // 标记正在追加数据
fetch(assetURL,{
headers: {Range: `bytes=${start}-${end}`}, // 发送 HTTP 请求,指定请求的字节范围
})
.then(response => {
if(response.status===206) { // 如果服务器支持范围请求,则继续处理
const contentRange=response.headers.get('Content-Range'); // 获取 Content-Range 响应头
if(contentRange&&!totalSize) {
// 解析 Content-Range: bytes start-end/total
const match=contentRange.match(/\/(\d+)$/); // 使用正则表达式解析 Content-Range 头
if(match) {
totalSize=parseInt(match[1],10); // 解析并设置视频总大小
}
}
return response.arrayBuffer(); // 将响应体转换为 ArrayBuffer 格式
}
throw new Error('Range request not supported'); // 如果服务器不支持范围请求,则抛出错误
})
.then(data => {
sourceBuffer.appendBuffer(data); // 将下载的数据追加到 SourceBuffer 中
loadedBytes+=data.byteLength; // 更新已加载的字节数
})
.catch(error => console.error('Fetch error:',error)); // 捕获并打印任何错误
}
// 初始加载第一个片段
fetchAndAppend(0,segmentSize-1); // 调用 fetchAndAppend 函数开始加载第一个分段
}
</script>
</body>
</html>
总结
- 客户端通过
Range
请求加载指定字节范围。 - 服务器返回 206 Partial Content 响应。
- 使用
MediaSource
动态拼接分段数据,实现无缝播放