1
This commit is contained in:
333
js/m3u8.downloader.js
Normal file
333
js/m3u8.downloader.js
Normal file
@@ -0,0 +1,333 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user