From 1a8fcf1b9f97c0a8a2134557ded10df618de32fc Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Fri, 28 Aug 2026 11:20:54 +0200 Subject: [PATCH] fix(web): face editor coordinates on a not-yet-loaded video (#31083) --- .../asset-viewer/VideoNativeViewer.svelte | 6 +++++- web/src/lib/utils/container-utils.ts | 8 ++++++++ web/src/lib/utils/layout-utils.spec.ts | 15 ++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte b/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte index 2663be3f98..74b0ea5861 100644 --- a/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte +++ b/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte @@ -76,6 +76,7 @@ let videoPlayer: HTMLVideoElement | undefined = $state(); let isLoading = $state(true); + let hasLoadedMetadata = $state(false); let assetFileUrl = $derived.by(() => { if (featureFlagsManager.value.realtimeTranscoding) { return getAssetHlsUrl(assetId); @@ -241,6 +242,7 @@ $effect(() => { // reactive on `assetFileUrl` changes + hasLoadedMetadata = false; if (videoPlayer && assetFileUrl) { hasFocused = false; rebuildCount = 0; @@ -385,6 +387,7 @@ {...useSwipe(onSwipe)} class="h-full object-contain" oncanplay={(e: Event) => handleCanPlay(e.currentTarget as HTMLVideoElement)} + onloadedmetadata={() => (hasLoadedMetadata = true)} onended={onVideoEnded} onseeking={onSeeking} onplaying={(e: Event) => { @@ -410,6 +413,7 @@ {...useSwipe(onSwipe)} class="h-full object-contain" oncanplay={(e) => handleCanPlay(e.currentTarget)} + onloadedmetadata={() => (hasLoadedMetadata = true)} onended={onVideoEnded} onseeking={onSeeking} onplaying={(e) => { @@ -489,7 +493,7 @@ {/if} - {#if assetViewerManager.isFaceEditMode && videoPlayer} + {#if assetViewerManager.isFaceEditMode && videoPlayer && hasLoadedMetadata} {/if} {/if} diff --git a/web/src/lib/utils/container-utils.ts b/web/src/lib/utils/container-utils.ts index 36e260fcc7..20f199167c 100644 --- a/web/src/lib/utils/container-utils.ts +++ b/web/src/lib/utils/container-utils.ts @@ -29,7 +29,12 @@ export type ContentMetrics = { offsetY: number; }; +const isMeasurable = (dimensions: Size) => dimensions.width > 0 && dimensions.height > 0; + export const scaleToCover = (dimensions: Size, container: Size): Size => { + if (!isMeasurable(dimensions)) { + return { width: 0, height: 0 }; + } const scaleX = container.width / dimensions.width; const scaleY = container.height / dimensions.height; const scale = Math.max(scaleX, scaleY); @@ -40,6 +45,9 @@ export const scaleToCover = (dimensions: Size, container: Size): Size => { }; export const scaleToFit = (dimensions: Size, container: Size): Size => { + if (!isMeasurable(dimensions)) { + return { width: 0, height: 0 }; + } const scaleX = container.width / dimensions.width; const scaleY = container.height / dimensions.height; const scale = Math.min(scaleX, scaleY); diff --git a/web/src/lib/utils/layout-utils.spec.ts b/web/src/lib/utils/layout-utils.spec.ts index 94f1ffb335..87455b2011 100644 --- a/web/src/lib/utils/layout-utils.spec.ts +++ b/web/src/lib/utils/layout-utils.spec.ts @@ -1,4 +1,4 @@ -import { scaleToFit } from '$lib/utils/container-utils'; +import { scaleToCover, scaleToFit } from '$lib/utils/container-utils'; describe('scaleToFit', () => { const tests = [ @@ -51,4 +51,17 @@ describe('scaleToFit', () => { expect(scaleToFit(dimensions, container)).toEqual(expected); }); } + + const unmeasurable = [ + { name: 'zero width and height', dimensions: { width: 0, height: 0 } }, + { name: 'zero width', dimensions: { width: 0, height: 1000 } }, + { name: 'zero height', dimensions: { width: 1000, height: 0 } }, + ]; + + for (const { name, dimensions } of unmeasurable) { + it(`should return an empty size for ${name}`, () => { + expect(scaleToFit(dimensions, { width: 500, height: 500 })).toEqual({ width: 0, height: 0 }); + expect(scaleToCover(dimensions, { width: 500, height: 500 })).toEqual({ width: 0, height: 0 }); + }); + } });