From c9f6d92a7fcccbe99e37e378a842eb7d62d48c00 Mon Sep 17 00:00:00 2001 From: lashman Date: Sat, 6 Jun 2026 21:54:01 +0300 Subject: [PATCH] seek via underlying media element for resume --- src/pages/PlayerPage.tsx | 56 +++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/src/pages/PlayerPage.tsx b/src/pages/PlayerPage.tsx index 9b2946a..9da389c 100644 --- a/src/pages/PlayerPage.tsx +++ b/src/pages/PlayerPage.tsx @@ -348,6 +348,19 @@ export default function PlayerPage() { } return '' })() + if (typeof window !== 'undefined' && playbackInfo) { + const transcodingUrl = resolvedSource?.TranscodingUrl + const supportsDirectPlay = resolvedSource?.SupportsDirectPlay + const hasRuntimeTicks = transcodingUrl ? transcodingUrl.includes('runtimeTicks=') : null + console.log('[player] stream resolved', { + id, + startTimeTicks, + supportsDirectPlay, + transcodingUrlPresent: !!transcodingUrl, + transcodingUrlHasRuntimeTicks: hasRuntimeTicks, + streamUrl: streamUrl.replace(/api_key=[^&]+/, 'api_key=***'), + }) + } /* Reset transient flags on item change */ useEffect(() => { @@ -1164,8 +1177,13 @@ export default function PlayerPage() { 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 */ } + const mediaEl = (p as any).media as HTMLMediaElement | undefined + const before = mediaEl ? mediaEl.currentTime : p.currentTime + if (before < target - 0.5 || before > target + 0.5) { + try { + if (mediaEl) mediaEl.currentTime = target + else p.currentTime = target + } catch { /* ignore */ } } p.play().catch(() => {}) } @@ -1403,13 +1421,29 @@ export default function PlayerPage() { setResumePromptOpen(false) const p = playerRef.current const pos = Number(item?.UserData?.PlaybackPositionTicks ?? 0) + const target = pos > 0 ? pos / 10_000_000 : 0 + if (typeof window !== 'undefined') { + console.log('[player] resume prompt clicked', { + id, + pos, + target, + mediaElementCurrentTime: (p as any)?.media?.currentTime, + streamUrlStart: streamUrl.includes('StartTimeTicks=') || streamUrl.includes('runtimeTicks='), + }) + } 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 */ } + // Vidstack has a known issue (GH #941) where setting + // currentTime on the MediaPlayerInstance for a direct-play + // MP4 source restarts the video from the beginning. The + // workaround is to set currentTime on the underlying + // HTMLMediaElement. For HLS it works either way; we use + // the underlying element for consistency. + pendingSeekRef.current = target + const mediaEl = (p as any).media as HTMLMediaElement | undefined + try { + if (mediaEl) mediaEl.currentTime = target + else p.currentTime = target + } catch { /* ignore */ } p.play().catch(() => {}) } }, @@ -1418,7 +1452,11 @@ export default function PlayerPage() { const p = playerRef.current if (p) { pendingSeekRef.current = 0 - try { p.currentTime = 0 } catch { /* ignore */ } + const mediaEl = (p as any).media as HTMLMediaElement | undefined + try { + if (mediaEl) mediaEl.currentTime = 0 + else p.currentTime = 0 + } catch { /* ignore */ } p.play().catch(() => {}) } },