From 427bff162868458ce78fe0260513c2fe201a79d4 Mon Sep 17 00:00:00 2001 From: Louis Holley Date: Tue, 24 Jun 2025 14:49:31 +0100 Subject: [PATCH 1/9] implement programmatic gif capture --- index.js | 159 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 113 insertions(+), 46 deletions(-) diff --git a/index.js b/index.js index 6df94f6..de82adb 100644 --- a/index.js +++ b/index.js @@ -77,7 +77,7 @@ const ERRORS = { // the different capture modes const CAPTURE_MODES = ["CANVAS", "VIEWPORT"] // the list of accepted trigger modes -const TRIGGER_MODES = ["DELAY", "FN_TRIGGER"] +const TRIGGER_MODES = ["DELAY", "FN_TRIGGER", "FN_TRIGGER_GIF"] // // UTILITY FUNCTIONS @@ -94,7 +94,7 @@ function isUrlValid(url) { } // is a trigger valid ? looks at the trigger mode and trigger settings -function isTriggerValid(triggerMode, delay) { +function isTriggerValid(triggerMode, delay, playbackFps) { if (!TRIGGER_MODES.includes(triggerMode)) { return false } @@ -106,8 +106,15 @@ function isTriggerValid(triggerMode, delay) { delay >= DELAY_MIN && delay <= DELAY_MAX ) + } else if (triggerMode === "FN_TRIGGER_GIF") { + return ( + typeof playbackFps !== undefined && + !isNaN(playbackFps) && + playbackFps >= GIF_DEFAULTS.MIN_FPS && + playbackFps <= GIF_DEFAULTS.MAX_FPS + ) } else if (triggerMode === "FN_TRIGGER") { - // fn trigger doesn't need any param + // fn trigger and fn trigger gif don't need any params return true } } @@ -323,6 +330,7 @@ const resizeCanvas = async (image, resX, resY) => { } const performCapture = async ( mode, + triggerMode, page, canvasSelector, resX, @@ -337,7 +345,14 @@ const performCapture = async ( // if viewport mode, use the native puppeteer page.screenshot if (mode === "VIEWPORT") { // we simply take a capture of the viewport - return captureViewport(page, gif, frameCount, captureInterval, playbackFps) + return captureViewport( + page, + triggerMode, + gif, + frameCount, + captureInterval, + playbackFps + ) } // if the mode is canvas, we need to execute som JS on the client to select // the canvas and generate a dataURL to bridge it in here @@ -345,6 +360,7 @@ const performCapture = async ( const canvas = await captureCanvas( page, canvasSelector, + triggerMode, gif, frameCount, captureInterval, @@ -419,7 +435,7 @@ const validateParams = ({ if (!url || !mode) throw ERRORS.MISSING_PARAMETERS if (!isUrlValid(url)) throw ERRORS.UNSUPPORTED_URL if (!CAPTURE_MODES.includes(mode)) throw ERRORS.INVALID_PARAMETERS - if (!isTriggerValid(triggerMode, delay)) + if (!isTriggerValid(triggerMode, delay, playbackFps)) throw ERRORS.INVALID_TRIGGER_PARAMETERS if (gif && !validateGifParams(frameCount, captureInterval, playbackFps)) @@ -456,17 +472,11 @@ const validateParams = ({ } } -async function captureViewport( - page, - isGif, +async function captureFramesWithTiming( + captureFrameFunction, frameCount, - captureInterval, - playbackFps + captureInterval ) { - if (!isGif) { - return await page.screenshot() - } - const frames = [] let lastCaptureStart = performance.now() @@ -474,11 +484,9 @@ async function captureViewport( // Record start time of screenshot operation const captureStart = performance.now() - // Capture raw pixels - const frameBuffer = await page.screenshot({ - encoding: "binary", - }) - frames.push(frameBuffer) + // Use the provided capture function to get the frame + const frame = await captureFrameFunction() + frames.push(frame) // Calculate how long the capture took const captureDuration = performance.now() - captureStart @@ -502,6 +510,69 @@ async function captureViewport( lastCaptureStart = performance.now() } + return frames +} + +async function captureFramesProgrammatically(page, captureFrameFunction) { + const frames = [] + + // set up the event listener and capture loop + await page.exposeFunction("captureFrame", async () => { + const frame = await captureFrameFunction() + frames.push(frame) + console.log(`programmatic frame ${frames.length} captured`) + return frames.length + }) + + // wait for events in browser context + await page.evaluate(function () { + return new Promise(function (resolve) { + window.addEventListener("fxhash-capture-frame", async event => { + const frameCount = await window.captureFrame() + + if ( + event.detail?.isLastFrame || + frameCount >= GIF_DEFAULTS.MAX_FRAMES + ) { + resolve() + } + }) + + // timeout fallback + setTimeout(() => resolve(), DELAY_MAX) + }) + }) + + return frames +} + +async function captureViewport( + page, + triggerMode, + isGif, + frameCount, + captureInterval, + playbackFps +) { + if (!isGif) { + return await page.screenshot() + } + + const captureViewportFrame = async () => { + return await page.screenshot({ + encoding: "binary", + }) + } + + const frames = + triggerMode === "FN_TRIGGER_GIF" + ? await captureFramesProgrammatically(page, captureViewportFrame) + : await captureFramesWithTiming( + captureViewportFrame, + frameCount, + captureInterval + ) + const viewport = page.viewport() return await captureFramesToGif( frames, @@ -514,6 +585,7 @@ async function captureViewport( async function captureCanvas( page, canvasSelector, + triggerMode, isGif, frameCount, captureInterval, @@ -532,37 +604,25 @@ async function captureCanvas( return Buffer.from(pureBase64, "base64") } - const frames = [] - let lastCaptureStart = Date.now() - - for (let i = 0; i < frameCount; i++) { - const captureStart = Date.now() - + const captureCanvasFrame = async () => { // Get raw pixel data from canvas const base64 = await page.$eval(canvasSelector, el => { if (!el || el.tagName !== "CANVAS") return null return el.toDataURL() }) - if (!base64) throw null - frames.push(base64) - - // Calculate timing adjustments - const captureDuration = Date.now() - captureStart - const adjustedInterval = Math.max(0, captureInterval - captureDuration) - - console.log(`Frame ${i + 1}/${frameCount}:`, { - captureDuration, - adjustedInterval, - totalFrameTime: Date.now() - lastCaptureStart, - }) - - if (adjustedInterval > 0) { - await sleep(adjustedInterval) - } - - lastCaptureStart = Date.now() + if (!base64) throw new Error("Canvas capture failed") + return base64 } + const frames = + triggerMode === "FN_TRIGGER_GIF" + ? await captureFramesProgrammatically(page, captureCanvasFrame) + : await captureFramesWithTiming( + captureCanvasFrame, + frameCount, + captureInterval + ) + const dimensions = await page.$eval(canvasSelector, el => ({ width: el.width, height: el.height, @@ -671,6 +731,7 @@ exports.handler = async (event, context) => { const processCapture = async () => { const capture = await performCapture( mode, + triggerMode, page, canvasSelector, resX, @@ -687,10 +748,16 @@ exports.handler = async (event, context) => { return upload } - if (useFallbackCaptureOnTimeout) { - await waitPreviewWithFallback(context, triggerMode, page, delay) + if (triggerMode === "FN_TRIGGER_GIF") { + // for FN_TRIGGER_GIF mode, skip preview waiting entirely + // the capture functions will handle event listening internally + console.log("Using FN_TRIGGER_GIF mode - skipping preview wait") } else { - await waitPreview(triggerMode, page, delay) + if (useFallbackCaptureOnTimeout) { + await waitPreviewWithFallback(context, triggerMode, page, delay) + } else { + await waitPreview(triggerMode, page, delay) + } } httpResponse = await processCapture() From a968cd7b9e3e555e74c09a4f8797c3c45ad06b24 Mon Sep 17 00:00:00 2001 From: Louis Holley Date: Tue, 24 Jun 2025 15:35:28 +0100 Subject: [PATCH 2/9] inject constants into page context --- index.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index de82adb..3c48c5e 100644 --- a/index.js +++ b/index.js @@ -525,23 +525,24 @@ async function captureFramesProgrammatically(page, captureFrameFunction) { }) // wait for events in browser context - await page.evaluate(function () { - return new Promise(function (resolve) { - window.addEventListener("fxhash-capture-frame", async event => { - const frameCount = await window.captureFrame() - - if ( - event.detail?.isLastFrame || - frameCount >= GIF_DEFAULTS.MAX_FRAMES - ) { - resolve() - } - }) + await page.evaluate( + function (maxFrames, delayMax) { + return new Promise(function (resolve) { + window.addEventListener("fxhash-capture-frame", async event => { + const frameCount = await window.captureFrame() + + if (event.detail?.isLastFrame || frameCount >= maxFrames) { + resolve() + } + }) - // timeout fallback - setTimeout(() => resolve(), DELAY_MAX) - }) - }) + // timeout fallback + setTimeout(() => resolve(), delayMax) + }) + }, + GIF_DEFAULTS.MAX_FRAMES, + DELAY_MAX + ) return frames } From bacb87260f5d0b902f6dceb694ee10eb8d08c661 Mon Sep 17 00:00:00 2001 From: Louis Holley Date: Tue, 24 Jun 2025 15:50:18 +0100 Subject: [PATCH 3/9] remove listener on last frame & debug --- index.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 3c48c5e..badfb2a 100644 --- a/index.js +++ b/index.js @@ -528,16 +528,28 @@ async function captureFramesProgrammatically(page, captureFrameFunction) { await page.evaluate( function (maxFrames, delayMax) { return new Promise(function (resolve) { - window.addEventListener("fxhash-capture-frame", async event => { + const handleFrameCapture = async event => { const frameCount = await window.captureFrame() + console.log(event) + console.log({ frameCount, maxFrames }) + console.log({ isLastFrame: event.detail?.isLastFrame }) if (event.detail?.isLastFrame || frameCount >= maxFrames) { + window.removeEventListener( + "fxhash-capture-frame", + handleFrameCapture + ) resolve() } - }) + } + + window.addEventListener("fxhash-capture-frame", handleFrameCapture) // timeout fallback - setTimeout(() => resolve(), delayMax) + setTimeout(() => { + window.removeEventListener("fxhash-capture-frame", handleFrameCapture) + resolve() + }, delayMax) }) }, GIF_DEFAULTS.MAX_FRAMES, From 1f7618d6460b3596682419fb17cfd9b7d397cd0a Mon Sep 17 00:00:00 2001 From: Louis Holley Date: Tue, 24 Jun 2025 15:53:53 +0100 Subject: [PATCH 4/9] add browser log debug --- index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/index.js b/index.js index badfb2a..2d6939c 100644 --- a/index.js +++ b/index.js @@ -516,6 +516,10 @@ async function captureFramesWithTiming( async function captureFramesProgrammatically(page, captureFrameFunction) { const frames = [] + page.on("console", msg => { + console.log("BROWSER:", msg.text()) + }) + // set up the event listener and capture loop await page.exposeFunction("captureFrame", async () => { const frame = await captureFrameFunction() From d12212fd8609141ffa1e45f937ddece1b5ffc6ac Mon Sep 17 00:00:00 2001 From: Louis Holley Date: Tue, 24 Jun 2025 15:56:08 +0100 Subject: [PATCH 5/9] stringify browser logs --- index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 2d6939c..fb85555 100644 --- a/index.js +++ b/index.js @@ -536,8 +536,10 @@ async function captureFramesProgrammatically(page, captureFrameFunction) { const frameCount = await window.captureFrame() console.log(event) - console.log({ frameCount, maxFrames }) - console.log({ isLastFrame: event.detail?.isLastFrame }) + console.log(JSON.stringify({ frameCount, maxFrames })) + console.log( + JSON.stringify({ isLastFrame: event.detail?.isLastFrame }) + ) if (event.detail?.isLastFrame || frameCount >= maxFrames) { window.removeEventListener( "fxhash-capture-frame", From aca53cf7fb0fe9fdbb89797b70a7fac10276c57d Mon Sep 17 00:00:00 2001 From: Louis Holley Date: Tue, 24 Jun 2025 15:58:14 +0100 Subject: [PATCH 6/9] more debug --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index fb85555..41f2675 100644 --- a/index.js +++ b/index.js @@ -535,7 +535,7 @@ async function captureFramesProgrammatically(page, captureFrameFunction) { const handleFrameCapture = async event => { const frameCount = await window.captureFrame() - console.log(event) + console.log(JSON.stringify(event)) console.log(JSON.stringify({ frameCount, maxFrames })) console.log( JSON.stringify({ isLastFrame: event.detail?.isLastFrame }) From 1918b04da2452cc933dc714d1f250e7cf78cc3c4 Mon Sep 17 00:00:00 2001 From: Louis Holley Date: Thu, 26 Jun 2025 16:00:24 +0100 Subject: [PATCH 7/9] Revert "implement programmatic gif capture" This reverts commit 427bff162868458ce78fe0260513c2fe201a79d4. --- index.js | 178 ++++++++++++++----------------------------------------- 1 file changed, 46 insertions(+), 132 deletions(-) diff --git a/index.js b/index.js index 41f2675..6df94f6 100644 --- a/index.js +++ b/index.js @@ -77,7 +77,7 @@ const ERRORS = { // the different capture modes const CAPTURE_MODES = ["CANVAS", "VIEWPORT"] // the list of accepted trigger modes -const TRIGGER_MODES = ["DELAY", "FN_TRIGGER", "FN_TRIGGER_GIF"] +const TRIGGER_MODES = ["DELAY", "FN_TRIGGER"] // // UTILITY FUNCTIONS @@ -94,7 +94,7 @@ function isUrlValid(url) { } // is a trigger valid ? looks at the trigger mode and trigger settings -function isTriggerValid(triggerMode, delay, playbackFps) { +function isTriggerValid(triggerMode, delay) { if (!TRIGGER_MODES.includes(triggerMode)) { return false } @@ -106,15 +106,8 @@ function isTriggerValid(triggerMode, delay, playbackFps) { delay >= DELAY_MIN && delay <= DELAY_MAX ) - } else if (triggerMode === "FN_TRIGGER_GIF") { - return ( - typeof playbackFps !== undefined && - !isNaN(playbackFps) && - playbackFps >= GIF_DEFAULTS.MIN_FPS && - playbackFps <= GIF_DEFAULTS.MAX_FPS - ) } else if (triggerMode === "FN_TRIGGER") { - // fn trigger and fn trigger gif don't need any params + // fn trigger doesn't need any param return true } } @@ -330,7 +323,6 @@ const resizeCanvas = async (image, resX, resY) => { } const performCapture = async ( mode, - triggerMode, page, canvasSelector, resX, @@ -345,14 +337,7 @@ const performCapture = async ( // if viewport mode, use the native puppeteer page.screenshot if (mode === "VIEWPORT") { // we simply take a capture of the viewport - return captureViewport( - page, - triggerMode, - gif, - frameCount, - captureInterval, - playbackFps - ) + return captureViewport(page, gif, frameCount, captureInterval, playbackFps) } // if the mode is canvas, we need to execute som JS on the client to select // the canvas and generate a dataURL to bridge it in here @@ -360,7 +345,6 @@ const performCapture = async ( const canvas = await captureCanvas( page, canvasSelector, - triggerMode, gif, frameCount, captureInterval, @@ -435,7 +419,7 @@ const validateParams = ({ if (!url || !mode) throw ERRORS.MISSING_PARAMETERS if (!isUrlValid(url)) throw ERRORS.UNSUPPORTED_URL if (!CAPTURE_MODES.includes(mode)) throw ERRORS.INVALID_PARAMETERS - if (!isTriggerValid(triggerMode, delay, playbackFps)) + if (!isTriggerValid(triggerMode, delay)) throw ERRORS.INVALID_TRIGGER_PARAMETERS if (gif && !validateGifParams(frameCount, captureInterval, playbackFps)) @@ -472,11 +456,17 @@ const validateParams = ({ } } -async function captureFramesWithTiming( - captureFrameFunction, +async function captureViewport( + page, + isGif, frameCount, - captureInterval + captureInterval, + playbackFps ) { + if (!isGif) { + return await page.screenshot() + } + const frames = [] let lastCaptureStart = performance.now() @@ -484,9 +474,11 @@ async function captureFramesWithTiming( // Record start time of screenshot operation const captureStart = performance.now() - // Use the provided capture function to get the frame - const frame = await captureFrameFunction() - frames.push(frame) + // Capture raw pixels + const frameBuffer = await page.screenshot({ + encoding: "binary", + }) + frames.push(frameBuffer) // Calculate how long the capture took const captureDuration = performance.now() - captureStart @@ -510,88 +502,6 @@ async function captureFramesWithTiming( lastCaptureStart = performance.now() } - return frames -} - -async function captureFramesProgrammatically(page, captureFrameFunction) { - const frames = [] - - page.on("console", msg => { - console.log("BROWSER:", msg.text()) - }) - - // set up the event listener and capture loop - await page.exposeFunction("captureFrame", async () => { - const frame = await captureFrameFunction() - frames.push(frame) - console.log(`programmatic frame ${frames.length} captured`) - return frames.length - }) - - // wait for events in browser context - await page.evaluate( - function (maxFrames, delayMax) { - return new Promise(function (resolve) { - const handleFrameCapture = async event => { - const frameCount = await window.captureFrame() - - console.log(JSON.stringify(event)) - console.log(JSON.stringify({ frameCount, maxFrames })) - console.log( - JSON.stringify({ isLastFrame: event.detail?.isLastFrame }) - ) - if (event.detail?.isLastFrame || frameCount >= maxFrames) { - window.removeEventListener( - "fxhash-capture-frame", - handleFrameCapture - ) - resolve() - } - } - - window.addEventListener("fxhash-capture-frame", handleFrameCapture) - - // timeout fallback - setTimeout(() => { - window.removeEventListener("fxhash-capture-frame", handleFrameCapture) - resolve() - }, delayMax) - }) - }, - GIF_DEFAULTS.MAX_FRAMES, - DELAY_MAX - ) - - return frames -} - -async function captureViewport( - page, - triggerMode, - isGif, - frameCount, - captureInterval, - playbackFps -) { - if (!isGif) { - return await page.screenshot() - } - - const captureViewportFrame = async () => { - return await page.screenshot({ - encoding: "binary", - }) - } - - const frames = - triggerMode === "FN_TRIGGER_GIF" - ? await captureFramesProgrammatically(page, captureViewportFrame) - : await captureFramesWithTiming( - captureViewportFrame, - frameCount, - captureInterval - ) - const viewport = page.viewport() return await captureFramesToGif( frames, @@ -604,7 +514,6 @@ async function captureViewport( async function captureCanvas( page, canvasSelector, - triggerMode, isGif, frameCount, captureInterval, @@ -623,24 +532,36 @@ async function captureCanvas( return Buffer.from(pureBase64, "base64") } - const captureCanvasFrame = async () => { + const frames = [] + let lastCaptureStart = Date.now() + + for (let i = 0; i < frameCount; i++) { + const captureStart = Date.now() + // Get raw pixel data from canvas const base64 = await page.$eval(canvasSelector, el => { if (!el || el.tagName !== "CANVAS") return null return el.toDataURL() }) - if (!base64) throw new Error("Canvas capture failed") - return base64 - } + if (!base64) throw null + frames.push(base64) - const frames = - triggerMode === "FN_TRIGGER_GIF" - ? await captureFramesProgrammatically(page, captureCanvasFrame) - : await captureFramesWithTiming( - captureCanvasFrame, - frameCount, - captureInterval - ) + // Calculate timing adjustments + const captureDuration = Date.now() - captureStart + const adjustedInterval = Math.max(0, captureInterval - captureDuration) + + console.log(`Frame ${i + 1}/${frameCount}:`, { + captureDuration, + adjustedInterval, + totalFrameTime: Date.now() - lastCaptureStart, + }) + + if (adjustedInterval > 0) { + await sleep(adjustedInterval) + } + + lastCaptureStart = Date.now() + } const dimensions = await page.$eval(canvasSelector, el => ({ width: el.width, @@ -750,7 +671,6 @@ exports.handler = async (event, context) => { const processCapture = async () => { const capture = await performCapture( mode, - triggerMode, page, canvasSelector, resX, @@ -767,16 +687,10 @@ exports.handler = async (event, context) => { return upload } - if (triggerMode === "FN_TRIGGER_GIF") { - // for FN_TRIGGER_GIF mode, skip preview waiting entirely - // the capture functions will handle event listening internally - console.log("Using FN_TRIGGER_GIF mode - skipping preview wait") + if (useFallbackCaptureOnTimeout) { + await waitPreviewWithFallback(context, triggerMode, page, delay) } else { - if (useFallbackCaptureOnTimeout) { - await waitPreviewWithFallback(context, triggerMode, page, delay) - } else { - await waitPreview(triggerMode, page, delay) - } + await waitPreview(triggerMode, page, delay) } httpResponse = await processCapture() From e75b3de3628c1250285586ddc42f430b9f584b99 Mon Sep 17 00:00:00 2001 From: louis holley Date: Fri, 27 Jun 2025 11:56:07 +0100 Subject: [PATCH 8/9] Revert "Revert "implement programmatic gif capture"" --- index.js | 178 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 132 insertions(+), 46 deletions(-) diff --git a/index.js b/index.js index 6df94f6..41f2675 100644 --- a/index.js +++ b/index.js @@ -77,7 +77,7 @@ const ERRORS = { // the different capture modes const CAPTURE_MODES = ["CANVAS", "VIEWPORT"] // the list of accepted trigger modes -const TRIGGER_MODES = ["DELAY", "FN_TRIGGER"] +const TRIGGER_MODES = ["DELAY", "FN_TRIGGER", "FN_TRIGGER_GIF"] // // UTILITY FUNCTIONS @@ -94,7 +94,7 @@ function isUrlValid(url) { } // is a trigger valid ? looks at the trigger mode and trigger settings -function isTriggerValid(triggerMode, delay) { +function isTriggerValid(triggerMode, delay, playbackFps) { if (!TRIGGER_MODES.includes(triggerMode)) { return false } @@ -106,8 +106,15 @@ function isTriggerValid(triggerMode, delay) { delay >= DELAY_MIN && delay <= DELAY_MAX ) + } else if (triggerMode === "FN_TRIGGER_GIF") { + return ( + typeof playbackFps !== undefined && + !isNaN(playbackFps) && + playbackFps >= GIF_DEFAULTS.MIN_FPS && + playbackFps <= GIF_DEFAULTS.MAX_FPS + ) } else if (triggerMode === "FN_TRIGGER") { - // fn trigger doesn't need any param + // fn trigger and fn trigger gif don't need any params return true } } @@ -323,6 +330,7 @@ const resizeCanvas = async (image, resX, resY) => { } const performCapture = async ( mode, + triggerMode, page, canvasSelector, resX, @@ -337,7 +345,14 @@ const performCapture = async ( // if viewport mode, use the native puppeteer page.screenshot if (mode === "VIEWPORT") { // we simply take a capture of the viewport - return captureViewport(page, gif, frameCount, captureInterval, playbackFps) + return captureViewport( + page, + triggerMode, + gif, + frameCount, + captureInterval, + playbackFps + ) } // if the mode is canvas, we need to execute som JS on the client to select // the canvas and generate a dataURL to bridge it in here @@ -345,6 +360,7 @@ const performCapture = async ( const canvas = await captureCanvas( page, canvasSelector, + triggerMode, gif, frameCount, captureInterval, @@ -419,7 +435,7 @@ const validateParams = ({ if (!url || !mode) throw ERRORS.MISSING_PARAMETERS if (!isUrlValid(url)) throw ERRORS.UNSUPPORTED_URL if (!CAPTURE_MODES.includes(mode)) throw ERRORS.INVALID_PARAMETERS - if (!isTriggerValid(triggerMode, delay)) + if (!isTriggerValid(triggerMode, delay, playbackFps)) throw ERRORS.INVALID_TRIGGER_PARAMETERS if (gif && !validateGifParams(frameCount, captureInterval, playbackFps)) @@ -456,17 +472,11 @@ const validateParams = ({ } } -async function captureViewport( - page, - isGif, +async function captureFramesWithTiming( + captureFrameFunction, frameCount, - captureInterval, - playbackFps + captureInterval ) { - if (!isGif) { - return await page.screenshot() - } - const frames = [] let lastCaptureStart = performance.now() @@ -474,11 +484,9 @@ async function captureViewport( // Record start time of screenshot operation const captureStart = performance.now() - // Capture raw pixels - const frameBuffer = await page.screenshot({ - encoding: "binary", - }) - frames.push(frameBuffer) + // Use the provided capture function to get the frame + const frame = await captureFrameFunction() + frames.push(frame) // Calculate how long the capture took const captureDuration = performance.now() - captureStart @@ -502,6 +510,88 @@ async function captureViewport( lastCaptureStart = performance.now() } + return frames +} + +async function captureFramesProgrammatically(page, captureFrameFunction) { + const frames = [] + + page.on("console", msg => { + console.log("BROWSER:", msg.text()) + }) + + // set up the event listener and capture loop + await page.exposeFunction("captureFrame", async () => { + const frame = await captureFrameFunction() + frames.push(frame) + console.log(`programmatic frame ${frames.length} captured`) + return frames.length + }) + + // wait for events in browser context + await page.evaluate( + function (maxFrames, delayMax) { + return new Promise(function (resolve) { + const handleFrameCapture = async event => { + const frameCount = await window.captureFrame() + + console.log(JSON.stringify(event)) + console.log(JSON.stringify({ frameCount, maxFrames })) + console.log( + JSON.stringify({ isLastFrame: event.detail?.isLastFrame }) + ) + if (event.detail?.isLastFrame || frameCount >= maxFrames) { + window.removeEventListener( + "fxhash-capture-frame", + handleFrameCapture + ) + resolve() + } + } + + window.addEventListener("fxhash-capture-frame", handleFrameCapture) + + // timeout fallback + setTimeout(() => { + window.removeEventListener("fxhash-capture-frame", handleFrameCapture) + resolve() + }, delayMax) + }) + }, + GIF_DEFAULTS.MAX_FRAMES, + DELAY_MAX + ) + + return frames +} + +async function captureViewport( + page, + triggerMode, + isGif, + frameCount, + captureInterval, + playbackFps +) { + if (!isGif) { + return await page.screenshot() + } + + const captureViewportFrame = async () => { + return await page.screenshot({ + encoding: "binary", + }) + } + + const frames = + triggerMode === "FN_TRIGGER_GIF" + ? await captureFramesProgrammatically(page, captureViewportFrame) + : await captureFramesWithTiming( + captureViewportFrame, + frameCount, + captureInterval + ) + const viewport = page.viewport() return await captureFramesToGif( frames, @@ -514,6 +604,7 @@ async function captureViewport( async function captureCanvas( page, canvasSelector, + triggerMode, isGif, frameCount, captureInterval, @@ -532,37 +623,25 @@ async function captureCanvas( return Buffer.from(pureBase64, "base64") } - const frames = [] - let lastCaptureStart = Date.now() - - for (let i = 0; i < frameCount; i++) { - const captureStart = Date.now() - + const captureCanvasFrame = async () => { // Get raw pixel data from canvas const base64 = await page.$eval(canvasSelector, el => { if (!el || el.tagName !== "CANVAS") return null return el.toDataURL() }) - if (!base64) throw null - frames.push(base64) - - // Calculate timing adjustments - const captureDuration = Date.now() - captureStart - const adjustedInterval = Math.max(0, captureInterval - captureDuration) - - console.log(`Frame ${i + 1}/${frameCount}:`, { - captureDuration, - adjustedInterval, - totalFrameTime: Date.now() - lastCaptureStart, - }) - - if (adjustedInterval > 0) { - await sleep(adjustedInterval) - } - - lastCaptureStart = Date.now() + if (!base64) throw new Error("Canvas capture failed") + return base64 } + const frames = + triggerMode === "FN_TRIGGER_GIF" + ? await captureFramesProgrammatically(page, captureCanvasFrame) + : await captureFramesWithTiming( + captureCanvasFrame, + frameCount, + captureInterval + ) + const dimensions = await page.$eval(canvasSelector, el => ({ width: el.width, height: el.height, @@ -671,6 +750,7 @@ exports.handler = async (event, context) => { const processCapture = async () => { const capture = await performCapture( mode, + triggerMode, page, canvasSelector, resX, @@ -687,10 +767,16 @@ exports.handler = async (event, context) => { return upload } - if (useFallbackCaptureOnTimeout) { - await waitPreviewWithFallback(context, triggerMode, page, delay) + if (triggerMode === "FN_TRIGGER_GIF") { + // for FN_TRIGGER_GIF mode, skip preview waiting entirely + // the capture functions will handle event listening internally + console.log("Using FN_TRIGGER_GIF mode - skipping preview wait") } else { - await waitPreview(triggerMode, page, delay) + if (useFallbackCaptureOnTimeout) { + await waitPreviewWithFallback(context, triggerMode, page, delay) + } else { + await waitPreview(triggerMode, page, delay) + } } httpResponse = await processCapture() From 61de0c3d18b78f9760e9bc108129ad8b85448a53 Mon Sep 17 00:00:00 2001 From: Louis Holley Date: Fri, 27 Jun 2025 12:20:13 +0100 Subject: [PATCH 9/9] remove captureFrame logs --- index.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/index.js b/index.js index 41f2675..3440965 100644 --- a/index.js +++ b/index.js @@ -516,15 +516,10 @@ async function captureFramesWithTiming( async function captureFramesProgrammatically(page, captureFrameFunction) { const frames = [] - page.on("console", msg => { - console.log("BROWSER:", msg.text()) - }) - // set up the event listener and capture loop await page.exposeFunction("captureFrame", async () => { const frame = await captureFrameFunction() frames.push(frame) - console.log(`programmatic frame ${frames.length} captured`) return frames.length }) @@ -535,11 +530,6 @@ async function captureFramesProgrammatically(page, captureFrameFunction) { const handleFrameCapture = async event => { const frameCount = await window.captureFrame() - console.log(JSON.stringify(event)) - console.log(JSON.stringify({ frameCount, maxFrames })) - console.log( - JSON.stringify({ isLastFrame: event.detail?.isLastFrame }) - ) if (event.detail?.isLastFrame || frameCount >= maxFrames) { window.removeEventListener( "fxhash-capture-frame",