fix(player): resume at saved position on the Android native path
The native (ExoPlayer) video path never applied the resume position, so "resume from where you left off" always played from the start on Android. Two layers each assumed the other did the seek: - The only code acting on `initialPosition` was handleCanPlay, an HTML5 <video> event handler. The native path has no <video> element, so `canplay` never fires and that seek never ran. - NativePlayerAdapter.load() had an initialPosition branch, but it only recorded the number, claiming "the native backend performs the actual seek internally". It does not: PlayItemRequest carries no start position, and loadWithMetadata -> prepare() always starts ExoPlayer at 0. - VideoPlayer never called adapter.load() at all, so even that branch was unreachable. The frontend therefore believed it had resumed (the seek bar showed the resume point) while ExoPlayer played from the beginning. NativePlayerAdapter.load() now issues the backend seek, excluding live streams (no resume point; seeking knocks the HLS window off its live edge). VideoPlayer calls it on the native branch and marks the initial seek as performed so the existing $effect does not fire a duplicate. The HTML5 path is untouched: seeking before metadata is clamped to 0, which is exactly what handleCanPlay waits for. Verified red->green: the new test failed with "Number of calls: 0" before the fix. Full frontend suite passes (933 tests); svelte-check and check:boundary are clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -721,6 +721,41 @@
|
||||
// No-op for the native adapter, which owns no DOM element.
|
||||
playerAdapter.attach(videoElement);
|
||||
playerController.setActiveAdapter(playerAdapter);
|
||||
|
||||
// The native (ExoPlayer) path has no <video> element, so `canplay`
|
||||
// never fires and the handleCanPlay initial-seek below never runs —
|
||||
// resume-at-position played from the beginning on Android. Hand the
|
||||
// resume point to the adapter, which issues the backend seek.
|
||||
//
|
||||
// HTML5 keeps its existing element-driven seek: seeking before the
|
||||
// element has metadata is clamped back to 0, which is precisely what
|
||||
// handleCanPlay waits for.
|
||||
// TRACES: UR-005 | DR-004, DR-028
|
||||
if (!useHtml5Element) {
|
||||
hasPerformedInitialSeek = true; // native path owns the initial seek
|
||||
lastAppliedInitialPosition = initialPosition;
|
||||
await playerAdapter.load(currentStreamUrl, {
|
||||
mediaId: media.id,
|
||||
mediaSourceId: mediaSourceId ?? null,
|
||||
needsTranscoding,
|
||||
initialPosition: initialPosition ?? 0,
|
||||
isLive,
|
||||
audioTrackIndex: null,
|
||||
knownDuration: media.durationMs ? media.durationMs / 1000 : 0,
|
||||
// ExoPlayer already received these as SubtitleConfigurations via
|
||||
// player_play_item; mapped to the adapter shape for the contract.
|
||||
subtitleTracks: sentSubtitleTracks.map((t) => ({
|
||||
index: t.streamIndex,
|
||||
url: t.url,
|
||||
language: t.srclang,
|
||||
label: t.label,
|
||||
mimeType: "text/vtt",
|
||||
})),
|
||||
});
|
||||
if (initialPosition && initialPosition > 0 && !isLive) {
|
||||
currentTime = initialPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!useHtml5Element) {
|
||||
|
||||
Reference in New Issue
Block a user