mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-03-07 03:07:29 +08:00
feat(player): enhance video playback with SSL error fallback and performance optimizations
- Add comprehensive SSL certificate error detection and automatic source switching - Implement smart video source fallback strategy with failed source tracking - Enhance video component with optimized event handlers and useCallback patterns - Add explicit playAsync() call in onLoad to improve auto-play reliability - Integrate performance monitoring with detailed logging throughout playback chain - Optimize Video component props with useMemo and custom useVideoHandlers hook - Add source matching fixes for fallback scenarios in DetailStore - Enhance error handling with user-friendly messages and recovery strategies
This commit is contained in:
@@ -16,11 +16,14 @@ interface DetailState {
|
||||
allSourcesLoaded: boolean;
|
||||
controller: AbortController | null;
|
||||
isFavorited: boolean;
|
||||
failedSources: Set<string>; // 记录失败的source列表
|
||||
|
||||
init: (q: string, preferredSource?: string, id?: string) => Promise<void>;
|
||||
setDetail: (detail: SearchResultWithResolution) => void;
|
||||
setDetail: (detail: SearchResultWithResolution) => Promise<void>;
|
||||
abort: () => void;
|
||||
toggleFavorite: () => Promise<void>;
|
||||
markSourceAsFailed: (source: string, reason: string) => void;
|
||||
getNextAvailableSource: (currentSource: string, episodeIndex: number) => SearchResultWithResolution | null;
|
||||
}
|
||||
|
||||
const useDetailStore = create<DetailState>((set, get) => ({
|
||||
@@ -33,8 +36,12 @@ const useDetailStore = create<DetailState>((set, get) => ({
|
||||
allSourcesLoaded: false,
|
||||
controller: null,
|
||||
isFavorited: false,
|
||||
failedSources: new Set(),
|
||||
|
||||
init: async (q, preferredSource, id) => {
|
||||
const perfStart = performance.now();
|
||||
console.info(`[PERF] DetailStore.init START - q: ${q}, preferredSource: ${preferredSource}, id: ${id}`);
|
||||
|
||||
const { controller: oldController } = get();
|
||||
if (oldController) {
|
||||
oldController.abort();
|
||||
@@ -55,9 +62,13 @@ const useDetailStore = create<DetailState>((set, get) => ({
|
||||
const { videoSource } = useSettingsStore.getState();
|
||||
|
||||
const processAndSetResults = async (results: SearchResult[], merge = false) => {
|
||||
const resolutionStart = performance.now();
|
||||
console.info(`[PERF] Resolution detection START - processing ${results.length} sources`);
|
||||
|
||||
const resultsWithResolution = await Promise.all(
|
||||
results.map(async (searchResult) => {
|
||||
let resolution;
|
||||
const m3u8Start = performance.now();
|
||||
try {
|
||||
if (searchResult.episodes && searchResult.episodes.length > 0) {
|
||||
resolution = await getResolutionFromM3U8(searchResult.episodes[0], signal);
|
||||
@@ -67,9 +78,14 @@ const useDetailStore = create<DetailState>((set, get) => ({
|
||||
console.info(`Failed to get resolution for ${searchResult.source_name}`, e);
|
||||
}
|
||||
}
|
||||
const m3u8End = performance.now();
|
||||
console.info(`[PERF] M3U8 resolution for ${searchResult.source_name}: ${(m3u8End - m3u8Start).toFixed(2)}ms (${resolution || 'failed'})`);
|
||||
return { ...searchResult, resolution };
|
||||
})
|
||||
);
|
||||
|
||||
const resolutionEnd = performance.now();
|
||||
console.info(`[PERF] Resolution detection COMPLETE - took ${(resolutionEnd - resolutionStart).toFixed(2)}ms`);
|
||||
|
||||
if (signal.aborted) return;
|
||||
|
||||
@@ -93,59 +109,205 @@ const useDetailStore = create<DetailState>((set, get) => ({
|
||||
try {
|
||||
// Optimization for favorite navigation
|
||||
if (preferredSource && id) {
|
||||
const { results: preferredResult } = await api.searchVideo(q, preferredSource, signal);
|
||||
const searchPreferredStart = performance.now();
|
||||
console.info(`[PERF] API searchVideo (preferred) START - source: ${preferredSource}, query: "${q}"`);
|
||||
|
||||
let preferredResult: SearchResult[] = [];
|
||||
let preferredSearchError: any = null;
|
||||
|
||||
try {
|
||||
const response = await api.searchVideo(q, preferredSource, signal);
|
||||
preferredResult = response.results;
|
||||
} catch (error) {
|
||||
preferredSearchError = error;
|
||||
console.error(`[ERROR] API searchVideo (preferred) FAILED - source: ${preferredSource}, error:`, error);
|
||||
}
|
||||
|
||||
const searchPreferredEnd = performance.now();
|
||||
console.info(`[PERF] API searchVideo (preferred) END - took ${(searchPreferredEnd - searchPreferredStart).toFixed(2)}ms, results: ${preferredResult.length}, error: ${!!preferredSearchError}`);
|
||||
|
||||
if (signal.aborted) return;
|
||||
|
||||
// 检查preferred source结果
|
||||
if (preferredResult.length > 0) {
|
||||
console.info(`[SUCCESS] Preferred source "${preferredSource}" found ${preferredResult.length} results for "${q}"`);
|
||||
await processAndSetResults(preferredResult, false);
|
||||
set({ loading: false });
|
||||
} else {
|
||||
// 降级策略:preferred source失败时立即尝试所有源
|
||||
if (preferredSearchError) {
|
||||
console.warn(`[FALLBACK] Preferred source "${preferredSource}" failed with error, trying all sources immediately`);
|
||||
} else {
|
||||
console.warn(`[FALLBACK] Preferred source "${preferredSource}" returned 0 results for "${q}", trying all sources immediately`);
|
||||
}
|
||||
|
||||
// 立即尝试所有源,不再依赖后台搜索
|
||||
const fallbackStart = performance.now();
|
||||
console.info(`[PERF] FALLBACK search (all sources) START - query: "${q}"`);
|
||||
|
||||
try {
|
||||
const { results: allResults } = await api.searchVideos(q);
|
||||
const fallbackEnd = performance.now();
|
||||
console.info(`[PERF] FALLBACK search END - took ${(fallbackEnd - fallbackStart).toFixed(2)}ms, total results: ${allResults.length}`);
|
||||
|
||||
const filteredResults = allResults.filter(item => item.title === q);
|
||||
console.info(`[FALLBACK] Filtered results: ${filteredResults.length} matches for "${q}"`);
|
||||
|
||||
if (filteredResults.length > 0) {
|
||||
console.info(`[SUCCESS] FALLBACK search found results, proceeding with ${filteredResults[0].source_name}`);
|
||||
await processAndSetResults(filteredResults, false);
|
||||
set({ loading: false });
|
||||
} else {
|
||||
console.error(`[ERROR] FALLBACK search found no matching results for "${q}"`);
|
||||
set({
|
||||
error: `未找到 "${q}" 的播放源,请检查标题或稍后重试`,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
console.error(`[ERROR] FALLBACK search FAILED:`, fallbackError);
|
||||
set({
|
||||
error: `搜索失败:${fallbackError instanceof Error ? fallbackError.message : '网络错误,请稍后重试'}`,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 后台搜索(如果preferred source成功的话)
|
||||
if (preferredResult.length > 0) {
|
||||
const searchAllStart = performance.now();
|
||||
console.info(`[PERF] API searchVideos (background) START`);
|
||||
|
||||
try {
|
||||
const { results: allResults } = await api.searchVideos(q);
|
||||
|
||||
const searchAllEnd = performance.now();
|
||||
console.info(`[PERF] API searchVideos (background) END - took ${(searchAllEnd - searchAllStart).toFixed(2)}ms, results: ${allResults.length}`);
|
||||
|
||||
if (signal.aborted) return;
|
||||
await processAndSetResults(allResults.filter(item => item.title === q), true);
|
||||
} catch (backgroundError) {
|
||||
console.warn(`[WARN] Background search failed, but preferred source already succeeded:`, backgroundError);
|
||||
}
|
||||
}
|
||||
// Then load all others in background
|
||||
const { results: allResults } = await api.searchVideos(q);
|
||||
if (signal.aborted) return;
|
||||
await processAndSetResults(allResults, true);
|
||||
} else {
|
||||
// Standard navigation: fetch resources, then fetch details one by one
|
||||
const allResources = await api.getResources(signal);
|
||||
const enabledResources = videoSource.enabledAll
|
||||
? allResources
|
||||
: allResources.filter((r) => videoSource.sources[r.key]);
|
||||
const resourcesStart = performance.now();
|
||||
console.info(`[PERF] API getResources START - query: "${q}"`);
|
||||
|
||||
try {
|
||||
const allResources = await api.getResources(signal);
|
||||
|
||||
const resourcesEnd = performance.now();
|
||||
console.info(`[PERF] API getResources END - took ${(resourcesEnd - resourcesStart).toFixed(2)}ms, resources: ${allResources.length}`);
|
||||
|
||||
const enabledResources = videoSource.enabledAll
|
||||
? allResources
|
||||
: allResources.filter((r) => videoSource.sources[r.key]);
|
||||
|
||||
let firstResultFound = false;
|
||||
const searchPromises = enabledResources.map(async (resource) => {
|
||||
try {
|
||||
const { results } = await api.searchVideo(q, resource.key, signal);
|
||||
if (results.length > 0) {
|
||||
await processAndSetResults(results, true);
|
||||
if (!firstResultFound) {
|
||||
set({ loading: false }); // Stop loading indicator on first result
|
||||
firstResultFound = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.info(`Failed to fetch from ${resource.name}:`, error);
|
||||
console.info(`[PERF] Enabled resources: ${enabledResources.length}/${allResources.length}`);
|
||||
|
||||
if (enabledResources.length === 0) {
|
||||
console.error(`[ERROR] No enabled resources available for search`);
|
||||
set({
|
||||
error: "没有可用的视频源,请检查设置或联系管理员",
|
||||
loading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(searchPromises);
|
||||
let firstResultFound = false;
|
||||
let totalResults = 0;
|
||||
const searchPromises = enabledResources.map(async (resource) => {
|
||||
try {
|
||||
const searchStart = performance.now();
|
||||
const { results } = await api.searchVideo(q, resource.key, signal);
|
||||
const searchEnd = performance.now();
|
||||
console.info(`[PERF] API searchVideo (${resource.name}) took ${(searchEnd - searchStart).toFixed(2)}ms, results: ${results.length}`);
|
||||
|
||||
if (results.length > 0) {
|
||||
totalResults += results.length;
|
||||
console.info(`[SUCCESS] Source "${resource.name}" found ${results.length} results for "${q}"`);
|
||||
await processAndSetResults(results, true);
|
||||
if (!firstResultFound) {
|
||||
set({ loading: false }); // Stop loading indicator on first result
|
||||
firstResultFound = true;
|
||||
console.info(`[SUCCESS] First result found from "${resource.name}", stopping loading indicator`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[WARN] Source "${resource.name}" returned 0 results for "${q}"`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[ERROR] Failed to fetch from ${resource.name}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(searchPromises);
|
||||
|
||||
// 检查是否找到任何结果
|
||||
if (totalResults === 0) {
|
||||
console.error(`[ERROR] All sources returned 0 results for "${q}"`);
|
||||
set({
|
||||
error: `未找到 "${q}" 的播放源,请尝试其他关键词或稍后重试`,
|
||||
loading: false
|
||||
});
|
||||
} else {
|
||||
console.info(`[SUCCESS] Standard search completed, total results: ${totalResults}`);
|
||||
}
|
||||
} catch (resourceError) {
|
||||
console.error(`[ERROR] Failed to get resources:`, resourceError);
|
||||
set({
|
||||
error: `获取视频源失败:${resourceError instanceof Error ? resourceError.message : '网络错误,请稍后重试'}`,
|
||||
loading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (get().searchResults.length === 0) {
|
||||
set({ error: "未找到任何播放源" });
|
||||
const favoriteCheckStart = performance.now();
|
||||
const finalState = get();
|
||||
|
||||
// 最终检查:如果所有搜索都完成但仍然没有结果
|
||||
if (finalState.searchResults.length === 0 && !finalState.error) {
|
||||
console.error(`[ERROR] All search attempts completed but no results found for "${q}"`);
|
||||
set({ error: `未找到 "${q}" 的播放源,请检查标题拼写或稍后重试` });
|
||||
} else if (finalState.searchResults.length > 0) {
|
||||
console.info(`[SUCCESS] DetailStore.init completed successfully with ${finalState.searchResults.length} sources`);
|
||||
}
|
||||
|
||||
if (get().detail) {
|
||||
const { source, id } = get().detail!;
|
||||
const isFavorited = await FavoriteManager.isFavorited(source, id.toString());
|
||||
set({ isFavorited });
|
||||
if (finalState.detail) {
|
||||
const { source, id } = finalState.detail;
|
||||
console.info(`[INFO] Checking favorite status for source: ${source}, id: ${id}`);
|
||||
try {
|
||||
const isFavorited = await FavoriteManager.isFavorited(source, id.toString());
|
||||
set({ isFavorited });
|
||||
console.info(`[INFO] Favorite status: ${isFavorited}`);
|
||||
} catch (favoriteError) {
|
||||
console.warn(`[WARN] Failed to check favorite status:`, favoriteError);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[WARN] No detail found after all search attempts for "${q}"`);
|
||||
}
|
||||
|
||||
const favoriteCheckEnd = performance.now();
|
||||
console.info(`[PERF] Favorite check took ${(favoriteCheckEnd - favoriteCheckStart).toFixed(2)}ms`);
|
||||
|
||||
} catch (e) {
|
||||
if ((e as Error).name !== "AbortError") {
|
||||
set({ error: e instanceof Error ? e.message : "获取数据失败" });
|
||||
console.error(`[ERROR] DetailStore.init caught unexpected error:`, e);
|
||||
const errorMessage = e instanceof Error ? e.message : "获取数据失败";
|
||||
set({ error: `搜索失败:${errorMessage}` });
|
||||
} else {
|
||||
console.info(`[INFO] DetailStore.init aborted by user`);
|
||||
}
|
||||
} finally {
|
||||
if (!signal.aborted) {
|
||||
set({ loading: false, allSourcesLoaded: true });
|
||||
console.info(`[INFO] DetailStore.init cleanup completed`);
|
||||
}
|
||||
|
||||
const perfEnd = performance.now();
|
||||
console.info(`[PERF] DetailStore.init COMPLETE - total time: ${(perfEnd - perfStart).toFixed(2)}ms`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -178,6 +340,64 @@ const useDetailStore = create<DetailState>((set, get) => ({
|
||||
const newIsFavorited = await FavoriteManager.toggle(source, id.toString(), favoriteItem);
|
||||
set({ isFavorited: newIsFavorited });
|
||||
},
|
||||
|
||||
markSourceAsFailed: (source: string, reason: string) => {
|
||||
const { failedSources } = get();
|
||||
const newFailedSources = new Set(failedSources);
|
||||
newFailedSources.add(source);
|
||||
|
||||
console.warn(`[SOURCE_FAILED] Marking source "${source}" as failed due to: ${reason}`);
|
||||
console.info(`[SOURCE_FAILED] Total failed sources: ${newFailedSources.size}`);
|
||||
|
||||
set({ failedSources: newFailedSources });
|
||||
},
|
||||
|
||||
getNextAvailableSource: (currentSource: string, episodeIndex: number) => {
|
||||
const { searchResults, failedSources } = get();
|
||||
|
||||
console.info(`[SOURCE_SELECTION] Looking for alternative to "${currentSource}" for episode ${episodeIndex + 1}`);
|
||||
console.info(`[SOURCE_SELECTION] Failed sources: [${Array.from(failedSources).join(', ')}]`);
|
||||
|
||||
// 过滤掉当前source和已失败的sources
|
||||
const availableSources = searchResults.filter(result =>
|
||||
result.source !== currentSource &&
|
||||
!failedSources.has(result.source) &&
|
||||
result.episodes &&
|
||||
result.episodes.length > episodeIndex
|
||||
);
|
||||
|
||||
console.info(`[SOURCE_SELECTION] Available sources: ${availableSources.length}`);
|
||||
availableSources.forEach(source => {
|
||||
console.info(`[SOURCE_SELECTION] - ${source.source} (${source.source_name}): ${source.episodes?.length || 0} episodes`);
|
||||
});
|
||||
|
||||
if (availableSources.length === 0) {
|
||||
console.error(`[SOURCE_SELECTION] No available sources for episode ${episodeIndex + 1}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 优先选择有高分辨率的source
|
||||
const sortedSources = availableSources.sort((a, b) => {
|
||||
const aResolution = a.resolution || '';
|
||||
const bResolution = b.resolution || '';
|
||||
|
||||
// 优先级: 1080p > 720p > 其他 > 无分辨率
|
||||
const resolutionPriority = (res: string) => {
|
||||
if (res.includes('1080')) return 4;
|
||||
if (res.includes('720')) return 3;
|
||||
if (res.includes('480')) return 2;
|
||||
if (res.includes('360')) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
return resolutionPriority(bResolution) - resolutionPriority(aResolution);
|
||||
});
|
||||
|
||||
const selectedSource = sortedSources[0];
|
||||
console.info(`[SOURCE_SELECTION] Selected fallback source: ${selectedSource.source} (${selectedSource.source_name}) with resolution: ${selectedSource.resolution || 'unknown'}`);
|
||||
|
||||
return selectedSource;
|
||||
},
|
||||
}));
|
||||
|
||||
export const sourcesSelector = (state: DetailState) => state.sources;
|
||||
|
||||
@@ -54,6 +54,7 @@ interface PlayerState {
|
||||
_isRecordSaveThrottled: boolean;
|
||||
// Internal helper
|
||||
_savePlayRecord: (updates?: Partial<PlayRecord>, options?: { immediate?: boolean }) => void;
|
||||
handleVideoError: (errorType: 'ssl' | 'network' | 'other', failedUrl: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
@@ -80,44 +81,156 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
setVideoRef: (ref) => set({ videoRef: ref }),
|
||||
|
||||
loadVideo: async ({ source, id, episodeIndex, position, title }) => {
|
||||
const perfStart = performance.now();
|
||||
console.info(`[PERF] PlayerStore.loadVideo START - source: ${source}, id: ${id}, title: ${title}`);
|
||||
|
||||
let detail = useDetailStore.getState().detail;
|
||||
let episodes = episodesSelectorBySource(source)(useDetailStore.getState());
|
||||
let episodes: string[] = [];
|
||||
|
||||
// 如果有detail,使用detail的source获取episodes;否则使用传入的source
|
||||
if (detail && detail.source) {
|
||||
console.info(`[INFO] Using existing detail source "${detail.source}" to get episodes`);
|
||||
episodes = episodesSelectorBySource(detail.source)(useDetailStore.getState());
|
||||
} else {
|
||||
console.info(`[INFO] No existing detail, using provided source "${source}" to get episodes`);
|
||||
episodes = episodesSelectorBySource(source)(useDetailStore.getState());
|
||||
}
|
||||
|
||||
set({
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
if (!detail || !episodes || episodes.length === 0 || detail.title !== title) {
|
||||
const needsDetailInit = !detail || !episodes || episodes.length === 0 || detail.title !== title;
|
||||
console.info(`[PERF] Detail check - needsInit: ${needsDetailInit}, hasDetail: ${!!detail}, episodesCount: ${episodes?.length || 0}`);
|
||||
|
||||
if (needsDetailInit) {
|
||||
const detailInitStart = performance.now();
|
||||
console.info(`[PERF] DetailStore.init START - ${title}`);
|
||||
|
||||
await useDetailStore.getState().init(title, source, id);
|
||||
|
||||
const detailInitEnd = performance.now();
|
||||
console.info(`[PERF] DetailStore.init END - took ${(detailInitEnd - detailInitStart).toFixed(2)}ms`);
|
||||
|
||||
detail = useDetailStore.getState().detail;
|
||||
episodes = episodesSelectorBySource(source)(useDetailStore.getState());
|
||||
|
||||
if (!detail) {
|
||||
console.info("Detail not found after initialization");
|
||||
console.error(`[ERROR] Detail not found after initialization for "${title}" (source: ${source}, id: ${id})`);
|
||||
|
||||
// 检查DetailStore的错误状态
|
||||
const detailStoreState = useDetailStore.getState();
|
||||
if (detailStoreState.error) {
|
||||
console.error(`[ERROR] DetailStore error: ${detailStoreState.error}`);
|
||||
set({
|
||||
isLoading: false,
|
||||
// 可以选择在这里设置一个错误状态,但playerStore可能没有error字段
|
||||
});
|
||||
} else {
|
||||
console.error(`[ERROR] DetailStore init completed but no detail found and no error reported`);
|
||||
set({ isLoading: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用DetailStore找到的实际source来获取episodes,而不是原始的preferredSource
|
||||
console.info(`[INFO] Using actual source "${detail.source}" instead of preferred source "${source}"`);
|
||||
episodes = episodesSelectorBySource(detail.source)(useDetailStore.getState());
|
||||
|
||||
if (!episodes || episodes.length === 0) {
|
||||
console.error(`[ERROR] No episodes found for "${title}" from source "${detail.source}" (${detail.source_name})`);
|
||||
|
||||
// 尝试从searchResults中直接获取episodes
|
||||
const detailStoreState = useDetailStore.getState();
|
||||
console.info(`[INFO] Available sources in searchResults: ${detailStoreState.searchResults.map(r => `${r.source}(${r.episodes?.length || 0} episodes)`).join(', ')}`);
|
||||
|
||||
// 如果当前source没有episodes,尝试使用第一个有episodes的source
|
||||
const sourceWithEpisodes = detailStoreState.searchResults.find(r => r.episodes && r.episodes.length > 0);
|
||||
if (sourceWithEpisodes) {
|
||||
console.info(`[FALLBACK] Using alternative source "${sourceWithEpisodes.source}" with ${sourceWithEpisodes.episodes.length} episodes`);
|
||||
episodes = sourceWithEpisodes.episodes;
|
||||
// 更新detail为有episodes的source
|
||||
detail = sourceWithEpisodes;
|
||||
} else {
|
||||
console.error(`[ERROR] No source with episodes found in searchResults`);
|
||||
set({ isLoading: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.info(`[SUCCESS] Detail and episodes loaded - source: ${detail.source_name}, episodes: ${episodes.length}`);
|
||||
} else {
|
||||
console.info(`[PERF] Skipping DetailStore.init - using cached data`);
|
||||
|
||||
// 即使是缓存的数据,也要确保使用正确的source获取episodes
|
||||
if (detail && detail.source && detail.source !== source) {
|
||||
console.info(`[INFO] Cached detail source "${detail.source}" differs from provided source "${source}", updating episodes`);
|
||||
episodes = episodesSelectorBySource(detail.source)(useDetailStore.getState());
|
||||
|
||||
if (!episodes || episodes.length === 0) {
|
||||
console.warn(`[WARN] Cached detail source "${detail.source}" has no episodes, trying provided source "${source}"`);
|
||||
episodes = episodesSelectorBySource(source)(useDetailStore.getState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 最终验证:确保我们有有效的detail和episodes数据
|
||||
if (!detail) {
|
||||
console.error(`[ERROR] Final check failed: detail is null`);
|
||||
set({ isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!episodes || episodes.length === 0) {
|
||||
console.error(`[ERROR] Final check failed: no episodes available for source "${detail.source}" (${detail.source_name})`);
|
||||
set({ isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
console.info(`[SUCCESS] Final validation passed - detail: ${detail.source_name}, episodes: ${episodes.length}`);
|
||||
|
||||
try {
|
||||
const playRecord = await PlayRecordManager.get(detail.source, detail.id.toString());
|
||||
const playerSettings = await PlayerSettingsManager.get(detail.source, detail.id.toString());
|
||||
const storageStart = performance.now();
|
||||
console.info(`[PERF] Storage operations START`);
|
||||
|
||||
const playRecord = await PlayRecordManager.get(detail!.source, detail!.id.toString());
|
||||
const storagePlayRecordEnd = performance.now();
|
||||
console.info(`[PERF] PlayRecordManager.get took ${(storagePlayRecordEnd - storageStart).toFixed(2)}ms`);
|
||||
|
||||
const playerSettings = await PlayerSettingsManager.get(detail!.source, detail!.id.toString());
|
||||
const storageEnd = performance.now();
|
||||
console.info(`[PERF] PlayerSettingsManager.get took ${(storageEnd - storagePlayRecordEnd).toFixed(2)}ms`);
|
||||
console.info(`[PERF] Total storage operations took ${(storageEnd - storageStart).toFixed(2)}ms`);
|
||||
|
||||
const initialPositionFromRecord = playRecord?.play_time ? playRecord.play_time * 1000 : 0;
|
||||
const savedPlaybackRate = playerSettings?.playbackRate || 1.0;
|
||||
|
||||
const episodesMappingStart = performance.now();
|
||||
const mappedEpisodes = episodes.map((ep, index) => ({
|
||||
url: ep,
|
||||
title: `第 ${index + 1} 集`,
|
||||
}));
|
||||
const episodesMappingEnd = performance.now();
|
||||
console.info(`[PERF] Episodes mapping (${episodes.length} episodes) took ${(episodesMappingEnd - episodesMappingStart).toFixed(2)}ms`);
|
||||
|
||||
set({
|
||||
isLoading: false,
|
||||
currentEpisodeIndex: episodeIndex,
|
||||
initialPosition: position || initialPositionFromRecord,
|
||||
playbackRate: savedPlaybackRate,
|
||||
episodes: episodes.map((ep, index) => ({
|
||||
url: ep,
|
||||
title: `第 ${index + 1} 集`,
|
||||
})),
|
||||
episodes: mappedEpisodes,
|
||||
introEndTime: playRecord?.introEndTime || playerSettings?.introEndTime,
|
||||
outroStartTime: playRecord?.outroStartTime || playerSettings?.outroStartTime,
|
||||
});
|
||||
|
||||
const perfEnd = performance.now();
|
||||
console.info(`[PERF] PlayerStore.loadVideo COMPLETE - total time: ${(perfEnd - perfStart).toFixed(2)}ms`);
|
||||
|
||||
} catch (error) {
|
||||
console.info("Failed to load play record", error);
|
||||
set({ isLoading: false });
|
||||
|
||||
const perfEnd = performance.now();
|
||||
console.info(`[PERF] PlayerStore.loadVideo ERROR - total time: ${(perfEnd - perfStart).toFixed(2)}ms`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -352,12 +465,105 @@ const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
outroStartTime: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
handleVideoError: async (errorType: 'ssl' | 'network' | 'other', failedUrl: string) => {
|
||||
const perfStart = performance.now();
|
||||
console.error(`[VIDEO_ERROR] Handling ${errorType} error for URL: ${failedUrl}`);
|
||||
|
||||
const detailStoreState = useDetailStore.getState();
|
||||
const { detail } = detailStoreState;
|
||||
const { currentEpisodeIndex } = get();
|
||||
|
||||
if (!detail) {
|
||||
console.error(`[VIDEO_ERROR] Cannot fallback - no detail available`);
|
||||
set({ isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记当前source为失败
|
||||
const currentSource = detail.source;
|
||||
const errorReason = `${errorType} error: ${failedUrl.substring(0, 100)}...`;
|
||||
useDetailStore.getState().markSourceAsFailed(currentSource, errorReason);
|
||||
|
||||
// 获取下一个可用的source
|
||||
const fallbackSource = useDetailStore.getState().getNextAvailableSource(currentSource, currentEpisodeIndex);
|
||||
|
||||
if (!fallbackSource) {
|
||||
console.error(`[VIDEO_ERROR] No fallback sources available for episode ${currentEpisodeIndex + 1}`);
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "播放失败",
|
||||
text2: "所有播放源都不可用,请稍后重试"
|
||||
});
|
||||
set({ isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
console.info(`[VIDEO_ERROR] Switching to fallback source: ${fallbackSource.source} (${fallbackSource.source_name})`);
|
||||
|
||||
try {
|
||||
// 更新DetailStore的当前detail为fallback source
|
||||
await useDetailStore.getState().setDetail(fallbackSource);
|
||||
|
||||
// 重新加载当前集数的episodes
|
||||
const newEpisodes = fallbackSource.episodes || [];
|
||||
if (newEpisodes.length > currentEpisodeIndex) {
|
||||
const mappedEpisodes = newEpisodes.map((ep, index) => ({
|
||||
url: ep,
|
||||
title: `第 ${index + 1} 集`,
|
||||
}));
|
||||
|
||||
set({
|
||||
episodes: mappedEpisodes,
|
||||
isLoading: false, // 让Video组件重新渲染
|
||||
});
|
||||
|
||||
const perfEnd = performance.now();
|
||||
console.info(`[VIDEO_ERROR] Successfully switched to fallback source in ${(perfEnd - perfStart).toFixed(2)}ms`);
|
||||
console.info(`[VIDEO_ERROR] New episode URL: ${newEpisodes[currentEpisodeIndex].substring(0, 100)}...`);
|
||||
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "已切换播放源",
|
||||
text2: `正在使用 ${fallbackSource.source_name}`
|
||||
});
|
||||
} else {
|
||||
console.error(`[VIDEO_ERROR] Fallback source doesn't have episode ${currentEpisodeIndex + 1}`);
|
||||
set({ isLoading: false });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[VIDEO_ERROR] Failed to switch to fallback source:`, error);
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default usePlayerStore;
|
||||
|
||||
export const selectCurrentEpisode = (state: PlayerState) => {
|
||||
if (state.episodes.length > state.currentEpisodeIndex) {
|
||||
return state.episodes[state.currentEpisodeIndex];
|
||||
// 增强数据安全性检查
|
||||
if (
|
||||
state.episodes &&
|
||||
Array.isArray(state.episodes) &&
|
||||
state.episodes.length > 0 &&
|
||||
state.currentEpisodeIndex >= 0 &&
|
||||
state.currentEpisodeIndex < state.episodes.length
|
||||
) {
|
||||
const episode = state.episodes[state.currentEpisodeIndex];
|
||||
// 确保episode有有效的URL
|
||||
if (episode && episode.url && episode.url.trim() !== "") {
|
||||
return episode;
|
||||
} else {
|
||||
// 仅在调试模式下打印
|
||||
if (__DEV__) {
|
||||
console.info(`[PERF] selectCurrentEpisode - episode found but invalid URL: ${episode?.url}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 仅在调试模式下打印
|
||||
if (__DEV__) {
|
||||
console.info(`[PERF] selectCurrentEpisode - no valid episode: episodes.length=${state.episodes?.length}, currentIndex=${state.currentEpisodeIndex}`);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user