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,