feat(video): add video upload with remux/transcode playback

Add video as a media kind in photos: streaming upload with per-kind
limits, ffprobe metadata, poster frame reusing the image variant
pipeline, and a playback object (remux when web-compatible, else
h264/yuv420p/aac transcode). Video jobs run on a separate pool with a
long lease. Frontend gains a video viewer, grid badge/duration and
video upload.
This commit is contained in:
iu
2026-10-11 20:37:53 +03:00
parent d059d042f0
commit d38ede0201
52 changed files with 2717 additions and 82 deletions
+44
View File
@@ -60,6 +60,11 @@
const panoTag = $derived(isPanoTag(current) || isPanoTag(fresh));
const pano = $derived(panoTag && !panoFailed);
const panoSrc = $derived(fullSrc || previewSrc);
const isVideo = $derived((current?.kind ?? fresh?.kind) === 'video');
const playbackSrc = $derived(fresh?.playback_url ?? current?.playback_url ?? '');
const videoPoster = $derived(
fresh?.preview_url ?? fresh?.thumb_url ?? current?.preview_url ?? current?.thumb_url ?? ''
);
$effect(() => {
const id = current?.id;
@@ -268,6 +273,10 @@
function onKey(ev: KeyboardEvent) {
if (isTyping()) return;
// Let the native video player own its playback keys.
if (isVideo && (ev.code === 'Space' || ev.key === 'ArrowLeft' || ev.key === 'ArrowRight')) {
return;
}
if (ev.key === 'Escape') {
if (!document.fullscreenElement) onClose();
return;
@@ -396,6 +405,41 @@
onFallback={() => (panoFailed = true)}
/>
</div>
{:else if isVideo}
<div
class="bg-canvas flex min-h-0 flex-1 items-center justify-center overflow-hidden"
role="group"
aria-label="Просмотр видео"
oncontextmenu={onContextMenu}
>
{#if playbackSrc}
<!-- svelte-ignore a11y_media_has_caption -->
<video
class="m-auto block max-h-full max-w-full"
src={playbackSrc}
poster={videoPoster}
controls
playsinline
preload="metadata"
></video>
{:else}
<div class="relative flex items-center justify-center">
{#if videoPoster}
<img
src={videoPoster}
alt={current?.original_filename ?? ''}
class="max-h-full max-w-full object-contain"
/>
{/if}
<span
class="absolute rounded bg-black/60 px-2 py-1 text-xs text-white"
role="status"
>
Обработка видео…
</span>
</div>
{/if}
</div>
{:else}
<div
bind:this={scroller}