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.
Files changed (21) hide show
  1. better_ffmpeg_progress-5.3.0/PKG-INFO +127 -0
  2. better_ffmpeg_progress-5.3.0/README.md +108 -0
  3. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/better_ffmpeg_progress.py +77 -27
  4. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/progress_bars.py +57 -19
  5. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/terminate_process.py +11 -33
  6. better_ffmpeg_progress-5.3.0/better_ffmpeg_progress/utils.py +284 -0
  7. better_ffmpeg_progress-5.3.0/better_ffmpeg_progress.egg-info/PKG-INFO +127 -0
  8. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/setup.py +1 -1
  9. better_ffmpeg_progress-5.1.1/PKG-INFO +0 -94
  10. better_ffmpeg_progress-5.1.1/README.md +0 -75
  11. better_ffmpeg_progress-5.1.1/better_ffmpeg_progress/utils.py +0 -151
  12. better_ffmpeg_progress-5.1.1/better_ffmpeg_progress.egg-info/PKG-INFO +0 -94
  13. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/LICENSE +0 -0
  14. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/__init__.py +0 -0
  15. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/enums.py +0 -0
  16. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress/exceptions.py +0 -0
  17. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress.egg-info/SOURCES.txt +0 -0
  18. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress.egg-info/dependency_links.txt +0 -0
  19. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress.egg-info/requires.txt +0 -0
  20. {better_ffmpeg_progress-5.1.1 → better_ffmpeg_progress-5.3.0}/better_ffmpeg_progress.egg-info/top_level.txt +0 -0
  21. {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 FfmpegProcessInterruptedError if not proceeding.
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
- # Raises FfmpegCommandError if the command is invalid
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: '{ffmpeg_log_level}'. "
79
- f"Must be one of {valid_levels} (case-insensitive)."
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
- f"ffmpeg_log_level must be an FfmpegLogLevel enum instance, a string, or None, "
84
- f"not {type(ffmpeg_log_level).__name__}"
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
- # Assumes last argument is output
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 self._print_detected_duration and self._duration_secs is not None:
105
- print(f"Detected duration: {self._duration_secs:.2f} seconds")
106
- elif self._print_detected_duration and self._duration_secs is None:
107
- print(
108
- "Could not detect duration. Progress bar may not show time remaining."
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", # stdout
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(self._ffmpeg_log_file, "w", encoding="utf-8") as f:
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
- if self._process.stdout:
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 parse_ffmpeg_progress_line
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 = ffmpeg_process_instance._input_filepath.name
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
- progress_val_secs = parse_ffmpeg_progress_line(
50
- stripped_line_bytes, duration_secs
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
- update_progress(task_id, completed=progress_val_secs)
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 = ffmpeg_process_instance._input_filepath.name
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
- progress_val_secs = parse_ffmpeg_progress_line(
104
- stripped_line_bytes, duration_secs
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
- progress_bar.refresh()
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.")