本地播放
This commit is contained in:
+86
-19
@@ -47,6 +47,10 @@ export interface M3U8DownloadTask {
|
||||
downloadMode?: 'browser' | 'filesystem';
|
||||
filesystemDirHandle?: FileSystemDirectoryHandle;
|
||||
m3u8Content?: string; // 原始 M3U8 内容,用于生成本地播放列表
|
||||
// 视频标识信息(用于区分不同视频)
|
||||
source?: string;
|
||||
videoId?: string;
|
||||
episodeIndex?: number;
|
||||
}
|
||||
|
||||
export interface M3U8DownloaderOptions {
|
||||
@@ -67,7 +71,16 @@ export class M3U8Downloader {
|
||||
/**
|
||||
* 创建下载任务
|
||||
*/
|
||||
async createTask(url: string, title: string, type: 'TS' | 'MP4' = 'TS'): Promise<string> {
|
||||
async createTask(
|
||||
url: string,
|
||||
title: string,
|
||||
type: 'TS' | 'MP4' = 'TS',
|
||||
metadata?: {
|
||||
source?: string;
|
||||
videoId?: string;
|
||||
episodeIndex?: number;
|
||||
}
|
||||
): Promise<string> {
|
||||
const taskId = 't_' + Date.now() + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
try {
|
||||
@@ -85,11 +98,11 @@ export class M3U8Downloader {
|
||||
// 自动选择最高清晰度
|
||||
url = streams[0].url;
|
||||
const subM3u8Content = await this.fetchM3U8(url);
|
||||
return this.processM3U8Content(taskId, url, title, type, subM3u8Content);
|
||||
return this.processM3U8Content(taskId, url, title, type, subM3u8Content, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
return this.processM3U8Content(taskId, url, title, type, m3u8Content);
|
||||
return this.processM3U8Content(taskId, url, title, type, m3u8Content, metadata);
|
||||
} catch (error) {
|
||||
throw new Error(`创建任务失败: ${error}`);
|
||||
}
|
||||
@@ -103,7 +116,12 @@ export class M3U8Downloader {
|
||||
url: string,
|
||||
title: string,
|
||||
type: 'TS' | 'MP4',
|
||||
m3u8Content: string
|
||||
m3u8Content: string,
|
||||
metadata?: {
|
||||
source?: string;
|
||||
videoId?: string;
|
||||
episodeIndex?: number;
|
||||
}
|
||||
): string {
|
||||
const task: M3U8DownloadTask = {
|
||||
id: taskId,
|
||||
@@ -115,7 +133,7 @@ export class M3U8Downloader {
|
||||
tsUrlList: [],
|
||||
requests: [],
|
||||
mediaFileList: [],
|
||||
downloadIndex: 0,
|
||||
downloadIndex: 0, // 初始化为 0,在 startTask 时会设置为正确的值
|
||||
downloading: false,
|
||||
durationSecond: 0,
|
||||
beginTime: new Date(),
|
||||
@@ -125,7 +143,7 @@ export class M3U8Downloader {
|
||||
retryCountdown: 0,
|
||||
rangeDownload: {
|
||||
isShowRange: false,
|
||||
startSegment: 1,
|
||||
startSegment: 0, // 改为从 0 开始
|
||||
endSegment: 0,
|
||||
targetSegment: 0,
|
||||
},
|
||||
@@ -137,6 +155,9 @@ export class M3U8Downloader {
|
||||
decryption: null,
|
||||
},
|
||||
m3u8Content, // 保存原始 M3U8 内容
|
||||
source: metadata?.source,
|
||||
videoId: metadata?.videoId,
|
||||
episodeIndex: metadata?.episodeIndex,
|
||||
};
|
||||
|
||||
// 解析 TS 片段
|
||||
@@ -185,6 +206,19 @@ export class M3U8Downloader {
|
||||
await this.getAESKey(task);
|
||||
}
|
||||
|
||||
// 重置下载索引到第一个未完成的片段
|
||||
if (task.status === 'ready' || task.status === 'pause') {
|
||||
// 找到第一个未完成的片段
|
||||
let firstIncompleteIndex = 0;
|
||||
for (let i = 0; i < task.finishList.length; i++) {
|
||||
if (task.finishList[i].status !== 'is-success') {
|
||||
firstIncompleteIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
task.downloadIndex = firstIncompleteIndex;
|
||||
}
|
||||
|
||||
task.status = 'downloading';
|
||||
this.currentTask = task;
|
||||
this.downloadTS(task);
|
||||
@@ -264,7 +298,7 @@ export class M3U8Downloader {
|
||||
|
||||
// 找到第一个失败的片段索引
|
||||
let firstErrorIndex = task.rangeDownload.endSegment;
|
||||
for (let i = task.rangeDownload.startSegment - 1; i < task.rangeDownload.endSegment; i++) {
|
||||
for (let i = task.rangeDownload.startSegment; i < task.rangeDownload.endSegment; i++) {
|
||||
if (task.finishList[i] && task.finishList[i].status === '') {
|
||||
firstErrorIndex = Math.min(firstErrorIndex, i);
|
||||
}
|
||||
@@ -405,7 +439,7 @@ export class M3U8Downloader {
|
||||
});
|
||||
} else {
|
||||
// 浏览器下载模式:保存到内存
|
||||
task.mediaFileList[index - task.rangeDownload.startSegment + 1] = convertedData;
|
||||
task.mediaFileList[index] = convertedData;
|
||||
task.finishList[index].status = 'is-success';
|
||||
task.finishNum++;
|
||||
|
||||
@@ -443,7 +477,7 @@ export class M3U8Downloader {
|
||||
});
|
||||
} else {
|
||||
// 浏览器下载模式:保存到内存
|
||||
task.mediaFileList[index - task.rangeDownload.startSegment + 1] = data;
|
||||
task.mediaFileList[index] = data;
|
||||
task.finishList[index].status = 'is-success';
|
||||
task.finishNum++;
|
||||
|
||||
@@ -664,7 +698,7 @@ export class M3U8Downloader {
|
||||
|
||||
transMuxer.on('data', (segment: any) => {
|
||||
// 第一个片段需要包含初始化段
|
||||
if (index === task.rangeDownload.startSegment - 1) {
|
||||
if (index === 0) {
|
||||
const combinedData = new Uint8Array(
|
||||
segment.initSegment.byteLength + segment.data.byteLength
|
||||
);
|
||||
@@ -713,10 +747,28 @@ export class M3U8Downloader {
|
||||
throw new Error('未选择保存目录');
|
||||
}
|
||||
|
||||
// 创建子目录结构:source/videoId/ep{episodeIndex+1}
|
||||
let targetDirHandle = task.filesystemDirHandle;
|
||||
|
||||
if (task.source && task.videoId && task.episodeIndex !== undefined) {
|
||||
try {
|
||||
// 创建 source 目录
|
||||
const sourceDirHandle = await task.filesystemDirHandle.getDirectoryHandle(task.source, { create: true });
|
||||
// 创建 videoId 目录
|
||||
const videoIdDirHandle = await sourceDirHandle.getDirectoryHandle(task.videoId, { create: true });
|
||||
// 创建 ep{n} 目录
|
||||
const epDirHandle = await videoIdDirHandle.getDirectoryHandle(`ep${task.episodeIndex + 1}`, { create: true });
|
||||
targetDirHandle = epDirHandle;
|
||||
} catch (error) {
|
||||
console.error('创建子目录失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const filename = `segment_${index.toString().padStart(5, '0')}.ts`;
|
||||
|
||||
try {
|
||||
const fileHandle = await task.filesystemDirHandle.getFileHandle(filename, { create: true });
|
||||
const fileHandle = await targetDirHandle.getFileHandle(filename, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
@@ -735,6 +787,21 @@ export class M3U8Downloader {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取目标目录句柄(如果有子目录结构)
|
||||
let targetDirHandle = task.filesystemDirHandle;
|
||||
|
||||
if (task.source && task.videoId && task.episodeIndex !== undefined) {
|
||||
try {
|
||||
const sourceDirHandle = await task.filesystemDirHandle.getDirectoryHandle(task.source, { create: false });
|
||||
const videoIdDirHandle = await sourceDirHandle.getDirectoryHandle(task.videoId, { create: false });
|
||||
const epDirHandle = await videoIdDirHandle.getDirectoryHandle(`ep${task.episodeIndex + 1}`, { create: false });
|
||||
targetDirHandle = epDirHandle;
|
||||
} catch (error) {
|
||||
console.error('获取子目录失败:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const lines = task.m3u8Content.split('\n');
|
||||
const modifiedLines: string[] = [];
|
||||
@@ -749,7 +816,7 @@ export class M3U8Downloader {
|
||||
if (task.aesConf.method && task.aesConf.method !== 'NONE') {
|
||||
// 如果有加密,保存密钥文件
|
||||
if (task.aesConf.key) {
|
||||
await this.saveKeyToFilesystem(task, task.aesConf.key);
|
||||
await this.saveKeyToFilesystem(task, task.aesConf.key, targetDirHandle);
|
||||
}
|
||||
const modifiedLine = line.replace(/URI="[^"]+"/g, 'URI="key.key"');
|
||||
modifiedLines.push(modifiedLine);
|
||||
@@ -759,7 +826,7 @@ export class M3U8Downloader {
|
||||
}
|
||||
// 替换视频片段 URL
|
||||
else if (trimmedLine && !trimmedLine.startsWith('#')) {
|
||||
if (segmentIndex >= task.rangeDownload.startSegment && segmentIndex < task.rangeDownload.endSegment) {
|
||||
if (segmentIndex < task.rangeDownload.endSegment) {
|
||||
const indent = line.match(/^\s*/)?.[0] || '';
|
||||
const filename = `segment_${segmentIndex.toString().padStart(5, '0')}.ts`;
|
||||
modifiedLines.push(indent + filename);
|
||||
@@ -776,12 +843,10 @@ export class M3U8Downloader {
|
||||
|
||||
// 保存播放列表
|
||||
const playlistContent = modifiedLines.join('\n');
|
||||
const fileHandle = await task.filesystemDirHandle.getFileHandle('playlist.m3u8', { create: true });
|
||||
const fileHandle = await targetDirHandle.getFileHandle('playlist.m3u8', { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(playlistContent);
|
||||
await writable.close();
|
||||
|
||||
console.log('本地播放列表生成成功');
|
||||
} catch (error) {
|
||||
console.error('生成播放列表失败:', error);
|
||||
}
|
||||
@@ -792,14 +857,16 @@ export class M3U8Downloader {
|
||||
*/
|
||||
private async saveKeyToFilesystem(
|
||||
task: M3U8DownloadTask,
|
||||
keyData: ArrayBuffer
|
||||
keyData: ArrayBuffer,
|
||||
targetDirHandle?: FileSystemDirectoryHandle
|
||||
): Promise<void> {
|
||||
if (!task.filesystemDirHandle) {
|
||||
const dirHandle = targetDirHandle || task.filesystemDirHandle;
|
||||
if (!dirHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileHandle = await task.filesystemDirHandle.getFileHandle('key.key', { create: true });
|
||||
const fileHandle = await dirHandle.getFileHandle('key.key', { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(keyData);
|
||||
await writable.close();
|
||||
|
||||
Reference in New Issue
Block a user