Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
playAudio?: boolean;
onVideoWaiting?: () => void;
onVideoPlaying?: () => void;
onAssetError?: () => void;
}

let {
Expand All @@ -53,7 +54,8 @@
showInfo = $bindable(false),
playAudio = false,
onVideoWaiting = () => {},
onVideoPlaying = () => {}
onVideoPlaying = () => {},
onAssetError = () => {}
}: Props = $props();
let instantTransition = slideshowStore.instantTransition;
let transitionDuration = $derived(
Expand Down Expand Up @@ -119,6 +121,7 @@
{playAudio}
{onVideoWaiting}
{onVideoPlaying}
{onAssetError}
bind:this={primaryAssetComponent}
bind:showInfo
/>
Expand All @@ -140,6 +143,7 @@
{playAudio}
{onVideoWaiting}
{onVideoPlaying}
{onAssetError}
bind:this={secondaryAssetComponent}
bind:showInfo
/>
Expand All @@ -163,6 +167,7 @@
{playAudio}
{onVideoWaiting}
{onVideoPlaying}
{onAssetError}
bind:this={primaryAssetComponent}
bind:showInfo
/>
Expand Down
22 changes: 18 additions & 4 deletions immichFrame.Web/src/lib/components/elements/asset.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { untrack } from 'svelte';
import {
type AlbumResponseDto,
type AssetResponseDto,
Expand Down Expand Up @@ -27,6 +28,7 @@
playAudio: boolean;
onVideoWaiting?: () => void;
onVideoPlaying?: () => void;
onAssetError?: () => void;
}

let {
Expand All @@ -45,16 +47,21 @@
showInfo = $bindable(false),
playAudio,
onVideoWaiting = () => {},
onVideoPlaying = () => {}
onVideoPlaying = () => {},
onAssetError = () => {}
}: Props = $props();

let debug = false;
const isVideo = $derived(isVideoAsset(asset[1]));

const animationDuration = $derived.by(() => {
asset[1].id;
return untrack(() => interval);
});

let videoElement = $state<HTMLVideoElement | null>(null);

$effect(() => {
// Track asset URL to cleanup when it changes
asset[0];
return () => {
if (videoElement) {
Expand Down Expand Up @@ -195,7 +202,7 @@
<div
class="relative w-full h-full {enableZoom ? 'zoom' : ''} {enablePan ? 'pan' : ''}"
style="
--interval: {interval + 2}s;
--interval: {animationDuration + 2}s;
--originX: {hasPerson && !isVideo ? getFaceMetric(0, 'centerX') + '%' : 'center'};
--originY: {hasPerson && !isVideo ? getFaceMetric(0, 'centerY') + '%' : 'center'};
--start-scale: {scaleValues.startScale};
Expand Down Expand Up @@ -246,7 +253,10 @@
}
}
}}
onerror={() => console.error('Video failed to load:', asset[0])}
onerror={() => {
console.error('Video failed to load:', asset[0]);
onAssetError();
}}
onwaiting={onVideoWaiting}
onplaying={onVideoPlaying}
></video>
Expand All @@ -257,6 +267,10 @@
: 'max-h-screen h-dvh-safe max-w-full object-contain'} w-full h-full"
src={asset[0]}
alt="data"
onerror={() => {
console.error('Image failed to load:', asset[0]);
onAssetError();
}}
/>
{/if}
</div>
Expand Down
33 changes: 13 additions & 20 deletions immichFrame.Web/src/lib/components/elements/progress-bar.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script lang="ts">
import { handlePromiseError } from '$lib/utils';
import { onMount, untrack } from 'svelte';
import { tweened } from 'svelte/motion';
import { Tween } from 'svelte/motion';
import { ProgressBarLocation, ProgressBarStatus } from './progress-bar.types';

interface Props {
Expand All @@ -26,20 +25,20 @@
onPaused = () => {}
}: Props = $props();

