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.
This commit is contained in:
Pas 2025-12-01 12:47:15 -07:00
parent 33b08b86cd
commit ad592edc65
4 changed files with 57 additions and 7 deletions

View file

@ -171,7 +171,9 @@ export function useScrape() {
async (media: ScrapeMedia, startFromSourceId?: string) => { async (media: ScrapeMedia, startFromSourceId?: string) => {
const providerInstance = getProviders(); const providerInstance = getProviders();
const allSources = providerInstance.listSources(); 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) // Start with all available sources (filtered by disabled and failed ones)
let baseSourceOrder = allSources 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 const filteredEmbedOrder = enableEmbedOrder
? preferredEmbedOrder.filter((id) => !disabledEmbeds.includes(id)) ? preferredEmbedOrder.filter(
(id) =>
!disabledEmbeds.includes(id) && !allFailedEmbedIds.includes(id),
)
: undefined; : undefined;
const providerApiUrl = getLoadbalancedProviderApiUrl(); const providerApiUrl = getLoadbalancedProviderApiUrl();

View file

@ -182,9 +182,10 @@ export function RealPlayerView() {
let startAt: number | undefined; let startAt: number | undefined;
if (startAtParam) startAt = parseTimestamp(startAtParam) ?? 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(); const playerStore = usePlayerStore.getState();
playerStore.clearFailedSources(); playerStore.clearFailedSources();
playerStore.clearFailedEmbeds();
playMedia( playMedia(
convertRunoutputToSource(out), convertRunoutputToSource(out),

View file

@ -23,7 +23,10 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const playbackError = usePlayerStore((s) => s.interface.error); const playbackError = usePlayerStore((s) => s.interface.error);
const currentSourceId = usePlayerStore((s) => s.sourceId); const currentSourceId = usePlayerStore((s) => s.sourceId);
const currentEmbedId = usePlayerStore((s) => s.embedId);
const addFailedSource = usePlayerStore((s) => s.addFailedSource); const addFailedSource = usePlayerStore((s) => s.addFailedSource);
const addFailedEmbed = usePlayerStore((s) => s.addFailedEmbed);
const failedEmbeds = usePlayerStore((s) => s.failedEmbeds);
const modal = useModal("error"); const modal = useModal("error");
const settingsRouter = useOverlayRouter("settings"); const settingsRouter = useOverlayRouter("settings");
const hasOpenedSettings = useRef(false); const hasOpenedSettings = useRef(false);
@ -35,17 +38,32 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) {
(s) => s.enableAutoResumeOnPlaybackError, (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(() => { useEffect(() => {
if (playbackError && currentSourceId) { if (playbackError && currentSourceId) {
// Only mark source as failed for fatal errors // Only mark source/embed as failed for fatal errors
const isFatalError = const isFatalError =
playbackError.type === "hls" playbackError.type === "hls"
? (playbackError.hls?.fatal ?? false) ? (playbackError.hls?.fatal ?? false)
: playbackError.type === "htmlvideo"; : playbackError.type === "htmlvideo";
if (isFatalError) { 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) { if (!hasOpenedSettings.current && !enableAutoResumeOnPlaybackError) {
@ -59,7 +77,10 @@ export function PlaybackErrorPart(props: PlaybackErrorPartProps) {
}, [ }, [
playbackError, playbackError,
currentSourceId, currentSourceId,
currentEmbedId,
failedEmbeds,
addFailedSource, addFailedSource,
addFailedEmbed,
settingsRouter, settingsRouter,
setLastSuccessfulSource, setLastSuccessfulSource,
enableAutoResumeOnPlaybackError, enableAutoResumeOnPlaybackError,

View file

@ -90,6 +90,7 @@ export interface SourceSlice {
}; };
meta: PlayerMeta | null; meta: PlayerMeta | null;
failedSources: string[]; failedSources: string[];
failedEmbeds: Record<string, string[]>; // sourceId -> array of failed embedIds
setStatus(status: PlayerStatus): void; setStatus(status: PlayerStatus): void;
setSource( setSource(
stream: SourceSliceSource, stream: SourceSliceSource,
@ -106,7 +107,9 @@ export interface SourceSlice {
setCaptionAsTrack(asTrack: boolean): void; setCaptionAsTrack(asTrack: boolean): void;
addExternalSubtitles(): Promise<void>; addExternalSubtitles(): Promise<void>;
addFailedSource(sourceId: string): void; addFailedSource(sourceId: string): void;
addFailedEmbed(sourceId: string, embedId: string): void;
clearFailedSources(): void; clearFailedSources(): void;
clearFailedEmbeds(): void;
reset(): void; reset(): void;
} }
@ -146,6 +149,7 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
status: playerStatus.IDLE, status: playerStatus.IDLE,
meta: null, meta: null,
failedSources: [], failedSources: [],
failedEmbeds: {},
caption: { caption: {
selected: null, selected: null,
asTrack: false, asTrack: false,
@ -269,11 +273,26 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (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() { clearFailedSources() {
set((s) => { set((s) => {
s.failedSources = []; s.failedSources = [];
}); });
}, },
clearFailedEmbeds() {
set((s) => {
s.failedEmbeds = {};
});
},
reset() { reset() {
set((s) => { set((s) => {
s.source = null; s.source = null;
@ -288,6 +307,7 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
s.status = playerStatus.IDLE; s.status = playerStatus.IDLE;
s.meta = null; s.meta = null;
s.failedSources = []; s.failedSources = [];
s.failedEmbeds = {};
s.caption = { s.caption = {
selected: null, selected: null,
asTrack: false, asTrack: false,