Fix android playback issue
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
import SleepTimerIndicator from "./SleepTimerIndicator.svelte";
|
||||
import CachedImage from "../common/CachedImage.svelte";
|
||||
import { sleepTimerActive, sleepTimerExpiredSignal } from "$lib/stores/sleepTimer";
|
||||
import { playbackPosition } from "$lib/stores/player";
|
||||
|
||||
interface Props {
|
||||
media: MediaItem | null;
|
||||
@@ -88,6 +89,16 @@
|
||||
|
||||
// Backend info from Rust (Rust decides which backend to use based on platform)
|
||||
let useHtml5Element = $state(true); // Default to HTML5, Rust will override if using native backend
|
||||
let backendChosen = false; // playerPlayItem succeeded and told us which backend to use
|
||||
let nativeUnlisteners: Array<() => void> = []; // raw-channel listeners for native backend mode
|
||||
// Position updates captured before a native seek can land after it and snap
|
||||
// the bar back; suppress backend position feeds briefly after each seek
|
||||
// (same idea as the MPV backend's last_seek_time suppression).
|
||||
let lastNativeSeekAt = 0;
|
||||
const NATIVE_SEEK_SETTLE_MS = 1500;
|
||||
function nativeSeekSettling(): boolean {
|
||||
return Date.now() - lastNativeSeekAt < NATIVE_SEEK_SETTLE_MS;
|
||||
}
|
||||
let didStartNativePlayback = $state(false); // Track if we started playback (to know if we should stop on unmount)
|
||||
let didStopBackendEarly = $state(false); // Track if we stopped backend early for non-transcoded content
|
||||
let swipeType = $state<"brightness" | null>(null);
|
||||
@@ -224,6 +235,17 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Native backend (Android ExoPlayer): drive the seek bar from the player
|
||||
// store, which is fed by the backend's PositionUpdate events. The legacy
|
||||
// "player://position-update" raw channel was never emitted by the backend,
|
||||
// so without this the bar only moves when the user scrubs.
|
||||
$effect(() => {
|
||||
const position = $playbackPosition;
|
||||
if (!useHtml5Element && !isDraggingSeekBar && !isSeeking && !nativeSeekSettling()) {
|
||||
currentTime = position;
|
||||
}
|
||||
});
|
||||
|
||||
// Set up HLS.js for HLS streams
|
||||
$effect(() => {
|
||||
if (!useHtml5Element || !videoElement || !currentStreamUrl) {
|
||||
@@ -465,6 +487,7 @@
|
||||
|
||||
// Rust tells us which backend it's using
|
||||
useHtml5Element = response.useHtml5Element;
|
||||
backendChosen = true;
|
||||
console.log(`[VideoPlayer] Backend: ${response.backend}, useHtml5Element: ${useHtml5Element}`);
|
||||
|
||||
// If using HTML5 element for non-transcoded content, stop the backend player
|
||||
@@ -486,38 +509,48 @@
|
||||
if (!useHtml5Element) {
|
||||
// Using native backend, subscribe to player events
|
||||
didStartNativePlayback = true; // Track that we started native playback
|
||||
const unlisten1 = await listen("player://position-update", (event: any) => {
|
||||
if (!isDraggingSeekBar) {
|
||||
currentTime = event.payload.position;
|
||||
}
|
||||
});
|
||||
|
||||
const unlisten2 = await listen("player://state-changed", (event: any) => {
|
||||
isPlaying = event.payload.state === "playing";
|
||||
});
|
||||
|
||||
// Clean up listeners on destroy
|
||||
onDestroy(() => {
|
||||
unlisten1();
|
||||
unlisten2();
|
||||
});
|
||||
isPlaying = (response.state?.kind ?? response.state) === "playing";
|
||||
// Cleanup happens in the component's top-level onDestroy. Calling
|
||||
// onDestroy() here — after an await — throws lifecycle_outside_component,
|
||||
// which the catch below used to misread as an init failure: it flipped
|
||||
// useHtml5Element to true, so every seek went down the HTML5 path and
|
||||
// never reached ExoPlayer (the video "seeked" then snapped back).
|
||||
nativeUnlisteners.push(
|
||||
await listen("player://position-update", (event: any) => {
|
||||
if (!isDraggingSeekBar && !isSeeking && !nativeSeekSettling()) {
|
||||
currentTime = event.payload.position;
|
||||
}
|
||||
})
|
||||
);
|
||||
nativeUnlisteners.push(
|
||||
await listen("player://state-changed", (event: any) => {
|
||||
isPlaying = event.payload.state === "playing";
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[VideoPlayer] Failed to initialize player:", err);
|
||||
// Fallback to HTML5 on error
|
||||
useHtml5Element = true;
|
||||
|
||||
// For non-transcoded content, try to stop any backend player that might have started
|
||||
if (!needsTranscoding) {
|
||||
try {
|
||||
await commands.playerStop();
|
||||
didStopBackendEarly = true;
|
||||
} catch (stopErr) {
|
||||
// Ignore errors when stopping
|
||||
}
|
||||
if (backendChosen) {
|
||||
// The backend already accepted the item; a later error (e.g. event
|
||||
// subscription) must not silently switch the seek/controls path to
|
||||
// HTML5 while the native backend keeps playing.
|
||||
console.warn("[VideoPlayer] Backend already initialized - keeping native mode despite error");
|
||||
} else {
|
||||
// For transcoded content, keep backend for seeking
|
||||
didStartNativePlayback = true;
|
||||
// Fallback to HTML5 on error
|
||||
useHtml5Element = true;
|
||||
|
||||
// For non-transcoded content, try to stop any backend player that might have started
|
||||
if (!needsTranscoding) {
|
||||
try {
|
||||
await commands.playerStop();
|
||||
didStopBackendEarly = true;
|
||||
} catch (stopErr) {
|
||||
// Ignore errors when stopping
|
||||
}
|
||||
} else {
|
||||
// For transcoded content, keep backend for seeking
|
||||
didStartNativePlayback = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -568,6 +601,12 @@
|
||||
clearInterval(debugLogInterval);
|
||||
}
|
||||
|
||||
// Remove native backend event listeners
|
||||
for (const unlisten of nativeUnlisteners) {
|
||||
unlisten();
|
||||
}
|
||||
nativeUnlisteners = [];
|
||||
|
||||
// Clean up HLS.js instance - prevent dual audio on unmount
|
||||
if (hls) {
|
||||
console.log("[VideoPlayer] Destroying HLS.js instance on unmount");
|
||||
@@ -924,10 +963,12 @@
|
||||
// For native backend, the backend handles everything internally
|
||||
if (!useHtml5Element) {
|
||||
// Backend already stopped, reloaded, and seeked if needed
|
||||
lastNativeSeekAt = Date.now();
|
||||
currentTime = response.position ?? targetTime;
|
||||
if (response.strategy === "reloadStream") {
|
||||
seekOffset = response.seekOffset ?? targetTime;
|
||||
currentStreamUrl = response.newUrl ?? currentStreamUrl;
|
||||
// Serde keeps these fields snake_case (only the "strategy" tag is camelCase)
|
||||
seekOffset = response.seek_offset ?? targetTime;
|
||||
currentStreamUrl = response.new_url ?? currentStreamUrl;
|
||||
} else {
|
||||
seekOffset = 0;
|
||||
}
|
||||
@@ -969,8 +1010,9 @@
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Update stream URL (this will trigger $effect to create new HLS instance)
|
||||
seekOffset = response.seekOffset ?? targetTime;
|
||||
currentStreamUrl = response.newUrl ?? currentStreamUrl;
|
||||
// Serde keeps these fields snake_case (only the "strategy" tag is camelCase)
|
||||
seekOffset = response.seek_offset ?? targetTime;
|
||||
currentStreamUrl = response.new_url ?? currentStreamUrl;
|
||||
currentTime = targetTime;
|
||||
|
||||
// Wait for video to be ready
|
||||
@@ -1238,7 +1280,8 @@
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Update stream URL (this will trigger $effect to create new HLS instance)
|
||||
currentStreamUrl = response.newUrl!;
|
||||
// Serde keeps new_url snake_case (only the "strategy" tag is camelCase)
|
||||
currentStreamUrl = response.new_url!;
|
||||
seekOffset = response.position!;
|
||||
|
||||
// Wait for video to be ready
|
||||
|
||||
Reference in New Issue
Block a user