const onChange = async (progressDuration: number) => {
progress = setDuration(progressDuration);
await play();
};

let progress = setDuration(duration);

$effect(() => {
handlePromiseError(onChange(duration));
const progress = new Tween(0, {
duration: (from: number, to: number) => {
if (to === 0) return 0;
return duration * 1000 * (to - from);
}
});

let completed = false;
$effect(() => {
if ($progress === 1) {
if (progress.current >= 1 && !completed) {
completed = true;
untrack(() => onDone());
} else if (progress.current < 1) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
completed = false;
}
});

Expand All @@ -58,7 +57,7 @@
export const pause = async () => {
status = ProgressBarStatus.Paused;
onPaused();
await progress.set($progress);
await progress.set(progress.current);
};

export const restart = async (autoplay: boolean) => {
Expand All @@ -73,19 +72,13 @@
status = ProgressBarStatus.Paused;
await progress.set(0);
};

function setDuration(newDuration: number) {
return tweened<number>(0, {
duration: (from: number, to: number) => (to ? newDuration * 1000 * (to - from) : 0)
});
}
</script>

{#if !hidden}
<span
id="progressbar"
class="fixed left-0 h-[3px] bg-primary z-[1000]
{location == ProgressBarLocation.Top ? 'top-0' : 'bottom-0'}"
style:width={`${$progress * 100}%`}
style:width={`${progress.current * 100}%`}
></span>
{/if}
96 changes: 75 additions & 21 deletions immichFrame.Web/src/lib/components/home-page/home-page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@

api.init();

// TODO: make this configurable?
const PRELOAD_ASSETS = 5;
const TRANSITION_WATCHDOG_MS = 10000;
const VIDEO_STALL_MS = 15000;
const CURSOR_HIDE_MS = 2000;
const RELOAD_ON_ERROR_MS = 30000;

let assetHistory: api.AssetResponseDto[] = [];
let assetBacklog: api.AssetResponseDto[] = [];
let assetHistory: api.AssetResponseDto[] = $state([]);
let assetBacklog: api.AssetResponseDto[] = $state([]);

let displayingAssets: api.AssetResponseDto[] = $state([]);

Expand All @@ -40,6 +43,11 @@
let progressBar: ProgressBar = $state() as ProgressBar;
let assetComponent: AssetComponentInstance = $state() as AssetComponentInstance;
let currentDuration: number = $state($configStore.interval ?? 20);

let watchdogTimer: number | undefined;
let videoStallTimeout: number | undefined;
let timeoutId: number | undefined;

let userPaused: boolean = $state(false);

let error: boolean = $state(false);
Expand All @@ -63,7 +71,6 @@
let refreshInterval: number;

let cursorVisible = $state(true);
let timeoutId: number;

const clientIdentifier = page.url.searchParams.get('client');
const authsecret = page.url.searchParams.get('authsecret');
Expand Down Expand Up @@ -93,7 +100,7 @@
const showCursor = () => {
cursorVisible = true;
clearTimeout(timeoutId);
timeoutId = setTimeout(hideCursor, 2000);
timeoutId = window.setTimeout(hideCursor, CURSOR_HIDE_MS);
};

async function updateAssetPromises() {
Expand All @@ -116,16 +123,14 @@
!displayingAssets.find((item) => item.id === key) &&
!assetBacklog.find((item) => item.id === key)
);
for (const key of keysToRemove) {
try {
const [url] = await assetPromisesDict[key];
revokeObjectUrl(url);
} catch (err) {
console.warn('Failed to resolve asset during cleanup:', err);
} finally {
delete assetPromisesDict[key];
}
}

keysToRemove.forEach((key) => {
const promise = assetPromisesDict[key];
delete assetPromisesDict[key];
promise
.then(([url]) => revokeObjectUrl(url))
.catch((err) => console.warn('Failed to resolve asset during cleanup:', err));
});
}

async function loadAssets() {
Expand All @@ -149,23 +154,58 @@
}
}

let isHandlingAssetTransition = false;
let isHandlingAssetTransition = $state(false);
let transitionEpoch = 0;
let pendingTransition: { previous: boolean; instant: boolean } | null = $state(null);

const handleDone = async (previous: boolean = false, instant: boolean = false) => {
if (isHandlingAssetTransition) {
pendingTransition = { previous, instant };
return;
}

const currentEpoch = ++transitionEpoch;
isHandlingAssetTransition = true;

clearTimeout(watchdogTimer);
clearTimeout(videoStallTimeout);
// Watchdog: If the transition (fetching/loading assets) hangs, force-release the lock.
watchdogTimer = window.setTimeout(() => {
if (currentEpoch === transitionEpoch && isHandlingAssetTransition) {
console.error('Transition watchdog triggered: Force-resetting lock due to hang');
isHandlingAssetTransition = false;

if (pendingTransition) {
const next = pendingTransition;
pendingTransition = null;
handleDone(next.previous, next.instant);
}
}
}, TRANSITION_WATCHDOG_MS);
Comment thread
3rob3 marked this conversation as resolved.

try {
userPaused = false;
progressBar.restart(false);
$instantTransition = instant;
if (previous) await getPreviousAssets();
else await getNextAssets();
await tick();
await tick();

if (currentEpoch !== transitionEpoch) return;

await assetComponent?.play?.();
progressBar.play();
} finally {
isHandlingAssetTransition = false;
if (currentEpoch === transitionEpoch) {
isHandlingAssetTransition = false;
clearTimeout(watchdogTimer);

if (pendingTransition) {
const next = pendingTransition;
pendingTransition = null;
handleDone(next.previous, next.instant);
}
}
}
};

Expand All @@ -182,7 +222,6 @@

const useSplit = shouldUseSplitView(assetBacklog);
const next = assetBacklog.splice(0, useSplit ? 2 : 1);
assetBacklog = [...assetBacklog];

if (displayingAssets.length) {
assetHistory.push(...displayingAssets);
Expand All @@ -204,7 +243,6 @@

const useSplit = shouldUseSplitView(assetHistory.slice(-2));
const next = assetHistory.splice(useSplit ? -2 : -1);
assetHistory = [...assetHistory];

if (displayingAssets.length) {
assetBacklog.unshift(...displayingAssets);
Expand Down Expand Up @@ -392,7 +430,7 @@
// 30 second reload on error
refreshInterval = window.setInterval(() => {
if (error) window.location.reload();
}, 30000);
}, RELOAD_ON_ERROR_MS);

if ($configStore.primaryColor) {
document.documentElement.style.setProperty('--primary-color', $configStore.primaryColor);
Expand Down Expand Up @@ -426,6 +464,9 @@
window.removeEventListener('mousemove', showCursor);
window.removeEventListener('click', showCursor);
window.clearInterval(refreshInterval);
window.clearTimeout(timeoutId);
window.clearTimeout(videoStallTimeout);
window.clearTimeout(watchdogTimer);
};
});

Expand Down Expand Up @@ -473,12 +514,25 @@
playAudio={$configStore.playAudio}
onVideoWaiting={async () => {
await progressBar.pause();
clearTimeout(videoStallTimeout);
if (userPaused) return;

videoStallTimeout = window.setTimeout(() => {
if (!userPaused) {
console.warn('Video stalled, skipping...');
handleDone(false, true);
}
}, Math.max(5000, Math.min(VIDEO_STALL_MS, currentDuration * 1000)));
}}
onVideoPlaying={async () => {
clearTimeout(videoStallTimeout);
if (!userPaused) {
await progressBar.play();
}
}}
onAssetError={async () => {
await handleDone(false, true);
}}
Comment thread
3rob3 marked this conversation as resolved.
/>
</div>

Expand Down
Loading