@@ -34,52 +34,117 @@ class Stream(TypedDict):
3434 width : int
3535
3636
37- class ProbeFormat (TypedDict ):
38- filename : str
39- duration : str
40-
41-
4237class ProbeOutput (TypedDict ):
4338 streams : T .List [Stream ]
44- format : T .Dict
4539
4640
4741class FFmpegNotFoundError (Exception ):
4842 pass
4943
5044
45+ _MAX_STDERR_LENGTH = 2048
46+
47+
48+ def _truncate_begin (s : str ) -> str :
49+ if _MAX_STDERR_LENGTH < len (s ):
50+ return "..." + s [- _MAX_STDERR_LENGTH :]
51+ else :
52+ return s
53+
54+
55+ def _truncate_end (s : str ) -> str :
56+ if _MAX_STDERR_LENGTH < len (s ):
57+ return s [:_MAX_STDERR_LENGTH ] + "..."
58+ else :
59+ return s
60+
61+
62+ class FFmpegCalledProcessError (Exception ):
63+ def __init__ (self , ex : subprocess .CalledProcessError ):
64+ self .inner_ex = ex
65+
66+ def __str__ (self ) -> str :
67+ msg = str (self .inner_ex )
68+ if self .inner_ex .stderr is not None :
69+ try :
70+ stderr = self .inner_ex .stderr .decode ("utf-8" )
71+ except UnicodeDecodeError :
72+ stderr = str (self .inner_ex .stderr )
73+ msg += f"\n STDERR: { _truncate_begin (stderr )} "
74+ return msg
75+
76+
5177class FFMPEG :
5278 def __init__ (
53- self , ffmpeg_path : str = "ffmpeg" , ffprobe_path : str = "ffprobe"
79+ self ,
80+ ffmpeg_path : str = "ffmpeg" ,
81+ ffprobe_path : str = "ffprobe" ,
82+ stderr : T .Optional [int ] = None ,
5483 ) -> None :
84+ """
85+ ffmpeg_path: path to ffmpeg binary
86+ ffprobe_path: path to ffprobe binary
87+ stderr: param passed to subprocess.run to control whether to capture stderr
88+ """
5589 self .ffmpeg_path = ffmpeg_path
5690 self .ffprobe_path = ffprobe_path
91+ self .stderr = stderr
5792
5893 def _run_ffprobe_json (self , cmd : T .List [str ]) -> T .Dict :
5994 full_cmd = [self .ffprobe_path , "-print_format" , "json" , * cmd ]
6095 LOG .info (f"Extracting video information: { ' ' .join (full_cmd )} " )
6196 try :
62- output = subprocess .check_output (full_cmd )
97+ completed = subprocess .run (
98+ full_cmd ,
99+ check = True ,
100+ stdout = subprocess .PIPE ,
101+ stderr = self .stderr ,
102+ )
63103 except FileNotFoundError :
64104 raise FFmpegNotFoundError (
65105 f'The ffprobe command "{ self .ffprobe_path } " not found'
66106 )
107+ except subprocess .CalledProcessError as ex :
108+ raise FFmpegCalledProcessError (ex ) from ex
109+
67110 try :
68- return json .loads (output )
111+ stdout = completed .stdout .decode ("utf-8" )
112+ except UnicodeDecodeError :
113+ raise RuntimeError (
114+ f"Error decoding ffprobe output as unicode: { _truncate_end (str (completed .stdout ))} "
115+ )
116+
117+ try :
118+ output = json .loads (stdout )
69119 except json .JSONDecodeError :
70120 raise RuntimeError (
71- f"Error JSON decoding ffprobe output: { output .decode ('utf-8' )} "
121+ f"Error JSON decoding ffprobe output: { _truncate_end (stdout )} "
122+ )
123+
124+ # This check is for macOS:
125+ # ffprobe -hide_banner -print_format json not_exists
126+ # you will get exit code == 0 with the following stdout and stderr:
127+ # {
128+ # }
129+ # not_exists: No such file or directory
130+ if not output :
131+ raise RuntimeError (
132+ f"Empty JSON ffprobe output with STDERR: { _truncate_begin (str (completed .stderr ))} "
72133 )
73134
135+ return output
136+
74137 def _run_ffmpeg (self , cmd : T .List [str ]) -> None :
75138 full_cmd = [self .ffmpeg_path , * cmd ]
76139 LOG .info (f"Extracting frames: { ' ' .join (full_cmd )} " )
77140 try :
78- subprocess .check_call (full_cmd )
141+ subprocess .run (full_cmd , check = True , stderr = self . stderr )
79142 except FileNotFoundError :
80143 raise FFmpegNotFoundError (
81144 f'The ffmpeg command "{ self .ffmpeg_path } " not found'
82145 )
146+ except subprocess .CalledProcessError as ex :
147+ raise FFmpegCalledProcessError (ex ) from ex
83148
84149 def probe_format_and_streams (self , video_path : Path ) -> ProbeOutput :
85150 cmd = [
@@ -90,24 +155,6 @@ def probe_format_and_streams(self, video_path: Path) -> ProbeOutput:
90155 ]
91156 return T .cast (ProbeOutput , self ._run_ffprobe_json (cmd ))
92157
93- def extract_stream (self , source : Path , dest : Path , stream_id : int ) -> None :
94- cmd = [
95- "-hide_banner" ,
96- "-i" ,
97- str (source ),
98- "-y" , # overwrite - potentially dangerous
99- "-nostats" ,
100- "-codec" ,
101- "copy" ,
102- "-map" ,
103- f"0:{ stream_id } " ,
104- "-f" ,
105- "rawvideo" ,
106- str (dest ),
107- ]
108-
109- self ._run_ffmpeg (cmd )
110-
111158 def extract_frames (
112159 self ,
113160 video_path : Path ,
0 commit comments