fix resume starting from beginning
This commit is contained in:
@@ -616,12 +616,19 @@ export function useSimilarItems(itemId?: string, limit = 12) {
|
||||
* server uses our DeviceProfile to decide direct-play vs HLS transcoding.
|
||||
* Without this call, the bare /main.m3u8 endpoint generates a playlist with
|
||||
* runtimeTicks=0, which makes segment fetches fail with 400.
|
||||
*
|
||||
* `enabled` lets callers gate the call on the saved resume position
|
||||
* being loaded: if a user lands on /play/:id?resume=true, the item
|
||||
* has to load first so the first PlaybackInfo request includes the
|
||||
* saved StartTimeTicks - otherwise the video streams from 0 and
|
||||
* reloads mid-playback when the second query returns.
|
||||
*/
|
||||
export function usePlaybackInfo(
|
||||
itemId?: string,
|
||||
startTimeTicks?: number,
|
||||
audioStreamIndex?: number,
|
||||
maxStreamingBitrate?: number,
|
||||
enabled: boolean = true,
|
||||
) {
|
||||
const api = useApi()
|
||||
const audioPassthrough = usePreferencesStore(s => s.audioPassthrough)
|
||||
@@ -659,7 +666,7 @@ export function usePlaybackInfo(
|
||||
} as any)
|
||||
return res.data
|
||||
},
|
||||
enabled: !!api && !!itemId,
|
||||
enabled: !!api && !!itemId && enabled,
|
||||
staleTime: 0,
|
||||
// Override the global 30-minute gcTime: each PlaybackInfoResponse
|
||||
// includes full MediaSources with encoding params, audio/video
|
||||
|
||||
@@ -63,6 +63,9 @@ export default function PlayerPage() {
|
||||
const playerRef = useRef<MediaPlayerInstance>(null)
|
||||
const qc = useQueryClient()
|
||||
const progressRef = useRef(0)
|
||||
// Seek the in-player resume prompt asks for, but only applied on the
|
||||
// next canPlay so the seek survives when the source isn't ready yet.
|
||||
const pendingSeekRef = useRef<number | null>(null)
|
||||
|
||||
// Start paused so the play icon shows until the player actually begins
|
||||
// playback - if autoplay succeeds, the onPlay handler flips this to false.
|
||||
@@ -291,6 +294,21 @@ export default function PlayerPage() {
|
||||
const resume = searchParams.get('resume') === 'true'
|
||||
const positionTicks = item?.UserData?.PlaybackPositionTicks
|
||||
const startTimeTicks = resume && positionTicks ? Number(positionTicks) : undefined
|
||||
// If the user clicked Resume from a deep link, the item hasn't
|
||||
// returned yet on the first render and startTimeTicks is undefined.
|
||||
// Block the first PlaybackInfo call until the saved position is
|
||||
// known - otherwise the server returns a stream that starts at 0
|
||||
// and the video buffers before the second query refires.
|
||||
const playbackInfoReady = !resume || startTimeTicks !== undefined
|
||||
if (typeof window !== 'undefined') {
|
||||
console.log('[player] resume state', {
|
||||
id,
|
||||
resume,
|
||||
positionTicks,
|
||||
startTimeTicks,
|
||||
playbackInfoReady,
|
||||
})
|
||||
}
|
||||
|
||||
/* Resolve the proper stream URL from Jellyfin's PlaybackInfo endpoint.
|
||||
* The server picks direct-play / direct-stream / transcoded HLS based on
|
||||
@@ -312,6 +330,7 @@ export default function PlayerPage() {
|
||||
startTimeTicks,
|
||||
streamAudioIndex ?? undefined,
|
||||
maxBitrate,
|
||||
playbackInfoReady,
|
||||
)
|
||||
const resolvedSource = playbackInfo?.MediaSources?.[0]
|
||||
const streamUrl = (() => {
|
||||
@@ -337,6 +356,7 @@ export default function PlayerPage() {
|
||||
setAudioIndex(null)
|
||||
setStreamAudioIndex(null)
|
||||
setEndCardOpen(false)
|
||||
pendingSeekRef.current = null
|
||||
usePlayerRuntimeStore.getState().resetForNewItem()
|
||||
}, [id, setPanel])
|
||||
|
||||
@@ -1140,6 +1160,15 @@ export default function PlayerPage() {
|
||||
// Apply the saved playback rate (default 1) so users who like
|
||||
// 1.5x get it on every episode without re-setting.
|
||||
if (p) applyPlaybackRate(playbackRate)
|
||||
// Apply any deferred seek queued by the resume prompt.
|
||||
if (p && pendingSeekRef.current != null) {
|
||||
const target = pendingSeekRef.current
|
||||
pendingSeekRef.current = null
|
||||
if (p.currentTime < target - 0.5 || p.currentTime > target + 0.5) {
|
||||
try { p.currentTime = target } catch { /* ignore */ }
|
||||
}
|
||||
p.play().catch(() => {})
|
||||
}
|
||||
}}
|
||||
onEnded={() => {
|
||||
// Queue takes priority - playlist play/shuffle should always
|
||||
@@ -1374,14 +1403,24 @@ export default function PlayerPage() {
|
||||
setResumePromptOpen(false)
|
||||
const p = playerRef.current
|
||||
const pos = Number(item?.UserData?.PlaybackPositionTicks ?? 0)
|
||||
if (p && pos > 0) p.currentTime = pos / 10_000_000
|
||||
p?.play().catch(() => {})
|
||||
if (p) {
|
||||
// Queue the seek: if the video isn't ready yet, onCanPlay
|
||||
// picks it up. The setter works on a paused player in
|
||||
// vidstack, but buffering the seek target can race the
|
||||
// canPlay event for the initial source.
|
||||
pendingSeekRef.current = pos > 0 ? pos / 10_000_000 : 0
|
||||
try { p.currentTime = pendingSeekRef.current } catch { /* ignore */ }
|
||||
p.play().catch(() => {})
|
||||
}
|
||||
},
|
||||
onRestart: () => {
|
||||
setResumePromptOpen(false)
|
||||
const p = playerRef.current
|
||||
if (p) p.currentTime = 0
|
||||
p?.play().catch(() => {})
|
||||
if (p) {
|
||||
pendingSeekRef.current = 0
|
||||
try { p.currentTime = 0 } catch { /* ignore */ }
|
||||
p.play().catch(() => {})
|
||||
}
|
||||
},
|
||||
}}
|
||||
recap={{
|
||||
|
||||
Reference in New Issue
Block a user