You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现XMLHttpRequest视频预加载的暂停与续传功能?

实现XHR2视频预加载的暂停与断点续传能力

我懂你现在的困扰——用abort()终止请求后就彻底断了,没法从暂停的位置接着下载。其实咱们可以利用HTTP的Range请求头来实现断点续传,让服务器只返回我们还没下载的部分,再把分段的Blob拼接起来就行。下面是修改后的完整代码,已经集成了暂停/续传功能,直接替换原代码就能用:

(function (global, factory) { 
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 
  typeof define === 'function' && define.amd ? define(factory) : 
  (global.Preload = factory()); 
}(this, (function () { 
  'use strict'; 

  function preloadOne(url, done) { 
    const item = this.getItemByUrl(url);
    // 如果已有未完成的分段,使用Range请求
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.responseType = 'blob';

    // 记录当前XHR实例,用于暂停
    item.xhr = xhr;

    if (item.startByte > 0) {
      xhr.setRequestHeader('Range', `bytes=${item.startByte}-`);
    }

    xhr.onprogress = event => { 
      if (!event.lengthComputable) return false;
      // 计算已下载的总字节数:已完成的起始位置 + 本次请求加载的字节
      const totalDownloaded = item.startByte + event.loaded;
      item.completion = parseInt((totalDownloaded / item.total) * 100);
      item.downloaded = totalDownloaded;
      this.updateProgressBar(item); 
    }; 

    xhr.onload = event => { 
      let type = event.target.response.type;
      const blobChunk = event.target.response;
      
      // 处理分段响应,获取总文件长度
      if (xhr.status === 206) {
        const contentRange = xhr.getResponseHeader('Content-Range');
        item.total = parseInt(contentRange.split('/')[1]);
      } else if (!item.total) {
        item.total = parseInt(xhr.getResponseHeader('Content-Length'));
      }

      // 将本次下载的Blob片段存入数组
      item.partialBlobs.push(blobChunk);
      // 更新已下载的起始位置
      item.startByte += blobChunk.size;

      // 如果已全部下载完成,合并Blob并生成URL
      if (item.startByte >= item.total) {
        const fullBlob = new Blob(item.partialBlobs, { type: type });
        let url = URL.createObjectURL(fullBlob);
        let responseURL = event.target.responseURL;
        item.blobUrl = url;
        item.fileName = responseURL.substring(responseURL.lastIndexOf('/') + 1);
        item.type = type;
        item.size = fullBlob.size;
        // 清理临时数据
        delete item.xhr;
        delete item.partialBlobs;
        done(item);
      }
    }; 

    xhr.send(); 
  } 

  function updateProgressBar(item) { 
    var sumCompletion = 0; 
    var maxCompletion = this.status.length * 100; 
    for (var itemStatus of this.status) { 
      if (itemStatus.completion) { 
        sumCompletion += itemStatus.completion; 
      } 
    } 
    var totalCompletion = parseInt((sumCompletion / maxCompletion) * 100); 
    if (!isNaN(totalCompletion)) { 
      this.onprogress({ progress: totalCompletion, item: item }); 
    } 
  } 

  function getItemByUrl(rawUrl) { 
    for (var item of this.status) { 
      if (item.url == rawUrl) return item 
    } 
  } 

  function fetch(list) { 
    return new Promise((resolve, reject) => { 
      this.loaded = list.length; 
      for (let item of list) { 
        this.status.push({ 
          url: item, 
          startByte: 0, // 记录已下载的起始位置
          partialBlobs: [], // 存储分段Blob
          xhr: null // 当前请求的XHR实例
        }); 
        this.preloadOne(item, item => { 
          this.onfetched(item); 
          this.loaded--; 
          if (this.loaded == 0) { 
            this.oncomplete(this.status); 
            resolve(this.status); 
          } 
        }); 
      } 
    }) 
  }

  // 新增暂停方法
  function pause(item) {
    if (item.xhr && item.startByte < item.total) {
      item.xhr.abort();
      delete item.xhr;
    }
  }

  // 新增续传方法
  function resume(item) {
    if (item.startByte < item.total && !item.xhr) {
      this.preloadOne(item.url, () => {});
    }
  }

  function Preload() { 
    return { 
      status: [], 
      loaded: false, 
      onprogress: () => {}, 
      oncomplete: () => {}, 
      onfetched: () => {}, 
      fetch, 
      updateProgressBar, 
      preloadOne, 
      getItemByUrl,
      pause,
      resume
    } 
  } 

  return Preload; 
})));

使用示例(包含暂停/续传调用)

let preloadInstance;

function preloadMe(){ 
  preloadInstance = Preload(); 
  preloadInstance.fetch([ 'https.../video.mp4' ]).then(items => { 
    console.log(items); 
  }); 
  preloadInstance.oncomplete = items => { 
    console.log(items); 
    console.log("The video has been preloaded!"); 
  } 
  preloadInstance.onprogress = event => { 
    console.log(event.progress + '%'); 
  } 
  preloadInstance.onfetched = item => { 
    console.log(item); 
  };
}

// 暂停第一个视频的下载
function pauseFirstVideo() {
  const firstItem = preloadInstance.status[0];
  preloadInstance.pause(firstItem);
  console.log('已暂停下载');
}

// 恢复第一个视频的下载
function resumeFirstVideo() {
  const firstItem = preloadInstance.status[0];
  preloadInstance.resume(firstItem);
  console.log('已恢复下载');
}

关键改动说明

  1. 状态扩展:给每个预加载项新增了startByte(记录已下载的字节位置)、partialBlobs(存储分段Blob)、xhr(当前请求实例)三个字段,用来跟踪断点信息。
  2. Range请求支持:在preloadOne中,如果已经下载过部分内容,会自动添加Range: bytes=startByte-请求头,告诉服务器只返回剩余部分。
  3. 分段Blob合并:每次下载完成一段Blob后,会存入partialBlobs数组,当全部下载完成时,再合并成完整的Blob并生成URL。
  4. 暂停/续传方法:pause方法会终止当前的XHR请求,resume方法会从startByte的位置重新发起Range请求,继续下载剩余内容。

需要注意的是,你的服务器必须支持Range请求(大部分现代服务器都默认支持),否则这个方案没法生效哦。

内容的提问来源于stack exchange,提问作者Sara Ree

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:44:43