11import logging
22import tempfile
3+ import subprocess
4+ import os
35from typing import List , Dict , Any , Optional
46from concurrent .futures import ThreadPoolExecutor , as_completed
57import cv2
@@ -97,6 +99,81 @@ def _get_video_metadata(self, video_path: str) -> Dict[str, Any]:
9799 logger .debug ("Cached metadata: fps=%.2f, frames=%d, duration=%.2fs" , fps , frame_count , duration )
98100 return metadata
99101
102+ def _get_video_codec (self , video_path : str ) -> str :
103+ """Get the video codec using ffprobe."""
104+ try :
105+ cmd = [
106+ "ffprobe" ,
107+ "-v" , "error" ,
108+ "-select_streams" , "v:0" ,
109+ "-show_entries" , "stream=codec_name" ,
110+ "-of" , "default=noprint_wrappers=1:nokey=1" ,
111+ video_path
112+ ]
113+ codec = subprocess .check_output (cmd ).decode ().strip ()
114+ return codec
115+ except Exception as e :
116+ logger .warning (f"Failed to detect codec for { video_path } : { e } " )
117+ return "unknown"
118+
119+ def _transcode_to_h264 (self , input_path : str , codec : str ) -> str :
120+ """
121+ Transcode video to H.264 codec using ffmpeg.
122+
123+ Arguments:
124+ input_path: Path to the input video file.
125+ codec: Detected codec of the input video.
126+
127+ Returns:
128+ Path to the transcoded video file.
129+ """
130+ if codec == "h264" :
131+ return input_path # No transcoding needed
132+
133+ transcoded_temp = tempfile .NamedTemporaryFile (suffix = '.mp4' , delete = False )
134+ transcoded_path = transcoded_temp .name
135+ transcoded_temp .close () # Close so ffmpeg can write to it
136+
137+ logger .info (f"Transcoding video ({ codec } -> h264) to ensure compatibility: { input_path } -> { transcoded_path } " )
138+
139+ # FFmpeg command: -y (overwrite), -i (input), -c:v libx264 (video codec), -c:a aac (audio), -preset fast
140+ cmd = [
141+ "ffmpeg" , "-y" , "-v" , "error" ,
142+ "-i" , input_path ,
143+ "-c:v" , "libx264" ,
144+ "-preset" , "fast" ,
145+ "-c:a" , "aac" ,
146+ transcoded_path
147+ ]
148+
149+ try :
150+ completed_process = subprocess .run (
151+ cmd ,
152+ check = True ,
153+ capture_output = True ,
154+ text = True ,
155+ )
156+ # Log any non-fatal stderr output for debugging at a lower level
157+ if completed_process .stderr :
158+ logger .debug (
159+ "ffmpeg stderr output during successful transcoding for %s: %s" ,
160+ input_path ,
161+ completed_process .stderr ,
162+ )
163+ return transcoded_path
164+ except subprocess .CalledProcessError as e :
165+ logger .error (
166+ "ffmpeg transcoding failed for %s with return code %s. stderr: %s" ,
167+ input_path ,
168+ e .returncode ,
169+ e .stderr ,
170+ )
171+ # Cleanup partial file if transcoding fails
172+ if os .path .exists (transcoded_path ):
173+ os .unlink (transcoded_path )
174+ raise
175+
176+
100177 def process_video_from_bytes (
101178 self ,
102179 video_bytes : bytes ,
@@ -107,20 +184,59 @@ def process_video_from_bytes(
107184 """Process video from uploaded bytes with automatic temp file cleanup."""
108185 logger .info ("Starting preprocessing: video_id=%s, filename=%s" , video_id , filename )
109186
110- with tempfile .NamedTemporaryFile (suffix = '.mp4' , delete = True ) as temp_file :
111- temp_file .write (video_bytes )
112- temp_file .flush ()
187+ input_path = None
188+ processing_path = None
113189
114- try :
115- return self .process_video (
116- video_path = temp_file .name ,
117- video_id = video_id ,
118- filename = filename ,
119- hashed_identifier = hashed_identifier
120- )
121- except Exception as e :
122- logger .error ("Processing failed for video_id=%s: %s" , video_id , e )
123- raise
190+ try :
191+ # Create a temp file for the original upload
192+ with tempfile .NamedTemporaryFile (suffix = '.mp4' , delete = False ) as input_temp :
193+ input_path = input_temp .name
194+ input_temp .write (video_bytes )
195+ input_temp .flush ()
196+
197+ processing_path = input_path
198+
199+ # Detect codec
200+ codec = self ._get_video_codec (input_path )
201+ logger .info (f"Detected video codec: { codec } " )
202+
203+ if codec != "h264" :
204+ processing_path = self ._transcode_to_h264 (input_path , codec )
205+ else :
206+ logger .info ("Video is already H.264, skipping transcoding" )
207+
208+ return self .process_video (
209+ video_path = processing_path ,
210+ video_id = video_id ,
211+ filename = filename ,
212+ hashed_identifier = hashed_identifier
213+ )
214+
215+ except subprocess .CalledProcessError as e :
216+ stderr_output = ""
217+ if getattr (e , "stderr" , None ):
218+ if isinstance (e .stderr , bytes ):
219+ stderr_output = e .stderr .decode ("utf-8" , errors = "replace" )
220+ else :
221+ stderr_output = str (e .stderr )
222+ if stderr_output :
223+ logger .error ("FFmpeg transcoding failed. Stderr: %s" , stderr_output )
224+ error_message = f"Failed to process video codec. FFmpeg stderr: { stderr_output } "
225+ else :
226+ logger .error ("FFmpeg transcoding failed: %s" , e )
227+ error_message = "Failed to process video codec"
228+ raise RuntimeError (error_message ) from e
229+ except Exception as e :
230+ logger .error ("Processing failed for video_id=%s: %s" , video_id , e )
231+ raise
232+ finally :
233+ # Cleanup all temp files
234+ if input_path and os .path .exists (input_path ):
235+ os .unlink (input_path )
236+
237+ # If processing_path is different (transcoded), clean it up too
238+ if processing_path and processing_path != input_path and os .path .exists (processing_path ):
239+ os .unlink (processing_path )
124240
125241 def process_video (
126242 self ,
@@ -198,7 +314,7 @@ def _process_single_chunk(
198314 Thread-safe for parallel execution.
199315 Returns None if processing fails.
200316 """
201- #TODO: Specifcy explicit return type and not just a dict in docstring
317+ #TODO: Specify explicit return type and not just a dict in docstring
202318 try :
203319 # Extract frames with complexity analysis
204320 frames , sampling_fps , complexity_score = self .extractor .extract_frames (video_path , chunk )
0 commit comments