import { useEffect } from 'react' import { useQueryClient } from '@tanstack/react-query' import { jellyfinClient, getMediaInfoApi } from '../api/jellyfin' import { browserDeviceProfile } from '../lib/device-profile' import type { BaseItemDto } from '../api/types' /** * Pre-fire Jellyfin's PlaybackInfo handshake (and a tiny range request * against the resolved stream URL) when the user has been hovering a * card long enough that they're likely to play it. The handshake is * the longest sync part of opening playback, so cache-warming it makes * the click→playing transition feel near-instant. * * - Skips synthetic TMDB-only items (no real Jellyfin id). * - Idempotent per item: React Query dedups, so multiple armed cards * pointing at the same id share one request. * - Best-effort: any error is swallowed silently; the player will * re-issue PlaybackInfo as it normally does if the cache is missing. */ export function usePrebuffer(item: BaseItemDto | null | undefined, armed: boolean) { const qc = useQueryClient() useEffect(() => { if (!armed) return if (!item || !item.Id || String(item.Id).startsWith('tmdb-')) return // Episodes / movies are the only item kinds that go through PlaybackInfo // for streaming; series / seasons would need a child resolution first. if (item.Type !== 'Movie' && item.Type !== 'Episode') return const itemId = item.Id let cancelled = false const key = ['jellyfin', 'playback-info', itemId, undefined, undefined, undefined] qc.fetchQuery({ queryKey: key, queryFn: async () => { const api = jellyfinClient.getApi() if (!api) return null const res = await getMediaInfoApi(api).getPostedPlaybackInfo({ itemId, playbackInfoDto: { UserId: jellyfinClient.getAuthState()!.userId, MaxStreamingBitrate: 140_000_000, DeviceProfile: browserDeviceProfile() as any, AutoOpenLiveStream: true, EnableDirectPlay: true, EnableDirectStream: true, EnableTranscoding: true, AllowVideoStreamCopy: true, AllowAudioStreamCopy: true, }, } as any) return res.data }, staleTime: 60_000, }).then(playbackInfo => { if (cancelled || !playbackInfo) return const auth = jellyfinClient.getAuthState() if (!auth) return const source: any = (playbackInfo as any).MediaSources?.[0] if (!source) return // Build the stream URL the way PlayerPage does and request the // first kilobyte. For direct-play this primes the HTTP cache; for // HLS transcodes this triggers Jellyfin to start the transcode // segment 0 ahead of time. const url = source.SupportsDirectPlay ? `${auth.serverUrl}/Videos/${itemId}/stream?static=true&MediaSourceId=${source.Id}&api_key=${auth.token}` : source.TranscodingUrl ? `${auth.serverUrl}${source.TranscodingUrl}` : null if (!url) return fetch(url, { method: 'GET', headers: { Range: 'bytes=0-32767' }, }).catch(() => { /* warm-only; ignore */ }) }).catch(() => { /* warm-only; ignore */ }) return () => { cancelled = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, [armed, item?.Id]) }