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:
 7def to_ms(**kwargs: Union[float, int, str]) -> int:
 8    hour = int(kwargs.get("hour", 0))
 9    minute = int(kwargs.get("min", 0))
10    sec = int(kwargs.get("sec", 0))
11    ms = int(kwargs.get("ms", 0))
12
13    return (hour * 60 * 60 * 1000) + (minute * 60 * 1000) + (sec * 1000) + ms
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.