better-ffmpeg-progress 2.1.5__tar.gz → 2.1.7__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.
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2021 GitHub.com/CrypticSignal
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1
+ MIT License
2
+
3
+ Copyright (c) 2021 GitHub.com/CrypticSignal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.1
2
+ Name: better-ffmpeg-progress
3
+ Version: 2.1.7
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: tqdm
17
+
18
+ <div align="center">
19
+
20
+ [![PyPI downloads](https://img.shields.io/pypi/dm/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
21
+ [![PyPI downloads](https://img.shields.io/pypi/dw/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
22
+ [![GitHub](https://img.shields.io/github/license/crypticsignal/better-ffmpeg-progress?label=License&color=blue)](LICENSE.txt)
23
+
24
+ # Better FFmpeg Progress
25
+
26
+ Runs an FFmpeg command and uses [tqdm](https://github.com/tqdm/tqdm) to show a progress bar.
27
+
28
+ </div>
29
+
30
+ ## Example:
31
+
32
+ ```
33
+ 39%|███████████████████████████████████████████ | 23.6/60.2 [00:19<00:34, 1.07s/s]
34
+ ```
35
+
36
+ Where:
37
+
38
+ - `39%` is the percentage progress.
39
+ - `23.6` seconds of the input file have been processed.
40
+ - `60.2` is the duration of the input file in seconds.
41
+ - `00:19` is the time elapsed since the FFmpeg process started.
42
+ - `00:34` is the estimated time required for the FFmpeg process to complete.
43
+ - `1.07` shows how many seconds of the input file are processed per second.
44
+
45
+ ## Installation:
46
+
47
+ ```
48
+ pip install better-ffmpeg-progress --upgrade
49
+ ```
50
+
51
+ ## Usage:
52
+
53
+ Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would to `subprocess.run()`.
54
+
55
+ Simple Example:
56
+
57
+ ```py
58
+ from better_ffmpeg_progress import FfmpegProcess
59
+ # Pass a list of FFmpeg arguments, like you would if using subprocess.run()
60
+ process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
61
+ # Use the run method to run the FFmpeg command.
62
+ process.run()
63
+ ```
64
+
65
+ Advanced Example:
66
+
67
+ ```py
68
+ from better_ffmpeg_progress import FfmpegProcess
69
+
70
+ def handle_progress_info(percentage, speed, eta, estimated_filesize):
71
+ print(f"Estimated Output Filesize: {estimated_filesize / 1_000_000} MB")
72
+
73
+ def handle_success():
74
+ # Code to run if the FFmpeg process completes successfully.
75
+ pass
76
+
77
+ def handle_error():
78
+ # Code to run if the FFmpeg process encounters an error.
79
+ pass
80
+
81
+ # Pass a list of FFmpeg arguments, like you would if using subprocess.run()
82
+ process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
83
+
84
+ ffmpeg_output_path = 'ffmpeg_output.txt'
85
+
86
+ # Use the run method to run the FFmpeg command.
87
+ process.run(progress_handler=handle_progress_info, ffmpeg_output_file=ffmpeg_output_path, success_handler=handle_success, error_handler=handle_error)
88
+ ```
89
+
90
+ The `run` method takes the following **optional** arguments:
91
+
92
+ - `progress_handler`
93
+
94
+ - You can create a function if you would like to do something with the following values:
95
+
96
+ - Percentage progress. [float]
97
+ - Speed, e.g. `22.3x` which means that 22.3 seconds of the input are processed every second. [string]
98
+ - ETA in seconds. [float]
99
+ - Estimated output filesize in bytes. [float]
100
+ - _Note: This is not accurate. Please take the value with a grain of salt._
101
+
102
+ The values will be `None` if unknown. The function will receive the aforementioned metrics as arguments, about two times per second.
103
+
104
+ - `ffmpeg_output_file` - A string path to define where you want the output of FFmpeg to be saved. By default, this is saved in a folder named "ffmpeg_output", with the filename `[<input_filename>].txt`.
105
+
106
+ - `success_handler` - A function to run if the FFmpeg process completes successfully.
107
+
108
+ - `error_handler` - A function to run if the FFmpeg process encounters an error.
109
+
110
+ ## Changelog:
111
+
112
+ [19/09/2022]
113
+
114
+ - Add the ability to specify a `success_handler` argument, a function to run if the FFmpeg process completes successfully.
115
+ - Add 0.001 to tqdm's `total` parameter to prevent the chance of getting `TqdmWarning: clamping frac to range [0, 1]`
116
+
117
+ [21/12/2022]
118
+
119
+ - [v2.0.7] Fix `'estimated_size' referenced before assignment` error.
120
+ - [v2.0.7] The progress bar now uses 1 decimal place for seconds processed and total duration.
121
+
122
+ [22/12/2022]
123
+
124
+ - [v2.0.8] Add the ability to specify an `error_handler` argument, a function to run if the FFmpeg process encounters an error.
125
+ - [v2.0.9] Fix a typo (commit [da45a84](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/da45a8416856ab7d3c7b748db5703fa3dbc65f60))
126
+
127
+ [07/02/2022]
128
+
129
+ - [v2.1.0] [Update function name](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/572fe8a0d71957d00b833134a4d35170630203fa) to fix `'process_complete_handler' is not defined` error.
130
+
131
+ [05/11/2023]
132
+
133
+ - [v2.1.2] [Do not exit the Python interpreter after the FFmpeg process is complete](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/0a358810773835297faae688689c6e0d8a5859ae)
134
+
135
+ [22/04/2024]
136
+
137
+ - [v2.1.3] Fix issue [#20](https://github.com/CrypticSignal/better-ffmpeg-progress/issues/20)
138
+
139
+ [28/04/2024]
140
+
141
+ - [v2.1.4] Fix issue [#21](https://github.com/CrypticSignal/better-ffmpeg-progress/issues/21)
142
+
143
+ [02/05/2024]
144
+
145
+ - [v2.1.5] Fix issue [#23](https://github.com/CrypticSignal/better-ffmpeg-progress/issues/23) and make an error message more specific. [Here](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/a6ef7f26d080b684144021301f3b2aa5e0834dae) is the relevant commit.
146
+
147
+ [18/10/2024]
148
+
149
+ - [v2.1.6] Notify the user if the input filepath or filename is incorrect.
150
+ - [v2.1.7] Refactor to use threads and queues.
@@ -1,122 +1,133 @@
1
- <div align="center">
2
-
3
- [![PyPI downloads](https://img.shields.io/pypi/dm/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
4
- [![PyPI downloads](https://img.shields.io/pypi/dw/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
5
- [![GitHub](https://img.shields.io/github/license/crypticsignal/better-ffmpeg-progress?label=License&color=blue)](LICENSE.txt)
6
-
7
- # Better FFmpeg Progress
8
-
9
- Runs an FFmpeg command and uses [tqdm](https://github.com/tqdm/tqdm) to show a progress bar.
10
-
11
- </div>
12
-
13
- ## Example:
14
-
15
- ```
16
- 39%|███████████████████████████████████████████ | 23.6/60.2 [00:19<00:34, 1.07s/s]
17
- ```
18
-
19
- Where:
20
-
21
- - `39%` is the percentage progress.
22
- - `23.6` seconds of the input file have been processed.
23
- - `60.2` is the duration of the input file in seconds.
24
- - `00:19` is the time elapsed since the FFmpeg process started.
25
- - `00:34` is the estimated time required for the FFmpeg process to complete.
26
- - `1.07` shows how many seconds of the input file are processed per second.
27
-
28
- ## Installation:
29
-
30
- `pip3 install better-ffmpeg-progress --upgrade`
31
-
32
- ## Usage:
33
-
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
-
38
- ```py
39
- from better_ffmpeg_progress import FfmpegProcess
40
- # Pass a list of FFmpeg arguments, like you would if using subprocess.run()
41
- process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
42
- # Use the run method to run the FFmpeg command.
43
- process.run()
44
- ```
45
-
46
- Advanced Example:
47
-
48
- ```py
49
- from better_ffmpeg_progress import FfmpegProcess
50
-
51
- def handle_progress_info(percentage, speed, eta, estimated_filesize):
52
- print(f"Estimated Output Filesize: {estimated_filesize / 1_000_000} MB")
53
-
54
- def handle_success():
55
- # Code to run if the FFmpeg process completes successfully.
56
- pass
57
-
58
- def handle_error():
59
- # Code to run if the FFmpeg process encounters an error.
60
- pass
61
-
62
- # Pass a list of FFmpeg arguments, like you would if using subprocess.run()
63
- process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
64
-
65
- ffmpeg_output_path = 'ffmpeg_output.txt'
66
-
67
- # Use the run method to run the FFmpeg command.
68
- process.run(progress_handler=handle_progress_info, ffmpeg_output_file=ffmpeg_output_path, success_handler=handle_success, error_handler=handle_error)
69
- ```
70
-
71
- The `run` method takes the following **optional** arguments:
72
-
73
- - `progress_handler`
74
-
75
- - You can create a function if you would like to do something with the following values:
76
-
77
- - Percentage progress. [float]
78
- - Speed, e.g. `22.3x` which means that 22.3 seconds of the input are processed every second. [string]
79
- - ETA in seconds. [float]
80
- - Estimated output filesize in bytes. [float]
81
- - _Note: This is not accurate. Please take the value with a grain of salt._
82
-
83
- The values will be `None` if unknown. The function will receive the aforementioned metrics as arguments, about two times per second.
84
-
85
- - `ffmpeg_output_file` - A string path to define where you want the output of FFmpeg to be saved. By default, this is saved in a folder named "ffmpeg_output", with the filename `[<input_filename>].txt`.
86
-
87
- - `success_handler` - A function to run if the FFmpeg process completes successfully.
88
-
89
- - `error_handler` - A function to run if the FFmpeg process encounters an error.
90
-
91
- ## Changelog:
92
-
93
- [19/09/2022]
94
-
95
- - Add the ability to specify a `success_handler` argument, a function to run if the FFmpeg process completes successfully.
96
- - Add 0.001 to tqdm's `total` parameter to prevent the chance of getting `TqdmWarning: clamping frac to range [0, 1]`
97
-
98
- [21/12/2022]
99
-
100
- - [v2.0.7] Fix `'estimated_size' referenced before assignment` error.
101
- - [v2.0.7] The progress bar now uses 1 decimal place for seconds processed and total duration.
102
-
103
- [22/12/2022]
104
-
105
- - [v2.0.8] Add the ability to specify an `error_handler` argument, a function to run if the FFmpeg process encounters an error.
106
- - [v2.0.9] Fix a typo (commit [da45a84](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/da45a8416856ab7d3c7b748db5703fa3dbc65f60))
107
-
108
- [07/02/2022]
109
-
110
- - [v2.1.0] [Update function name](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/572fe8a0d71957d00b833134a4d35170630203fa) to fix `'process_complete_handler' is not defined` error.
111
-
112
- [05/11/2023]
113
-
114
- - [v2.1.2] [Do not exit the Python interpreter after the FFmpeg process is complete](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/0a358810773835297faae688689c6e0d8a5859ae)
115
-
116
- [22/04/2024]
117
-
118
- - [v2.1.3] Fix issue [#20](https://github.com/CrypticSignal/better-ffmpeg-progress/issues/20)
119
-
120
- [28/04/2024]
121
-
122
- - [v2.1.4] Fix issue [#21](https://github.com/CrypticSignal/better-ffmpeg-progress/issues/21)
1
+ <div align="center">
2
+
3
+ [![PyPI downloads](https://img.shields.io/pypi/dm/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
4
+ [![PyPI downloads](https://img.shields.io/pypi/dw/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
5
+ [![GitHub](https://img.shields.io/github/license/crypticsignal/better-ffmpeg-progress?label=License&color=blue)](LICENSE.txt)
6
+
7
+ # Better FFmpeg Progress
8
+
9
+ Runs an FFmpeg command and uses [tqdm](https://github.com/tqdm/tqdm) to show a progress bar.
10
+
11
+ </div>
12
+
13
+ ## Example:
14
+
15
+ ```
16
+ 39%|███████████████████████████████████████████ | 23.6/60.2 [00:19<00:34, 1.07s/s]
17
+ ```
18
+
19
+ Where:
20
+
21
+ - `39%` is the percentage progress.
22
+ - `23.6` seconds of the input file have been processed.
23
+ - `60.2` is the duration of the input file in seconds.
24
+ - `00:19` is the time elapsed since the FFmpeg process started.
25
+ - `00:34` is the estimated time required for the FFmpeg process to complete.
26
+ - `1.07` shows how many seconds of the input file are processed per second.
27
+
28
+ ## Installation:
29
+
30
+ ```
31
+ pip install better-ffmpeg-progress --upgrade
32
+ ```
33
+
34
+ ## Usage:
35
+
36
+ Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would to `subprocess.run()`.
37
+
38
+ Simple Example:
39
+
40
+ ```py
41
+ from better_ffmpeg_progress import FfmpegProcess
42
+ # Pass a list of FFmpeg arguments, like you would if using subprocess.run()
43
+ process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
44
+ # Use the run method to run the FFmpeg command.
45
+ process.run()
46
+ ```
47
+
48
+ Advanced Example:
49
+
50
+ ```py
51
+ from better_ffmpeg_progress import FfmpegProcess
52
+
53
+ def handle_progress_info(percentage, speed, eta, estimated_filesize):
54
+ print(f"Estimated Output Filesize: {estimated_filesize / 1_000_000} MB")
55
+
56
+ def handle_success():
57
+ # Code to run if the FFmpeg process completes successfully.
58
+ pass
59
+
60
+ def handle_error():
61
+ # Code to run if the FFmpeg process encounters an error.
62
+ pass
63
+
64
+ # Pass a list of FFmpeg arguments, like you would if using subprocess.run()
65
+ process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:a", "libmp3lame", "output.mp3"])
66
+
67
+ ffmpeg_output_path = 'ffmpeg_output.txt'
68
+
69
+ # Use the run method to run the FFmpeg command.
70
+ process.run(progress_handler=handle_progress_info, ffmpeg_output_file=ffmpeg_output_path, success_handler=handle_success, error_handler=handle_error)
71
+ ```
72
+
73
+ The `run` method takes the following **optional** arguments:
74
+
75
+ - `progress_handler`
76
+
77
+ - You can create a function if you would like to do something with the following values:
78
+
79
+ - Percentage progress. [float]
80
+ - Speed, e.g. `22.3x` which means that 22.3 seconds of the input are processed every second. [string]
81
+ - ETA in seconds. [float]
82
+ - Estimated output filesize in bytes. [float]
83
+ - _Note: This is not accurate. Please take the value with a grain of salt._
84
+
85
+ The values will be `None` if unknown. The function will receive the aforementioned metrics as arguments, about two times per second.
86
+
87
+ - `ffmpeg_output_file` - A string path to define where you want the output of FFmpeg to be saved. By default, this is saved in a folder named "ffmpeg_output", with the filename `[<input_filename>].txt`.
88
+
89
+ - `success_handler` - A function to run if the FFmpeg process completes successfully.
90
+
91
+ - `error_handler` - A function to run if the FFmpeg process encounters an error.
92
+
93
+ ## Changelog:
94
+
95
+ [19/09/2022]
96
+
97
+ - Add the ability to specify a `success_handler` argument, a function to run if the FFmpeg process completes successfully.
98
+ - Add 0.001 to tqdm's `total` parameter to prevent the chance of getting `TqdmWarning: clamping frac to range [0, 1]`
99
+
100
+ [21/12/2022]
101
+
102
+ - [v2.0.7] Fix `'estimated_size' referenced before assignment` error.
103
+ - [v2.0.7] The progress bar now uses 1 decimal place for seconds processed and total duration.
104
+
105
+ [22/12/2022]
106
+
107
+ - [v2.0.8] Add the ability to specify an `error_handler` argument, a function to run if the FFmpeg process encounters an error.
108
+ - [v2.0.9] Fix a typo (commit [da45a84](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/da45a8416856ab7d3c7b748db5703fa3dbc65f60))
109
+
110
+ [07/02/2022]
111
+
112
+ - [v2.1.0] [Update function name](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/572fe8a0d71957d00b833134a4d35170630203fa) to fix `'process_complete_handler' is not defined` error.
113
+
114
+ [05/11/2023]
115
+
116
+ - [v2.1.2] [Do not exit the Python interpreter after the FFmpeg process is complete](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/0a358810773835297faae688689c6e0d8a5859ae)
117
+
118
+ [22/04/2024]
119
+
120
+ - [v2.1.3] Fix issue [#20](https://github.com/CrypticSignal/better-ffmpeg-progress/issues/20)
121
+
122
+ [28/04/2024]
123
+
124
+ - [v2.1.4] Fix issue [#21](https://github.com/CrypticSignal/better-ffmpeg-progress/issues/21)
125
+
126
+ [02/05/2024]
127
+
128
+ - [v2.1.5] Fix issue [#23](https://github.com/CrypticSignal/better-ffmpeg-progress/issues/23) and make an error message more specific. [Here](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/a6ef7f26d080b684144021301f3b2aa5e0834dae) is the relevant commit.
129
+
130
+ [18/10/2024]
131
+
132
+ - [v2.1.6] Notify the user if the input filepath or filename is incorrect.
133
+ - [v2.1.7] Refactor to use threads and queues.
@@ -1 +1 @@
1
- from .better_ffmpeg_progress import FfmpegProcess
1
+ from .better_ffmpeg_progress import FfmpegProcess
@@ -0,0 +1,209 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+ from queue import Empty, Queue
4
+ import subprocess
5
+ import sys
6
+ from threading import Thread
7
+ from typing import Callable, List, Optional, Union
8
+
9
+ from ffmpeg import probe
10
+ from tqdm import tqdm
11
+
12
+
13
+ @dataclass
14
+ class FfmpegProgress:
15
+ """Data class to store FFmpeg progress information."""
16
+ percentage: float = 0.0
17
+ speed: float = 0.0
18
+ eta: Optional[float] = None
19
+ estimated_size: Optional[int] = None
20
+
21
+
22
+ class FfmpegProcess:
23
+ """
24
+ Args:
25
+ command: A list of arguments to pass to FFmpeg.
26
+ ffmpeg_loglevel: Desired FFmpeg log level. Default is "verbose".
27
+
28
+ Raises:
29
+ FileNotFoundError: If FFmpeg cannot find the input filepath or filename.
30
+ ValueError: If the list of arguments does not include "-i"
31
+ """
32
+
33
+ WANTED_METRICS = frozenset({"out_time_ms", "total_size", "speed"})
34
+
35
+ def __init__(self, command: List[str], ffmpeg_loglevel: str = "verbose"):
36
+ if "-i" not in command:
37
+ raise ValueError("FFmpeg command must include '-i'")
38
+
39
+ self._ffmpeg_args = command + ["-hide_banner", "-loglevel", ffmpeg_loglevel]
40
+ self._output_filepath = Path(command[-1])
41
+ self._progress = FfmpegProgress()
42
+ self._duration_secs: Optional[float] = None
43
+ self._current_size: int = 0
44
+ self._previous_seconds_processed: float = 0
45
+ self._progress_bar: Optional[tqdm] = None
46
+
47
+ self._get_input_file_duration()
48
+
49
+ def _get_input_file_duration(self) -> None:
50
+ """Use ffprobe to get the duration of the input file"""
51
+ index_of_filepath = self._ffmpeg_args.index("-i") + 1
52
+ self._input_filepath = Path(self._ffmpeg_args[index_of_filepath])
53
+
54
+ try:
55
+ self._duration_secs = round(float(probe(self._input_filepath)["format"]["duration"]), 2)
56
+ print(f"The duration of {self._input_filepath.name} has been detected as {self._duration_secs} seconds")
57
+ # -progress pipe:1 sends progress metrics to stdout AND -stats_period sets the period at which encoding progress/statistics are updated
58
+ # -nostats disables encoding progress/statistics in stderr
59
+ self._ffmpeg_args.extend(["-progress", "pipe:1", "-stats_period", "0.1", "-nostats"])
60
+ except Exception:
61
+ print(f"Could not detect the duration of {self._input_filepath.name}. Percentage progress, ETA and estimated filesize will be unavailable.")
62
+ self._duration_secs = None
63
+
64
+ def _check_output_exists(self) -> bool:
65
+ """Check if output file exists and handle overwrite prompt."""
66
+ if self._output_filepath.exists() and "-y" not in self._ffmpeg_args:
67
+ choice = input(f"{self._output_filepath} already exists. Overwrite? [Y/N]: ").lower()
68
+ if choice != "y":
69
+ print("FFmpeg process cancelled. Output file exists and overwrite declined.")
70
+ return False
71
+ self._ffmpeg_args.insert(1, "-y")
72
+ return True
73
+
74
+ def _update_progress(self, line: str, metric: str, value: str,
75
+ progress_handler: Optional[Callable]) -> None:
76
+ """Update progress information based on FFmpeg output."""
77
+ if metric == "total_size":
78
+ self._current_size = int(value)
79
+
80
+ elif metric == "out_time_ms":
81
+ seconds_processed = round((int(value) / 1_000_000), 2)
82
+
83
+ if progress_handler is None and self._progress_bar:
84
+ seconds_increase = seconds_processed - self._previous_seconds_processed
85
+ self._progress_bar.update(seconds_increase)
86
+ self._previous_seconds_processed = seconds_processed
87
+ return
88
+
89
+ if self._duration_secs:
90
+ self._progress.percentage = (seconds_processed / self._duration_secs) * 100
91
+ if self._current_size and self._progress.percentage > 0:
92
+ self._progress.estimated_size = int(self._current_size * (100 / self._progress.percentage))
93
+
94
+ elif metric == "speed":
95
+ speed_str = value.rstrip('x')
96
+ if speed_str != "0":
97
+ self._progress.speed = float(speed_str)
98
+ if self._duration_secs:
99
+ seconds_remaining = self._duration_secs - (self._previous_seconds_processed or 0)
100
+ self._progress.eta = seconds_remaining / self._progress.speed
101
+
102
+ if line == "progress=end":
103
+ self._progress.percentage = 100
104
+ self._progress.eta = 0
105
+
106
+ if progress_handler:
107
+ progress_handler(
108
+ self._progress.percentage,
109
+ self._progress.speed,
110
+ self._progress.eta,
111
+ self._progress.estimated_size
112
+ )
113
+
114
+ @staticmethod
115
+ def _handle_pipe(pipe, queue: Queue) -> None:
116
+ """Read from pipe and put lines into queue."""
117
+ try:
118
+ for line in pipe:
119
+ queue.put(line.strip())
120
+ finally:
121
+ pipe.close()
122
+
123
+ def _write_ffmpeg_output(self, stderr_output: str, output_file: Optional[Union[str, Path]]) -> None:
124
+ """Write FFmpeg stderr to a txt file."""
125
+ output_dir = Path("ffmpeg_output")
126
+ output_dir.mkdir(exist_ok=True)
127
+
128
+ log_file = output_file or output_dir / f"[{self._input_filepath.name}].txt"
129
+ log_file = Path(log_file)
130
+
131
+ log_file.write_text(stderr_output)
132
+ print(f"Done! FFmpeg output logged to: {log_file}")
133
+
134
+ def run(self,
135
+ output_file: Optional[Union[str, Path]] = None,
136
+ progress_handler: Optional[Callable] = None,
137
+ success_handler: Optional[Callable] = None,
138
+ error_handler: Optional[Callable] = None) -> None:
139
+ """
140
+ Args:
141
+ output_file: Optional filepath to write FFmpeg output to
142
+ progress_handler: Optional function which receives progress metrics
143
+ success_handler: Optional function to handle successful completion of the FFmpeg process
144
+ error_handler: Optional function for FFmpeg process error handling
145
+ """
146
+ if not self._check_output_exists():
147
+ return
148
+
149
+ print(f"Executing: {' '.join(self._ffmpeg_args)}")
150
+
151
+ if self._duration_secs and not progress_handler:
152
+ self._progress_bar = tqdm(
153
+ total=self._duration_secs,
154
+ unit="s",
155
+ dynamic_ncols=True,
156
+ leave=False
157
+ )
158
+
159
+ process = subprocess.Popen(
160
+ self._ffmpeg_args,
161
+ stdout=subprocess.PIPE,
162
+ stderr=subprocess.PIPE,
163
+ text=True
164
+ )
165
+
166
+ stdout_queue, stderr_queue = Queue(), Queue()
167
+ # These threads put FFmpeg stdout and stderr into the queues
168
+ Thread(target=self._handle_pipe, args=(process.stdout, stdout_queue), daemon=True).start()
169
+ Thread(target=self._handle_pipe, args=(process.stderr, stderr_queue), daemon=True).start()
170
+
171
+ stderr_output = []
172
+
173
+ try:
174
+ while process.poll() is None:
175
+ # Handle stdout (progress metrics are sent here due as we are passing the -progress pipe:1 options to FFmpeg)
176
+ try:
177
+ stdout = stdout_queue.get_nowait()
178
+ if stdout:
179
+ try:
180
+ metric, value = stdout.split("=")
181
+ if metric in self.WANTED_METRICS and value and "N/A" not in value:
182
+ self._update_progress(stdout, metric, value, progress_handler)
183
+ except ValueError:
184
+ continue
185
+ except Empty:
186
+ pass
187
+ # Handle stderr (regular FFmpeg output)
188
+ try:
189
+ stderr = stderr_queue.get_nowait()
190
+ if "No such file" in stderr:
191
+ raise FileNotFoundError(f"Input file not found: {self._input_filepath}")
192
+ stderr_output.append(stderr)
193
+ except Empty:
194
+ pass
195
+
196
+ if process.returncode != 0:
197
+ if error_handler:
198
+ error_handler()
199
+ else:
200
+ sys.exit("\nFFmpeg process failed")
201
+
202
+ if success_handler:
203
+ success_handler()
204
+
205
+ finally:
206
+ if self._progress_bar:
207
+ self._progress_bar.close()
208
+
209
+ self._write_ffmpeg_output("\n".join(stderr_output), output_file)