better-ffmpeg-progress 2.6.1__tar.gz → 3.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- better_ffmpeg_progress-3.1.0/PKG-INFO +79 -0
- better_ffmpeg_progress-3.1.0/README.md +61 -0
- better_ffmpeg_progress-3.1.0/better_ffmpeg_progress/better_ffmpeg_progress.py +277 -0
- better_ffmpeg_progress-3.1.0/better_ffmpeg_progress.egg-info/PKG-INFO +79 -0
- {better_ffmpeg_progress-2.6.1 → better_ffmpeg_progress-3.1.0}/setup.py +1 -1
- better_ffmpeg_progress-2.6.1/PKG-INFO +0 -113
- better_ffmpeg_progress-2.6.1/README.md +0 -95
- better_ffmpeg_progress-2.6.1/better_ffmpeg_progress/better_ffmpeg_progress.py +0 -436
- better_ffmpeg_progress-2.6.1/better_ffmpeg_progress.egg-info/PKG-INFO +0 -113
- {better_ffmpeg_progress-2.6.1 → better_ffmpeg_progress-3.1.0}/LICENSE +0 -0
- {better_ffmpeg_progress-2.6.1 → better_ffmpeg_progress-3.1.0}/better_ffmpeg_progress/__init__.py +0 -0
- {better_ffmpeg_progress-2.6.1 → better_ffmpeg_progress-3.1.0}/better_ffmpeg_progress/utils.py +0 -0
- {better_ffmpeg_progress-2.6.1 → better_ffmpeg_progress-3.1.0}/better_ffmpeg_progress.egg-info/SOURCES.txt +0 -0
- {better_ffmpeg_progress-2.6.1 → better_ffmpeg_progress-3.1.0}/better_ffmpeg_progress.egg-info/dependency_links.txt +0 -0
- {better_ffmpeg_progress-2.6.1 → better_ffmpeg_progress-3.1.0}/better_ffmpeg_progress.egg-info/requires.txt +0 -0
- {better_ffmpeg_progress-2.6.1 → better_ffmpeg_progress-3.1.0}/better_ffmpeg_progress.egg-info/top_level.txt +0 -0
- {better_ffmpeg_progress-2.6.1 → better_ffmpeg_progress-3.1.0}/setup.cfg +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: better-ffmpeg-progress
|
|
3
|
+
Version: 3.1.0
|
|
4
|
+
Summary: Run FFmpeg & see percentage progress + ETA.
|
|
5
|
+
Home-page: https://github.com/CrypticSignal/better-ffmpeg-progress
|
|
6
|
+
Author: GitHub.com/CrypticSignal
|
|
7
|
+
Author-email: theaudiophile@outlook.com
|
|
8
|
+
Keywords: ffmpeg,progress
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: ffmpeg-python
|
|
16
|
+
Requires-Dist: psutil
|
|
17
|
+
Requires-Dist: rich
|
|
18
|
+
|
|
19
|
+
<div align="center">
|
|
20
|
+
|
|
21
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
22
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
23
|
+

