如何实现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('已恢复下载'); }
关键改动说明
- 状态扩展:给每个预加载项新增了
startByte(记录已下载的字节位置)、partialBlobs(存储分段Blob)、xhr(当前请求实例)三个字段,用来跟踪断点信息。 - Range请求支持:在
preloadOne中,如果已经下载过部分内容,会自动添加Range: bytes=startByte-请求头,告诉服务器只返回剩余部分。 - 分段Blob合并:每次下载完成一段Blob后,会存入
partialBlobs数组,当全部下载完成时,再合并成完整的Blob并生成URL。 - 暂停/续传方法:
pause方法会终止当前的XHR请求,resume方法会从startByte的位置重新发起Range请求,继续下载剩余内容。
需要注意的是,你的服务器必须支持Range请求(大部分现代服务器都默认支持),否则这个方案没法生效哦。
内容的提问来源于stack exchange,提问作者Sara Ree
相关产品推荐
相关产品推荐

