学习HTTP Range

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>

总结

  1. 客户端通过 Range 请求加载指定字节范围。
  2. 服务器返回 206 Partial Content 响应。
  3. 使用 MediaSource 动态拼接分段数据,实现无缝播放
相关推荐
Spcarrydoinb41 分钟前
python学习笔记—17—数据容器之字符串
笔记·python·学习
夜半被帅醒1 小时前
【JAVA】Java开发小游戏 - 简单的2D平台跳跃游戏 基本的2D平台跳跃游戏框架,适合初学者学习和理解Java游戏开发的基础概念
java·学习·游戏
杂货铺的小掌柜2 小时前
spring mvc源码学习笔记之八
学习·spring·mvc
勿忘初心913 小时前
Android车机DIY开发之学习篇(二)编译Kernel以正点原子为例
android·arm开发·单片机·嵌入式硬件·学习·eclipse
夕洪蚀4 小时前
Java阶段四03
java·开发语言·学习
吃着火锅x唱着歌5 小时前
PHP7内核剖析 学习笔记 第五章 PHP的编译与执行(1)
笔记·学习·php
fly_vip5 小时前
ubuntu 20.04 安装docker--小白学习之路
学习·ubuntu·docker
王子良.5 小时前
Hadoop3.x 万字解析,从入门到剖析源码
大数据·开发语言·hadoop·经验分享·学习·开源
兔子宇航员03016 小时前
PySpark学习笔记3-案例练习
笔记·学习