|
|
24
|
+
[](LICENSE.txt)
|
|
25
|
+
|
|
26
|
+
# Better FFmpeg Progress
|
|
27
|
+
Runs an FFmpeg command and shows a progress bar with percentage progress, time elapsed and ETA.
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
FFmpeg outputs something like:
|
|
31
|
+
```
|
|
32
|
+
frame= 692 fps= 58 q=28.0 size= 5376KiB time=00:00:28.77 bitrate=1530.3kbits/s speed=2.43x
|
|
33
|
+
```
|
|
34
|
+
Better FFmpeg Progress outputs something like:
|
|
35
|
+
```
|
|
36
|
+
⠏ Processing abc.webm ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
|
|
37
|
+
```
|
|
38
|
+
Where:
|
|
39
|
+
- `Processing abc.webm` is the description of the progresss bar.
|
|
40
|
+
- `23%` is the percentage progress.
|
|
41
|
+
- `0:00:04` is the time (H:MM:SS) elapsed.
|
|
42
|
+
- `00:15` is the estimated time until the FFmpeg process completes.
|
|
43
|
+
|
|
44
|
+
As you can see, the output of Better FFmpeg Progress is much more useful.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
```
|
|
48
|
+
pip install better-ffmpeg-progress --upgrade
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would to `subprocess.run()` or `subprocess.Popen()`.
|
|
53
|
+
|
|
54
|
+
Here's a simple example:
|
|
55
|
+
```py
|
|
56
|
+
from better_ffmpeg_progress import FfmpegProcess
|
|
57
|
+
|
|
58
|
+
process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:v", "libx265", "output.mp4"])
|
|
59
|
+
|
|
60
|
+
# return_code will be 0 if the process was successful
|
|
61
|
+
return_code = process.run()
|
|
62
|
+
|
|
63
|
+
if return_code == 0:
|
|
64
|
+
# Code to run if the process was successful
|
|
65
|
+
pass
|
|
66
|
+
else:
|
|
67
|
+
# Code to run if the process was unsuccessful
|
|
68
|
+
pass
|
|
69
|
+
```
|
|
70
|
+
## Optional Arguments
|
|
71
|
+
An instance of the `FfmpegProcess` class takes the following **optional** arguments:
|
|
72
|
+
|
|
73
|
+
- `ffmpeg_log_level` - Desired FFmpeg log level. Default: `"verbose"`
|
|
74
|
+
- `ffmpeg_log_file` - The filepath to save the FFmpeg log to.
|
|
75
|
+
- `print_detected_duration` - Print the detected duration of the input file. Default: `True`
|
|
76
|
+
- `print_stderr_new_line` - If better progress information cannot be shown, print FFmpeg stderr in a new line instead of replacing the current line in the terminal. Default: `False`
|
|
77
|
+
|
|
78
|
+
The `run` method takes the following **optional** argument:
|
|
79
|
+
- `print_command` - Print the FFmpeg command being executed. Default: `False`
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
4
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
5
|
+

|
|
6
|
+
[](LICENSE.txt)
|
|
7
|
+
|
|
8
|
+
# Better FFmpeg Progress
|
|
9
|
+
Runs an FFmpeg command and shows a progress bar with percentage progress, time elapsed and ETA.
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
FFmpeg outputs something like:
|
|
13
|
+
```
|
|
14
|
+
frame= 692 fps= 58 q=28.0 size= 5376KiB time=00:00:28.77 bitrate=1530.3kbits/s speed=2.43x
|
|
15
|
+
```
|
|
16
|
+
Better FFmpeg Progress outputs something like:
|
|
17
|
+
```
|
|
18
|
+
⠏ Processing abc.webm ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
|
|
19
|
+
```
|
|
20
|
+
Where:
|
|
21
|
+
- `Processing abc.webm` is the description of the progresss bar.
|
|
22
|
+
- `23%` is the percentage progress.
|
|
23
|
+
- `0:00:04` is the time (H:MM:SS) elapsed.
|
|
24
|
+
- `00:15` is the estimated time until the FFmpeg process completes.
|
|
25
|
+
|
|
26
|
+
As you can see, the output of Better FFmpeg Progress is much more useful.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
```
|
|
30
|
+
pip install better-ffmpeg-progress --upgrade
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would to `subprocess.run()` or `subprocess.Popen()`.
|
|
35
|
+
|
|
36
|
+
Here's a simple example:
|
|
37
|
+
```py
|
|
38
|
+
from better_ffmpeg_progress import FfmpegProcess
|
|
39
|
+
|
|
40
|
+
process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:v", "libx265", "output.mp4"])
|
|
41
|
+
|
|
42
|
+
# return_code will be 0 if the process was successful
|
|
43
|
+
return_code = process.run()
|
|
44
|
+
|
|
45
|
+
if return_code == 0:
|
|
46
|
+
# Code to run if the process was successful
|
|
47
|
+
pass
|
|
48
|
+
else:
|
|
49
|
+
# Code to run if the process was unsuccessful
|
|
50
|
+
pass
|
|
51
|
+
```
|
|
52
|
+
## Optional Arguments
|
|
53
|
+
An instance of the `FfmpegProcess` class takes the following **optional** arguments:
|
|
54
|
+
|
|
55
|
+
- `ffmpeg_log_level` - Desired FFmpeg log level. Default: `"verbose"`
|
|
56
|
+
- `ffmpeg_log_file` - The filepath to save the FFmpeg log to.
|
|
57
|
+
- `print_detected_duration` - Print the detected duration of the input file. Default: `True`
|
|
58
|
+
- `print_stderr_new_line` - If better progress information cannot be shown, print FFmpeg stderr in a new line instead of replacing the current line in the terminal. Default: `False`
|
|
59
|
+
|
|
60
|
+
The `run` method takes the following **optional** argument:
|
|
61
|
+
- `print_command` - Print the FFmpeg command being executed. Default: `False`
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import psutil
|
|
5
|
+
from queue import Empty, Queue
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from threading import Thread
|
|
9
|
+
from typing import List, Optional, Union
|
|
10
|
+
|
|
11
|
+
from ffmpeg import probe
|
|
12
|
+
from rich.progress import (
|
|
13
|
+
Progress,
|
|
14
|
+
TextColumn,
|
|
15
|
+
BarColumn,
|
|
16
|
+
TaskProgressColumn,
|
|
17
|
+
TimeRemainingColumn,
|
|
18
|
+
TimeElapsedColumn,
|
|
19
|
+
SpinnerColumn,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FfmpegLogLevel(Enum):
|
|
24
|
+
QUIET = "quiet"
|
|
25
|
+
PANIC = "panic"
|
|
26
|
+
FATAL = "fatal"
|
|
27
|
+
ERROR = "error"
|
|
28
|
+
WARNING = "warning"
|
|
29
|
+
INFO = "info"
|
|
30
|
+
VERBOSE = "verbose"
|
|
31
|
+
DEBUG = "debug"
|
|
32
|
+
TRACE = "trace"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FfmpegProcess:
|
|
36
|
+
TIME_MS_PREFIX = "out_time_ms="
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _read_pipe(pipe: subprocess.PIPE, queue: Queue, stdout: bool = False) -> None:
|
|
40
|
+
"""Read from pipe and put lines into queue."""
|
|
41
|
+
try:
|
|
42
|
+
with pipe:
|
|
43
|
+
for line in iter(pipe.readline, ""):
|
|
44
|
+
line = line.strip()
|
|
45
|
+
if line:
|
|
46
|
+
if stdout:
|
|
47
|
+
if line.startswith("out_time_ms="):
|
|
48
|
+
queue.put(line)
|
|
49
|
+
else:
|
|
50
|
+
queue.put(line)
|
|
51
|
+
except (IOError, ValueError) as e:
|
|
52
|
+
print(f"Error reading pipe: {e}")
|
|
53
|
+
finally:
|
|
54
|
+
pipe.close()
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def _validate_command(cls, command: List[str]) -> bool:
|
|
58
|
+
if not shutil.which("ffmpeg"):
|
|
59
|
+
print("Error: FFmpeg executable not found in PATH")
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
if "-i" not in command:
|
|
63
|
+
print("Error: FFmpeg command must include '-i'")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
input_idx = command.index("-i") + 1
|
|
67
|
+
if input_idx >= len(command):
|
|
68
|
+
print("Error: No input file specified after -i")
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
input_file = Path(command[input_idx])
|
|
72
|
+
if not input_file.exists():
|
|
73
|
+
print(f"Error: Input file not found: {input_file}")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
if input_idx + 1 >= len(command):
|
|
77
|
+
print("Error: No output file specified")
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
command: List[str],
|
|
85
|
+
ffmpeg_log_level: Optional[FfmpegLogLevel] = None,
|
|
86
|
+
ffmpeg_log_file: Optional[Union[str, Path]] = None,
|
|
87
|
+
print_detected_duration: bool = False,
|
|
88
|
+
print_stderr_new_line: bool = False,
|
|
89
|
+
):
|
|
90
|
+
if not self._validate_command(command):
|
|
91
|
+
self.return_code = 1
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if ffmpeg_log_level and ffmpeg_log_level not in FfmpegLogLevel:
|
|
95
|
+
print(
|
|
96
|
+
f"ffmpeg_log_level must be a lowercase variant of: {list(FfmpegLogLevel.__members__)}"
|
|
97
|
+
)
|
|
98
|
+
self.return_code = 1
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
input_idx = command.index("-i") + 1
|
|
102
|
+
self._input_filepath = Path(command[input_idx])
|
|
103
|
+
self._output_filepath = Path(command[-1])
|
|
104
|
+
self._ffmpeg_log_level = (
|
|
105
|
+
ffmpeg_log_level if ffmpeg_log_level else FfmpegLogLevel.VERBOSE.value
|
|
106
|
+
)
|
|
107
|
+
self._ffmpeg_log_file = Path(
|
|
108
|
+
ffmpeg_log_file
|
|
109
|
+
if ffmpeg_log_file
|
|
110
|
+
else f"{self._input_filepath.name}_log.txt"
|
|
111
|
+
)
|
|
112
|
+
self._print_detected_duration = print_detected_duration
|
|
113
|
+
self._print_stderr_new_line = print_stderr_new_line
|
|
114
|
+
|
|
115
|
+
with open(self._ffmpeg_log_file, "w"):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
probe_data = probe(self._input_filepath)
|
|
120
|
+
self._duration_secs = float(probe_data["format"]["duration"])
|
|
121
|
+
if self._print_detected_duration:
|
|
122
|
+
print(f"Detected duration: {self._duration_secs:.2f} seconds")
|
|
123
|
+
except Exception:
|
|
124
|
+
self._duration_secs = None
|
|
125
|
+
|
|
126
|
+
self._ffmpeg_command = [
|
|
127
|
+
command[0],
|
|
128
|
+
"-hide_banner",
|
|
129
|
+
"-loglevel",
|
|
130
|
+
self._ffmpeg_log_level,
|
|
131
|
+
"-progress",
|
|
132
|
+
"pipe:1",
|
|
133
|
+
"-stats_period",
|
|
134
|
+
"0.1",
|
|
135
|
+
"-nostats",
|
|
136
|
+
*command[1:],
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
self.return_code = 0
|
|
140
|
+
|
|
141
|
+
def _process_stderr(self, stderr: str) -> None:
|
|
142
|
+
if self._duration_secs is None:
|
|
143
|
+
print(stderr, end="\n" if self._print_stderr_new_line else "\r", flush=True)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
with open(self._ffmpeg_log_file, "a", encoding="utf-8") as f:
|
|
147
|
+
f.write(f"{stderr}\n")
|
|
148
|
+
except IOError as e:
|
|
149
|
+
print(f"Error writing to log file: {e}")
|
|
150
|
+
self.return_code = 1
|
|
151
|
+
|
|
152
|
+
def run(self, print_command: bool = False) -> int:
|
|
153
|
+
if hasattr(self, "return_code") and self.return_code != 0:
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
if self._output_filepath.exists() and "-y" not in self._ffmpeg_command:
|
|
157
|
+
if (
|
|
158
|
+
input(
|
|
159
|
+
f"{self._output_filepath} already exists. Overwrite? [Y/N]: "
|
|
160
|
+
).lower()
|
|
161
|
+
!= "y"
|
|
162
|
+
):
|
|
163
|
+
print(
|
|
164
|
+
"FFmpeg process cancelled. Output file exists and overwrite declined."
|
|
165
|
+
)
|
|
166
|
+
return 1
|
|
167
|
+
self._ffmpeg_command.insert(1, "-y")
|
|
168
|
+
|
|
169
|
+
if print_command:
|
|
170
|
+
print(f"Executing: {' '.join(self._ffmpeg_command)}")
|
|
171
|
+
|
|
172
|
+
def _contains_shell_operators(command: List[str]) -> bool:
|
|
173
|
+
shell_operators = {"|", ">", "<", ">>", "&&", "||"}
|
|
174
|
+
return any(op in command for op in shell_operators)
|
|
175
|
+
|
|
176
|
+
# If command contains shell operators, turn the list into a string.
|
|
177
|
+
if _contains_shell_operators(self._ffmpeg_command):
|
|
178
|
+
self._ffmpeg_command = " ".join(self._ffmpeg_command)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
process = subprocess.Popen(
|
|
182
|
+
self._ffmpeg_command,
|
|
183
|
+
shell=isinstance(self._ffmpeg_command, str),
|
|
184
|
+
stdout=subprocess.PIPE,
|
|
185
|
+
stderr=subprocess.PIPE,
|
|
186
|
+
text=True,
|
|
187
|
+
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
|
188
|
+
if os.name == "nt"
|
|
189
|
+
else 0,
|
|
190
|
+
)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
print(f"Error starting FFmpeg process: {e}")
|
|
193
|
+
return 1
|
|
194
|
+
|
|
195
|
+
stdout_queue, stderr_queue = Queue(), Queue()
|
|
196
|
+
# Start stdout and stderr pipe readers
|
|
197
|
+
Thread(
|
|
198
|
+
target=self._read_pipe,
|
|
199
|
+
args=(process.stdout, stdout_queue, True),
|
|
200
|
+
daemon=True,
|
|
201
|
+
).start()
|
|
202
|
+
Thread(
|
|
203
|
+
target=self._read_pipe,
|
|
204
|
+
args=(process.stderr, stderr_queue, False),
|
|
205
|
+
daemon=True,
|
|
206
|
+
).start()
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
with Progress(
|
|
210
|
+
SpinnerColumn(),
|
|
211
|
+
TextColumn("[progress.description]{task.description}"),
|
|
212
|
+
BarColumn(),
|
|
213
|
+
TaskProgressColumn(),
|
|
214
|
+
TimeElapsedColumn(),
|
|
215
|
+
TimeRemainingColumn(compact=True),
|
|
216
|
+
refresh_per_second=10,
|
|
217
|
+
) as progress_bar:
|
|
218
|
+
task_id = None
|
|
219
|
+
if self._duration_secs:
|
|
220
|
+
task_id = progress_bar.add_task(
|
|
221
|
+
f"Processing {self._input_filepath.name}",
|
|
222
|
+
total=self._duration_secs,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
while process.poll() is None:
|
|
226
|
+
# stdout
|
|
227
|
+
if task_id is not None and not stdout_queue.empty():
|
|
228
|
+
try:
|
|
229
|
+
line = stdout_queue.get_nowait()
|
|
230
|
+
if line.startswith(self.TIME_MS_PREFIX):
|
|
231
|
+
try:
|
|
232
|
+
value = int(line.split("=")[1]) / 1_000_000
|
|
233
|
+
if value <= self._duration_secs:
|
|
234
|
+
progress_bar.update(task_id, completed=value)
|
|
235
|
+
except ValueError:
|
|
236
|
+
pass
|
|
237
|
+
except Empty:
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
# stderr
|
|
241
|
+
if not stderr_queue.empty():
|
|
242
|
+
try:
|
|
243
|
+
self._process_stderr(stderr_queue.get_nowait())
|
|
244
|
+
except Empty:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
# Process remaining stderr
|
|
248
|
+
while not stderr_queue.empty():
|
|
249
|
+
try:
|
|
250
|
+
self._process_stderr(stderr_queue.get_nowait())
|
|
251
|
+
except Empty:
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
progress_bar.columns = (
|
|
255
|
+
TextColumn("[progress.description]{task.description}"),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if process.returncode != 0:
|
|
259
|
+
progress_bar.update(
|
|
260
|
+
task_id,
|
|
261
|
+
description=f"The FFmpeg process did not complete successfully. Check out {self._ffmpeg_log_file} for details.",
|
|
262
|
+
)
|
|
263
|
+
return 1
|
|
264
|
+
|
|
265
|
+
progress_bar.update(
|
|
266
|
+
task_id,
|
|
267
|
+
description=f"✓ Processed {self._input_filepath.name}",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
except KeyboardInterrupt:
|
|
271
|
+
try:
|
|
272
|
+
psutil.Process(process.pid).terminate()
|
|
273
|
+
except psutil.NoSuchProcess:
|
|
274
|
+
pass
|
|
275
|
+
return 1
|
|
276
|
+
|
|
277
|
+
return 0 if process.returncode == 0 else 1
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: better-ffmpeg-progress
|
|
3
|
+
Version: 3.1.0
|
|
4
|
+
Summary: Run FFmpeg & see percentage progress + ETA.
|
|
5
|
+
Home-page: https://github.com/CrypticSignal/better-ffmpeg-progress
|
|
6
|
+
Author: GitHub.com/CrypticSignal
|
|
7
|
+
Author-email: theaudiophile@outlook.com
|
|
8
|
+
Keywords: ffmpeg,progress
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: ffmpeg-python
|
|
16
|
+
Requires-Dist: psutil
|
|
17
|
+
Requires-Dist: rich
|
|
18
|
+
|
|
19
|
+
<div align="center">
|
|
20
|
+
|
|
21
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
22
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
23
|
+

|
|
24
|
+
[](LICENSE.txt)
|
|
25
|
+
|
|
26
|
+
# Better FFmpeg Progress
|
|
27
|
+
Runs an FFmpeg command and shows a progress bar with percentage progress, time elapsed and ETA.
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
FFmpeg outputs something like:
|
|
31
|
+
```
|
|
32
|
+
frame= 692 fps= 58 q=28.0 size= 5376KiB time=00:00:28.77 bitrate=1530.3kbits/s speed=2.43x
|
|
33
|
+
```
|
|
34
|
+
Better FFmpeg Progress outputs something like:
|
|
35
|
+
```
|
|
36
|
+
⠏ Processing abc.webm ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
|
|
37
|
+
```
|
|
38
|
+
Where:
|
|
39
|
+
- `Processing abc.webm` is the description of the progresss bar.
|
|
40
|
+
- `23%` is the percentage progress.
|
|
41
|
+
- `0:00:04` is the time (H:MM:SS) elapsed.
|
|
42
|
+
- `00:15` is the estimated time until the FFmpeg process completes.
|
|
43
|
+
|
|
44
|
+
As you can see, the output of Better FFmpeg Progress is much more useful.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
```
|
|
48
|
+
pip install better-ffmpeg-progress --upgrade
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would to `subprocess.run()` or `subprocess.Popen()`.
|
|
53
|
+
|
|
54
|
+
Here's a simple example:
|
|
55
|
+
```py
|
|
56
|
+
from better_ffmpeg_progress import FfmpegProcess
|
|
57
|
+
|
|
58
|
+
process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:v", "libx265", "output.mp4"])
|
|
59
|
+
|
|
60
|
+
# return_code will be 0 if the process was successful
|
|
61
|
+
return_code = process.run()
|
|
62
|
+
|
|
63
|
+
if return_code == 0:
|
|
64
|
+
# Code to run if the process was successful
|
|
65
|
+
pass
|
|
66
|
+
else:
|
|
67
|
+
# Code to run if the process was unsuccessful
|
|
68
|
+
pass
|
|
69
|
+
```
|
|
70
|
+
## Optional Arguments
|
|
71
|
+
An instance of the `FfmpegProcess` class takes the following **optional** arguments:
|
|
72
|
+
|
|
73
|
+
- `ffmpeg_log_level` - Desired FFmpeg log level. Default: `"verbose"`
|
|
74
|
+
- `ffmpeg_log_file` - The filepath to save the FFmpeg log to.
|
|
75
|
+
- `print_detected_duration` - Print the detected duration of the input file. Default: `True`
|
|
76
|
+
- `print_stderr_new_line` - If better progress information cannot be shown, print FFmpeg stderr in a new line instead of replacing the current line in the terminal. Default: `False`
|
|
77
|
+
|
|
78
|
+
The `run` method takes the following **optional** argument:
|
|
79
|
+
- `print_command` - Print the FFmpeg command being executed. Default: `False`
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as f:
|
|
|
5
5
|
|
|
6
6
|
setuptools.setup(
|
|
7
7
|
name="better-ffmpeg-progress",
|
|
8
|
-
version="
|
|
8
|
+
version="3.1.0",
|
|
9
9
|
author="GitHub.com/CrypticSignal",
|
|
10
10
|
author_email="theaudiophile@outlook.com",
|
|
11
11
|
description="Run FFmpeg & see percentage progress + ETA.",
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: better-ffmpeg-progress
|
|
3
|
-
Version: 2.6.1
|
|
4
|
-
Summary: Run FFmpeg & see percentage progress + ETA.
|
|
5
|
-
Home-page: https://github.com/CrypticSignal/better-ffmpeg-progress
|
|
6
|
-
Author: GitHub.com/CrypticSignal
|
|
7
|
-
Author-email: theaudiophile@outlook.com
|
|
8
|
-
Keywords: ffmpeg,progress
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.7
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
License-File: LICENSE
|
|
15
|
-
Requires-Dist: ffmpeg-python
|
|
16
|
-
Requires-Dist: psutil
|
|
17
|
-
Requires-Dist: rich
|
|
18
|
-
|
|
19
|
-
<div align="center">
|
|
20
|
-
|
|
21
|
-
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
22
|
-
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
23
|
-

|
|
24
|
-
[](LICENSE.txt)
|
|
25
|
-
|
|
26
|
-
# Better FFmpeg Progress
|
|
27
|
-
Runs an FFmpeg command and shows a progress bar with percentage progress, time elapsed and ETA.
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
FFmpeg outputs something like:
|
|
31
|
-
```
|
|
32
|
-
frame= 692 fps= 58 q=28.0 size= 5376KiB time=00:00:28.77 bitrate=1530.3kbits/s speed=2.43x
|
|
33
|
-
```
|
|
34
|
-
Better FFmpeg Progress outputs something like:
|
|
35
|
-
```
|
|
36
|
-
⠏ Processing abc.webm ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
|
|
37
|
-
```
|
|
38
|
-
Where:
|
|
39
|
-
- `Processing abc.webm` is the description of the progresss bar.
|
|
40
|
-
- `23%` is the percentage progress.
|
|
41
|
-
- `0:00:04` is the time (H:MM:SS) elapsed.
|
|
42
|
-
- `00:15` is the estimated time until the FFmpeg process completes.
|
|
43
|
-
|
|
44
|
-
As you can see, the output of Better FFmpeg Progress is much more useful.
|
|
45
|
-
|
|
46
|
-
## Installation
|
|
47
|
-
```
|
|
48
|
-
pip install better-ffmpeg-progress --upgrade
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Usage
|
|
52
|
-
Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would to `subprocess.run()`.
|
|
53
|
-
|
|
54
|
-
Simple Example:
|
|
55
|
-
```py
|
|
56
|
-
from better_ffmpeg_progress import FfmpegProcess
|
|
57
|
-
# Pass a list of FFmpeg arguments, like you would if using subprocess.run()
|
|
58
|
-
process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
|
|
59
|
-
# Use the run method to run the FFmpeg command.
|
|
60
|
-
process.run()
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Advanced Example:
|
|
64
|
-
```py
|
|
65
|
-
from better_ffmpeg_progress import FfmpegProcess
|
|
66
|
-
|
|
67
|
-
def handle_progress_info(percentage, speed, eta, estimated_filesize):
|
|
68
|
-
print(f"Estimated Output Filesize: {estimated_filesize / 1_000_000} MB")
|
|
69
|
-
|
|
70
|
-
def handle_success():
|
|
71
|
-
# Code to run if the FFmpeg process completes successfully.
|
|
72
|
-
pass
|
|
73
|
-
|
|
74
|
-
def handle_error():
|
|
75
|
-
# Code to run if the FFmpeg process encounters an error.
|
|
76
|
-
pass
|
|
77
|
-
|
|
78
|
-
# Pass a list of FFmpeg arguments, like you would if using subprocess.run() or subprocess.Popen()
|
|
79
|
-
process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
|
|
80
|
-
|
|
81
|
-
# Use the run method to run the FFmpeg command.
|
|
82
|
-
process.run(
|
|
83
|
-
progress_bar_description="Converting file...",
|
|
84
|
-
progress_handler=handle_progress_info,
|
|
85
|
-
log_file="ffmpeg_log.txt",
|
|
86
|
-
success_handler=handle_success,
|
|
87
|
-
error_handler=handle_error,
|
|
88
|
-
)
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## Optional Arguments
|
|
92
|
-
An instance of the `FfmpegProcess` class takes the following **optional** arguments:
|
|
93
|
-
|
|
94
|
-
- `ffmpeg_loglevel`: Desired FFmpeg log level. Default: `"verbose"`
|
|
95
|
-
- `print_stderr_new_line`: If better progress information cannot be shown, print FFmpeg stderr in a new line instead of replacing the current line in the terminal. Default: False
|
|
96
|
-
- `print_detected_duration`: Print the detected duration of the input file. Default: True
|
|
97
|
-
|
|
98
|
-
The `run` method takes the following **optional** arguments:
|
|
99
|
-
- `print_command` - Print the FFmpeg command being executed. Default: `False`
|
|
100
|
-
- `progress_bar_description` - An optional string to set a custom description for the progress bar. The default description is `Processing <file>`. This can be an empty string if you don't want the progress bar to have a description.
|
|
101
|
-
- `progress_handler`
|
|
102
|
-
- You can create a function if you would like to do something with the following values:
|
|
103
|
-
- Percentage progress. [float]
|
|
104
|
-
- Speed, e.g. `22.3x` which means that 22.3 seconds of the input are processed every second. [string]
|
|
105
|
-
- ETA in seconds. [float]
|
|
106
|
-
- Estimated output filesize in bytes. [float]
|
|
107
|
-
- _Note: This is not accurate. Please take the value with a grain of salt._
|
|
108
|
-
|
|
109
|
-
The values will be `None` if unknown. The function will receive the current values of these metrics as arguments, every 0.1s.
|
|
110
|
-
|
|
111
|
-
- `log_file` - The filepath to save the FFmpeg log to.
|
|
112
|
-
- `success_handler` - A function to run if the FFmpeg process completes successfully.
|
|
113
|
-
- `error_handler` - A function to run if the FFmpeg process encounters an error.
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
4
|
-
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
5
|
-

|
|
6
|
-
[](LICENSE.txt)
|
|
7
|
-
|
|
8
|
-
# Better FFmpeg Progress
|
|
9
|
-
Runs an FFmpeg command and shows a progress bar with percentage progress, time elapsed and ETA.
|
|
10
|
-
</div>
|
|
11
|
-
|
|
12
|
-
FFmpeg outputs something like:
|
|
13
|
-
```
|
|
14
|
-
frame= 692 fps= 58 q=28.0 size= 5376KiB time=00:00:28.77 bitrate=1530.3kbits/s speed=2.43x
|
|
15
|
-
```
|
|
16
|
-
Better FFmpeg Progress outputs something like:
|
|
17
|
-
```
|
|
18
|
-
⠏ Processing abc.webm ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
|
|
19
|
-
```
|
|
20
|
-
Where:
|
|
21
|
-
- `Processing abc.webm` is the description of the progresss bar.
|
|
22
|
-
- `23%` is the percentage progress.
|
|
23
|
-
- `0:00:04` is the time (H:MM:SS) elapsed.
|
|
24
|
-
- `00:15` is the estimated time until the FFmpeg process completes.
|
|
25
|
-
|
|
26
|
-
As you can see, the output of Better FFmpeg Progress is much more useful.
|
|
27
|
-
|
|
28
|
-
## Installation
|
|
29
|
-
```
|
|
30
|
-
pip install better-ffmpeg-progress --upgrade
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Usage
|
|
34
|
-
Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would to `subprocess.run()`.
|
|
35
|
-
|
|
36
|
-
Simple Example:
|
|
37
|
-
```py
|
|
38
|
-
from better_ffmpeg_progress import FfmpegProcess
|
|
39
|
-
# Pass a list of FFmpeg arguments, like you would if using subprocess.run()
|
|
40
|
-
process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
|
|
41
|
-
# Use the run method to run the FFmpeg command.
|
|
42
|
-
process.run()
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Advanced Example:
|
|
46
|
-
```py
|
|
47
|
-
from better_ffmpeg_progress import FfmpegProcess
|
|
48
|
-
|
|
49
|
-
def handle_progress_info(percentage, speed, eta, estimated_filesize):
|
|
50
|
-
print(f"Estimated Output Filesize: {estimated_filesize / 1_000_000} MB")
|
|
51
|
-
|
|
52
|
-
def handle_success():
|
|
53
|
-
# Code to run if the FFmpeg process completes successfully.
|
|
54
|
-
pass
|
|
55
|
-
|
|
56
|
-
def handle_error():
|
|
57
|
-
# Code to run if the FFmpeg process encounters an error.
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
# Pass a list of FFmpeg arguments, like you would if using subprocess.run() or subprocess.Popen()
|
|
61
|
-
process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
|
|
62
|
-
|
|
63
|
-
# Use the run method to run the FFmpeg command.
|
|
64
|
-
process.run(
|
|
65
|
-
progress_bar_description="Converting file...",
|
|
66
|
-
progress_handler=handle_progress_info,
|
|
67
|
-
log_file="ffmpeg_log.txt",
|
|
68
|
-
success_handler=handle_success,
|
|
69
|
-
error_handler=handle_error,
|
|
70
|
-
)
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Optional Arguments
|
|
74
|
-
An instance of the `FfmpegProcess` class takes the following **optional** arguments:
|
|
75
|
-
|
|
76
|
-
- `ffmpeg_loglevel`: Desired FFmpeg log level. Default: `"verbose"`
|
|
77
|
-
- `print_stderr_new_line`: If better progress information cannot be shown, print FFmpeg stderr in a new line instead of replacing the current line in the terminal. Default: False
|
|
78
|
-
- `print_detected_duration`: Print the detected duration of the input file. Default: True
|
|
79
|
-
|
|
80
|
-
The `run` method takes the following **optional** arguments:
|
|
81
|
-
- `print_command` - Print the FFmpeg command being executed. Default: `False`
|
|
82
|
-
- `progress_bar_description` - An optional string to set a custom description for the progress bar. The default description is `Processing <file>`. This can be an empty string if you don't want the progress bar to have a description.
|
|
83
|
-
- `progress_handler`
|
|
84
|
-
- You can create a function if you would like to do something with the following values:
|
|
85
|
-
- Percentage progress. [float]
|
|
86
|
-
- Speed, e.g. `22.3x` which means that 22.3 seconds of the input are processed every second. [string]
|
|
87
|
-
- ETA in seconds. [float]
|
|
88
|
-
- Estimated output filesize in bytes. [float]
|
|
89
|
-
- _Note: This is not accurate. Please take the value with a grain of salt._
|
|
90
|
-
|
|
91
|
-
The values will be `None` if unknown. The function will receive the current values of these metrics as arguments, every 0.1s.
|
|
92
|
-
|
|
93
|
-
- `log_file` - The filepath to save the FFmpeg log to.
|
|
94
|
-
- `success_handler` - A function to run if the FFmpeg process completes successfully.
|
|
95
|
-
- `error_handler` - A function to run if the FFmpeg process encounters an error.
|
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from queue import Empty, Queue
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
7
|
-
from threading import Thread
|
|
8
|
-
from typing import Callable, List, Optional, Union
|
|
9
|
-
|
|
10
|
-
from .utils import print_with_prefix
|
|
11
|
-
|
|
12
|
-
from ffmpeg import probe
|
|
13
|
-
import psutil
|
|
14
|
-
from rich.progress import (
|
|
15
|
-
Progress,
|
|
16
|
-
TextColumn,
|
|
17
|
-
BarColumn,
|
|
18
|
-
TaskProgressColumn,
|
|
19
|
-
TimeRemainingColumn,
|
|
20
|
-
TimeElapsedColumn,
|
|
21
|
-
SpinnerColumn,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass
|
|
26
|
-
class Metrics:
|
|
27
|
-
"""Data class to store FFmpeg progress information."""
|
|
28
|
-
|
|
29
|
-
percentage: float = 0.0
|
|
30
|
-
seconds_processed: float = 0
|
|
31
|
-
speed: float = None
|
|
32
|
-
eta: Optional[float] = None
|
|
33
|
-
estimated_size: Optional[int] = None
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class FfmpegProcess:
|
|
37
|
-
"""
|
|
38
|
-
Args:
|
|
39
|
-
command: A list of arguments to pass to FFmpeg.
|
|
40
|
-
ffmpeg_loglevel: Desired FFmpeg log level. Default: "verbose"
|
|
41
|
-
print_stderr_new_line: If better progress information cannot be shown, print FFmpeg stderr in a new line instead of replacing the current line in the terminal. Default: False
|
|
42
|
-
print_detected_duration: Print the detected duration of the input file. Default: True
|
|
43
|
-
|
|
44
|
-
Raises:
|
|
45
|
-
FileNotFoundError: If FFmpeg cannot find the input filepath or filename.
|
|
46
|
-
ValueError: If the list of arguments does not include "-i"
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
WANTED_METRICS = frozenset({"out_time_ms", "total_size", "speed"})
|
|
50
|
-
|
|
51
|
-
def __init__(
|
|
52
|
-
self,
|
|
53
|
-
command: List[str],
|
|
54
|
-
ffmpeg_loglevel: str = "verbose",
|
|
55
|
-
print_detected_duration: bool = False,
|
|
56
|
-
print_stderr_new_line: bool = False,
|
|
57
|
-
):
|
|
58
|
-
if "-i" not in command:
|
|
59
|
-
raise ValueError("FFmpeg command must include '-i'")
|
|
60
|
-
|
|
61
|
-
# -progress pipe:1 sends progress metrics to stdout
|
|
62
|
-
# -stats_period sets the period at which encoding progress/statistics are updated
|
|
63
|
-
extra_ffmpeg_options = [
|
|
64
|
-
"-hide_banner",
|
|
65
|
-
"-loglevel",
|
|
66
|
-
ffmpeg_loglevel,
|
|
67
|
-
"-progress",
|
|
68
|
-
"pipe:1",
|
|
69
|
-
"-stats_period",
|
|
70
|
-
"0.1",
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
command[1:1] = extra_ffmpeg_options
|
|
74
|
-
|
|
75
|
-
self._ffmpeg_command = command
|
|
76
|
-
self._output_filepath = Path(command[-1])
|
|
77
|
-
self._print_detected_duration = print_detected_duration
|
|
78
|
-
self._print_stderr_new_line = print_stderr_new_line
|
|
79
|
-
self._metrics = Metrics()
|
|
80
|
-
self._duration_secs: Optional[float]
|
|
81
|
-
self._current_size: int = 0
|
|
82
|
-
self._ffmpeg_log_file: Optional[Path] = None
|
|
83
|
-
self._error_messages: list[str] = []
|
|
84
|
-
self._get_input_file_duration()
|
|
85
|
-
|
|
86
|
-
def _get_input_file_duration(self) -> None:
|
|
87
|
-
"""Use ffprobe to get the duration of the input file"""
|
|
88
|
-
index_of_filepath = self._ffmpeg_command.index("-i") + 1
|
|
89
|
-
self._input_filepath = Path(self._ffmpeg_command[index_of_filepath])
|
|
90
|
-
|
|
91
|
-
try:
|
|
92
|
-
self._duration_secs = float(
|
|
93
|
-
probe(self._input_filepath)["format"]["duration"]
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
if self._print_detected_duration:
|
|
97
|
-
print_with_prefix(
|
|
98
|
-
"[Better FFmpeg Progress] ",
|
|
99
|
-
f"The duration of {self._input_filepath.name} has been detected as {self._duration_secs} seconds",
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
# -nostats ensures that progress information is not sent to stderr as this info is not needed in the log file, e.g.
|
|
103
|
-
# frame= 1381 fps=254 q=18.0 size= 46592KiB time=00:00:46.07 bitrate=8283.1kbits/s speed=8.47x
|
|
104
|
-
self._ffmpeg_command.extend(["-nostats"])
|
|
105
|
-
except Exception:
|
|
106
|
-
print_with_prefix(
|
|
107
|
-
"[Better FFmpeg Progress] ",
|
|
108
|
-
f"Could not detect the duration of '{self._input_filepath.name}'. Percentage progress, ETA and estimated filesize will be unavailable.\n",
|
|
109
|
-
)
|
|
110
|
-
self._duration_secs = None
|
|
111
|
-
|
|
112
|
-
def _should_overwrite(self) -> bool:
|
|
113
|
-
if self._output_filepath.exists() and "-y" not in self._ffmpeg_command:
|
|
114
|
-
choice = input(
|
|
115
|
-
f"{self._output_filepath} already exists. Overwrite? [Y/N]: "
|
|
116
|
-
).lower()
|
|
117
|
-
|
|
118
|
-
if choice != "y":
|
|
119
|
-
print_with_prefix(
|
|
120
|
-
"[Better FFmpeg Progress] ",
|
|
121
|
-
"FFmpeg process cancelled. Output file exists and overwrite declined.",
|
|
122
|
-
)
|
|
123
|
-
return False
|
|
124
|
-
|
|
125
|
-
self._ffmpeg_command.insert(1, "-y")
|
|
126
|
-
|
|
127
|
-
return True
|
|
128
|
-
|
|
129
|
-
def _update_metrics(
|
|
130
|
-
self, line: str, metric: str, value: str, progress_handler: Optional[Callable]
|
|
131
|
-
) -> None:
|
|
132
|
-
"""Update progress information based on FFmpeg output."""
|
|
133
|
-
if metric == "total_size":
|
|
134
|
-
self._current_size = int(value)
|
|
135
|
-
|
|
136
|
-
elif metric == "out_time_ms":
|
|
137
|
-
self._metrics.seconds_processed = int(value) / 1_000_000
|
|
138
|
-
|
|
139
|
-
if self._duration_secs:
|
|
140
|
-
self._metrics.percentage = (
|
|
141
|
-
self._metrics.seconds_processed / self._duration_secs
|
|
142
|
-
) * 100
|
|
143
|
-
if self._current_size and self._metrics.percentage > 0:
|
|
144
|
-
self._metrics.estimated_size = int(
|
|
145
|
-
self._current_size * (100 / self._metrics.percentage)
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
elif metric == "speed":
|
|
149
|
-
speed_float = float(value.rstrip("x"))
|
|
150
|
-
if speed_float > 0:
|
|
151
|
-
self._metrics.speed = float(speed_float)
|
|
152
|
-
if self._duration_secs:
|
|
153
|
-
seconds_remaining = (
|
|
154
|
-
self._duration_secs - self._metrics.seconds_processed
|
|
155
|
-
)
|
|
156
|
-
self._metrics.eta = seconds_remaining / self._metrics.speed
|
|
157
|
-
|
|
158
|
-
if line == "progress=end":
|
|
159
|
-
self._metrics.percentage = 100
|
|
160
|
-
self._metrics.eta = 0
|
|
161
|
-
|
|
162
|
-
if progress_handler:
|
|
163
|
-
progress_handler(
|
|
164
|
-
self._metrics.percentage,
|
|
165
|
-
self._metrics.speed,
|
|
166
|
-
self._metrics.eta,
|
|
167
|
-
self._metrics.estimated_size,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
@staticmethod
|
|
171
|
-
def _handle_pipe(pipe, queue: Queue) -> None:
|
|
172
|
-
"""Read from the pipe and put the lines into the queue."""
|
|
173
|
-
try:
|
|
174
|
-
for line in pipe:
|
|
175
|
-
queue.put(line.strip())
|
|
176
|
-
finally:
|
|
177
|
-
pipe.close()
|
|
178
|
-
|
|
179
|
-
def _contains_shell_operators(self, command: List[str]) -> bool:
|
|
180
|
-
"""Check if the command contains shell operators."""
|
|
181
|
-
shell_operators = {"|", ">", "<", ">>", "&&", "||"}
|
|
182
|
-
return any(op in command for op in shell_operators)
|
|
183
|
-
|
|
184
|
-
def _start_process(
|
|
185
|
-
self,
|
|
186
|
-
progress_bar: Optional[Progress],
|
|
187
|
-
task_id: Optional[int],
|
|
188
|
-
progress_handler: Optional[Callable],
|
|
189
|
-
) -> int:
|
|
190
|
-
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
|
|
191
|
-
|
|
192
|
-
# If command contains shell operators, turn the list into a string.
|
|
193
|
-
if self._contains_shell_operators(self._ffmpeg_command):
|
|
194
|
-
final_command = " ".join(self._ffmpeg_command)
|
|
195
|
-
else:
|
|
196
|
-
final_command = self._ffmpeg_command
|
|
197
|
-
|
|
198
|
-
self._write_to_log_file(
|
|
199
|
-
f"This file contains the log for the following command:\n{final_command if isinstance(final_command, str) else ' '.join(final_command)}\n",
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
process = subprocess.Popen(
|
|
203
|
-
final_command,
|
|
204
|
-
stdout=subprocess.PIPE,
|
|
205
|
-
stderr=subprocess.PIPE,
|
|
206
|
-
text=True,
|
|
207
|
-
shell=isinstance(final_command, str),
|
|
208
|
-
creationflags=creationflags,
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
stdout_queue, stderr_queue = Queue(), Queue()
|
|
212
|
-
Thread(
|
|
213
|
-
target=self._handle_pipe, args=(process.stdout, stdout_queue), daemon=True
|
|
214
|
-
).start()
|
|
215
|
-
Thread(
|
|
216
|
-
target=self._handle_pipe, args=(process.stderr, stderr_queue), daemon=True
|
|
217
|
-
).start()
|
|
218
|
-
|
|
219
|
-
error_words = [
|
|
220
|
-
"error",
|
|
221
|
-
"failed",
|
|
222
|
-
"invalid",
|
|
223
|
-
"trailing",
|
|
224
|
-
"unable",
|
|
225
|
-
"unknown",
|
|
226
|
-
"no such file",
|
|
227
|
-
"permission denied",
|
|
228
|
-
"not found",
|
|
229
|
-
"unrecognized",
|
|
230
|
-
"undefined",
|
|
231
|
-
"unsupported",
|
|
232
|
-
"does not exist",
|
|
233
|
-
"missing",
|
|
234
|
-
"not available",
|
|
235
|
-
"cannot",
|
|
236
|
-
"could not",
|
|
237
|
-
"bad",
|
|
238
|
-
"wrong",
|
|
239
|
-
"forbidden",
|
|
240
|
-
]
|
|
241
|
-
|
|
242
|
-
try:
|
|
243
|
-
while process.poll() is None:
|
|
244
|
-
if self._duration_secs is not None:
|
|
245
|
-
try:
|
|
246
|
-
stdout = stdout_queue.get_nowait()
|
|
247
|
-
|
|
248
|
-
if stdout:
|
|
249
|
-
metric, value = stdout.split("=")
|
|
250
|
-
if (
|
|
251
|
-
metric in self.WANTED_METRICS
|
|
252
|
-
and value
|
|
253
|
-
and "N/A" not in value
|
|
254
|
-
):
|
|
255
|
-
self._update_metrics(
|
|
256
|
-
stdout, metric, value, progress_handler
|
|
257
|
-
)
|
|
258
|
-
if (
|
|
259
|
-
metric == "out_time_ms"
|
|
260
|
-
and progress_bar
|
|
261
|
-
and task_id is not None
|
|
262
|
-
):
|
|
263
|
-
progress_bar.update(
|
|
264
|
-
task_id,
|
|
265
|
-
completed=self._metrics.seconds_processed,
|
|
266
|
-
speed=self._metrics.speed,
|
|
267
|
-
)
|
|
268
|
-
except Empty:
|
|
269
|
-
pass
|
|
270
|
-
|
|
271
|
-
try:
|
|
272
|
-
stderr = stderr_queue.get_nowait()
|
|
273
|
-
|
|
274
|
-
if stderr:
|
|
275
|
-
self._write_to_log_file(stderr)
|
|
276
|
-
|
|
277
|
-
if any(keyword in stderr.lower() for keyword in error_words):
|
|
278
|
-
self._error_messages.append(stderr)
|
|
279
|
-
|
|
280
|
-
if self._duration_secs is None:
|
|
281
|
-
print(
|
|
282
|
-
stderr,
|
|
283
|
-
end="\n" if self._print_stderr_new_line else "\r",
|
|
284
|
-
flush=True,
|
|
285
|
-
)
|
|
286
|
-
except Empty:
|
|
287
|
-
pass
|
|
288
|
-
|
|
289
|
-
except KeyboardInterrupt:
|
|
290
|
-
self._kill_process_and_children(process.pid)
|
|
291
|
-
self._write_to_log_file("\n[KeyboardInterrupt] FFmpeg process killed.")
|
|
292
|
-
sys.exit("\n[KeyboardInterrupt] FFmpeg process killed.")
|
|
293
|
-
|
|
294
|
-
# After the process ends, drain any remaining stderr
|
|
295
|
-
while True:
|
|
296
|
-
try:
|
|
297
|
-
stderr = stderr_queue.get_nowait()
|
|
298
|
-
if stderr:
|
|
299
|
-
self._write_to_log_file(stderr)
|
|
300
|
-
|
|
301
|
-
if any(keyword in stderr.lower() for keyword in error_words):
|
|
302
|
-
self._error_messages.append(stderr)
|
|
303
|
-
|
|
304
|
-
if self._duration_secs is None:
|
|
305
|
-
print(
|
|
306
|
-
stderr,
|
|
307
|
-
end="\n" if self._print_stderr_new_line else "\r",
|
|
308
|
-
flush=True,
|
|
309
|
-
)
|
|
310
|
-
except Empty:
|
|
311
|
-
break
|
|
312
|
-
|
|
313
|
-
return process.returncode
|
|
314
|
-
|
|
315
|
-
def run(
|
|
316
|
-
self,
|
|
317
|
-
log_file: Optional[Union[str, Path]] = "ffmpeg_log.txt",
|
|
318
|
-
print_command=False,
|
|
319
|
-
progress_bar_description: str = None,
|
|
320
|
-
progress_handler: Optional[Callable] = None,
|
|
321
|
-
success_handler: Optional[Callable] = None,
|
|
322
|
-
error_handler: Optional[Callable] = None,
|
|
323
|
-
) -> None:
|
|
324
|
-
"""
|
|
325
|
-
Args:
|
|
326
|
-
log_file: Optional filepath to write FFmpeg output to
|
|
327
|
-
print_command: Print the FFmpeg command being executed. Default: False
|
|
328
|
-
progress_bar_description: Optional string to set a custom description for the progress bar
|
|
329
|
-
progress_handler: Optional function which receives progress metrics
|
|
330
|
-
success_handler: Optional function to handle successful completion of the FFmpeg process
|
|
331
|
-
error_handler: Optional function for FFmpeg process error handling
|
|
332
|
-
"""
|
|
333
|
-
if not self._should_overwrite():
|
|
334
|
-
return
|
|
335
|
-
|
|
336
|
-
self._ffmpeg_log_file = Path(log_file)
|
|
337
|
-
|
|
338
|
-
with open(self._ffmpeg_log_file, "w"):
|
|
339
|
-
pass
|
|
340
|
-
|
|
341
|
-
if print_command:
|
|
342
|
-
print(f"Executing: {' '.join(self._ffmpeg_command)}")
|
|
343
|
-
|
|
344
|
-
if self._duration_secs and not progress_handler:
|
|
345
|
-
with Progress(
|
|
346
|
-
SpinnerColumn(),
|
|
347
|
-
TextColumn("[progress.description]{task.description}"),
|
|
348
|
-
BarColumn(),
|
|
349
|
-
TaskProgressColumn(),
|
|
350
|
-
TimeElapsedColumn(),
|
|
351
|
-
TimeRemainingColumn(compact=True),
|
|
352
|
-
refresh_per_second=10,
|
|
353
|
-
) as progress_bar:
|
|
354
|
-
task_id = progress_bar.add_task(
|
|
355
|
-
progress_bar_description
|
|
356
|
-
if progress_bar_description is not None
|
|
357
|
-
else f"Processing {self._input_filepath.name}",
|
|
358
|
-
total=self._duration_secs,
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
return_code = self._start_process(
|
|
362
|
-
progress_bar,
|
|
363
|
-
task_id,
|
|
364
|
-
None,
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
if return_code == 0:
|
|
368
|
-
progress_bar.columns = (
|
|
369
|
-
TextColumn("[progress.description]{task.description}"),
|
|
370
|
-
)
|
|
371
|
-
progress_bar.update(
|
|
372
|
-
task_id,
|
|
373
|
-
description=f"✓ Processed 100% of {self._input_filepath.name}",
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
else:
|
|
377
|
-
return_code = self._start_process(
|
|
378
|
-
None,
|
|
379
|
-
None,
|
|
380
|
-
progress_handler,
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
if return_code == 0:
|
|
384
|
-
print_with_prefix(
|
|
385
|
-
"\n" if self._duration_secs is None else "",
|
|
386
|
-
"FFmpeg process completed.",
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
self._handle_process_ended(return_code, error_handler, success_handler)
|
|
390
|
-
|
|
391
|
-
def _handle_process_ended(
|
|
392
|
-
self,
|
|
393
|
-
return_code: int,
|
|
394
|
-
error_handler: Optional[Callable],
|
|
395
|
-
success_handler: Optional[Callable],
|
|
396
|
-
) -> None:
|
|
397
|
-
"""Handle the completion of the FFmpeg process."""
|
|
398
|
-
if return_code != 0:
|
|
399
|
-
if error_handler:
|
|
400
|
-
error_handler()
|
|
401
|
-
sys.exit(
|
|
402
|
-
"\nThe FFmpeg process encounted an error. Custom error handler executed."
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
print_with_prefix(
|
|
406
|
-
"[Better FFmpeg Progress] ",
|
|
407
|
-
f"FFmpeg process failed with return code {return_code}.\nError(s):",
|
|
408
|
-
)
|
|
409
|
-
print_with_prefix(
|
|
410
|
-
"[Better FFmpeg Progress] ", "\n".join(self._error_messages)
|
|
411
|
-
)
|
|
412
|
-
print_with_prefix(
|
|
413
|
-
"[Better FFmpeg Progress] ",
|
|
414
|
-
f"Check out '{self._ffmpeg_log_file}' for more details.",
|
|
415
|
-
)
|
|
416
|
-
sys.exit(1)
|
|
417
|
-
|
|
418
|
-
if success_handler:
|
|
419
|
-
success_handler()
|
|
420
|
-
|
|
421
|
-
def _kill_process_and_children(self, proc_pid: int) -> None:
|
|
422
|
-
"""Kill the FFmpeg process and its children."""
|
|
423
|
-
try:
|
|
424
|
-
process = psutil.Process(proc_pid)
|
|
425
|
-
for child in process.children(recursive=True):
|
|
426
|
-
child.terminate()
|
|
427
|
-
# Terminate the main process
|
|
428
|
-
process.terminate()
|
|
429
|
-
except psutil.NoSuchProcess:
|
|
430
|
-
pass
|
|
431
|
-
|
|
432
|
-
def _write_to_log_file(self, message: str, mode: str = "a") -> None:
|
|
433
|
-
if self._ffmpeg_log_file:
|
|
434
|
-
with open(self._ffmpeg_log_file, mode) as f:
|
|
435
|
-
f.write(f"{message}\n")
|
|
436
|
-
f.flush() # Ensure the message is written immediately
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: better-ffmpeg-progress
|
|
3
|
-
Version: 2.6.1
|
|
4
|
-
Summary: Run FFmpeg & see percentage progress + ETA.
|
|
5
|
-
Home-page: https://github.com/CrypticSignal/better-ffmpeg-progress
|
|
6
|
-
Author: GitHub.com/CrypticSignal
|
|
7
|
-
Author-email: theaudiophile@outlook.com
|
|
8
|
-
Keywords: ffmpeg,progress
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.7
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
License-File: LICENSE
|
|
15
|
-
Requires-Dist: ffmpeg-python
|
|
16
|
-
Requires-Dist: psutil
|
|
17
|
-
Requires-Dist: rich
|
|
18
|
-
|
|
19
|
-
<div align="center">
|
|
20
|
-
|
|
21
|
-
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
22
|
-
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
23
|
-

|
|
24
|
-
[](LICENSE.txt)
|
|
25
|
-
|
|
26
|
-
# Better FFmpeg Progress
|
|
27
|
-
Runs an FFmpeg command and shows a progress bar with percentage progress, time elapsed and ETA.
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
FFmpeg outputs something like:
|
|
31
|
-
```
|
|
32
|
-
frame= 692 fps= 58 q=28.0 size= 5376KiB time=00:00:28.77 bitrate=1530.3kbits/s speed=2.43x
|
|
33
|
-
```
|
|
34
|
-
Better FFmpeg Progress outputs something like:
|
|
35
|
-
```
|
|
36
|
-
⠏ Processing abc.webm ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
|
|
37
|
-
```
|
|
38
|
-
Where:
|
|
39
|
-
- `Processing abc.webm` is the description of the progresss bar.
|
|
40
|
-
- `23%` is the percentage progress.
|
|
41
|
-
- `0:00:04` is the time (H:MM:SS) elapsed.
|
|
42
|
-
- `00:15` is the estimated time until the FFmpeg process completes.
|
|
43
|
-
|
|
44
|
-
As you can see, the output of Better FFmpeg Progress is much more useful.
|
|
45
|
-
|
|
46
|
-
## Installation
|
|
47
|
-
```
|
|
48
|
-
pip install better-ffmpeg-progress --upgrade
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Usage
|
|
52
|
-
Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would to `subprocess.run()`.
|
|
53
|
-
|
|
54
|
-
Simple Example:
|
|
55
|
-
```py
|
|
56
|
-
from better_ffmpeg_progress import FfmpegProcess
|
|
57
|
-
# Pass a list of FFmpeg arguments, like you would if using subprocess.run()
|
|
58
|
-
process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
|
|
59
|
-
# Use the run method to run the FFmpeg command.
|
|
60
|
-
process.run()
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Advanced Example:
|
|
64
|
-
```py
|
|
65
|
-
from better_ffmpeg_progress import FfmpegProcess
|
|
66
|
-
|
|
67
|
-
def handle_progress_info(percentage, speed, eta, estimated_filesize):
|
|
68
|
-
print(f"Estimated Output Filesize: {estimated_filesize / 1_000_000} MB")
|
|
69
|
-
|
|
70
|
-
def handle_success():
|
|
71
|
-
# Code to run if the FFmpeg process completes successfully.
|
|
72
|
-
pass
|
|
73
|
-
|
|
74
|
-
def handle_error():
|
|
75
|
-
# Code to run if the FFmpeg process encounters an error.
|
|
76
|
-
pass
|
|
77
|
-
|
|
78
|
-
# Pass a list of FFmpeg arguments, like you would if using subprocess.run() or subprocess.Popen()
|
|
79
|
-
process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
|
|
80
|
-
|
|
81
|
-
# Use the run method to run the FFmpeg command.
|
|
82
|
-
process.run(
|
|
83
|
-
progress_bar_description="Converting file...",
|
|
84
|
-
progress_handler=handle_progress_info,
|
|
85
|
-
log_file="ffmpeg_log.txt",
|
|
86
|
-
success_handler=handle_success,
|
|
87
|
-
error_handler=handle_error,
|
|
88
|
-
)
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## Optional Arguments
|
|
92
|
-
An instance of the `FfmpegProcess` class takes the following **optional** arguments:
|
|
93
|
-
|
|
94
|
-
- `ffmpeg_loglevel`: Desired FFmpeg log level. Default: `"verbose"`
|
|
95
|
-
- `print_stderr_new_line`: If better progress information cannot be shown, print FFmpeg stderr in a new line instead of replacing the current line in the terminal. Default: False
|
|
96
|
-
- `print_detected_duration`: Print the detected duration of the input file. Default: True
|
|
97
|
-
|
|
98
|
-
The `run` method takes the following **optional** arguments:
|
|
99
|
-
- `print_command` - Print the FFmpeg command being executed. Default: `False`
|
|
100
|
-
- `progress_bar_description` - An optional string to set a custom description for the progress bar. The default description is `Processing <file>`. This can be an empty string if you don't want the progress bar to have a description.
|
|
101
|
-
- `progress_handler`
|
|
102
|
-
- You can create a function if you would like to do something with the following values:
|
|
103
|
-
- Percentage progress. [float]
|
|
104
|
-
- Speed, e.g. `22.3x` which means that 22.3 seconds of the input are processed every second. [string]
|
|
105
|
-
- ETA in seconds. [float]
|
|
106
|
-
- Estimated output filesize in bytes. [float]
|
|
107
|
-
- _Note: This is not accurate. Please take the value with a grain of salt._
|
|
108
|
-
|
|
109
|
-
The values will be `None` if unknown. The function will receive the current values of these metrics as arguments, every 0.1s.
|
|
110
|
-
|
|
111
|
-
- `log_file` - The filepath to save the FFmpeg log to.
|
|
112
|
-
- `success_handler` - A function to run if the FFmpeg process completes successfully.
|
|
113
|
-
- `error_handler` - A function to run if the FFmpeg process encounters an error.
|
|
File without changes
|
{better_ffmpeg_progress-2.6.1 → better_ffmpeg_progress-3.1.0}/better_ffmpeg_progress/__init__.py
RENAMED
|
File without changes
|
{better_ffmpeg_progress-2.6.1 → better_ffmpeg_progress-3.1.0}/better_ffmpeg_progress/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|