ffmpeg_progress_yield.ffmpeg_progress_yield
1import re 2import subprocess 3from typing import Any, Callable, Iterator, List, Union 4 5 6def to_ms(**kwargs: Union[float, int, str]) -> int: 7 hour = int(kwargs.get("hour", 0)) 8 minute = int(kwargs.get("min", 0)) 9 sec = int(kwargs.get("sec", 0)) 10 ms = int(kwargs.get("ms", 0)) 11 12 return (hour * 60 * 60 * 1000) + (minute * 60 * 1000) + (sec * 1000) + ms 13 14 15class FfmpegProgress: 16 DUR_REGEX = re.compile( 17 r"Duration: (?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})\.(?P<ms>\d{2})" 18 ) 19 TIME_REGEX = re.compile( 20 r"out_time=(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})\.(?P<ms>\d{2})" 21 ) 22 23 def __init__(self, cmd: List[str], dry_run: bool = False) -> None: 24 """Initialize the FfmpegProgress class. 25 26 Args: 27 cmd (List[str]): A list of command line elements, e.g. ["ffmpeg", "-i", ...] 28 dry_run (bool, optional): Only show what would be done. Defaults to False. 29 """ 30 self.cmd = cmd 31 self.stderr: Union[str, None] = None 32 self.dry_run = dry_run 33 self.process: Any = None 34 self.stderr_callback: Union[Callable[[str], None], None] = None 35 36 def set_stderr_callback(self, callback: Callable[[str], None]) -> None: 37 """ 38 Set a callback function to be called on stderr output. 39 The callback function must accept a single string argument. 40 Note that this is called on every line of stderr output, so it can be called a lot. 41 Also note that stdout/stderr are joined into one stream, so you might get stdout output in the callback. 42 43 Args: 44 callback (Callable[[str], None]): A callback function that accepts a single string argument. 45 """ 46 if not callable(callback) or len(callback.__code__.co_varnames) != 1: 47 raise ValueError( 48 "Callback must be a function that accepts only one argument" 49 ) 50 51 self.stderr_callback = callback 52 53 def run_command_with_progress(self, popen_kwargs={}) -> Iterator[int]: 54 """ 55 Run an ffmpeg command, trying to capture the process output and calculate 56 the duration / progress. 57 Yields the progress in percent. 58 59 Args: 60 popen_kwargs (dict): A dict to specify extra arguments to the popen call, e.g. { creationflags: CREATE_NO_WINDOW } 61 62 Raises: 63 RuntimeError: If the command fails, an exception is raised. 64 65 Yields: 66 Iterator[int]: A generator that yields the progress in percent. 67 """ 68 if self.dry_run: 69 return 70 71 total_dur = None 72 73 cmd_with_progress = ( 74 [self.cmd[0]] + ["-progress", "-", "-nostats"] + self.cmd[1:] 75 ) 76 77 stderr = [] 78 79 self.process = subprocess.Popen( 80 cmd_with_progress, 81 stdin=subprocess.PIPE, # Apply stdin isolation by creating separate pipe. 82 stdout=subprocess.PIPE, 83 stderr=subprocess.STDOUT, 84 universal_newlines=False, 85 **popen_kwargs, 86 ) 87 88 yield 0 89 90 while True: 91 if self.process.stdout is None: 92 continue 93 94 stderr_line = ( 95 self.process.stdout.readline().decode("utf-8", errors="replace").strip() 96 ) 97 98 if self.stderr_callback: 99 self.stderr_callback(stderr_line) 100 101 if stderr_line == "" and self.process.poll() is not None: 102 break 103 104 stderr.append(stderr_line.strip()) 105 106 self.stderr = "\n".join(stderr) 107 108 total_dur_match = FfmpegProgress.DUR_REGEX.search(stderr_line) 109 if total_dur is None and total_dur_match: 110 total_dur = to_ms(**total_dur_match.groupdict()) 111 continue 112 113 if total_dur: 114 progress_time = FfmpegProgress.TIME_REGEX.search(stderr_line) 115 if progress_time: 116 elapsed_time = to_ms(**progress_time.groupdict()) 117 yield int(elapsed_time / total_dur * 100) 118 119 if self.process is None or self.process.returncode != 0: 120 _pretty_stderr = "\n".join(stderr) 121 raise RuntimeError(f"Error running command {self.cmd}: {_pretty_stderr}") 122 123 yield 100 124 self.process = None 125 126 def quit_gracefully(self) -> None: 127 """ 128 Quit the ffmpeg process by sending 'q' 129 130 Raises: 131 RuntimeError: If no process is found. 132 """ 133 if self.process is None: 134 raise RuntimeError("No process found. Did you run the command?") 135 136 self.process.communicate(input=b"q") 137 138 def quit(self) -> None: 139 """ 140 Quit the ffmpeg process by sending SIGKILL. 141 142 Raises: 143 RuntimeError: If no process is found. 144 """ 145 if self.process is None: 146 raise RuntimeError("No process found. Did you run the command?") 147 148 self.process.kill()
def
to_ms(**kwargs: Union[float, int, str]) -> int:
class
FfmpegProgress:
16class FfmpegProgress: 17 DUR_REGEX = re.compile( 18 r"Duration: (?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})\.(?P<ms>\d{2})" 19 ) 20 TIME_REGEX = re.compile( 21 r"out_time=(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})\.(?P<ms>\d{2})" 22 ) 23 24 def __init__(self, cmd: List[str], dry_run: bool = False) -> None: 25 """Initialize the FfmpegProgress class. 26 27 Args: 28 cmd (List[str]): A list of command line elements, e.g. ["ffmpeg", "-i", ...] 29 dry_run (bool, optional): Only show what would be done. Defaults to False. 30 """ 31 self.cmd = cmd 32 self.stderr: Union[str, None] = None 33 self.dry_run = dry_run 34 self.process: Any = None 35 self.stderr_callback: Union[Callable[[str], None], None] = None 36 37 def set_stderr_callback(self, callback: Callable[[str], None]) -> None: 38 """ 39 Set a callback function to be called on stderr output. 40 The callback function must accept a single string argument. 41 Note that this is called on every line of stderr output, so it can be called a lot. 42 Also note that stdout/stderr are joined into one stream, so you might get stdout output in the callback. 43 44 Args: 45 callback (Callable[[str], None]): A callback function that accepts a single string argument. 46 """ 47 if not callable(callback) or len(callback.__code__.co_varnames) != 1: 48 raise ValueError( 49 "Callback must be a function that accepts only one argument" 50 ) 51 52 self.stderr_callback = callback 53 54 def run_command_with_progress(self, popen_kwargs={}) -> Iterator[int]: 55 """ 56 Run an ffmpeg command, trying to capture the process output and calculate 57 the duration / progress. 58 Yields the progress in percent. 59 60 Args: 61 popen_kwargs (dict): A dict to specify extra arguments to the popen call, e.g. { creationflags: CREATE_NO_WINDOW } 62 63 Raises: 64 RuntimeError: If the command fails, an exception is raised. 65 66 Yields: 67 Iterator[int]: A generator that yields the progress in percent. 68 """ 69 if self.dry_run: 70 return 71 72 total_dur = None 73 74 cmd_with_progress = ( 75 [self.cmd[0]] + ["-progress", "-", "-nostats"] + self.cmd[1:] 76 ) 77 78 stderr = [] 79 80 self.process = subprocess.Popen( 81 cmd_with_progress, 82 stdin=subprocess.PIPE, # Apply stdin isolation by creating separate pipe. 83 stdout=subprocess.PIPE, 84 stderr=subprocess.STDOUT, 85 universal_newlines=False, 86 **popen_kwargs, 87 ) 88 89 yield 0 90 91 while True: 92 if self.process.stdout is None: 93 continue 94 95 stderr_line = ( 96 self.process.stdout.readline().decode("utf-8", errors="replace").strip() 97 ) 98 99 if self.stderr_callback: 100 self.stderr_callback(stderr_line) 101 102 if stderr_line == "" and self.process.poll() is not None: 103 break 104 105 stderr.append(stderr_line.strip()) 106 107 self.stderr = "\n".join(stderr) 108 109 total_dur_match = FfmpegProgress.DUR_REGEX.search(stderr_line) 110 if total_dur is None and total_dur_match: 111 total_dur = to_ms(**total_dur_match.groupdict()) 112 continue 113 114 if total_dur: 115 progress_time = FfmpegProgress.TIME_REGEX.search(stderr_line) 116 if progress_time: 117 elapsed_time = to_ms(**progress_time.groupdict()) 118 yield int(elapsed_time / total_dur * 100) 119 120 if self.process is None or self.process.returncode != 0: 121 _pretty_stderr = "\n".join(stderr) 122 raise RuntimeError(f"Error running command {self.cmd}: {_pretty_stderr}") 123 124 yield 100 125 self.process = None 126 127 def quit_gracefully(self) -> None: 128 """ 129 Quit the ffmpeg process by sending 'q' 130 131 Raises: 132 RuntimeError: If no process is found. 133 """ 134 if self.process is None: 135 raise RuntimeError("No process found. Did you run the command?") 136 137 self.process.communicate(input=b"q") 138 139 def quit(self) -> None: 140 """ 141 Quit the ffmpeg process by sending SIGKILL. 142 143 Raises: 144 RuntimeError: If no process is found. 145 """ 146 if self.process is None: 147 raise RuntimeError("No process found. Did you run the command?") 148 149 self.process.kill()
FfmpegProgress(cmd: List[str], dry_run: bool = False)
24 def __init__(self, cmd: List[str], dry_run: bool = False) -> None: 25 """Initialize the FfmpegProgress class. 26 27 Args: 28 cmd (List[str]): A list of command line elements, e.g. ["ffmpeg", "-i", ...] 29 dry_run (bool, optional): Only show what would be done. Defaults to False. 30 """ 31 self.cmd = cmd 32 self.stderr: Union[str, None] = None 33 self.dry_run = dry_run 34 self.process: Any = None 35 self.stderr_callback: Union[Callable[[str], None], None] = None
Initialize the FfmpegProgress class.
Args
- cmd (List[str]): A list of command line elements, e.g. ["ffmpeg", "-i", ...]
- dry_run (bool, optional): Only show what would be done. Defaults to False.
DUR_REGEX = re.compile('Duration: (?P<hour>\\d{2}):(?P<min>\\d{2}):(?P<sec>\\d{2})\\.(?P<ms>\\d{2})')
TIME_REGEX = re.compile('out_time=(?P<hour>\\d{2}):(?P<min>\\d{2}):(?P<sec>\\d{2})\\.(?P<ms>\\d{2})')
def
set_stderr_callback(self, callback: Callable[[str], NoneType]) -> None:
37 def set_stderr_callback(self, callback: Callable[[str], None]) -> None: 38 """ 39 Set a callback function to be called on stderr output. 40 The callback function must accept a single string argument. 41 Note that this is called on every line of stderr output, so it can be called a lot. 42 Also note that stdout/stderr are joined into one stream, so you might get stdout output in the callback. 43 44 Args: 45 callback (Callable[[str], None]): A callback function that accepts a single string argument. 46 """ 47 if not callable(callback) or len(callback.__code__.co_varnames) != 1: 48 raise ValueError( 49 "Callback must be a function that accepts only one argument" 50 ) 51 52 self.stderr_callback = callback
Set a callback function to be called on stderr output. The callback function must accept a single string argument. Note that this is called on every line of stderr output, so it can be called a lot. Also note that stdout/stderr are joined into one stream, so you might get stdout output in the callback.
Args
- callback (Callable[[str], None]): A callback function that accepts a single string argument.
def
run_command_with_progress(self, popen_kwargs={}) -> Iterator[int]:
54 def run_command_with_progress(self, popen_kwargs={}) -> Iterator[int]: 55 """ 56 Run an ffmpeg command, trying to capture the process output and calculate 57 the duration / progress. 58 Yields the progress in percent. 59 60 Args: 61 popen_kwargs (dict): A dict to specify extra arguments to the popen call, e.g. { creationflags: CREATE_NO_WINDOW } 62 63 Raises: 64 RuntimeError: If the command fails, an exception is raised. 65 66 Yields: 67 Iterator[int]: A generator that yields the progress in percent. 68 """ 69 if self.dry_run: 70 return 71 72 total_dur = None 73 74 cmd_with_progress = ( 75 [self.cmd[0]] + ["-progress", "-", "-nostats"] + self.cmd[1:] 76 ) 77 78 stderr = [] 79 80 self.process = subprocess.Popen( 81 cmd_with_progress, 82 stdin=subprocess.PIPE, # Apply stdin isolation by creating separate pipe. 83 stdout=subprocess.PIPE, 84 stderr=subprocess.STDOUT, 85 universal_newlines=False, 86 **popen_kwargs, 87 ) 88 89 yield 0 90 91 while True: 92 if self.process.stdout is None: 93 continue 94 95 stderr_line = ( 96 self.process.stdout.readline().decode("utf-8", errors="replace").strip() 97 ) 98 99 if self.stderr_callback: 100 self.stderr_callback(stderr_line) 101 102 if stderr_line == "" and self.process.poll() is not None: 103 break 104 105 stderr.append(stderr_line.strip()) 106 107 self.stderr = "\n".join(stderr) 108 109 total_dur_match = FfmpegProgress.DUR_REGEX.search(stderr_line) 110 if total_dur is None and total_dur_match: 111 total_dur = to_ms(**total_dur_match.groupdict()) 112 continue 113 114 if total_dur: 115 progress_time = FfmpegProgress.TIME_REGEX.search(stderr_line) 116 if progress_time: 117 elapsed_time = to_ms(**progress_time.groupdict()) 118 yield int(elapsed_time / total_dur * 100) 119 120 if self.process is None or self.process.returncode != 0: 121 _pretty_stderr = "\n".join(stderr) 122 raise RuntimeError(f"Error running command {self.cmd}: {_pretty_stderr}") 123 124 yield 100 125 self.process = None
Run an ffmpeg command, trying to capture the process output and calculate the duration / progress. Yields the progress in percent.
Args
- popen_kwargs (dict): A dict to specify extra arguments to the popen call, e.g. { creationflags: CREATE_NO_WINDOW }
Raises
- RuntimeError: If the command fails, an exception is raised.
Yields
Iterator[int]: A generator that yields the progress in percent.
def
quit_gracefully(self) -> None:
127 def quit_gracefully(self) -> None: 128 """ 129 Quit the ffmpeg process by sending 'q' 130 131 Raises: 132 RuntimeError: If no process is found. 133 """ 134 if self.process is None: 135 raise RuntimeError("No process found. Did you run the command?") 136 137 self.process.communicate(input=b"q")
Quit the ffmpeg process by sending 'q'
Raises
- RuntimeError: If no process is found.
def
quit(self) -> None:
139 def quit(self) -> None: 140 """ 141 Quit the ffmpeg process by sending SIGKILL. 142 143 Raises: 144 RuntimeError: If no process is found. 145 """ 146 if self.process is None: 147 raise RuntimeError("No process found. Did you run the command?") 148 149 self.process.kill()
Quit the ffmpeg process by sending SIGKILL.
Raises
- RuntimeError: If no process is found.