@@ -43,6 +43,33 @@ export interface BeginFrameResult {
4343// the compositor is paused).
4444const lastFrameCache = new WeakMap < Page , Buffer > ( ) ;
4545
46+ const PENDING_FRAME_RETRIES = 5 ;
47+
48+ async function sendBeginFrame (
49+ client : import ( "puppeteer-core" ) . CDPSession ,
50+ params : Parameters < typeof client . send < "HeadlessExperimental.beginFrame" > > [ 1 ] ,
51+ ) {
52+ for ( let attempt = 0 ; ; attempt ++ ) {
53+ try {
54+ return await client . send ( "HeadlessExperimental.beginFrame" , params ) ;
55+ } catch ( err : unknown ) {
56+ const msg = err instanceof Error ? err . message : String ( err ) ;
57+ const isPending = msg . includes ( "Another frame is pending" ) ;
58+ if ( isPending && attempt < PENDING_FRAME_RETRIES ) {
59+ await new Promise ( ( r ) => setTimeout ( r , 50 * 2 ** attempt ) ) ;
60+ continue ;
61+ }
62+ if ( isPending ) {
63+ throw new Error (
64+ `[BeginFrame] Frame still pending after ${ PENDING_FRAME_RETRIES } retries — CPU overloaded by parallel renders. ` +
65+ `Reduce concurrent renders or use --docker for isolation.` ,
66+ ) ;
67+ }
68+ throw err ;
69+ }
70+ }
71+ }
72+
4673export async function beginFrameCapture (
4774 page : Page ,
4875 options : CaptureOptions ,
@@ -51,39 +78,34 @@ export async function beginFrameCapture(
5178) : Promise < BeginFrameResult > {
5279 const client = await getCdpSession ( page ) ;
5380
54- const format = options . format === "png" ? "png" : "jpeg" ;
55- const result = await client . send ( "HeadlessExperimental.beginFrame" , {
56- frameTimeTicks,
57- interval,
58- screenshot : {
59- format,
60- quality : format === "jpeg" ? ( options . quality ?? 80 ) : undefined ,
61- optimizeForSpeed : true ,
62- } ,
63- } ) ;
81+ const isPng = options . format === "png" ;
82+ const screenshot = {
83+ format : isPng ? "png" : "jpeg" ,
84+ quality : isPng ? undefined : ( options . quality ?? 80 ) ,
85+ optimizeForSpeed : true ,
86+ } as const ;
87+
88+ const result = await sendBeginFrame ( client , { frameTimeTicks, interval, screenshot } ) ;
6489
6590 let buffer : Buffer ;
6691 if ( result . screenshotData ) {
6792 buffer = Buffer . from ( result . screenshotData , "base64" ) ;
6893 lastFrameCache . set ( page , buffer ) ;
6994 } else {
70- // hasDamage=false — nothing changed visually. Reuse the last frame.
7195 const cached = lastFrameCache . get ( page ) ;
7296 if ( cached ) {
7397 buffer = cached ;
7498 } else {
75- // No cached frame yet (shouldn't happen — frame 0 always has damage) .
76- // Issue another beginFrame with a tiny time advance to force a composite .
77- const retry = await client . send ( "HeadlessExperimental.beginFrame" , {
99+ // Frame 0 always has damage, so this path is near-unreachable .
100+ // Force a composite with a tiny time advance.
101+ const fallback = await sendBeginFrame ( client , {
78102 frameTimeTicks : frameTimeTicks + 0.001 ,
79103 interval,
80- screenshot : {
81- format,
82- quality : format === "jpeg" ? ( options . quality ?? 80 ) : undefined ,
83- optimizeForSpeed : true ,
84- } ,
104+ screenshot,
85105 } ) ;
86- buffer = retry . screenshotData ? Buffer . from ( retry . screenshotData , "base64" ) : Buffer . alloc ( 0 ) ;
106+ buffer = fallback . screenshotData
107+ ? Buffer . from ( fallback . screenshotData , "base64" )
108+ : Buffer . alloc ( 0 ) ;
87109 if ( buffer . length > 0 ) lastFrameCache . set ( page , buffer ) ;
88110 }
89111 }
0 commit comments