feat(preview): play videos inline in the lightbox overlay
Switch the preview overlay from a still <img> to a <video> tag when the focused photo's Type is "video". Uses PhotoPrism's /api/v1/videos/:hash endpoint with the existing previewToken, falls back to a still thumb as the poster, and autoplays muted so the controls reveal without clobbering whatever else is on the page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,10 +8,10 @@
|
|||||||
previewNext,
|
previewNext,
|
||||||
previewPrev
|
previewPrev
|
||||||
} from '$lib/stores/preview.svelte';
|
} from '$lib/stores/preview.svelte';
|
||||||
import { thumbUrl } from '$lib/stores/session.svelte';
|
import { thumbUrl, videoUrl } from '$lib/stores/session.svelte';
|
||||||
import { setFocused } from '$lib/stores/selection.svelte';
|
import { setFocused } from '$lib/stores/selection.svelte';
|
||||||
import RightSidebar from '$lib/components/sidebar/RightSidebar.svelte';
|
import RightSidebar from '$lib/components/sidebar/RightSidebar.svelte';
|
||||||
import { primaryFile, type PpPhoto } from '$lib/types/photoprism';
|
import { isVideo, primaryFile, videoFile, type PpPhoto } from '$lib/types/photoprism';
|
||||||
|
|
||||||
const photoQuery = createQuery<PpPhoto>(() => ({
|
const photoQuery = createQuery<PpPhoto>(() => ({
|
||||||
queryKey: ['photo', preview.uid ?? ''],
|
queryKey: ['photo', preview.uid ?? ''],
|
||||||
@@ -141,12 +141,25 @@
|
|||||||
<p class="text-sm text-red-300">Failed to load photo.</p>
|
<p class="text-sm text-red-300">Failed to load photo.</p>
|
||||||
{:else if photoQuery.data}
|
{:else if photoQuery.data}
|
||||||
{@const pf = primaryFile(photoQuery.data)}
|
{@const pf = primaryFile(photoQuery.data)}
|
||||||
|
{#if isVideo(photoQuery.data)}
|
||||||
|
{@const vf = videoFile(photoQuery.data)}
|
||||||
|
<!-- svelte-ignore a11y_media_has_caption -->
|
||||||
|
<video
|
||||||
|
src={videoUrl(vf.Hash)}
|
||||||
|
poster={thumbUrl(pf.Hash, 'fit_1920')}
|
||||||
|
controls
|
||||||
|
autoplay
|
||||||
|
muted
|
||||||
|
class="max-h-full max-w-full rounded-md object-contain shadow-2xl"
|
||||||
|
></video>
|
||||||
|
{:else}
|
||||||
<img
|
<img
|
||||||
src={thumbUrl(pf.Hash, 'fit_1920')}
|
src={thumbUrl(pf.Hash, 'fit_1920')}
|
||||||
alt={photoQuery.data.OriginalName ?? pf.Name ?? 'Photo'}
|
alt={photoQuery.data.OriginalName ?? pf.Name ?? 'Photo'}
|
||||||
class="max-h-full max-w-full rounded-md object-contain shadow-2xl"
|
class="max-h-full max-w-full rounded-md object-contain shadow-2xl"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: metadata sidebar -->
|
<!-- Right: metadata sidebar -->
|
||||||
|
|||||||
@@ -87,3 +87,14 @@ export function thumbUrl(hash: string, size = 'tile_500'): string {
|
|||||||
if (!session.previewToken) return '';
|
if (!session.previewToken) return '';
|
||||||
return `/api/v1/t/${hash}/${session.previewToken}/${size}`;
|
return `/api/v1/t/${hash}/${session.previewToken}/${size}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a video stream URL. PhotoPrism's endpoint is
|
||||||
|
* /api/v1/videos/:hash/:token/:format — same previewToken as thumbnails.
|
||||||
|
* `format=avc` is the standard h264 transcode; HEVC sources are
|
||||||
|
* transcoded on first request and cached server-side.
|
||||||
|
*/
|
||||||
|
export function videoUrl(hash: string, format = 'avc'): string {
|
||||||
|
if (!session.previewToken) return '';
|
||||||
|
return `/api/v1/videos/${hash}/${session.previewToken}/${format}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -174,6 +174,19 @@ export function primaryFile(p: PpPhoto): PpFile {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isVideo(p: PpPhoto): boolean {
|
||||||
|
return p.Type === 'video';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return the Files[] entry that carries the actual video stream. Falls back
|
||||||
|
* to primaryFile() if no video MediaType is present (shouldn't happen for
|
||||||
|
* Type === 'video' but keeps the call site total). */
|
||||||
|
export function videoFile(p: PpPhoto): PpFile {
|
||||||
|
const files = p.Files ?? [];
|
||||||
|
const v = files.find((f) => f.MediaType?.startsWith('video/'));
|
||||||
|
return v ?? primaryFile(p);
|
||||||
|
}
|
||||||
|
|
||||||
export interface PpFile {
|
export interface PpFile {
|
||||||
UID: string;
|
UID: string;
|
||||||
Hash: string;
|
Hash: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user