From ad592edc65538fd4e41a356031849ae5bcde826a Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:47:15 -0700 Subject: [PATCH] Track and handle failed embeds in player sources Introduces tracking of failed embeds per source in the player store, adds logic to mark embeds as failed on playback errors, and filters out failed embeds when selecting sources. Also ensures failed sources and embeds are cleared when a working source is found. This improves error handling and fallback behavior for sources with multiple embeds. --- src/hooks/useProviderScrape.tsx | 14 +++++++--- src/pages/PlayerView.tsx | 3 ++- src/pages/parts/player/PlaybackErrorPart.tsx | 27 +++++++++++++++++--- src/stores/player/slices/source.ts | 20 +++++++++++++++ 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx index 57e4aed5..2492f6b9 100644 --- a/src/hooks/useProviderScrape.tsx +++ b/src/hooks/useProviderScrape.tsx @@ -171,7 +171,9 @@ export function useScrape() { async (media: ScrapeMedia, startFromSourceId?: string) => { const providerInstance = getProviders(); const allSources = providerInstance.listSources(); - const failedSources = usePlayerStore.getState().failedSources; + const playerState = usePlayerStore.getState(); + const failedSources = playerState.failedSources; + const failedEmbeds = playerState.failedEmbeds; // Start with all available sources (filtered by disabled and failed ones) let baseSourceOrder = allSources @@ -220,9 +222,15 @@ export function useScrape() { } } - // Filter out disabled embeds from the embed order + // Collect all failed embed IDs across all sources + const allFailedEmbedIds = Object.values(failedEmbeds).flat(); + + // Filter out disabled and failed embeds from the embed order const filteredEmbedOrder = enableEmbedOrder - ? preferredEmbedOrder.filter((id) => !disabledEmbeds.includes(id)) + ? preferredEmbedOrder.filter( + (id) => + !disabledEmbeds.includes(id) && !allFailedEmbedIds.includes(id), + ) : undefined; const providerApiUrl = getLoadbalancedProviderApiUrl(); diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index 76759197..9ceabcc9 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -182,9 +182,10 @@ export function RealPlayerView() { let startAt: number | undefined; if (startAtParam) startAt = parseTimestamp(startAtParam) ?? undefined; - // Clear failed sources when we successfully find a working source + // Clear failed sources and embeds when we successfully find a working source const playerStore = usePlayerStore.getState(); playerStore.clearFailedSources(); + playerStore.clearFailedEmbeds(); playMedia( convertRunoutputToSource(out), diff --git a/src/pages/parts/player/PlaybackErrorPart.tsx b/src/pages/parts/player/PlaybackErrorPart.tsx index 1026a237..dfc90c55 100644 --- a/src/pages/parts/player/PlaybackErrorPart.tsx +++ b/src/pages/parts/player/PlaybackErrorPart.tsx @@ -23,7 +23,10 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) { const { t } = useTranslation(); const playbackError = usePlayerStore((s) => s.interface.error); const currentSourceId = usePlayerStore((s) => s.sourceId); + const currentEmbedId = usePlayerStore((s) => s.embedId); const addFailedSource = usePlayerStore((s) => s.addFailedSource); + const addFailedEmbed = usePlayerStore((s) => s.addFailedEmbed); + const failedEmbeds = usePlayerStore((s) => s.failedEmbeds); const modal = useModal("error"); const settingsRouter = useOverlayRouter("settings"); const hasOpenedSettings = useRef(false); @@ -35,17 +38,32 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) { (s) => s.enableAutoResumeOnPlaybackError, ); - // Mark the failed source and handle UI when a playback error occurs + // Mark the failed source/embed and handle UI when a playback error occurs useEffect(() => { if (playbackError && currentSourceId) { - // Only mark source as failed for fatal errors + // Only mark source/embed as failed for fatal errors const isFatalError = playbackError.type === "hls" ? (playbackError.hls?.fatal ?? false) : playbackError.type === "htmlvideo"; if (isFatalError) { - addFailedSource(currentSourceId); + // If there's an active embed, disable that embed instead of the source + if (currentEmbedId) { + addFailedEmbed(currentSourceId, currentEmbedId); + + // Check if all embeds for this source have now failed + // If so, disable the entire source + const failedEmbedsForSource = failedEmbeds[currentSourceId] || []; + // For now, we'll assume if we have 2+ failed embeds for a source, disable it + // This is a simple heuristic - we could make it more sophisticated + if (failedEmbedsForSource.length >= 2) { + addFailedSource(currentSourceId); + } + } else { + // No embed active, disable the source + addFailedSource(currentSourceId); + } } if (!hasOpenedSettings.current && !enableAutoResumeOnPlaybackError) { @@ -59,7 +77,10 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) { }, [ playbackError, currentSourceId, + currentEmbedId, + failedEmbeds, addFailedSource, + addFailedEmbed, settingsRouter, setLastSuccessfulSource, enableAutoResumeOnPlaybackError, diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts index d58153c9..41de37e6 100644 --- a/src/stores/player/slices/source.ts +++ b/src/stores/player/slices/source.ts @@ -90,6 +90,7 @@ export interface SourceSlice { }; meta: PlayerMeta | null; failedSources: string[]; + failedEmbeds: Record; // sourceId -> array of failed embedIds setStatus(status: PlayerStatus): void; setSource( stream: SourceSliceSource, @@ -106,7 +107,9 @@ export interface SourceSlice { setCaptionAsTrack(asTrack: boolean): void; addExternalSubtitles(): Promise; addFailedSource(sourceId: string): void; + addFailedEmbed(sourceId: string, embedId: string): void; clearFailedSources(): void; + clearFailedEmbeds(): void; reset(): void; } @@ -146,6 +149,7 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ status: playerStatus.IDLE, meta: null, failedSources: [], + failedEmbeds: {}, caption: { selected: null, asTrack: false, @@ -269,11 +273,26 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ } }); }, + addFailedEmbed(sourceId: string, embedId: string) { + set((s) => { + if (!s.failedEmbeds[sourceId]) { + s.failedEmbeds[sourceId] = []; + } + if (!s.failedEmbeds[sourceId].includes(embedId)) { + s.failedEmbeds[sourceId] = [...s.failedEmbeds[sourceId], embedId]; + } + }); + }, clearFailedSources() { set((s) => { s.failedSources = []; }); }, + clearFailedEmbeds() { + set((s) => { + s.failedEmbeds = {}; + }); + }, reset() { set((s) => { s.source = null; @@ -288,6 +307,7 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ s.status = playerStatus.IDLE; s.meta = null; s.failedSources = []; + s.failedEmbeds = {}; s.caption = { selected: null, asTrack: false,