Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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);

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);
}
}
}
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/>
</div>

Expand Down
Loading