pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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.

Pas ad592edc 33b08b86

+57 -7
+11 -3
src/hooks/useProviderScrape.tsx
··· 171 171 async (media: ScrapeMedia, startFromSourceId?: string) => { 172 172 const providerInstance = getProviders(); 173 173 const allSources = providerInstance.listSources(); 174 - const failedSources = usePlayerStore.getState().failedSources; 174 + const playerState = usePlayerStore.getState(); 175 + const failedSources = playerState.failedSources; 176 + const failedEmbeds = playerState.failedEmbeds; 175 177 176 178 // Start with all available sources (filtered by disabled and failed ones) 177 179 let baseSourceOrder = allSources ··· 220 222 } 221 223 } 222 224 223 - // Filter out disabled embeds from the embed order 225 + // Collect all failed embed IDs across all sources 226 + const allFailedEmbedIds = Object.values(failedEmbeds).flat(); 227 + 228 + // Filter out disabled and failed embeds from the embed order 224 229 const filteredEmbedOrder = enableEmbedOrder 225 - ? preferredEmbedOrder.filter((id) => !disabledEmbeds.includes(id)) 230 + ? preferredEmbedOrder.filter( 231 + (id) => 232 + !disabledEmbeds.includes(id) && !allFailedEmbedIds.includes(id), 233 + ) 226 234 : undefined; 227 235 228 236 const providerApiUrl = getLoadbalancedProviderApiUrl();
+2 -1
src/pages/PlayerView.tsx
··· 182 182 let startAt: number | undefined; 183 183 if (startAtParam) startAt = parseTimestamp(startAtParam) ?? undefined; 184 184 185 - // Clear failed sources when we successfully find a working source 185 + // Clear failed sources and embeds when we successfully find a working source 186 186 const playerStore = usePlayerStore.getState(); 187 187 playerStore.clearFailedSources(); 188 + playerStore.clearFailedEmbeds(); 188 189 189 190 playMedia( 190 191 convertRunoutputToSource(out),
+24 -3
src/pages/parts/player/PlaybackErrorPart.tsx
··· 23 23 const { t } = useTranslation(); 24 24 const playbackError = usePlayerStore((s) => s.interface.error); 25 25 const currentSourceId = usePlayerStore((s) => s.sourceId); 26 + const currentEmbedId = usePlayerStore((s) => s.embedId); 26 27 const addFailedSource = usePlayerStore((s) => s.addFailedSource); 28 + const addFailedEmbed = usePlayerStore((s) => s.addFailedEmbed); 29 + const failedEmbeds = usePlayerStore((s) => s.failedEmbeds); 27 30 const modal = useModal("error"); 28 31 const settingsRouter = useOverlayRouter("settings"); 29 32 const hasOpenedSettings = useRef(false); ··· 35 38 (s) => s.enableAutoResumeOnPlaybackError, 36 39 ); 37 40 38 - // Mark the failed source and handle UI when a playback error occurs 41 + // Mark the failed source/embed and handle UI when a playback error occurs 39 42 useEffect(() => { 40 43 if (playbackError && currentSourceId) { 41 - // Only mark source as failed for fatal errors 44 + // Only mark source/embed as failed for fatal errors 42 45 const isFatalError = 43 46 playbackError.type === "hls" 44 47 ? (playbackError.hls?.fatal ?? false) 45 48 : playbackError.type === "htmlvideo"; 46 49 47 50 if (isFatalError) { 48 - addFailedSource(currentSourceId); 51 + // If there's an active embed, disable that embed instead of the source 52 + if (currentEmbedId) { 53 + addFailedEmbed(currentSourceId, currentEmbedId); 54 + 55 + // Check if all embeds for this source have now failed 56 + // If so, disable the entire source 57 + const failedEmbedsForSource = failedEmbeds[currentSourceId] || []; 58 + // For now, we'll assume if we have 2+ failed embeds for a source, disable it 59 + // This is a simple heuristic - we could make it more sophisticated 60 + if (failedEmbedsForSource.length >= 2) { 61 + addFailedSource(currentSourceId); 62 + } 63 + } else { 64 + // No embed active, disable the source 65 + addFailedSource(currentSourceId); 66 + } 49 67 } 50 68 51 69 if (!hasOpenedSettings.current && !enableAutoResumeOnPlaybackError) { ··· 59 77 }, [ 60 78 playbackError, 61 79 currentSourceId, 80 + currentEmbedId, 81 + failedEmbeds, 62 82 addFailedSource, 83 + addFailedEmbed, 63 84 settingsRouter, 64 85 setLastSuccessfulSource, 65 86 enableAutoResumeOnPlaybackError,
+20
src/stores/player/slices/source.ts
··· 90 90 }; 91 91 meta: PlayerMeta | null; 92 92 failedSources: string[]; 93 + failedEmbeds: Record<string, string[]>; // sourceId -> array of failed embedIds 93 94 setStatus(status: PlayerStatus): void; 94 95 setSource( 95 96 stream: SourceSliceSource, ··· 106 107 setCaptionAsTrack(asTrack: boolean): void; 107 108 addExternalSubtitles(): Promise<void>; 108 109 addFailedSource(sourceId: string): void; 110 + addFailedEmbed(sourceId: string, embedId: string): void; 109 111 clearFailedSources(): void; 112 + clearFailedEmbeds(): void; 110 113 reset(): void; 111 114 } 112 115 ··· 146 149 status: playerStatus.IDLE, 147 150 meta: null, 148 151 failedSources: [], 152 + failedEmbeds: {}, 149 153 caption: { 150 154 selected: null, 151 155 asTrack: false, ··· 269 273 } 270 274 }); 271 275 }, 276 + addFailedEmbed(sourceId: string, embedId: string) { 277 + set((s) => { 278 + if (!s.failedEmbeds[sourceId]) { 279 + s.failedEmbeds[sourceId] = []; 280 + } 281 + if (!s.failedEmbeds[sourceId].includes(embedId)) { 282 + s.failedEmbeds[sourceId] = [...s.failedEmbeds[sourceId], embedId]; 283 + } 284 + }); 285 + }, 272 286 clearFailedSources() { 273 287 set((s) => { 274 288 s.failedSources = []; 289 + }); 290 + }, 291 + clearFailedEmbeds() { 292 + set((s) => { 293 + s.failedEmbeds = {}; 275 294 }); 276 295 }, 277 296 reset() { ··· 288 307 s.status = playerStatus.IDLE; 289 308 s.meta = null; 290 309 s.failedSources = []; 310 + s.failedEmbeds = {}; 291 311 s.caption = { 292 312 selected: null, 293 313 asTrack: false,