本地播放
This commit is contained in:
+83
-81
@@ -1614,7 +1614,10 @@ function PlayPageClient() {
|
|||||||
* 检查 File System API 本地下载
|
* 检查 File System API 本地下载
|
||||||
*/
|
*/
|
||||||
const checkFileSystemDownload = async (
|
const checkFileSystemDownload = async (
|
||||||
title: string
|
title: string,
|
||||||
|
source?: string,
|
||||||
|
videoId?: string,
|
||||||
|
episodeIndex?: number
|
||||||
): Promise<{ hasLocal: boolean; dirHandle?: FileSystemDirectoryHandle }> => {
|
): Promise<{ hasLocal: boolean; dirHandle?: FileSystemDirectoryHandle }> => {
|
||||||
try {
|
try {
|
||||||
// 从 IndexedDB 读取目录句柄
|
// 从 IndexedDB 读取目录句柄
|
||||||
@@ -1653,10 +1656,20 @@ function PlayPageClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查是否存在 playlist.m3u8 文件
|
// 如果有 source、videoId 和 episodeIndex,检查子目录
|
||||||
await dirHandle.getFileHandle('playlist.m3u8', { create: false });
|
if (source && videoId && episodeIndex !== undefined) {
|
||||||
console.log('找到本地下载文件:', title);
|
const sourceDirHandle = await dirHandle.getDirectoryHandle(source, { create: false });
|
||||||
resolve({ hasLocal: true, dirHandle });
|
const videoIdDirHandle = await sourceDirHandle.getDirectoryHandle(videoId, { create: false });
|
||||||
|
const epDirHandle = await videoIdDirHandle.getDirectoryHandle(`ep${episodeIndex + 1}`, { create: false });
|
||||||
|
|
||||||
|
// 检查是否存在 playlist.m3u8 文件
|
||||||
|
await epDirHandle.getFileHandle('playlist.m3u8', { create: false });
|
||||||
|
console.log('找到本地下载文件:', title, `(${source}/${videoId}/ep${episodeIndex + 1})`);
|
||||||
|
resolve({ hasLocal: true, dirHandle: epDirHandle });
|
||||||
|
} else {
|
||||||
|
// 缺少必要参数
|
||||||
|
resolve({ hasLocal: false });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 文件不存在
|
// 文件不存在
|
||||||
resolve({ hasLocal: false });
|
resolve({ hasLocal: false });
|
||||||
@@ -1971,23 +1984,74 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
// 检查是否有 File System API 本地下载的文件
|
// 检查是否有 File System API 本地下载的文件
|
||||||
const episodeTitle = detailData?.episodes_titles?.[episodeIndex] || `第${episodeIndex + 1}集`;
|
const episodeTitle = detailData?.episodes_titles?.[episodeIndex] || `第${episodeIndex + 1}集`;
|
||||||
const fileSystemCheck = await checkFileSystemDownload(episodeTitle);
|
const fileSystemCheck = await checkFileSystemDownload(
|
||||||
|
episodeTitle,
|
||||||
|
currentSource || undefined,
|
||||||
|
currentId || undefined,
|
||||||
|
episodeIndex
|
||||||
|
);
|
||||||
|
|
||||||
if (fileSystemCheck.hasLocal && fileSystemCheck.dirHandle) {
|
if (fileSystemCheck.hasLocal && fileSystemCheck.dirHandle) {
|
||||||
// 使用本地文件播放
|
// 使用本地文件播放
|
||||||
try {
|
try {
|
||||||
|
// 读取 m3u8 文件
|
||||||
const fileHandle = await fileSystemCheck.dirHandle.getFileHandle('playlist.m3u8', { create: false });
|
const fileHandle = await fileSystemCheck.dirHandle.getFileHandle('playlist.m3u8', { create: false });
|
||||||
const file = await fileHandle.getFile();
|
const file = await fileHandle.getFile();
|
||||||
const content = await file.text();
|
let content = await file.text();
|
||||||
|
|
||||||
// 创建 Blob URL
|
// 解析 m3u8 文件,为每个 ts 文件创建 Blob URL
|
||||||
const blob = new Blob([content], { type: 'application/vnd.apple.mpegurl' });
|
const lines = content.split('\n');
|
||||||
newUrl = URL.createObjectURL(blob);
|
const modifiedLines: string[] = [];
|
||||||
|
const blobUrls: string[] = []; // 保存 Blob URL 以便后续清理
|
||||||
|
|
||||||
// 保存目录句柄到 ref,供 HLS loader 使用
|
for (const line of lines) {
|
||||||
(window as any).__localFileDirHandle = fileSystemCheck.dirHandle;
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
console.log('使用 File System API 本地文件播放:', episodeTitle);
|
// 如果是 ts 文件
|
||||||
|
if (trimmedLine.endsWith('.ts')) {
|
||||||
|
try {
|
||||||
|
// 读取 ts 文件
|
||||||
|
const tsFileHandle = await fileSystemCheck.dirHandle.getFileHandle(trimmedLine, { create: false });
|
||||||
|
const tsFile = await tsFileHandle.getFile();
|
||||||
|
|
||||||
|
// 创建 Blob URL
|
||||||
|
const blobUrl = URL.createObjectURL(tsFile);
|
||||||
|
blobUrls.push(blobUrl);
|
||||||
|
|
||||||
|
// 替换为 Blob URL
|
||||||
|
modifiedLines.push(line.replace(trimmedLine, blobUrl));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`读取 ts 文件失败: ${trimmedLine}`, error);
|
||||||
|
modifiedLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果是加密密钥
|
||||||
|
else if (trimmedLine.includes('key.key')) {
|
||||||
|
try {
|
||||||
|
const keyFileHandle = await fileSystemCheck.dirHandle.getFileHandle('key.key', { create: false });
|
||||||
|
const keyFile = await keyFileHandle.getFile();
|
||||||
|
const keyBlobUrl = URL.createObjectURL(keyFile);
|
||||||
|
blobUrls.push(keyBlobUrl);
|
||||||
|
modifiedLines.push(line.replace('key.key', keyBlobUrl));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('读取密钥文件失败:', error);
|
||||||
|
modifiedLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
modifiedLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建修改后的 m3u8 的 Blob URL
|
||||||
|
const modifiedContent = modifiedLines.join('\n');
|
||||||
|
const m3u8Blob = new Blob([modifiedContent], { type: 'application/vnd.apple.mpegurl' });
|
||||||
|
newUrl = URL.createObjectURL(m3u8Blob);
|
||||||
|
|
||||||
|
// 保存 Blob URLs 到 window,以便在切换视频时清理
|
||||||
|
(window as any).__localFileBlobUrls = blobUrls;
|
||||||
|
|
||||||
|
console.log('使用 File System API 本地文件播放(Blob URL 模式):', episodeTitle);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('读取本地文件失败:', error);
|
console.error('读取本地文件失败:', error);
|
||||||
}
|
}
|
||||||
@@ -2088,7 +2152,11 @@ function PlayPageClient() {
|
|||||||
// M3U8格式 - 使用新的下载器,TS 格式
|
// M3U8格式 - 使用新的下载器,TS 格式
|
||||||
try {
|
try {
|
||||||
const downloadTitle = `${videoTitle}_第${episodeIndex + 1}集`;
|
const downloadTitle = `${videoTitle}_第${episodeIndex + 1}集`;
|
||||||
await addDownloadTask(proxyUrl, downloadTitle, 'TS');
|
await addDownloadTask(proxyUrl, downloadTitle, 'TS', {
|
||||||
|
source: currentSource || undefined,
|
||||||
|
videoId: currentId || undefined,
|
||||||
|
episodeIndex,
|
||||||
|
});
|
||||||
successCount++;
|
successCount++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`添加下载任务失败 (第${episodeIndex + 1}集):`, error);
|
console.error(`添加下载任务失败 (第${episodeIndex + 1}集):`, error);
|
||||||
@@ -2799,64 +2867,6 @@ function PlayPageClient() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建本地文件 HLS loader 的工厂函数
|
|
||||||
const createLocalFileHlsLoader = (HlsLib: any, dirHandle: FileSystemDirectoryHandle) => {
|
|
||||||
return class LocalFileHlsLoader extends HlsLib.DefaultConfig.loader {
|
|
||||||
constructor(config: any) {
|
|
||||||
super(config);
|
|
||||||
const originalLoad = this.load.bind(this);
|
|
||||||
|
|
||||||
this.load = async function (context: any, config: any, callbacks: any) {
|
|
||||||
try {
|
|
||||||
const url = context.url;
|
|
||||||
|
|
||||||
// 提取文件名
|
|
||||||
let filename = '';
|
|
||||||
if (url.includes('blob:')) {
|
|
||||||
// 如果是 blob URL,从 M3U8 内容中提取文件名
|
|
||||||
filename = url.split('/').pop() || '';
|
|
||||||
} else {
|
|
||||||
filename = url.split('/').pop() || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('尝试加载本地文件:', filename);
|
|
||||||
|
|
||||||
// 从 File System API 读取文件
|
|
||||||
const fileHandle = await dirHandle.getFileHandle(filename, { create: false });
|
|
||||||
const file = await fileHandle.getFile();
|
|
||||||
|
|
||||||
let data: string | ArrayBuffer;
|
|
||||||
if (filename.endsWith('.m3u8')) {
|
|
||||||
data = await file.text();
|
|
||||||
} else {
|
|
||||||
data = await file.arrayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用成功回调
|
|
||||||
callbacks.onSuccess(
|
|
||||||
{
|
|
||||||
url: url,
|
|
||||||
data: data,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
trequest: performance.now(),
|
|
||||||
tfirst: performance.now(),
|
|
||||||
tload: performance.now(),
|
|
||||||
loaded: file.size,
|
|
||||||
total: file.size,
|
|
||||||
},
|
|
||||||
context
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载本地文件失败:', error);
|
|
||||||
// 如果本地文件加载失败,回退到网络加载
|
|
||||||
originalLoad(context, config, callbacks);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 创建自定义 HLS loader 的工厂函数
|
// 创建自定义 HLS loader 的工厂函数
|
||||||
const createCustomHlsLoader = (HlsLib: any) => {
|
const createCustomHlsLoader = (HlsLib: any) => {
|
||||||
return class CustomHlsJsLoader extends HlsLib.DefaultConfig.loader {
|
return class CustomHlsJsLoader extends HlsLib.DefaultConfig.loader {
|
||||||
@@ -5051,9 +5061,6 @@ function PlayPageClient() {
|
|||||||
// 每次创建HLS实例时,都读取最新的blockAdEnabled状态
|
// 每次创建HLS实例时,都读取最新的blockAdEnabled状态
|
||||||
const shouldUseCustomLoader = blockAdEnabledRef.current;
|
const shouldUseCustomLoader = blockAdEnabledRef.current;
|
||||||
|
|
||||||
// 检查是否有本地文件目录句柄
|
|
||||||
const localFileDirHandle = (window as any).__localFileDirHandle as FileSystemDirectoryHandle | undefined;
|
|
||||||
|
|
||||||
// 从localStorage读取缓冲策略
|
// 从localStorage读取缓冲策略
|
||||||
const bufferStrategy = typeof window !== 'undefined'
|
const bufferStrategy = typeof window !== 'undefined'
|
||||||
? localStorage.getItem('bufferStrategy') || 'medium'
|
? localStorage.getItem('bufferStrategy') || 'medium'
|
||||||
@@ -5099,12 +5106,7 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
// 选择合适的 Loader
|
// 选择合适的 Loader
|
||||||
let loaderClass;
|
let loaderClass;
|
||||||
if (localFileDirHandle) {
|
if (shouldUseCustomLoader) {
|
||||||
// 使用本地文件 Loader
|
|
||||||
const LocalFileHlsLoader = createLocalFileHlsLoader(Hls, localFileDirHandle);
|
|
||||||
loaderClass = LocalFileHlsLoader;
|
|
||||||
console.log('使用本地文件 HLS Loader');
|
|
||||||
} else if (shouldUseCustomLoader) {
|
|
||||||
// 使用自定义广告过滤 Loader
|
// 使用自定义广告过滤 Loader
|
||||||
loaderClass = CustomHlsJsLoader;
|
loaderClass = CustomHlsJsLoader;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,7 +7,16 @@ import { M3U8Downloader, M3U8DownloadTask } from '@/lib/m3u8-downloader';
|
|||||||
interface DownloadContextType {
|
interface DownloadContextType {
|
||||||
downloader: M3U8Downloader;
|
downloader: M3U8Downloader;
|
||||||
tasks: M3U8DownloadTask[];
|
tasks: M3U8DownloadTask[];
|
||||||
addDownloadTask: (url: string, title: string, type?: 'TS' | 'MP4') => Promise<void>;
|
addDownloadTask: (
|
||||||
|
url: string,
|
||||||
|
title: string,
|
||||||
|
type?: 'TS' | 'MP4',
|
||||||
|
metadata?: {
|
||||||
|
source?: string;
|
||||||
|
videoId?: string;
|
||||||
|
episodeIndex?: number;
|
||||||
|
}
|
||||||
|
) => Promise<void>;
|
||||||
startTask: (taskId: string) => void;
|
startTask: (taskId: string) => void;
|
||||||
pauseTask: (taskId: string) => void;
|
pauseTask: (taskId: string) => void;
|
||||||
cancelTask: (taskId: string) => void;
|
cancelTask: (taskId: string) => void;
|
||||||
@@ -62,9 +71,18 @@ export function DownloadProvider({ children }: { children: React.ReactNode }) {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const addDownloadTask = useCallback(async (url: string, title: string, type: 'TS' | 'MP4' = 'TS') => {
|
const addDownloadTask = useCallback(async (
|
||||||
|
url: string,
|
||||||
|
title: string,
|
||||||
|
type: 'TS' | 'MP4' = 'TS',
|
||||||
|
metadata?: {
|
||||||
|
source?: string;
|
||||||
|
videoId?: string;
|
||||||
|
episodeIndex?: number;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const taskId = await downloader.createTask(url, title, type);
|
const taskId = await downloader.createTask(url, title, type, metadata);
|
||||||
|
|
||||||
// 读取下载模式设置
|
// 读取下载模式设置
|
||||||
const downloadMode = typeof window !== 'undefined'
|
const downloadMode = typeof window !== 'undefined'
|
||||||
@@ -76,43 +94,59 @@ export function DownloadProvider({ children }: { children: React.ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
const dbName = 'MoonTVPlus';
|
const dbName = 'MoonTVPlus';
|
||||||
const storeName = 'dirHandles';
|
const storeName = 'dirHandles';
|
||||||
const request = indexedDB.open(dbName, 1);
|
|
||||||
|
|
||||||
request.onupgradeneeded = (event) => {
|
// 使用 Promise 包装 IndexedDB 操作,确保在启动任务前完成
|
||||||
const db = (event.target as IDBOpenDBRequest).result;
|
await new Promise<void>((resolve, reject) => {
|
||||||
if (!db.objectStoreNames.contains(storeName)) {
|
const request = indexedDB.open(dbName, 1);
|
||||||
db.createObjectStore(storeName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onsuccess = (event) => {
|
request.onupgradeneeded = (event) => {
|
||||||
const db = (event.target as IDBOpenDBRequest).result;
|
const db = (event.target as IDBOpenDBRequest).result;
|
||||||
|
if (!db.objectStoreNames.contains(storeName)) {
|
||||||
// 检查 object store 是否存在
|
db.createObjectStore(storeName);
|
||||||
if (!db.objectStoreNames.contains(storeName)) {
|
|
||||||
console.warn('Object store 不存在,跳过读取');
|
|
||||||
db.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const transaction = db.transaction([storeName], 'readonly');
|
|
||||||
const store = transaction.objectStore(storeName);
|
|
||||||
const getRequest = store.get('downloadDir');
|
|
||||||
|
|
||||||
getRequest.onsuccess = () => {
|
|
||||||
const dirHandle = getRequest.result as FileSystemDirectoryHandle | undefined;
|
|
||||||
if (dirHandle) {
|
|
||||||
// 更新任务的下载模式和目录句柄
|
|
||||||
const task = downloader.getTask(taskId);
|
|
||||||
if (task) {
|
|
||||||
task.downloadMode = 'filesystem';
|
|
||||||
task.filesystemDirHandle = dirHandle;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('未找到保存目录,使用浏览器下载模式');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const db = (event.target as IDBOpenDBRequest).result;
|
||||||
|
|
||||||
|
// 检查 object store 是否存在
|
||||||
|
if (!db.objectStoreNames.contains(storeName)) {
|
||||||
|
console.warn('Object store 不存在,跳过读取');
|
||||||
|
db.close();
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transaction = db.transaction([storeName], 'readonly');
|
||||||
|
const store = transaction.objectStore(storeName);
|
||||||
|
const getRequest = store.get('downloadDir');
|
||||||
|
|
||||||
|
getRequest.onsuccess = () => {
|
||||||
|
const dirHandle = getRequest.result as FileSystemDirectoryHandle | undefined;
|
||||||
|
if (dirHandle) {
|
||||||
|
// 更新任务的下载模式和目录句柄
|
||||||
|
const task = downloader.getTask(taskId);
|
||||||
|
if (task) {
|
||||||
|
task.downloadMode = 'filesystem';
|
||||||
|
task.filesystemDirHandle = dirHandle;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('未找到保存目录,使用浏览器下载模式');
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
getRequest.onerror = () => {
|
||||||
|
db.close();
|
||||||
|
reject(new Error('读取目录句柄失败'));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error('打开 IndexedDB 失败'));
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('读取目录句柄失败:', error);
|
console.error('读取目录句柄失败:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-19
@@ -47,6 +47,10 @@ export interface M3U8DownloadTask {
|
|||||||
downloadMode?: 'browser' | 'filesystem';
|
downloadMode?: 'browser' | 'filesystem';
|
||||||
filesystemDirHandle?: FileSystemDirectoryHandle;
|
filesystemDirHandle?: FileSystemDirectoryHandle;
|
||||||
m3u8Content?: string; // 原始 M3U8 内容,用于生成本地播放列表
|
m3u8Content?: string; // 原始 M3U8 内容,用于生成本地播放列表
|
||||||
|
// 视频标识信息(用于区分不同视频)
|
||||||
|
source?: string;
|
||||||
|
videoId?: string;
|
||||||
|
episodeIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface M3U8DownloaderOptions {
|
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);
|
const taskId = 't_' + Date.now() + Math.random().toString(36).substr(2, 9);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -85,11 +98,11 @@ export class M3U8Downloader {
|
|||||||
// 自动选择最高清晰度
|
// 自动选择最高清晰度
|
||||||
url = streams[0].url;
|
url = streams[0].url;
|
||||||
const subM3u8Content = await this.fetchM3U8(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) {
|
} catch (error) {
|
||||||
throw new Error(`创建任务失败: ${error}`);
|
throw new Error(`创建任务失败: ${error}`);
|
||||||
}
|
}
|
||||||
@@ -103,7 +116,12 @@ export class M3U8Downloader {
|
|||||||
url: string,
|
url: string,
|
||||||
title: string,
|
title: string,
|
||||||
type: 'TS' | 'MP4',
|
type: 'TS' | 'MP4',
|
||||||
m3u8Content: string
|
m3u8Content: string,
|
||||||
|
metadata?: {
|
||||||
|
source?: string;
|
||||||
|
videoId?: string;
|
||||||
|
episodeIndex?: number;
|
||||||
|
}
|
||||||
): string {
|
): string {
|
||||||
const task: M3U8DownloadTask = {
|
const task: M3U8DownloadTask = {
|
||||||
id: taskId,
|
id: taskId,
|
||||||
@@ -115,7 +133,7 @@ export class M3U8Downloader {
|
|||||||
tsUrlList: [],
|
tsUrlList: [],
|
||||||
requests: [],
|
requests: [],
|
||||||
mediaFileList: [],
|
mediaFileList: [],
|
||||||
downloadIndex: 0,
|
downloadIndex: 0, // 初始化为 0,在 startTask 时会设置为正确的值
|
||||||
downloading: false,
|
downloading: false,
|
||||||
durationSecond: 0,
|
durationSecond: 0,
|
||||||
beginTime: new Date(),
|
beginTime: new Date(),
|
||||||
@@ -125,7 +143,7 @@ export class M3U8Downloader {
|
|||||||
retryCountdown: 0,
|
retryCountdown: 0,
|
||||||
rangeDownload: {
|
rangeDownload: {
|
||||||
isShowRange: false,
|
isShowRange: false,
|
||||||
startSegment: 1,
|
startSegment: 0, // 改为从 0 开始
|
||||||
endSegment: 0,
|
endSegment: 0,
|
||||||
targetSegment: 0,
|
targetSegment: 0,
|
||||||
},
|
},
|
||||||
@@ -137,6 +155,9 @@ export class M3U8Downloader {
|
|||||||
decryption: null,
|
decryption: null,
|
||||||
},
|
},
|
||||||
m3u8Content, // 保存原始 M3U8 内容
|
m3u8Content, // 保存原始 M3U8 内容
|
||||||
|
source: metadata?.source,
|
||||||
|
videoId: metadata?.videoId,
|
||||||
|
episodeIndex: metadata?.episodeIndex,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 解析 TS 片段
|
// 解析 TS 片段
|
||||||
@@ -185,6 +206,19 @@ export class M3U8Downloader {
|
|||||||
await this.getAESKey(task);
|
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';
|
task.status = 'downloading';
|
||||||
this.currentTask = task;
|
this.currentTask = task;
|
||||||
this.downloadTS(task);
|
this.downloadTS(task);
|
||||||
@@ -264,7 +298,7 @@ export class M3U8Downloader {
|
|||||||
|
|
||||||
// 找到第一个失败的片段索引
|
// 找到第一个失败的片段索引
|
||||||
let firstErrorIndex = task.rangeDownload.endSegment;
|
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 === '') {
|
if (task.finishList[i] && task.finishList[i].status === '') {
|
||||||
firstErrorIndex = Math.min(firstErrorIndex, i);
|
firstErrorIndex = Math.min(firstErrorIndex, i);
|
||||||
}
|
}
|
||||||
@@ -405,7 +439,7 @@ export class M3U8Downloader {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 浏览器下载模式:保存到内存
|
// 浏览器下载模式:保存到内存
|
||||||
task.mediaFileList[index - task.rangeDownload.startSegment + 1] = convertedData;
|
task.mediaFileList[index] = convertedData;
|
||||||
task.finishList[index].status = 'is-success';
|
task.finishList[index].status = 'is-success';
|
||||||
task.finishNum++;
|
task.finishNum++;
|
||||||
|
|
||||||
@@ -443,7 +477,7 @@ export class M3U8Downloader {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 浏览器下载模式:保存到内存
|
// 浏览器下载模式:保存到内存
|
||||||
task.mediaFileList[index - task.rangeDownload.startSegment + 1] = data;
|
task.mediaFileList[index] = data;
|
||||||
task.finishList[index].status = 'is-success';
|
task.finishList[index].status = 'is-success';
|
||||||
task.finishNum++;
|
task.finishNum++;
|
||||||
|
|
||||||
@@ -664,7 +698,7 @@ export class M3U8Downloader {
|
|||||||
|
|
||||||
transMuxer.on('data', (segment: any) => {
|
transMuxer.on('data', (segment: any) => {
|
||||||
// 第一个片段需要包含初始化段
|
// 第一个片段需要包含初始化段
|
||||||
if (index === task.rangeDownload.startSegment - 1) {
|
if (index === 0) {
|
||||||
const combinedData = new Uint8Array(
|
const combinedData = new Uint8Array(
|
||||||
segment.initSegment.byteLength + segment.data.byteLength
|
segment.initSegment.byteLength + segment.data.byteLength
|
||||||
);
|
);
|
||||||
@@ -713,10 +747,28 @@ export class M3U8Downloader {
|
|||||||
throw new Error('未选择保存目录');
|
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`;
|
const filename = `segment_${index.toString().padStart(5, '0')}.ts`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileHandle = await task.filesystemDirHandle.getFileHandle(filename, { create: true });
|
const fileHandle = await targetDirHandle.getFileHandle(filename, { create: true });
|
||||||
const writable = await fileHandle.createWritable();
|
const writable = await fileHandle.createWritable();
|
||||||
await writable.write(data);
|
await writable.write(data);
|
||||||
await writable.close();
|
await writable.close();
|
||||||
@@ -735,6 +787,21 @@ export class M3U8Downloader {
|
|||||||
return;
|
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 {
|
try {
|
||||||
const lines = task.m3u8Content.split('\n');
|
const lines = task.m3u8Content.split('\n');
|
||||||
const modifiedLines: string[] = [];
|
const modifiedLines: string[] = [];
|
||||||
@@ -749,7 +816,7 @@ export class M3U8Downloader {
|
|||||||
if (task.aesConf.method && task.aesConf.method !== 'NONE') {
|
if (task.aesConf.method && task.aesConf.method !== 'NONE') {
|
||||||
// 如果有加密,保存密钥文件
|
// 如果有加密,保存密钥文件
|
||||||
if (task.aesConf.key) {
|
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"');
|
const modifiedLine = line.replace(/URI="[^"]+"/g, 'URI="key.key"');
|
||||||
modifiedLines.push(modifiedLine);
|
modifiedLines.push(modifiedLine);
|
||||||
@@ -759,7 +826,7 @@ export class M3U8Downloader {
|
|||||||
}
|
}
|
||||||
// 替换视频片段 URL
|
// 替换视频片段 URL
|
||||||
else if (trimmedLine && !trimmedLine.startsWith('#')) {
|
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 indent = line.match(/^\s*/)?.[0] || '';
|
||||||
const filename = `segment_${segmentIndex.toString().padStart(5, '0')}.ts`;
|
const filename = `segment_${segmentIndex.toString().padStart(5, '0')}.ts`;
|
||||||
modifiedLines.push(indent + filename);
|
modifiedLines.push(indent + filename);
|
||||||
@@ -776,12 +843,10 @@ export class M3U8Downloader {
|
|||||||
|
|
||||||
// 保存播放列表
|
// 保存播放列表
|
||||||
const playlistContent = modifiedLines.join('\n');
|
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();
|
const writable = await fileHandle.createWritable();
|
||||||
await writable.write(playlistContent);
|
await writable.write(playlistContent);
|
||||||
await writable.close();
|
await writable.close();
|
||||||
|
|
||||||
console.log('本地播放列表生成成功');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('生成播放列表失败:', error);
|
console.error('生成播放列表失败:', error);
|
||||||
}
|
}
|
||||||
@@ -792,14 +857,16 @@ export class M3U8Downloader {
|
|||||||
*/
|
*/
|
||||||
private async saveKeyToFilesystem(
|
private async saveKeyToFilesystem(
|
||||||
task: M3U8DownloadTask,
|
task: M3U8DownloadTask,
|
||||||
keyData: ArrayBuffer
|
keyData: ArrayBuffer,
|
||||||
|
targetDirHandle?: FileSystemDirectoryHandle
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!task.filesystemDirHandle) {
|
const dirHandle = targetDirHandle || task.filesystemDirHandle;
|
||||||
|
if (!dirHandle) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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();
|
const writable = await fileHandle.createWritable();
|
||||||
await writable.write(keyData);
|
await writable.write(keyData);
|
||||||
await writable.close();
|
await writable.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user