From 843a8dcd698b82cae29b193c8b3bbd7dbff4c80b Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Sat, 3 Feb 2024 00:33:52 -0800 Subject: [PATCH] HTML5Video ops: use async/await --- .../composer/CameraCaptureModal.tsx | 38 +++++++++++-------- src/common/util/videoUtils.ts | 28 +++++++++----- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/apps/chat/components/composer/CameraCaptureModal.tsx b/src/apps/chat/components/composer/CameraCaptureModal.tsx index cb147f668..6c78a6ed4 100644 --- a/src/apps/chat/components/composer/CameraCaptureModal.tsx +++ b/src/apps/chat/components/composer/CameraCaptureModal.tsx @@ -7,7 +7,7 @@ import InfoIcon from '@mui/icons-material/Info'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import { InlineError } from '~/common/components/InlineError'; -import { downloadVideoFrameAsPNG, renderVideoFrameToFile } from '~/common/util/videoUtils'; +import { downloadVideoFrameAsPNG, renderVideoFrameAsPNGFile } from '~/common/util/videoUtils'; import { useCameraCapture } from '~/common/components/useCameraCapture'; @@ -16,11 +16,12 @@ export function CameraCaptureModal(props: { onAttachImage: (file: File) => void // onOCR: (ocrText: string) => void } }) { - // state - // const [ocrProgress/*, setOCRProgress*/] = React.useState(null); - const [showInfo, setShowInfo] = React.useState(false); - // camera operations + // state + const [showInfo, setShowInfo] = React.useState(false); + // const [ocrProgress/*, setOCRProgress*/] = React.useState(null); + + // external state const { videoRef, cameras, cameraIdx, setCameraIdx, @@ -29,10 +30,14 @@ export function CameraCaptureModal(props: { } = useCameraCapture(); - const stopAndClose = () => { + // derived state + const { onCloseModal, onAttachImage } = props; + + + const stopAndClose = React.useCallback(() => { resetVideo(); - props.onCloseModal(); - }; + onCloseModal(); + }, [onCloseModal, resetVideo]); /*const handleVideoOCRClicked = async () => { if (!videoRef.current) return; @@ -53,18 +58,21 @@ export function CameraCaptureModal(props: { props.onOCR(result.data.text); };*/ - const handleVideoSnapClicked = () => { + const handleVideoSnapClicked = React.useCallback(async () => { if (!videoRef.current) return; - renderVideoFrameToFile(videoRef.current, 'camera', (file) => { - props.onAttachImage(file); + try { + const file = await renderVideoFrameAsPNGFile(videoRef.current, 'camera'); + onAttachImage(file); stopAndClose(); - }); - }; + } catch (error) { + console.error('Error capturing video frame:', error); + } + }, [onAttachImage, stopAndClose, videoRef]); - const handleVideoDownloadClicked = () => { + const handleVideoDownloadClicked = React.useCallback(() => { if (!videoRef.current) return; downloadVideoFrameAsPNG(videoRef.current, 'camera'); - }; + }, [videoRef]); return ( diff --git a/src/common/util/videoUtils.ts b/src/common/util/videoUtils.ts index 48b0a3fc7..36e3f3571 100644 --- a/src/common/util/videoUtils.ts +++ b/src/common/util/videoUtils.ts @@ -5,7 +5,7 @@ */ export function downloadVideoFrameAsPNG(videoElement: HTMLVideoElement, prefixName: string) { - // video to canvas to png + // current video frame -> canvas -> dataURL PNG const renderedFrame = _renderVideoFrameToCanvas(videoElement); const imageDataURL = renderedFrame.toDataURL('image/png'); @@ -13,20 +13,20 @@ export function downloadVideoFrameAsPNG(videoElement: HTMLVideoElement, prefixNa const link = document.createElement('a'); link.download = _prettyFileName(prefixName, renderedFrame); link.href = imageDataURL; + document.body.appendChild(link); // Ensure visibility in the DOM for Firefox link.click(); + document.body.removeChild(link); // Clean up } -export function renderVideoFrameToFile(videoElement: HTMLVideoElement, prefixName: string, callback: (file: File) => void) { - // video to canvas +export async function renderVideoFrameAsPNGFile(videoElement: HTMLVideoElement, prefixName: string): Promise { + // current video frame -> canvas -> Blob PNG const renderedFrame = _renderVideoFrameToCanvas(videoElement); + const blob = await _canvasToBlob(renderedFrame, 'image/png'); - // canvas to blob to file to callback - renderedFrame.toBlob((blob) => { - if (blob) { - const file = new File([blob], _prettyFileName(prefixName, renderedFrame), { type: blob.type }); - callback(file); - } - }, 'image/png'); + // to File + if (!blob) + throw new Error('Failed to convert canvas to Blob'); + return new File([blob], _prettyFileName(prefixName, renderedFrame), { type: blob.type }); } function _prettyFileName(prefixName: string, renderedFrame: HTMLCanvasElement) { @@ -45,3 +45,11 @@ function _renderVideoFrameToCanvas(videoElement: HTMLVideoElement): HTMLCanvasEl return canvas; } +/** + * Creates a Blob object representing the image contained in the canvas + * @param canvas The canvas element to convert to a Blob. + * @param imageFormat Browsers are required to support image/png; many will support additional formats including image/jpeg and image/webp. + */ +async function _canvasToBlob(canvas: HTMLCanvasElement, imageFormat: string = 'image/png'): Promise { + return new Promise((resolve) => canvas.toBlob(resolve, imageFormat)); +} \ No newline at end of file