This commit is contained in:
ChuXun
2025-10-19 20:55:27 +08:00
parent e879ccefb3
commit 53f9554f38
99 changed files with 22308 additions and 2 deletions

333
js/m3u8.downloader.js Normal file
View 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();
}
}