@@ -200,26 +200,95 @@ export const videoProcessingTask = schemaTask({
200200
201201 if ( steps . includes ( "thumbnails" ) ) {
202202 await logger . trace ( "Step thumbnails" , async ( span ) => {
203- const frameFilePath = `${ nativeFilePath } .webp` ;
203+ const framesDir = path . join ( workingDir , "frames" ) ;
204+ await mkdir ( framesDir , { recursive : true } ) ;
205+
206+ // First get video duration and fps
207+ const { stdout : videoInfo } =
208+ await execa `ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,duration -of json ${ nativeFilePath } ` ;
209+ const info = JSON . parse ( videoInfo ) ;
210+
211+ // Parse frame rate (comes as ratio like "24000/1001")
212+ const [ num , den ] = info . streams [ 0 ] . r_frame_rate . split ( "/" ) ;
213+ const fps = Number ( num ) / ( Number ( den ) || 1 ) ;
214+
215+ // Get duration in seconds
216+ const duration = Math . min ( Number ( info . streams [ 0 ] . duration ) || 60 , 60 ) ; // Cap at 60 seconds
217+
218+ // Calculate sampling interval to get ~10 frames from first minute
219+ // but ensure we don't sample faster than 1 frame per second
220+ const targetFrames = 10 ;
221+ const interval = Math . max ( duration / targetFrames , 1 ) ;
222+
223+ logger . info ( "Video analysis" , {
224+ fps,
225+ duration,
226+ samplingInterval : interval ,
227+ expectedFrames : Math . floor ( duration / interval ) ,
228+ } ) ;
204229
205- await logger . trace ( "FFMPEG thumbnail" , async ( span ) => {
206- span . setAttributes ( {
207- input : nativeFilePath ,
208- output : frameFilePath ,
209- command : "ffmpeg -frames:v 1 -q:v 75 -f image2" ,
210- } ) ;
230+ // Extract frames using calculated interval
231+ await logger . trace ( "FFMPEG multiple thumbnails" , async ( span ) => {
232+ try {
233+ await execa `ffmpeg -i ${ nativeFilePath } -vf fps=1/${ interval } -t ${ duration } -q:v 75 -f image2 ${ framesDir } /frame_%03d.webp` ;
234+ } catch ( error ) {
235+ logger . error ( "Failed to extract frames" , { error } ) ;
236+ // Fallback to single frame if multiple frames fail
237+ await execa `ffmpeg -i ${ nativeFilePath } -frames:v 1 -q:v 75 -f image2 ${ framesDir } /frame_001.webp` ;
238+ }
239+ } ) ;
211240
212- await execa `ffmpeg -i ${ nativeFilePath } -frames:v 1 -q:v 75 -f image2 ${ frameFilePath } ` ;
241+ // Find the brightest frame
242+ const files = readdirSync ( framesDir ) . sort ( ) ; // Ensure consistent order
243+ let brightestFrame = "" ;
244+ let maxBrightness = - 1 ;
213245
214- span . end ( ) ;
215- } ) ;
246+ if ( files . length === 0 ) {
247+ throw new Error ( "No frames were extracted" ) ;
248+ }
249+
250+ // Default to first frame in case brightness calculation fails
251+ brightestFrame = path . join ( framesDir , files [ 0 ] ) ;
252+
253+ try {
254+ await Promise . all (
255+ files . map ( async ( file ) => {
256+ const framePath = path . join ( framesDir , file ) ;
257+ try {
258+ const stats = await sharp ( framePath ) . stats ( ) ;
259+
260+ // Calculate perceived brightness using the luminance formula
261+ const brightness =
262+ stats . channels [ 0 ] . mean * 0.299 + // Red
263+ stats . channels [ 1 ] . mean * 0.587 + // Green
264+ stats . channels [ 2 ] . mean * 0.114 ; // Blue
265+
266+ if ( brightness > maxBrightness ) {
267+ maxBrightness = brightness ;
268+ brightestFrame = framePath ;
269+ }
270+ } catch ( error ) {
271+ logger . error ( "Failed to process frame" , { file, error } ) ;
272+ }
273+ } )
274+ ) ;
275+
276+ logger . info ( "Selected brightest frame" , {
277+ brightness : maxBrightness ,
278+ frame : brightestFrame ,
279+ totalFrames : files . length ,
280+ } ) ;
281+ } catch ( error ) {
282+ logger . error ( "Failed to process frames for brightness" , { error } ) ;
283+ // We'll use the default first frame that was set earlier
284+ }
216285
217286 const uploadPromises : Promise < unknown > [ ] = [ ] ;
218287
219288 await Promise . all ( [
220289 logger
221290 . trace ( "Sharp large thumbnail" , async ( span ) => {
222- const buffer = await sharp ( frameFilePath )
291+ const buffer = await sharp ( brightestFrame )
223292 . webp ( { quality : 90 , effort : 6 , alphaQuality : 90 } )
224293 . toBuffer ( ) ;
225294
@@ -243,7 +312,7 @@ export const videoProcessingTask = schemaTask({
243312 } ) ,
244313 logger
245314 . trace ( "Sharp small thumbnail" , async ( span ) => {
246- const buffer = await sharp ( frameFilePath )
315+ const buffer = await sharp ( brightestFrame )
247316 . resize ( 1280 , 720 , { fit : "cover" } )
248317 . webp ( {
249318 quality : 70 ,
0 commit comments