@@ -77,7 +77,7 @@ const ERRORS = {
7777// the different capture modes
7878const CAPTURE_MODES = [ "CANVAS" , "VIEWPORT" ]
7979// the list of accepted trigger modes
80- const TRIGGER_MODES = [ "DELAY" , "FN_TRIGGER" ]
80+ const TRIGGER_MODES = [ "DELAY" , "FN_TRIGGER" , "FN_TRIGGER_GIF" ]
8181
8282//
8383// UTILITY FUNCTIONS
@@ -94,7 +94,7 @@ function isUrlValid(url) {
9494}
9595
9696// is a trigger valid ? looks at the trigger mode and trigger settings
97- function isTriggerValid ( triggerMode , delay ) {
97+ function isTriggerValid ( triggerMode , delay , playbackFps ) {
9898 if ( ! TRIGGER_MODES . includes ( triggerMode ) ) {
9999 return false
100100 }
@@ -106,8 +106,15 @@ function isTriggerValid(triggerMode, delay) {
106106 delay >= DELAY_MIN &&
107107 delay <= DELAY_MAX
108108 )
109+ } else if ( triggerMode === "FN_TRIGGER_GIF" ) {
110+ return (
111+ typeof playbackFps !== undefined &&
112+ ! isNaN ( playbackFps ) &&
113+ playbackFps >= GIF_DEFAULTS . MIN_FPS &&
114+ playbackFps <= GIF_DEFAULTS . MAX_FPS
115+ )
109116 } else if ( triggerMode === "FN_TRIGGER" ) {
110- // fn trigger doesn 't need any param
117+ // fn trigger and fn trigger gif don 't need any params
111118 return true
112119 }
113120}
@@ -323,6 +330,7 @@ const resizeCanvas = async (image, resX, resY) => {
323330}
324331const performCapture = async (
325332 mode ,
333+ triggerMode ,
326334 page ,
327335 canvasSelector ,
328336 resX ,
@@ -337,14 +345,22 @@ const performCapture = async (
337345 // if viewport mode, use the native puppeteer page.screenshot
338346 if ( mode === "VIEWPORT" ) {
339347 // we simply take a capture of the viewport
340- return captureViewport ( page , gif , frameCount , captureInterval , playbackFps )
348+ return captureViewport (
349+ page ,
350+ triggerMode ,
351+ gif ,
352+ frameCount ,
353+ captureInterval ,
354+ playbackFps
355+ )
341356 }
342357 // if the mode is canvas, we need to execute som JS on the client to select
343358 // the canvas and generate a dataURL to bridge it in here
344359 else if ( mode === "CANVAS" ) {
345360 const canvas = await captureCanvas (
346361 page ,
347362 canvasSelector ,
363+ triggerMode ,
348364 gif ,
349365 frameCount ,
350366 captureInterval ,
@@ -419,7 +435,7 @@ const validateParams = ({
419435 if ( ! url || ! mode ) throw ERRORS . MISSING_PARAMETERS
420436 if ( ! isUrlValid ( url ) ) throw ERRORS . UNSUPPORTED_URL
421437 if ( ! CAPTURE_MODES . includes ( mode ) ) throw ERRORS . INVALID_PARAMETERS
422- if ( ! isTriggerValid ( triggerMode , delay ) )
438+ if ( ! isTriggerValid ( triggerMode , delay , playbackFps ) )
423439 throw ERRORS . INVALID_TRIGGER_PARAMETERS
424440
425441 if ( gif && ! validateGifParams ( frameCount , captureInterval , playbackFps ) )
@@ -456,29 +472,21 @@ const validateParams = ({
456472 }
457473}
458474
459- async function captureViewport (
460- page ,
461- isGif ,
475+ async function captureFramesWithTiming (
476+ captureFrameFunction ,
462477 frameCount ,
463- captureInterval ,
464- playbackFps
478+ captureInterval
465479) {
466- if ( ! isGif ) {
467- return await page . screenshot ( )
468- }
469-
470480 const frames = [ ]
471481 let lastCaptureStart = performance . now ( )
472482
473483 for ( let i = 0 ; i < frameCount ; i ++ ) {
474484 // Record start time of screenshot operation
475485 const captureStart = performance . now ( )
476486
477- // Capture raw pixels
478- const frameBuffer = await page . screenshot ( {
479- encoding : "binary" ,
480- } )
481- frames . push ( frameBuffer )
487+ // Use the provided capture function to get the frame
488+ const frame = await captureFrameFunction ( )
489+ frames . push ( frame )
482490
483491 // Calculate how long the capture took
484492 const captureDuration = performance . now ( ) - captureStart
@@ -502,6 +510,69 @@ async function captureViewport(
502510 lastCaptureStart = performance . now ( )
503511 }
504512
513+ return frames
514+ }
515+
516+ async function captureFramesProgrammatically ( page , captureFrameFunction ) {
517+ const frames = [ ]
518+
519+ // set up the event listener and capture loop
520+ await page . exposeFunction ( "captureFrame" , async ( ) => {
521+ const frame = await captureFrameFunction ( )
522+ frames . push ( frame )
523+ console . log ( `programmatic frame ${ frames . length } captured` )
524+ return frames . length
525+ } )
526+
527+ // wait for events in browser context
528+ await page . evaluate ( function ( ) {
529+ return new Promise ( function ( resolve ) {
530+ window . addEventListener ( "fxhash-capture-frame" , async event => {
531+ const frameCount = await window . captureFrame ( )
532+
533+ if (
534+ event . detail ?. isLastFrame ||
535+ frameCount >= GIF_DEFAULTS . MAX_FRAMES
536+ ) {
537+ resolve ( )
538+ }
539+ } )
540+
541+ // timeout fallback
542+ setTimeout ( ( ) => resolve ( ) , DELAY_MAX )
543+ } )
544+ } )
545+
546+ return frames
547+ }
548+
549+ async function captureViewport (
550+ page ,
551+ triggerMode ,
552+ isGif ,
553+ frameCount ,
554+ captureInterval ,
555+ playbackFps
556+ ) {
557+ if ( ! isGif ) {
558+ return await page . screenshot ( )
559+ }
560+
561+ const captureViewportFrame = async ( ) => {
562+ return await page . screenshot ( {
563+ encoding : "binary" ,
564+ } )
565+ }
566+
567+ const frames =
568+ triggerMode === "FN_TRIGGER_GIF"
569+ ? await captureFramesProgrammatically ( page , captureViewportFrame )
570+ : await captureFramesWithTiming (
571+ captureViewportFrame ,
572+ frameCount ,
573+ captureInterval
574+ )
575+
505576 const viewport = page . viewport ( )
506577 return await captureFramesToGif (
507578 frames ,
@@ -514,6 +585,7 @@ async function captureViewport(
514585async function captureCanvas (
515586 page ,
516587 canvasSelector ,
588+ triggerMode ,
517589 isGif ,
518590 frameCount ,
519591 captureInterval ,
@@ -532,37 +604,25 @@ async function captureCanvas(
532604 return Buffer . from ( pureBase64 , "base64" )
533605 }
534606
535- const frames = [ ]
536- let lastCaptureStart = Date . now ( )
537-
538- for ( let i = 0 ; i < frameCount ; i ++ ) {
539- const captureStart = Date . now ( )
540-
607+ const captureCanvasFrame = async ( ) => {
541608 // Get raw pixel data from canvas
542609 const base64 = await page . $eval ( canvasSelector , el => {
543610 if ( ! el || el . tagName !== "CANVAS" ) return null
544611 return el . toDataURL ( )
545612 } )
546- if ( ! base64 ) throw null
547- frames . push ( base64 )
548-
549- // Calculate timing adjustments
550- const captureDuration = Date . now ( ) - captureStart
551- const adjustedInterval = Math . max ( 0 , captureInterval - captureDuration )
552-
553- console . log ( `Frame ${ i + 1 } /${ frameCount } :` , {
554- captureDuration,
555- adjustedInterval,
556- totalFrameTime : Date . now ( ) - lastCaptureStart ,
557- } )
558-
559- if ( adjustedInterval > 0 ) {
560- await sleep ( adjustedInterval )
561- }
562-
563- lastCaptureStart = Date . now ( )
613+ if ( ! base64 ) throw new Error ( "Canvas capture failed" )
614+ return base64
564615 }
565616
617+ const frames =
618+ triggerMode === "FN_TRIGGER_GIF"
619+ ? await captureFramesProgrammatically ( page , captureCanvasFrame )
620+ : await captureFramesWithTiming (
621+ captureCanvasFrame ,
622+ frameCount ,
623+ captureInterval
624+ )
625+
566626 const dimensions = await page . $eval ( canvasSelector , el => ( {
567627 width : el . width ,
568628 height : el . height ,
@@ -671,6 +731,7 @@ exports.handler = async (event, context) => {
671731 const processCapture = async ( ) => {
672732 const capture = await performCapture (
673733 mode ,
734+ triggerMode ,
674735 page ,
675736 canvasSelector ,
676737 resX ,
@@ -687,10 +748,16 @@ exports.handler = async (event, context) => {
687748 return upload
688749 }
689750
690- if ( useFallbackCaptureOnTimeout ) {
691- await waitPreviewWithFallback ( context , triggerMode , page , delay )
751+ if ( triggerMode === "FN_TRIGGER_GIF" ) {
752+ // for FN_TRIGGER_GIF mode, skip preview waiting entirely
753+ // the capture functions will handle event listening internally
754+ console . log ( "Using FN_TRIGGER_GIF mode - skipping preview wait" )
692755 } else {
693- await waitPreview ( triggerMode , page , delay )
756+ if ( useFallbackCaptureOnTimeout ) {
757+ await waitPreviewWithFallback ( context , triggerMode , page , delay )
758+ } else {
759+ await waitPreview ( triggerMode , page , delay )
760+ }
694761 }
695762
696763 httpResponse = await processCapture ( )
0 commit comments