333 lines
11 KiB
JavaScript
333 lines
11 KiB
JavaScript
class Downloader {
|
|
constructor(fragments = [], thread = 6) {
|
|
this.fragments = fragments; // 切片列表
|
|
this.allFragments = fragments; // 储存所有原始切片列表
|
|
this.thread = thread; // 线程数
|
|
this.events = {}; // events
|
|
this.decrypt = null; // 解密函数
|
|
this.transcode = null; // 转码函数
|
|
this.init();
|
|
}
|
|
/**
|
|
* 初始化所有变量
|
|
*/
|
|
init() {
|
|
this.index = 0; // 当前任务索引
|
|
this.buffer = []; // 储存的buffer
|
|
this.state = 'waiting'; // 下载器状态 waiting running done abort
|
|
this.success = 0; // 成功下载数量
|
|
this.errorList = new Set(); // 下载错误的列表
|
|
this.buffersize = 0; // 已下载buffer大小
|
|
this.duration = 0; // 已下载时长
|
|
this.pushIndex = 0; // 推送顺序下载索引
|
|
this.controller = []; // 储存中断控制器
|
|
this.running = 0; // 正在下载数量
|
|
}
|
|
/**
|
|
* 设置监听
|
|
* @param {string} eventName 监听名
|
|
* @param {Function} callBack
|
|
*/
|
|
on(eventName, callBack) {
|
|
if (this.events[eventName]) {
|
|
this.events[eventName].push(callBack);
|
|
} else {
|
|
this.events[eventName] = [callBack];
|
|
}
|
|
}
|
|
/**
|
|
* 触发监听器
|
|
* @param {string} eventName 监听名
|
|
* @param {...any} args
|
|
*/
|
|
emit(eventName, ...args) {
|
|
if (this.events[eventName]) {
|
|
this.events[eventName].forEach(callBack => {
|
|
callBack(...args);
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* 设定解密函数
|
|
* @param {Function} callback
|
|
*/
|
|
setDecrypt(callback) {
|
|
this.decrypt = callback;
|
|
}
|
|
/**
|
|
* 设定转码函数
|
|
* @param {Function} callback
|
|
*/
|
|
setTranscode(callback) {
|
|
this.transcode = callback;
|
|
}
|
|
/**
|
|
* 停止下载 没有目标 停止所有线程
|
|
* @param {number} index 停止下载目标
|
|
*/
|
|
stop(index = undefined) {
|
|
if (index !== undefined) {
|
|
this.controller[index] && this.controller[index].abort();
|
|
return;
|
|
}
|
|
this.controller.forEach(controller => { controller.abort() });
|
|
this.state = 'abort';
|
|
}
|
|
/**
|
|
* 检查对象是否错误列表内
|
|
* @param {object} fragment 切片对象
|
|
* @returns {boolean}
|
|
*/
|
|
isErrorItem(fragment) {
|
|
return this.errorList.has(fragment);
|
|
}
|
|
/**
|
|
* 返回所有错误列表
|
|
*/
|
|
get errorItem() {
|
|
return this.errorList;
|
|
}
|
|
/**
|
|
* 按照顺序推送buffer数据
|
|
*/
|
|
sequentialPush() {
|
|
if (!this.events["sequentialPush"]) { return; }
|
|
for (; this.pushIndex < this.fragments.length; this.pushIndex++) {
|
|
if (this.buffer[this.pushIndex]) {
|
|
this.emit('sequentialPush', this.buffer[this.pushIndex]);
|
|
delete this.buffer[this.pushIndex];
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/**
|
|
* 限定下载范围
|
|
* @param {number} start 下载范围 开始索引
|
|
* @param {number} end 下载范围 结束索引
|
|
* @returns {boolean}
|
|
*/
|
|
range(start = 0, end = this.fragments.length) {
|
|
if (start > end) {
|
|
this.emit('error', 'start > end');
|
|
return false;
|
|
}
|
|
if (end > this.fragments.length) {
|
|
this.emit('error', 'end > total');
|
|
return false;
|
|
}
|
|
if (start >= this.fragments.length) {
|
|
this.emit('error', 'start >= total');
|
|
return false;
|
|
}
|
|
if (start != 0 || end != this.fragments.length) {
|
|
this.fragments = this.fragments.slice(start, end);
|
|
// 更改过下载范围 重新设定index
|
|
this.fragments.forEach((fragment, index) => {
|
|
fragment.index = index;
|
|
});
|
|
}
|
|
// 总数为空 抛出错误
|
|
if (this.fragments.length == 0) {
|
|
this.emit('error', 'List is empty');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* 获取切片总数量
|
|
* @returns {number}
|
|
*/
|
|
get total() {
|
|
return this.fragments.length;
|
|
}
|
|
/**
|
|
* 获取切片总时间
|
|
* @returns {number}
|
|
*/
|
|
get totalDuration() {
|
|
return this.fragments.reduce((total, fragment) => total + fragment.duration, 0);
|
|
}
|
|
/**
|
|
* 切片对象数组的 setter getter
|
|
*/
|
|
set fragments(fragments) {
|
|
// 增加index参数 为多线程异步下载 根据index属性顺序保存
|
|
this._fragments = fragments.map((fragment, index) => ({ ...fragment, index }));
|
|
}
|
|
get fragments() {
|
|
return this._fragments;
|
|
}
|
|
/**
|
|
* 获取 #EXT-X-MAP 标签的文件url
|
|
* @returns {string}
|
|
*/
|
|
get mapTag() {
|
|
if (this.fragments[0].initSegment && this.fragments[0].initSegment.url) {
|
|
return this.fragments[0].initSegment.url;
|
|
}
|
|
return "";
|
|
}
|
|
/**
|
|
* 添加一条新资源
|
|
* @param {Object} fragment
|
|
*/
|
|
push(fragment) {
|
|
fragment.index = this.fragments.length;
|
|
this.fragments.push(fragment);
|
|
}
|
|
/**
|
|
* 下载器 使用fetch下载文件
|
|
* @param {object} fragment 重新下载的对象
|
|
*/
|
|
downloader(fragment = null) {
|
|
if (this.state === 'abort') { return; }
|
|
// 是否直接下载对象
|
|
const directDownload = !!fragment;
|
|
|
|
// 非直接下载对象 从this.fragments获取下一条资源 若不存在跳出
|
|
if (!directDownload && !this.fragments[this.index]) { return; }
|
|
|
|
// fragment是数字 直接从this.fragments获取
|
|
if (typeof fragment === 'number') {
|
|
fragment = this.fragments[fragment];
|
|
}
|
|
|
|
// 不存在下载对象 从提取fragments
|
|
fragment ??= this.fragments[this.index++];
|
|
this.state = 'running';
|
|
this.running++;
|
|
|
|
// 资源已下载 跳过
|
|
// if (this.buffer[fragment.index]) { return; }
|
|
|
|
// 停止下载控制器
|
|
const controller = new AbortController();
|
|
this.controller[fragment.index] = controller;
|
|
const options = { signal: controller.signal };
|
|
|
|
// 下载前触发事件
|
|
this.emit('start', fragment, options);
|
|
|
|
// 开始下载
|
|
fetch(fragment.url, options)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(response.status, { cause: 'HTTPError' });
|
|
}
|
|
const reader = response.body.getReader();
|
|
const contentLength = parseInt(response.headers.get('content-length')) || 0;
|
|
fragment.contentType = response.headers.get('content-type') ?? 'null';
|
|
let receivedLength = 0;
|
|
const chunks = [];
|
|
const pump = async () => {
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) { break; }
|
|
|
|
// 流式下载
|
|
fragment.fileStream ? fragment.fileStream.write(new Uint8Array(value)) : chunks.push(value);
|
|
|
|
receivedLength += value.length;
|
|
this.emit('itemProgress', fragment, false, receivedLength, contentLength, value);
|
|
}
|
|
if (fragment.fileStream) {
|
|
return new ArrayBuffer();
|
|
}
|
|
const allChunks = new Uint8Array(receivedLength);
|
|
let position = 0;
|
|
for (const chunk of chunks) {
|
|
allChunks.set(chunk, position);
|
|
position += chunk.length;
|
|
}
|
|
this.emit('itemProgress', fragment, true);
|
|
return allChunks.buffer;
|
|
}
|
|
return pump();
|
|
})
|
|
.then(buffer => {
|
|
this.emit('rawBuffer', buffer, fragment);
|
|
// 存在解密函数 调用解密函数 否则直接返回buffer
|
|
return this.decrypt ? this.decrypt(buffer, fragment) : buffer;
|
|
})
|
|
.then(buffer => {
|
|
this.emit('decryptedData', buffer, fragment);
|
|
// 存在转码函数 调用转码函数 否则直接返回buffer
|
|
return this.transcode ? this.transcode(buffer, fragment) : buffer;
|
|
})
|
|
.then(buffer => {
|
|
// 储存解密/转码后的buffer
|
|
this.buffer[fragment.index] = buffer;
|
|
|
|
// 成功数+1 累计buffer大小和视频时长
|
|
this.success++;
|
|
this.buffersize += buffer.byteLength;
|
|
this.duration += fragment.duration ?? 0;
|
|
|
|
// 下载对象来自错误列表 从错误列表内删除
|
|
this.errorList.has(fragment) && this.errorList.delete(fragment);
|
|
|
|
// 推送顺序下载
|
|
this.sequentialPush();
|
|
|
|
this.emit('completed', buffer, fragment);
|
|
|
|
// 下载完成
|
|
if (this.success == this.fragments.length) {
|
|
this.state = 'done';
|
|
this.emit('allCompleted', this.buffer, this.fragments);
|
|
}
|
|
}).catch((error) => {
|
|
console.log(error);
|
|
if (error.name == 'AbortError') {
|
|
this.emit('stop', fragment, error);
|
|
return;
|
|
}
|
|
this.emit('downloadError', fragment, error);
|
|
|
|
// 储存下载错误切片
|
|
!this.errorList.has(fragment) && this.errorList.add(fragment);
|
|
}).finally(() => {
|
|
this.running--;
|
|
// 下载下一个切片
|
|
if (!directDownload && this.index < this.fragments.length) {
|
|
this.downloader();
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* 开始下载 准备数据 调用下载器
|
|
* @param {number} start 下载范围 开始索引
|
|
* @param {number} end 下载范围 结束索引
|
|
*/
|
|
start(start = 0, end = this.fragments.length) {
|
|
// 检查下载器状态
|
|
if (this.state == 'running') {
|
|
this.emit('error', 'state running');
|
|
return;
|
|
}
|
|
// 从下载范围内 切出需要下载的部分
|
|
if (!this.range(start, end)) {
|
|
return;
|
|
}
|
|
// 初始化变量
|
|
this.init();
|
|
// 开始下载 多少线程开启多少个下载器
|
|
for (let i = 0; i < this.thread && i < this.fragments.length; i++) {
|
|
this.downloader();
|
|
}
|
|
}
|
|
/**
|
|
* 销毁 初始化所有变量
|
|
*/
|
|
destroy() {
|
|
this.stop();
|
|
this._fragments = [];
|
|
this.allFragments = [];
|
|
this.thread = 6;
|
|
this.events = {};
|
|
this.decrypt = null;
|
|
this.transcode = null;
|
|
this.init();
|
|
}
|
|
} |