From 31a7556b27c308d3ced761f5cbd52c9fb038daf4 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Fri, 22 Mar 2024 11:32:34 -0500 Subject: [PATCH] Update error handling in makeVideoScreenshot Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com> --- ts/types/VisualAttachment.ts | 77 +++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/ts/types/VisualAttachment.ts b/ts/types/VisualAttachment.ts index 67e130393c..1a730ca9a1 100644 --- a/ts/types/VisualAttachment.ts +++ b/ts/types/VisualAttachment.ts @@ -11,6 +11,7 @@ import { strictAssert } from '../util/assert'; import { canvasToBlob } from '../util/canvasToBlob'; import { KIBIBYTE } from './AttachmentSize'; import { explodePromise } from '../util/explodePromise'; +import { SECOND } from '../util/durations'; export { blobToArrayBuffer }; @@ -209,44 +210,64 @@ export type MakeVideoScreenshotOptionsType = Readonly<{ logger: Pick; }>; -async function loadVideo({ +const MAKE_VIDEO_SCREENSHOT_TIMEOUT = 30 * SECOND; + +function captureScreenshot( + video: HTMLVideoElement, + contentType: MIMEType +): Promise { + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const context = canvas.getContext('2d'); + strictAssert(context, 'Failed to get canvas context'); + context.drawImage(video, 0, 0, canvas.width, canvas.height); + return canvasToBlob(canvas, contentType); +} + +export async function makeVideoScreenshot({ objectUrl, + contentType = IMAGE_PNG, logger, -}: MakeVideoScreenshotOptionsType): Promise { +}: MakeVideoScreenshotOptionsType): Promise { + const signal = AbortSignal.timeout(MAKE_VIDEO_SCREENSHOT_TIMEOUT); const video = document.createElement('video'); - const { promise, resolve, reject } = explodePromise(); - video.addEventListener('loadeddata', resolve); + + const { promise: videoLoadedAndSeeked, resolve, reject } = explodePromise(); + + function onLoaded() { + if (signal.aborted) { + return; + } + video.addEventListener('seeked', resolve); + video.currentTime = 1.0; + } + + function onAborted() { + reject(signal.reason); + } + + video.addEventListener('loadeddata', onLoaded); video.addEventListener('error', reject); - video.src = objectUrl; + signal.addEventListener('abort', onAborted); + try { - await promise; + video.src = objectUrl; + await videoLoadedAndSeeked; + return await captureScreenshot(video, contentType); } catch (error) { - logger.error('loadVideo error', toLogFormat(video.error)); + logger.error('makeVideoScreenshot error:', toLogFormat(error)); throw error; } finally { - video.removeEventListener('loadeddata', resolve); + // hard reset the video element so it doesn't keep loading + video.src = ''; + video.load(); + + video.removeEventListener('loadeddata', onLoaded); video.removeEventListener('error', reject); + video.removeEventListener('seeked', resolve); + signal.removeEventListener('abort', onAborted); } - return video; -} - -export async function makeVideoScreenshot({ - objectUrl, - contentType = IMAGE_PNG, - logger, -}: MakeVideoScreenshotOptionsType): Promise { - const video = await loadVideo({ objectUrl, logger }); - await new Promise(res => { - video.currentTime = 1.0; - video.addEventListener('seeked', res, { once: true }); - }); - const canvas = document.createElement('canvas'); - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - const context = canvas.getContext('2d'); - strictAssert(context, 'Failed to get canvas context'); - context.drawImage(video, 0, 0, canvas.width, canvas.height); - return canvasToBlob(canvas, contentType); } export function makeObjectUrl(