Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 46 additions & 132 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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
}
}
Expand Down Expand Up @@ -330,7 +323,6 @@ const resizeCanvas = async (image, resX, resY) => {
}
const performCapture = async (
mode,
triggerMode,
page,
canvasSelector,
resX,
Expand All @@ -345,22 +337,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,
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
else if (mode === "CANVAS") {
const canvas = await captureCanvas(
page,
canvasSelector,
triggerMode,
gif,
frameCount,
captureInterval,
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -472,21 +456,29 @@ 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()

for (let i = 0; i < frameCount; i++) {
// 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
Expand All @@ -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,
Expand All @@ -604,7 +514,6 @@ async function captureViewport(
async function captureCanvas(
page,
canvasSelector,
triggerMode,
isGif,
frameCount,
captureInterval,
Expand All @@ -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,
Expand Down Expand Up @@ -750,7 +671,6 @@ exports.handler = async (event, context) => {
const processCapture = async () => {
const capture = await performCapture(
mode,
triggerMode,
page,
canvasSelector,
resX,
Expand All @@ -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()
Expand Down