better-ffmpeg-progress 5.1.1__tar.gz → 5.3.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-5.3.0/PKG-INFO +127 -0
- better_ffmpeg_progress-5.3.0/README.md +108 -0
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/better_ffmpeg_progress.py +77 -27
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/progress_bars.py +57 -19
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/terminate_process.py +11 -33
- better_ffmpeg_progress-5.3.0/better_ffmpeg_progress/utils.py +284 -0
- better_ffmpeg_progress-5.3.0/better_ffmpeg_progress.egg-info/PKG-INFO +127 -0
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/setup.py +1 -1
- better_ffmpeg_progress-5.1.1/PKG-INFO +0 -94
- better_ffmpeg_progress-5.1.1/README.md +0 -75
- better_ffmpeg_progress-5.1.1/better_ffmpeg_progress/utils.py +0 -151
- better_ffmpeg_progress-5.1.1/better_ffmpeg_progress.egg-info/PKG-INFO +0 -94
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/LICENSE +0 -0
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/__init__.py +0 -0
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/enums.py +0 -0
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/exceptions.py +0 -0
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress.egg-info/SOURCES.txt +0 -0
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress.egg-info/dependency_links.txt +0 -0
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress.egg-info/requires.txt +0 -0
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress.egg-info/top_level.txt +0 -0
- {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/setup.cfg +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: better-ffmpeg-progress
|
|
3
|
+
Version: 5.3.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: psutil
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Requires-Dist: rich
|
|
18
|
+
Requires-Dist: tqdm
|
|
19
|
+
|
|
20
|
+
<div align="center">
|
|
21
|
+
|
|
22
|
+
# Better FFmpeg Progress
|
|
23
|
+
Runs an FFmpeg command and shows a progress bar with percentage progress, elapsed time and ETA.
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
FFmpeg outputs something like:
|
|
27
|
+
```
|
|
28
|
+
frame= 692 fps= 58 q=28.0 size= 5376KiB time=00:00:28.77 bitrate=1530.3kbits/s speed=2.43x
|
|
29
|
+
```
|
|
30
|
+
Better FFmpeg Progress outputs something like:
|
|
31
|
+
```
|
|
32
|
+
⠏ Processing abc.webm (5.50 MB / ~23.91 MB) ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
|
|
33
|
+
```
|
|
34
|
+
Where:
|
|
35
|
+
- `Processing abc.webm` is the description of the progresss bar. Filenames longer than 60 characters are truncated.
|
|
36
|
+
- `(5.50 MB / ~23.91 MB)` is the current output size and the estimated final output size.
|
|
37
|
+
- `23%` is the percentage progress.
|
|
38
|
+
- `0:00:04` is the time elapsed.
|
|
39
|
+
- `00:15` is the estimated time until the FFmpeg process completes.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
```bash
|
|
43
|
+
pip install better-ffmpeg-progress --upgrade
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would when using `subprocess.run()` or `subprocess.Popen()`. Example:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from better_ffmpeg_progress import FfmpegProcess, FfmpegProcessError
|
|
51
|
+
|
|
52
|
+
command = [
|
|
53
|
+
"ffmpeg",
|
|
54
|
+
"-i",
|
|
55
|
+
"https://media.xiph.org/video/derf/y4m/ducks_take_off_1080p50.y4m",
|
|
56
|
+
"-map",
|
|
57
|
+
"0:V",
|
|
58
|
+
"-c:V",
|
|
59
|
+
"libx264",
|
|
60
|
+
"-preset",
|
|
61
|
+
"ultrafast",
|
|
62
|
+
"-f",
|
|
63
|
+
"null",
|
|
64
|
+
"-",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
process = FfmpegProcess(command)
|
|
69
|
+
# Uncomment this line to use tqdm instead of Rich.
|
|
70
|
+
# process.use_tqdm = True
|
|
71
|
+
|
|
72
|
+
process.run()
|
|
73
|
+
except FfmpegProcessError as e:
|
|
74
|
+
print(
|
|
75
|
+
"An error occurred when running Better FFmpeg Progress:\n"
|
|
76
|
+
f"{e}"
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Trimming
|
|
81
|
+
If your command trims the input with `-ss`, `-sseof`, `-t` or `-to` (as input or output options), the expected duration of the output is calculated automatically, so the progress bar and ETA reflect the trimmed output rather than the whole input file. This adjustment is skipped when `-copyts` is used.
|
|
82
|
+
|
|
83
|
+
## Duration Override
|
|
84
|
+
By default, Better FFmpeg Progress detects the duration of the input file and uses it to calculate percentage progress and ETA.
|
|
85
|
+
|
|
86
|
+
For partial, damaged, streamed or synthetic inputs, FFmpeg may be unable to detect the duration correctly. In these cases, you can provide the duration manually using `duration_override`.
|
|
87
|
+
|
|
88
|
+
The value should be the duration of the input file, in seconds:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from better_ffmpeg_progress import FfmpegProcess
|
|
92
|
+
|
|
93
|
+
command = [
|
|
94
|
+
"ffmpeg",
|
|
95
|
+
"-i",
|
|
96
|
+
"partial-input.flv",
|
|
97
|
+
"-c",
|
|
98
|
+
"copy",
|
|
99
|
+
"recovered-output.mkv",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
process = FfmpegProcess(
|
|
103
|
+
command,
|
|
104
|
+
duration_override=1222.64,
|
|
105
|
+
print_detected_duration=True,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
process.run()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Optional Arguments
|
|
112
|
+
An instance of `FfmpegProcess` accepts the following optional arguments:
|
|
113
|
+
* `ffmpeg_log_level` — Set a value for FFmpeg's `-loglevel` option. Default: `"verbose"`.
|
|
114
|
+
* `ffmpeg_log_file` — Filepath or file-like object to which FFmpeg stderr will be written. Default: `<input filename>_ffmpeg_log.txt`.
|
|
115
|
+
* `print_detected_duration` — Print the detected duration. Default: `False`.
|
|
116
|
+
* `duration_override` — Specify input file duration in seconds to use for progress calculation, instead of trying to detect this automatically with FFprobe. Default: `None`.
|
|
117
|
+
|
|
118
|
+
The `run` method accepts the following optional argument:
|
|
119
|
+
* `print_command` — Print the FFmpeg command being executed. Default: `False`.
|
|
120
|
+
|
|
121
|
+
## Using tqdm
|
|
122
|
+
By default, the [Rich](https://github.com/Textualize/rich) library is used to display a progress bar. [tqdm](https://github.com/tqdm/tqdm) can be used instead by setting `process.use_tqdm = True`:
|
|
123
|
+
```python
|
|
124
|
+
process = FfmpegProcess(command)
|
|
125
|
+
process.use_tqdm = True
|
|
126
|
+
process.run()
|
|
127
|
+
```
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Better FFmpeg Progress
|
|
4
|
+
Runs an FFmpeg command and shows a progress bar with percentage progress, elapsed time and ETA.
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
FFmpeg outputs something like:
|
|
8
|
+
```
|
|
9
|
+
frame= 692 fps= 58 q=28.0 size= 5376KiB time=00:00:28.77 bitrate=1530.3kbits/s speed=2.43x
|
|
10
|
+
```
|
|
11
|
+
Better FFmpeg Progress outputs something like:
|
|
12
|
+
```
|
|
13
|
+
⠏ Processing abc.webm (5.50 MB / ~23.91 MB) ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
|
|
14
|
+
```
|
|
15
|
+
Where:
|
|
16
|
+
- `Processing abc.webm` is the description of the progresss bar. Filenames longer than 60 characters are truncated.
|
|
17
|
+
- `(5.50 MB / ~23.91 MB)` is the current output size and the estimated final output size.
|
|
18
|
+
- `23%` is the percentage progress.
|
|
19
|
+
- `0:00:04` is the time elapsed.
|
|
20
|
+
- `00:15` is the estimated time until the FFmpeg process completes.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
```bash
|
|
24
|
+
pip install better-ffmpeg-progress --upgrade
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would when using `subprocess.run()` or `subprocess.Popen()`. Example:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from better_ffmpeg_progress import FfmpegProcess, FfmpegProcessError
|
|
32
|
+
|
|
33
|
+
command = [
|
|
34
|
+
"ffmpeg",
|
|
35
|
+
"-i",
|
|
36
|
+
"https://media.xiph.org/video/derf/y4m/ducks_take_off_1080p50.y4m",
|
|
37
|
+
"-map",
|
|
38
|
+
"0:V",
|
|
39
|
+
"-c:V",
|
|
40
|
+
"libx264",
|
|
41
|
+
"-preset",
|
|
42
|
+
"ultrafast",
|
|
43
|
+
"-f",
|
|
44
|
+
"null",
|
|
45
|
+
"-",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
process = FfmpegProcess(command)
|
|
50
|
+
# Uncomment this line to use tqdm instead of Rich.
|
|
51
|
+
# process.use_tqdm = True
|
|
52
|
+
|
|
53
|
+
process.run()
|
|
54
|
+
except FfmpegProcessError as e:
|
|
55
|
+
print(
|
|
56
|
+
"An error occurred when running Better FFmpeg Progress:\n"
|
|
57
|
+
f"{e}"
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Trimming
|
|
62
|
+
If your command trims the input with `-ss`, `-sseof`, `-t` or `-to` (as input or output options), the expected duration of the output is calculated automatically, so the progress bar and ETA reflect the trimmed output rather than the whole input file. This adjustment is skipped when `-copyts` is used.
|
|
63
|
+
|
|
64
|
+
## Duration Override
|
|
65
|
+
By default, Better FFmpeg Progress detects the duration of the input file and uses it to calculate percentage progress and ETA.
|
|
66
|
+
|
|
67
|
+
For partial, damaged, streamed or synthetic inputs, FFmpeg may be unable to detect the duration correctly. In these cases, you can provide the duration manually using `duration_override`.
|
|
68
|
+
|
|
69
|
+
The value should be the duration of the input file, in seconds:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from better_ffmpeg_progress import FfmpegProcess
|
|
73
|
+
|
|
74
|
+
command = [
|
|
75
|
+
"ffmpeg",
|
|
76
|
+
"-i",
|
|
77
|
+
"partial-input.flv",
|
|
78
|
+
"-c",
|
|
79
|
+
"copy",
|
|
80
|
+
"recovered-output.mkv",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
process = FfmpegProcess(
|
|
84
|
+
command,
|
|
85
|
+
duration_override=1222.64,
|
|
86
|
+
print_detected_duration=True,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
process.run()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Optional Arguments
|
|
93
|
+
An instance of `FfmpegProcess` accepts the following optional arguments:
|
|
94
|
+
* `ffmpeg_log_level` — Set a value for FFmpeg's `-loglevel` option. Default: `"verbose"`.
|
|
95
|
+
* `ffmpeg_log_file` — Filepath or file-like object to which FFmpeg stderr will be written. Default: `<input filename>_ffmpeg_log.txt`.
|
|
96
|
+
* `print_detected_duration` — Print the detected duration. Default: `False`.
|
|
97
|
+
* `duration_override` — Specify input file duration in seconds to use for progress calculation, instead of trying to detect this automatically with FFprobe. Default: `None`.
|
|
98
|
+
|
|
99
|
+
The `run` method accepts the following optional argument:
|
|
100
|
+
* `print_command` — Print the FFmpeg command being executed. Default: `False`.
|
|
101
|
+
|
|
102
|
+
## Using tqdm
|
|
103
|
+
By default, the [Rich](https://github.com/Textualize/rich) library is used to display a progress bar. [tqdm](https://github.com/tqdm/tqdm) can be used instead by setting `process.use_tqdm = True`:
|
|
104
|
+
```python
|
|
105
|
+
process = FfmpegProcess(command)
|
|
106
|
+
process.use_tqdm = True
|
|
107
|
+
process.run()
|
|
108
|
+
```
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import math
|
|
1
2
|
import os
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
import subprocess
|
|
@@ -14,6 +15,7 @@ from .exceptions import (
|
|
|
14
15
|
from .utils import (
|
|
15
16
|
validate_ffmpeg_command,
|
|
16
17
|
get_media_duration,
|
|
18
|
+
get_expected_output_duration,
|
|
17
19
|
check_shell_needed_for_command,
|
|
18
20
|
)
|
|
19
21
|
from .terminate_process import terminate_ffmpeg_process
|
|
@@ -27,18 +29,16 @@ class FfmpegProcess:
|
|
|
27
29
|
"""
|
|
28
30
|
Handles the logic for overwriting an existing output file.
|
|
29
31
|
Modifies self._ffmpeg_command to include -y if overwrite is confirmed.
|
|
30
|
-
Raises FfmpegProcessUserCancelledError or
|
|
32
|
+
Raises FfmpegProcessUserCancelledError or
|
|
33
|
+
FfmpegProcessInterruptedError if not proceeding.
|
|
31
34
|
"""
|
|
32
35
|
try:
|
|
33
36
|
answer = (
|
|
34
|
-
input(
|
|
35
|
-
f"Output file {self._output_filepath} already exists. Overwrite? [y/N]: "
|
|
36
|
-
)
|
|
37
|
+
input(f"Output file {self._output_filepath} already exists. Overwrite? [y/N]: ")
|
|
37
38
|
.strip()
|
|
38
39
|
.lower()
|
|
39
40
|
)
|
|
40
41
|
if answer == "y":
|
|
41
|
-
# Insert -y
|
|
42
42
|
self._ffmpeg_command.insert(1, _FFMPEG_OVERWRITE_FLAG)
|
|
43
43
|
else:
|
|
44
44
|
raise FfmpegProcessUserCancelledError(
|
|
@@ -59,8 +59,24 @@ class FfmpegProcess:
|
|
|
59
59
|
ffmpeg_log_level: Optional[Union[FfmpegLogLevel, str]] = None,
|
|
60
60
|
ffmpeg_log_file: Optional[Union[str, os.PathLike]] = None,
|
|
61
61
|
print_detected_duration: bool = False,
|
|
62
|
+
duration_override: Optional[float] = None,
|
|
62
63
|
):
|
|
63
|
-
|
|
64
|
+
"""
|
|
65
|
+
Create an FFmpeg process with progress reporting.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
command:
|
|
69
|
+
FFmpeg command as a list of arguments.
|
|
70
|
+
ffmpeg_log_level:
|
|
71
|
+
FFmpeg log level enum or case-insensitive string.
|
|
72
|
+
ffmpeg_log_file:
|
|
73
|
+
File path or file-like object used for FFmpeg stderr.
|
|
74
|
+
print_detected_duration:
|
|
75
|
+
Print the duration used for progress calculation.
|
|
76
|
+
duration_override:
|
|
77
|
+
Specify input file duration in seconds to use for progress calculation, instead of trying to detect this automatically with FFprobe. This is useful for partial, damaged, streamed, or
|
|
78
|
+
synthetic inputs whose duration cannot be detected correctly.
|
|
79
|
+
"""
|
|
64
80
|
validate_ffmpeg_command(command)
|
|
65
81
|
|
|
66
82
|
ffmpeg_log_level_val: str
|
|
@@ -75,20 +91,24 @@ class FfmpegProcess:
|
|
|
75
91
|
except ValueError:
|
|
76
92
|
valid_levels = [e.value for e in FfmpegLogLevel]
|
|
77
93
|
raise FfmpegCommandError(
|
|
78
|
-
f"Invalid ffmpeg_log_level string:
|
|
79
|
-
f"Must be one of
|
|
94
|
+
f"Invalid ffmpeg_log_level string: "
|
|
95
|
+
f"'{ffmpeg_log_level}'. Must be one of "
|
|
96
|
+
f"{valid_levels} (case-insensitive)."
|
|
80
97
|
)
|
|
81
98
|
else:
|
|
82
99
|
raise TypeError(
|
|
83
|
-
|
|
84
|
-
f"not
|
|
100
|
+
"ffmpeg_log_level must be an FfmpegLogLevel enum instance, "
|
|
101
|
+
f"a string, or None, not "
|
|
102
|
+
f"{type(ffmpeg_log_level).__name__}"
|
|
85
103
|
)
|
|
104
|
+
|
|
86
105
|
self._ffmpeg_log_level_val = ffmpeg_log_level_val
|
|
87
106
|
|
|
88
107
|
input_file_index = command.index("-i")
|
|
89
108
|
input_file_path_str = command[input_file_index + 1]
|
|
90
109
|
self._input_filepath = Path(input_file_path_str)
|
|
91
|
-
|
|
110
|
+
|
|
111
|
+
# Assumes last argument is output.
|
|
92
112
|
self._output_filepath = Path(command[-1])
|
|
93
113
|
|
|
94
114
|
if ffmpeg_log_file is None:
|
|
@@ -99,14 +119,44 @@ class FfmpegProcess:
|
|
|
99
119
|
self._ffmpeg_log_file = ffmpeg_log_file
|
|
100
120
|
|
|
101
121
|
self._print_detected_duration = print_detected_duration
|
|
102
|
-
self._duration_secs = get_media_duration(input_file_path_str)
|
|
103
122
|
|
|
104
|
-
if
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
if duration_override is not None:
|
|
124
|
+
if isinstance(duration_override, bool):
|
|
125
|
+
raise TypeError("duration_override must be a number of seconds, not bool")
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
duration_value = float(duration_override)
|
|
129
|
+
except (TypeError, ValueError) as e:
|
|
130
|
+
raise TypeError("duration_override must be a number of seconds or None") from e
|
|
131
|
+
|
|
132
|
+
if not math.isfinite(duration_value) or duration_value <= 0:
|
|
133
|
+
raise ValueError("duration_override must be a finite number greater than 0")
|
|
134
|
+
|
|
135
|
+
self._duration_secs = duration_value
|
|
136
|
+
else:
|
|
137
|
+
self._duration_secs = get_media_duration(input_file_path_str)
|
|
138
|
+
|
|
139
|
+
input_duration_secs = self._duration_secs
|
|
140
|
+
|
|
141
|
+
# Account for trimming (-ss, -sseof, -t, -to) so the progress bar reflects the
|
|
142
|
+
# duration of the output rather than the whole input.
|
|
143
|
+
if self._duration_secs is not None:
|
|
144
|
+
self._duration_secs = get_expected_output_duration(command, self._duration_secs)
|
|
145
|
+
|
|
146
|
+
if duration_override is not None:
|
|
147
|
+
print(f"Using duration override: {input_duration_secs:.2f} seconds")
|
|
148
|
+
elif self._print_detected_duration:
|
|
149
|
+
if self._duration_secs is None:
|
|
150
|
+
print("Could not detect duration. Progress bar may not show time remaining.")
|
|
151
|
+
else:
|
|
152
|
+
print(f"Detected duration: {input_duration_secs:.2f} seconds")
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
self._duration_secs is not None
|
|
156
|
+
and self._duration_secs != input_duration_secs
|
|
157
|
+
and (duration_override is not None or self._print_detected_duration)
|
|
158
|
+
):
|
|
159
|
+
print(f"Expected output duration after trimming: {self._duration_secs:.2f} seconds")
|
|
110
160
|
|
|
111
161
|
self._ffmpeg_command = [
|
|
112
162
|
command[0],
|
|
@@ -114,14 +164,12 @@ class FfmpegProcess:
|
|
|
114
164
|
"-loglevel",
|
|
115
165
|
self._ffmpeg_log_level_val,
|
|
116
166
|
"-progress",
|
|
117
|
-
"pipe:1",
|
|
167
|
+
"pipe:1",
|
|
118
168
|
"-nostats",
|
|
119
169
|
]
|
|
120
170
|
self._ffmpeg_command.extend(command[1:])
|
|
121
171
|
|
|
122
|
-
is_overwrite_in_user_command = any(
|
|
123
|
-
arg == _FFMPEG_OVERWRITE_FLAG for arg in command[1:]
|
|
124
|
-
)
|
|
172
|
+
is_overwrite_in_user_command = any(arg == _FFMPEG_OVERWRITE_FLAG for arg in command[1:])
|
|
125
173
|
|
|
126
174
|
if self._output_filepath.exists() and not is_overwrite_in_user_command:
|
|
127
175
|
self._handle_overwrite_prompt()
|
|
@@ -158,7 +206,11 @@ class FfmpegProcess:
|
|
|
158
206
|
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
159
207
|
|
|
160
208
|
if isinstance(self._ffmpeg_log_file, Path):
|
|
161
|
-
with open(
|
|
209
|
+
with open(
|
|
210
|
+
self._ffmpeg_log_file,
|
|
211
|
+
"w",
|
|
212
|
+
encoding="utf-8",
|
|
213
|
+
) as f:
|
|
162
214
|
self._process = subprocess.Popen(
|
|
163
215
|
current_ffmpeg_command,
|
|
164
216
|
shell=self._shell_needed,
|
|
@@ -185,13 +237,11 @@ class FfmpegProcess:
|
|
|
185
237
|
except KeyboardInterrupt:
|
|
186
238
|
self._terminate()
|
|
187
239
|
finally:
|
|
188
|
-
if self._process:
|
|
189
|
-
|
|
190
|
-
self._process.stdout.close()
|
|
240
|
+
if self._process and self._process.stdout:
|
|
241
|
+
self._process.stdout.close()
|
|
191
242
|
|
|
192
243
|
def _terminate(self):
|
|
193
244
|
if self._process:
|
|
194
245
|
terminate_ffmpeg_process(self._process)
|
|
195
246
|
else:
|
|
196
247
|
exit()
|
|
197
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import subprocess
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
4
|
|
|
5
5
|
from rich.progress import (
|
|
6
6
|
Progress,
|
|
@@ -12,16 +12,26 @@ from rich.progress import (
|
|
|
12
12
|
)
|
|
13
13
|
from tqdm import tqdm
|
|
14
14
|
|
|
15
|
-
from .utils import
|
|
15
|
+
from .utils import (
|
|
16
|
+
estimate_final_size_mb,
|
|
17
|
+
parse_ffmpeg_progress_line,
|
|
18
|
+
parse_ffmpeg_total_size_line,
|
|
19
|
+
truncate_filename,
|
|
20
|
+
)
|
|
16
21
|
from .exceptions import FfmpegProcessError
|
|
17
22
|
|
|
18
23
|
if TYPE_CHECKING:
|
|
19
24
|
from .better_ffmpeg_progress import FfmpegProcess
|
|
20
25
|
|
|
26
|
+
_MAX_FILENAME_LENGTH = 60
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _format_size(current_size_mb: float, estimated_size_mb: Optional[float]) -> str:
|
|
30
|
+
estimate = f"~{estimated_size_mb:.2f} MB" if estimated_size_mb is not None else "~? MB"
|
|
31
|
+
return f"({current_size_mb:.2f} MB / {estimate})"
|
|
32
|
+
|
|
21
33
|
|
|
22
|
-
def use_rich(
|
|
23
|
-
ffmpeg_process_instance: "FfmpegProcess", process: subprocess.Popen
|
|
24
|
-
) -> None:
|
|
34
|
+
def use_rich(ffmpeg_process_instance: "FfmpegProcess", process: subprocess.Popen) -> None:
|
|
25
35
|
task_id = None
|
|
26
36
|
progress_bar_instance = Progress(
|
|
27
37
|
TextColumn("[progress.description]{task.description}"),
|
|
@@ -33,11 +43,14 @@ def use_rich(
|
|
|
33
43
|
)
|
|
34
44
|
|
|
35
45
|
duration_secs = ffmpeg_process_instance._duration_secs
|
|
36
|
-
input_filename =
|
|
46
|
+
input_filename = truncate_filename(
|
|
47
|
+
ffmpeg_process_instance._input_filepath.name, _MAX_FILENAME_LENGTH
|
|
48
|
+
)
|
|
37
49
|
log_file = ffmpeg_process_instance._ffmpeg_log_file
|
|
38
50
|
|
|
39
51
|
with progress_bar_instance as progress_bar:
|
|
40
52
|
if duration_secs:
|
|
53
|
+
current_size_mb = 0.0
|
|
41
54
|
task_id = progress_bar.add_task(
|
|
42
55
|
f"Processing {input_filename}",
|
|
43
56
|
total=duration_secs,
|
|
@@ -46,11 +59,25 @@ def use_rich(
|
|
|
46
59
|
|
|
47
60
|
for line_bytes in process.stdout:
|
|
48
61
|
stripped_line_bytes = line_bytes.strip()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
|
|
63
|
+
size_mb = parse_ffmpeg_total_size_line(stripped_line_bytes)
|
|
64
|
+
if size_mb is not None:
|
|
65
|
+
current_size_mb = size_mb
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
# FFmpeg writes total_size before out_time_us in each progress block,
|
|
69
|
+
# so updating here refreshes the bar once per block with the latest size.
|
|
70
|
+
progress_val_secs = parse_ffmpeg_progress_line(stripped_line_bytes, duration_secs)
|
|
52
71
|
if progress_val_secs is not None and task_id is not None:
|
|
53
|
-
|
|
72
|
+
estimated_size_mb = estimate_final_size_mb(
|
|
73
|
+
current_size_mb, progress_val_secs, duration_secs
|
|
74
|
+
)
|
|
75
|
+
update_progress(
|
|
76
|
+
task_id,
|
|
77
|
+
completed=progress_val_secs,
|
|
78
|
+
description=f"Processing {input_filename} "
|
|
79
|
+
f"{_format_size(current_size_mb, estimated_size_mb)}",
|
|
80
|
+
)
|
|
54
81
|
else:
|
|
55
82
|
print(f"Processing '{input_filename}'...")
|
|
56
83
|
|
|
@@ -66,7 +93,7 @@ def use_rich(
|
|
|
66
93
|
)
|
|
67
94
|
update_progress(
|
|
68
95
|
task_id,
|
|
69
|
-
description=f"✓ Processed '{input_filename}'",
|
|
96
|
+
description=f"✓ Processed '{input_filename}' ({current_size_mb:.2f} MB)",
|
|
70
97
|
)
|
|
71
98
|
else:
|
|
72
99
|
if task_id is not None:
|
|
@@ -75,9 +102,7 @@ def use_rich(
|
|
|
75
102
|
raise FfmpegProcessError(f"FFmpeg process failed. Check '{log_file}' for details.")
|
|
76
103
|
|
|
77
104
|
|
|
78
|
-
def use_tqdm(
|
|
79
|
-
ffmpeg_process_instance: "FfmpegProcess", process: subprocess.Popen
|
|
80
|
-
) -> None:
|
|
105
|
+
def use_tqdm(ffmpeg_process_instance: "FfmpegProcess", process: subprocess.Popen) -> None:
|
|
81
106
|
progress_bar = None
|
|
82
107
|
try:
|
|
83
108
|
width = os.get_terminal_size().columns
|
|
@@ -85,9 +110,13 @@ def use_tqdm(
|
|
|
85
110
|
width = 80
|
|
86
111
|
|
|
87
112
|
duration_secs = ffmpeg_process_instance._duration_secs
|
|
88
|
-
input_filename =
|
|
113
|
+
input_filename = truncate_filename(
|
|
114
|
+
ffmpeg_process_instance._input_filepath.name, _MAX_FILENAME_LENGTH
|
|
115
|
+
)
|
|
89
116
|
log_file = ffmpeg_process_instance._ffmpeg_log_file
|
|
90
117
|
|
|
118
|
+
current_size_mb = 0.0
|
|
119
|
+
|
|
91
120
|
if duration_secs:
|
|
92
121
|
progress_bar = tqdm(
|
|
93
122
|
mininterval=0.5,
|
|
@@ -100,12 +129,20 @@ def use_tqdm(
|
|
|
100
129
|
|
|
101
130
|
for line_bytes in process.stdout:
|
|
102
131
|
stripped_line_bytes = line_bytes.strip()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
|
|
133
|
+
size_mb = parse_ffmpeg_total_size_line(stripped_line_bytes)
|
|
134
|
+
if size_mb is not None:
|
|
135
|
+
current_size_mb = size_mb
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
progress_val_secs = parse_ffmpeg_progress_line(stripped_line_bytes, duration_secs)
|
|
106
139
|
if progress_val_secs is not None and progress_bar is not None:
|
|
140
|
+
estimated_size_mb = estimate_final_size_mb(
|
|
141
|
+
current_size_mb, progress_val_secs, duration_secs
|
|
142
|
+
)
|
|
107
143
|
progress_bar.n = progress_val_secs
|
|
108
|
-
|
|
144
|
+
# set_postfix_str refreshes the bar by default
|
|
145
|
+
progress_bar.set_postfix_str(_format_size(current_size_mb, estimated_size_mb))
|
|
109
146
|
else:
|
|
110
147
|
print(f"Processing '{input_filename}'...")
|
|
111
148
|
|
|
@@ -115,6 +152,7 @@ def use_tqdm(
|
|
|
115
152
|
if progress_bar:
|
|
116
153
|
progress_bar.n = duration_secs
|
|
117
154
|
progress_bar.set_description(f"✓ Processed '{input_filename}'")
|
|
155
|
+
progress_bar.set_postfix_str(f"{current_size_mb:.2f} MB")
|
|
118
156
|
progress_bar.close()
|
|
119
157
|
else:
|
|
120
158
|
print(f"✓ Processed '{input_filename}'")
|
|
@@ -32,9 +32,7 @@ def _terminate_children_processes(parent_proc: psutil.Process):
|
|
|
32
32
|
if not children:
|
|
33
33
|
return
|
|
34
34
|
|
|
35
|
-
print(
|
|
36
|
-
f"Attempting to terminate {len(children)} child process(es) of {parent_proc.pid}..."
|
|
37
|
-
)
|
|
35
|
+
print(f"Attempting to terminate {len(children)} child process(es) of {parent_proc.pid}...")
|
|
38
36
|
for child in children:
|
|
39
37
|
try:
|
|
40
38
|
if child.is_running():
|
|
@@ -68,21 +66,15 @@ def _terminate_parent_process_windows(proc: psutil.Process):
|
|
|
68
66
|
try:
|
|
69
67
|
# Check the status after the timeout
|
|
70
68
|
if proc.is_running():
|
|
71
|
-
print(
|
|
72
|
-
f"FFmpeg process {proc.pid} still running after {_TERMINATION_TIMEOUT}s."
|
|
73
|
-
)
|
|
69
|
+
print(f"FFmpeg process {proc.pid} still running after {_TERMINATION_TIMEOUT}s.")
|
|
74
70
|
else:
|
|
75
|
-
print(
|
|
76
|
-
f"FFmpeg process termination took longer than {_TERMINATION_TIMEOUT}s."
|
|
77
|
-
)
|
|
71
|
+
print(f"FFmpeg process termination took longer than {_TERMINATION_TIMEOUT}s.")
|
|
78
72
|
except psutil.NoSuchProcess:
|
|
79
73
|
print(
|
|
80
74
|
f"FFmpeg process not found after {_TERMINATION_TIMEOUT}s. It seems like it has terminated."
|
|
81
75
|
)
|
|
82
76
|
except psutil.Error as e:
|
|
83
|
-
print(
|
|
84
|
-
f"Error during status check for process {proc.pid} after timeout:\n{e}"
|
|
85
|
-
)
|
|
77
|
+
print(f"Error during status check for process {proc.pid} after timeout:\n{e}")
|
|
86
78
|
except psutil.AccessDenied:
|
|
87
79
|
print(f"Access denied when trying to terminate FFmpeg process {proc.pid}.")
|
|
88
80
|
except psutil.Error as e:
|
|
@@ -106,14 +98,10 @@ def _terminate_parent_process_unix(proc: psutil.Process, original_pid: int):
|
|
|
106
98
|
if not proc.is_running():
|
|
107
99
|
terminated_by_pg = True
|
|
108
100
|
except psutil.TimeoutExpired:
|
|
109
|
-
print(
|
|
110
|
-
f"Process group {pgid_to_signal} did not exit after SIGTERM, sending SIGKILL..."
|
|
111
|
-
)
|
|
101
|
+
print(f"Process group {pgid_to_signal} did not exit after SIGTERM, sending SIGKILL...")
|
|
112
102
|
os.killpg(pgid_to_signal, SIGKILL)
|
|
113
103
|
try:
|
|
114
|
-
proc.wait(
|
|
115
|
-
timeout=_TERMINATION_TIMEOUT
|
|
116
|
-
) # Wait for SIGKILL on process group
|
|
104
|
+
proc.wait(timeout=_TERMINATION_TIMEOUT) # Wait for SIGKILL on process group
|
|
117
105
|
if not proc.is_running():
|
|
118
106
|
terminated_by_pg = True
|
|
119
107
|
except psutil.TimeoutExpired:
|
|
@@ -132,13 +120,9 @@ def _terminate_parent_process_unix(proc: psutil.Process, original_pid: int):
|
|
|
132
120
|
# os.killpg failed
|
|
133
121
|
except OSError as e:
|
|
134
122
|
target_desc = (
|
|
135
|
-
f"process group {pgid_to_signal}"
|
|
136
|
-
if pgid_to_signal != 0
|
|
137
|
-
else f"process {original_pid}"
|
|
138
|
-
)
|
|
139
|
-
print(
|
|
140
|
-
f"Error sending signal to {target_desc}: {e}. Fallback to individual process."
|
|
123
|
+
f"process group {pgid_to_signal}" if pgid_to_signal != 0 else f"process {original_pid}"
|
|
141
124
|
)
|
|
125
|
+
print(f"Error sending signal to {target_desc}: {e}. Fallback to individual process.")
|
|
142
126
|
|
|
143
127
|
if terminated_by_pg:
|
|
144
128
|
return
|
|
@@ -159,9 +143,7 @@ def _terminate_parent_process_unix(proc: psutil.Process, original_pid: int):
|
|
|
159
143
|
f"SIGTERM for FFmpeg process (PID: {proc.pid}) timed out after {_TERMINATION_TIMEOUT}s."
|
|
160
144
|
)
|
|
161
145
|
except psutil.Error as e:
|
|
162
|
-
print(
|
|
163
|
-
f"Error during SIGTERM for FFmpeg process (PID: {proc.pid}):\n{e}"
|
|
164
|
-
)
|
|
146
|
+
print(f"Error during SIGTERM for FFmpeg process (PID: {proc.pid}):\n{e}")
|
|
165
147
|
if not proc.is_running():
|
|
166
148
|
return
|
|
167
149
|
|
|
@@ -177,9 +159,7 @@ def _terminate_parent_process_unix(proc: psutil.Process, original_pid: int):
|
|
|
177
159
|
proc.kill()
|
|
178
160
|
print(f"SIGKILL sent to FFmpeg process (PID: {proc.pid}).")
|
|
179
161
|
except psutil.Error as e:
|
|
180
|
-
print(
|
|
181
|
-
f"Error during SIGKILL for FFmpeg process (PID: {proc.pid}):\n{e}"
|
|
182
|
-
)
|
|
162
|
+
print(f"Error during SIGKILL for FFmpeg process (PID: {proc.pid}):\n{e}")
|
|
183
163
|
else:
|
|
184
164
|
print(f"It seems like FFmpeg process {proc.pid} is no longer running.")
|
|
185
165
|
except psutil.NoSuchProcess:
|
|
@@ -239,9 +219,7 @@ def terminate_ffmpeg_process(process: Optional[subprocess.Popen]):
|
|
|
239
219
|
f"Main FFmpeg process {original_pid} became inaccessible during termination sequence."
|
|
240
220
|
)
|
|
241
221
|
except Exception as e:
|
|
242
|
-
print(
|
|
243
|
-
f"Unexpected error during main process termination for {original_pid}: {e}"
|
|
244
|
-
)
|
|
222
|
+
print(f"Unexpected error during main process termination for {original_pid}: {e}")
|
|
245
223
|
|
|
246
224
|
if process.poll() is None:
|
|
247
225
|
exit("It seems like the FFmpeg process has not terminated.")
|