hooks for jellyfin data, playback, tmdb, player chrome
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user