33import shutil
44import tempfile
55import traceback
6- import json
6+ import subprocess
77
88
99async def process_video_with_ffmpeg (input_path , output_path , params ):
10- """Process a video with ffmpeg based on the provided parameters."""
1110 current_input = input_path
1211 temp_files = [input_path ]
1312
1413 try :
15- # Debug: Print all parameters
16- print ("Processing video with parameters:" , params )
17-
18- # Handle segment removal
1914 if params .get ("remove_segment_start" ) and params .get ("remove_segment_end" ):
2015 start = float (params ["remove_segment_start" ])
2116 end = float (params ["remove_segment_end" ])
@@ -53,9 +48,6 @@ async def process_video_with_ffmpeg(input_path, output_path, params):
5348 )
5449 stdout , stderr = await process .communicate ()
5550
56- if process .returncode != 0 :
57- print (f"FFmpeg before segment error: { stderr .decode ()} " )
58-
5951 cmd_after = [
6052 "ffmpeg" ,
6153 "-y" ,
@@ -81,9 +73,6 @@ async def process_video_with_ffmpeg(input_path, output_path, params):
8173 )
8274 stdout , stderr = await process .communicate ()
8375
84- if process .returncode != 0 :
85- print (f"FFmpeg after segment error: { stderr .decode ()} " )
86-
8776 concat_file = tempfile .mktemp (suffix = "_concat.txt" )
8877 temp_files .append (concat_file )
8978
@@ -122,30 +111,18 @@ async def process_video_with_ffmpeg(input_path, output_path, params):
122111 )
123112 stdout , stderr = await process .communicate ()
124113
125- if process .returncode != 0 :
126- print (f"FFmpeg concat error: { stderr .decode ()} " )
127- else :
128- current_input = segment_output
114+ current_input = segment_output
129115
130116 for file in [before_segment , after_segment , concat_file ]:
131117 try :
132118 if os .path .exists (file ):
133119 os .unlink (file )
134120 except :
135121 pass
136-
137- # Handle zoom effect with individual coordinates
138- print ("Processing zoom effect...." ,
139- params .get ("zoom_top_left_x" ),
140- params .get ("zoom_top_left_y" ),
141- params .get ("zoom_bottom_right_x" ),
142- params .get ("zoom_bottom_right_y" ),
143- params .get ("zoom_timestamp" ))
144122
145123 if (params .get ("zoom_top_left_x" ) and params .get ("zoom_top_left_y" ) and
146124 params .get ("zoom_bottom_right_x" ) and params .get ("zoom_bottom_right_y" )):
147125 try :
148- # Parse zoom parameters from individual coordinates
149126 zoom_top_left = [
150127 float (params .get ("zoom_top_left_x" )),
151128 float (params .get ("zoom_top_left_y" ))
@@ -155,30 +132,23 @@ async def process_video_with_ffmpeg(input_path, output_path, params):
155132 float (params .get ("zoom_bottom_right_y" ))
156133 ]
157134 zoom_duration = float (params .get ("zoom_duration" , 2.0 ))
158- zoom_timestamp = params .get ("zoom_timestamp" )
159-
160- print (f"Parsed zoom parameters: top_left={ zoom_top_left } , bottom_right={ zoom_bottom_right } , duration={ zoom_duration } , timestamp={ zoom_timestamp } " )
135+ zoom_start_frame = params .get ("zoom_start_frame" )
161136
162- # Call zoom_in function with timestamp
163137 zoom_output , zoom_temp_files = await zoom_in (
164138 current_input ,
165139 output_path ,
166140 zoom_top_left ,
167141 zoom_bottom_right ,
168142 zoom_duration ,
169- zoom_timestamp
143+ zoom_start_frame
170144 )
171145
172- # Update current input and temp files
173146 if zoom_output and zoom_output != current_input :
174147 current_input = zoom_output
175148 temp_files .extend (zoom_temp_files )
176149 except Exception as e :
177- print (f"Error applying zoom effect: { str (e )} " )
178150 traceback .print_exc ()
179- # Continue with processing without zoom effect
180151
181- # For MP4 output, just ensure it's in compatible format
182152 final_mp4 = output_path
183153 cmd = [
184154 "ffmpeg" ,
@@ -204,198 +174,131 @@ async def process_video_with_ffmpeg(input_path, output_path, params):
204174 stdout , stderr = await process .communicate ()
205175
206176 if process .returncode != 0 :
207- print (f"FFmpeg final MP4 conversion error: { stderr .decode ()} " )
208177 shutil .copy (current_input , final_mp4 )
209178
210179 current_input = final_mp4
211180 return current_input , temp_files
212181
213182 except Exception as e :
214- print (f"Error in process_video_with_ffmpeg: { str (e )} " )
215183 traceback .print_exc ()
216184 return input_path , temp_files
217185
218- async def zoom_in (input_path , output_path , top_left = None , bottom_right = None , zoom_duration = 2.0 , zoom_timestamp = None ):
186+ async def zoom_in (input_path , output_path , top_left = None , bottom_right = None , zoom_duration = 2.0 , zoom_start_frame = None ):
187+ from ffmpy import FFmpeg
219188 temp_files = []
220189
221190 try :
222- # Validate inputs
223191 if not input_path or not os .path .exists (input_path ):
224- print (f"Input file does not exist: { input_path } " )
225192 return input_path , temp_files
226193
227- # Get zoom timestamp (when to start the zoom effect)
228- if zoom_timestamp is None :
229- print ("No zoom timestamp provided, defaulting to 2 seconds" )
230- zoom_timestamp = 2.0
194+ if zoom_start_frame is None :
195+ zoom_start_frame = 60
231196 else :
232197 try :
233- zoom_timestamp = float (zoom_timestamp )
234- print (f"Using zoom timestamp: { zoom_timestamp } seconds" )
198+ zoom_start_frame = float (zoom_start_frame )
235199 except (ValueError , TypeError ):
236- print ("Invalid zoom timestamp, defaulting to 2 seconds" )
237- zoom_timestamp = 2.0
200+ zoom_start_frame = 60
238201
239- # Default values if coordinates are None
240202 if top_left is None :
241- print ("Using default top_left coordinates (0.25, 0.25)" )
242203 top_left = [0.25 , 0.25 ]
243204
244205 if bottom_right is None :
245- print ("Using default bottom_right coordinates (0.75, 0.75)" )
246206 bottom_right = [0.75 , 0.75 ]
247207
248- # Print coordinates for debugging
249- print (f"Zoom coordinates: top_left={ top_left } , bottom_right={ bottom_right } " )
250-
251- # Safely extract coordinates
252208 try :
253209 x1 , y1 = float (top_left [0 ]), float (top_left [1 ])
254210 x2 , y2 = float (bottom_right [0 ]), float (bottom_right [1 ])
255211 except (TypeError , ValueError , IndexError ) as e :
256- print (f"Error converting coordinates to float: { e } " )
257- print ("Using default coordinates" )
258212 x1 , y1 = 0.25 , 0.25
259213 x2 , y2 = 0.75 , 0.75
260214
261- # Ensure coordinates are within valid range
262215 x1 = max (0.0 , min (0.9 , x1 ))
263216 y1 = max (0.0 , min (0.9 , y1 ))
264217 x2 = max (0.1 , min (1.0 , x2 ))
265218 y2 = max (0.1 , min (1.0 , y2 ))
266219
267- # Ensure x2 > x1 and y2 > y1
268220 if x2 <= x1 :
269221 x1 , x2 = 0.25 , 0.75
270222 if y2 <= y1 :
271223 y1 , y2 = 0.25 , 0.75
272224
273- # Get video dimensions
274- # cmd_dimensions = [
275- # "ffprobe",
276- # "-v", "error",
277- # "-select_streams", "v:0",
278- # "-show_entries", "stream=width,height",
279- # "-of", "csv=s=x:p=0",
280- # input_path
281- # ]
282-
283- # process = await asyncio.create_subprocess_exec(
284- # *cmd_dimensions,
285- # stdout=asyncio.subprocess.PIPE,
286- # stderr=asyncio.subprocess.PIPE,
287- # )
288- # stdout, stderr = await process.communicate()
225+ duration_cmd = f"ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \" { input_path } \" "
289226
290- # try:
291- # dimensions = stdout.decode().strip().split('x')
292- # width = int(dimensions[0])
293- # height = int(dimensions[1])
294- # print(f"Video dimensions: {width}x{height}")
295- # except (ValueError, IndexError):
296- # print("Could not determine video dimensions, using defaults")
297- # width, height = 1920, 1080
298-
299- # Get video duration
300- cmd_duration = [
301- "ffprobe" ,
302- "-v" , "error" ,
303- "-show_entries" , "format=duration" ,
304- "-of" , "default=noprint_wrappers=1:nokey=1" ,
305- input_path
306- ]
307-
308- process = await asyncio .create_subprocess_exec (
309- * cmd_duration ,
227+ process = await asyncio .create_subprocess_shell (
228+ duration_cmd ,
310229 stdout = asyncio .subprocess .PIPE ,
311230 stderr = asyncio .subprocess .PIPE ,
312231 )
313232 stdout , stderr = await process .communicate ()
314233
315234 try :
316- video_duration = float (stdout .decode ().strip ())
317- print (f"Video duration: { video_duration } seconds" )
318- except (ValueError , TypeError ):
319- print (f"Could not determine video duration, using default" )
235+ output = stdout .decode ().strip ()
236+ video_duration = float (output )
237+ except (ValueError , TypeError ) as e :
320238 video_duration = 10.0
321239
322240 zoom_duration = min (float (zoom_duration ), video_duration )
323-
324- # Apply the zoom pan effect directly without box
241+
325242 zoom_output = tempfile .mktemp (suffix = "_zoomed.mp4" )
326243 temp_files .append (zoom_output )
327244
328- # Calculate the center of the specified area
329245 center_x = (x1 + x2 ) / 2
330246 center_y = (y1 + y2 ) / 2
331247
332- # Create a complex filter for the zoom pan effect
333- fps = 25
334- zoom_in_frames = int (0.3 * fps )
335- hold_frames = int (2 * fps ) # 2 seconds to hold
336- zoom_out_frames = int (0.3 * fps )
337- total_frames = zoom_in_frames + hold_frames + zoom_out_frames
248+ # zoom_start_frame = 60 # *** TODO: causing issues when replaced with param
249+ zoom_in_frames = 15 # Zoom in over 30 frames
250+ zoom_out_frames = 15 # Zoom out over 30 frames
251+ max_zoom = 2.0 # Maximum zoom level
252+ hold_frames = 60
338253
339- # Max zoom factor (reduced from 2.0 to 1.5)
340- max_zoom = 1.5
341-
342- # Set fixed dimensions
343254 width , height = 1920 , 1080
344255
345- # Update the complex filter to remove padding/black bars
346256 complex_filter = (
347- f"[0:v]split=3[v1][v2][v3];"
348- f"[v1]trim=0:{ zoom_timestamp } ,setpts=PTS-STARTPTS,scale={ width } :{ height } [intro];" # Direct scaling
349- f"[v2]trim={ zoom_timestamp } :{ zoom_timestamp + zoom_duration } ,setpts=PTS-STARTPTS,"
350- f"scale={ width } :{ height } ," # Direct scaling without preserving aspect ratio
351- f"zoompan=z='if(lt(on,{ zoom_in_frames } ),"
352- f" 1+({ max_zoom } -1)*on/{ zoom_in_frames } ," # zoom in phase
353- f" if(lt(on,{ zoom_in_frames + hold_frames } ),"
354- f" { max_zoom } ," # hold phase
355- f" { max_zoom } -({ max_zoom } -1)*(on-{ zoom_in_frames + hold_frames } )/{ zoom_out_frames } " # zoom out phase
356- f" )"
357- f" )':"
358- f"x='iw*{ center_x } -iw/zoom*{ center_x } ':y='ih*{ center_y } -ih/zoom*{ center_y } ':"
359- f"d={ total_frames } :s={ width } x{ height } :fps={ fps } [zoom];"
360- f"[v3]trim={ zoom_timestamp + zoom_duration } :{ video_duration } ,setpts=PTS-STARTPTS,scale={ width } :{ height } [outro];" # Direct scaling
361- f"[intro][zoom][outro]concat=n=3:v=1:a=0[outv]"
257+ f"[0:v]zoompan="
258+ f"z='if(between(on,{ zoom_start_frame } ,{ zoom_start_frame + zoom_in_frames + hold_frames + zoom_out_frames } ),"
259+ f"if(lt(on-{ zoom_start_frame } ,{ zoom_in_frames } ),"
260+ f"1+(({ max_zoom } -1)*(on-{ zoom_start_frame } )/{ zoom_in_frames } ),"
261+ f"if(lt(on-{ zoom_start_frame } ,{ zoom_in_frames + hold_frames } ),"
262+ f"{ max_zoom } ,"
263+ f"{ max_zoom } -(({ max_zoom } -1)*((on-{ zoom_start_frame } -{ zoom_in_frames } -{ hold_frames } ))/{ zoom_out_frames } )"
264+ f")),1)':"
265+ f"x='iw*{ center_x } -iw/zoom*{ center_x } ':"
266+ f"y='ih*{ center_y } -ih/zoom*{ center_y } ':"
267+ f"d=0:"
268+ f"s={ width } x{ height } [outv]"
362269 )
363270
364- cmd_zoom = [
365- "ffmpeg" ,
366- "-y" ,
367- "-i" , input_path ,
368- "-filter_complex" , complex_filter ,
369- "-map" , "[outv]" ,
370- "-map" , "0:a?" , # Make audio mapping optional with ?
371- "-c:v" , "libx264" ,
372- "-tag:v" , "avc3" ,
373- "-profile:v" , "baseline" ,
374- "-level" , "3.0" ,
375- "-pix_fmt" , "yuv420p" ,
376- "-preset" , "fast" ,
377- "-c:a" , "copy" ,
378- zoom_output ,
379- ]
380-
381- print (f"Running zoom pan effect command: { ' ' .join (cmd_zoom )} " )
271+ ff = FFmpeg (
272+ inputs = {input_path : None },
273+ outputs = {
274+ zoom_output : (
275+ f"-filter_complex \" { complex_filter } \" "
276+ f"-map \" [outv]\" "
277+ f"-map 0:a? "
278+ f"-c:v libx264 "
279+ f"-pix_fmt yuv420p "
280+ f"-movflags +faststart "
281+ f"-preset fast "
282+ f"-c:a aac "
283+ f"-y"
284+ )
285+ }
286+ )
382287
288+ cmd_parts = ff .cmd .split ()
383289 process = await asyncio .create_subprocess_exec (
384- * cmd_zoom ,
290+ * cmd_parts ,
385291 stdout = asyncio .subprocess .PIPE ,
386292 stderr = asyncio .subprocess .PIPE ,
387293 )
388294 stdout , stderr = await process .communicate ()
389295
390296 if process .returncode != 0 :
391- print (f"FFmpeg zoom pan effect error: { stderr .decode ()} " )
392- print (stderr .decode ())
393- # Return the input if zoom fails
297+ print ("FFmpeg error:" , stderr .decode ())
394298 return input_path , temp_files
395299
396300 return zoom_output , temp_files
397301
398302 except Exception as e :
399- print (f"Error in zoom_in: { str (e )} " )
400303 traceback .print_exc ()
401304 return input_path , temp_files
0 commit comments