From c778516ce2064e41160a5d5b92a3e0200dc651c8 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 26 Feb 2025 12:55:32 -0600 Subject: [PATCH] fix(web): tag people in video (#16351) --- .../face-editor/face-editor.svelte | 83 +++++++++++++------ .../asset-viewer/photo-viewer.svelte | 2 +- .../asset-viewer/video-native-viewer.svelte | 22 ++++- 3 files changed, 79 insertions(+), 28 deletions(-) diff --git a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte index fdf42000f06..afe45331e43 100644 --- a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte +++ b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte @@ -12,13 +12,13 @@ import { handleError } from '$lib/utils/handle-error'; interface Props { - imgElement: HTMLImageElement; + htmlElement: HTMLImageElement | HTMLVideoElement; containerWidth: number; containerHeight: number; assetId: string; } - let { imgElement, containerWidth, containerHeight, assetId }: Props = $props(); + let { htmlElement, containerWidth, containerHeight, assetId }: Props = $props(); let canvasEl: HTMLCanvasElement | undefined = $state(); let canvas: Canvas | undefined = $state(); @@ -39,7 +39,7 @@ }; const setupCanvas = () => { - if (!canvasEl || !imgElement) { + if (!canvasEl || !htmlElement) { return; } @@ -68,7 +68,7 @@ }); $effect(() => { - const { actualWidth, actualHeight } = getContainedSize(imgElement); + const { actualWidth, actualHeight } = getContainedSize(htmlElement); const offsetArea = { width: (containerWidth - actualWidth) / 2, height: (containerHeight - actualHeight) / 2, @@ -103,15 +103,30 @@ positionFaceSelector(); }); - const getContainedSize = (img: HTMLImageElement): { actualWidth: number; actualHeight: number } => { - const ratio = img.naturalWidth / img.naturalHeight; - let actualWidth = img.height * ratio; - let actualHeight = img.height; - if (actualWidth > img.width) { - actualWidth = img.width; - actualHeight = img.width / ratio; + const getContainedSize = ( + img: HTMLImageElement | HTMLVideoElement, + ): { actualWidth: number; actualHeight: number } => { + if (img instanceof HTMLImageElement) { + const ratio = img.naturalWidth / img.naturalHeight; + let actualWidth = img.height * ratio; + let actualHeight = img.height; + if (actualWidth > img.width) { + actualWidth = img.width; + actualHeight = img.width / ratio; + } + return { actualWidth, actualHeight }; + } else if (img instanceof HTMLVideoElement) { + const ratio = img.videoWidth / img.videoHeight; + let actualWidth = img.clientHeight * ratio; + let actualHeight = img.clientHeight; + if (actualWidth > img.clientWidth) { + actualWidth = img.clientWidth; + actualHeight = img.clientWidth / ratio; + } + return { actualWidth, actualHeight }; } - return { actualWidth, actualHeight }; + + return { actualWidth: 0, actualHeight: 0 }; }; const cancel = () => { @@ -202,12 +217,12 @@ }); const getFaceCroppedCoordinates = () => { - if (!faceRect || !imgElement) { + if (!faceRect || !htmlElement) { return; } const { left, top, width, height } = faceRect.getBoundingRect(); - const { actualWidth, actualHeight } = getContainedSize(imgElement); + const { actualWidth, actualHeight } = getContainedSize(htmlElement); const offsetArea = { width: (containerWidth - actualWidth) / 2, @@ -220,19 +235,35 @@ const y2Coeff = (top + height - offsetArea.height) / actualHeight; // transpose to the natural image location - const x1 = x1Coeff * imgElement.naturalWidth; - const y1 = y1Coeff * imgElement.naturalHeight; - const x2 = x2Coeff * imgElement.naturalWidth; - const y2 = y2Coeff * imgElement.naturalHeight; + if (htmlElement instanceof HTMLImageElement) { + const x1 = x1Coeff * htmlElement.naturalWidth; + const y1 = y1Coeff * htmlElement.naturalHeight; + const x2 = x2Coeff * htmlElement.naturalWidth; + const y2 = y2Coeff * htmlElement.naturalHeight; - return { - imageWidth: imgElement.naturalWidth, - imageHeight: imgElement.naturalHeight, - x: Math.floor(x1), - y: Math.floor(y1), - width: Math.floor(x2 - x1), - height: Math.floor(y2 - y1), - }; + return { + imageWidth: htmlElement.naturalWidth, + imageHeight: htmlElement.naturalHeight, + x: Math.floor(x1), + y: Math.floor(y1), + width: Math.floor(x2 - x1), + height: Math.floor(y2 - y1), + }; + } else if (htmlElement instanceof HTMLVideoElement) { + const x1 = x1Coeff * htmlElement.videoWidth; + const y1 = y1Coeff * htmlElement.videoHeight; + const x2 = x2Coeff * htmlElement.videoWidth; + const y2 = y2Coeff * htmlElement.videoHeight; + + return { + imageWidth: htmlElement.videoWidth, + imageHeight: htmlElement.videoHeight, + x: Math.floor(x1), + y: Math.floor(y1), + width: Math.floor(x2 - x1), + height: Math.floor(y2 - y1), + }; + } }; const tagFace = async (person: PersonResponseDto) => { diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 582f56fab3f..0d79b9e5fc6 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -234,7 +234,7 @@ {#if isFaceEditMode.value} - + {/if} {/if} diff --git a/web/src/lib/components/asset-viewer/video-native-viewer.svelte b/web/src/lib/components/asset-viewer/video-native-viewer.svelte index ea75ea069ba..46a0301306b 100644 --- a/web/src/lib/components/asset-viewer/video-native-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-native-viewer.svelte @@ -9,6 +9,8 @@ import type { SwipeCustomEvent } from 'svelte-gestures'; import { fade } from 'svelte/transition'; import { t } from 'svelte-i18n'; + import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; + import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; interface Props { assetId: string; @@ -84,9 +86,23 @@ onPreviousAsset(); } }; + + let containerWidth = $state(0); + let containerHeight = $state(0); + + $effect(() => { + if (isFaceEditMode.value) { + videoPlayer?.pause(); + } + }); -
+
{/if} + + {#if isFaceEditMode.value} + + {/if}