@@ -12,6 +12,15 @@ export type VideoOptions = Omit<SatoriOptions, 'width' | 'height'> & {
1212 bitrate ?: number
1313 quality ?: number
1414 groupOfPictures ?: number
15+ /**
16+ * Number of frames whose Satori + sharp work can be in flight at once. The
17+ * H.264 encoder still consumes frames strictly in order — concurrency lets
18+ * upcoming frames render while the current one is being encoded, and lets
19+ * sharp use its libuv threadpool for multiple frames in parallel.
20+ *
21+ * Default: 4 (matches the default libuv UV_THREADPOOL_SIZE).
22+ */
23+ concurrency ?: number
1524}
1625
1726export type FrameContext = {
@@ -79,6 +88,7 @@ export async function video(
7988 bitrate,
8089 quality,
8190 groupOfPictures,
91+ concurrency = 4 ,
8292 ...rest
8393 } = options
8494
@@ -93,9 +103,13 @@ export async function video(
93103 if ( fps <= 0 ) {
94104 throw new Error ( 'satori/video: fps must be > 0' )
95105 }
106+ if ( ! Number . isInteger ( concurrency ) || concurrency < 1 ) {
107+ throw new Error ( 'satori/video: concurrency must be a positive integer' )
108+ }
96109
97110 const satoriOptions = { ...rest , width, height } as SatoriOptions
98111 const totalFrames = computeTotalFrames ( duration , fps )
112+ const windowSize = Math . min ( concurrency , totalFrames )
99113
100114 const encoder = await createH264MP4Encoder ( )
101115 encoder . width = width
@@ -109,17 +123,33 @@ export async function video(
109123 . slice ( 2 ) } .mp4`
110124 encoder . initialize ( )
111125
126+ // Sliding window of in-flight render promises. Producers run ahead of the
127+ // encoder so Satori + sharp can overlap with the synchronous WASM encode.
128+ const inFlight = new Map < number , Promise < Buffer > > ( )
129+ const startFrame = ( i : number ) => {
130+ const p = renderFrame (
131+ renderer ,
132+ satoriOptions ,
133+ width ,
134+ height ,
135+ i ,
136+ totalFrames ,
137+ fps
138+ )
139+ // Suppress unhandled-rejection warnings for frames we may never await
140+ // (e.g. an earlier frame throws and we bail out of the loop).
141+ p . catch ( ( ) => undefined )
142+ inFlight . set ( i , p )
143+ }
144+
145+ let nextStart = 0
146+ while ( nextStart < windowSize ) startFrame ( nextStart ++ )
147+
112148 try {
113- for ( let frame = 0 ; frame < totalFrames ; frame ++ ) {
114- const rgba = await renderFrame (
115- renderer ,
116- satoriOptions ,
117- width ,
118- height ,
119- frame ,
120- totalFrames ,
121- fps
122- )
149+ for ( let i = 0 ; i < totalFrames ; i ++ ) {
150+ const rgba = await inFlight . get ( i ) !
151+ inFlight . delete ( i )
152+ if ( nextStart < totalFrames ) startFrame ( nextStart ++ )
123153 encoder . addFrameRgba ( rgba )
124154 }
125155 encoder . finalize ( )
0 commit comments