-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathpath.py
More file actions
282 lines (223 loc) · 8.66 KB
/
path.py
File metadata and controls
282 lines (223 loc) · 8.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
from __future__ import annotations
import logging
import re
import shlex
from os import devnull
from os import name as _os_name
from os import path as _path
from shutil import which
from subprocess import DEVNULL, PIPE, STDOUT, run
from packaging.version import Version
from . import plugins
from .errors import FFmpegioError
logger = logging.getLogger("ffmpegio")
# fmt:off
__all__ = [
"found", "where", "find", "ffmpeg", "ffprobe", "versions", "DEVNULL",
"PIPE", "STDOUT", "devnull", "FFmpegNotFound"
]
# fmt:on
class FFmpegNotFound(FFmpegioError):
def __init__(self):
super().__init__(
"FFmpeg executables not found. Run `ffmpegio.set_path()` first or "
"place FFmpeg executables in auto-detectable path locations."
)
# add FFmpeg directory to the system path as given in system environment variable FFMPEG_DIR
FFMPEG_BIN = None
FFPROBE_BIN = None
FFMPEG_VER = None
# shlex.join added in Python38
shlex_join = (
shlex.join
if hasattr(shlex, "join")
else lambda args: " ".join(shlex.quote(arg) for arg in args)
)
def found():
"""`True` if ffmpeg and ffprobe binaries are located
:return: True if both ffmpeg and ffprobe are found
:rtype: bool
"""
return bool(FFMPEG_BIN and FFPROBE_BIN)
def where(probe=False):
"""Get the path to FFmpeg/FFprobe executable
:param probe: True to return FFprobe path instead, defaults to False
:type probe: bool, optional
:return: Path to FFmpeg/FFprobe exectutable
:rtype: str or None
"""
path = FFPROBE_BIN if probe else FFMPEG_BIN
if not path:
raise FFmpegNotFound()
return path
def find(ffmpeg_path=None, ffprobe_path=None):
"""Set FFmpeg and FFprobe executables
:param ffmpeg_path: Full path to either the ffmpeg executable file or
to the folder housing both ffmpeg and ffprobe, defaults to None
:type ffmpeg_path: str, optional
:param ffprobe_path: Full path to the ffprobe executable file, defaults to None
:type ffprobe_path: str, optional
:returns: ffmpeg path, ffprobe path, and ffmpeg version
:rtype: Tuple[str,str,str]
If `ffmpeg_path` specifies a directory, the names of the executables are
auto-set to `ffmpeg` and `ffprobe`.
If the file locations are specified, the presence of the files will be
tested and an exception will be raised if both ffmpeg and ffprobe are not
valid executables.
If no argument is specified, the executables are auto-detected in the following orders.
(1) `ffmpeg` and `ffprobe` commands, i.e., the path to the parent directory
is included in the system PATH environmental variable.
(2) Run the `finder` plugin functions in the LIFO order and use the first valid
paths. There are two plugins currently offered: `ffmpegio-plugin-downloader`
and `ffmpegio-plugin-static-ffmpeg`.
(3) In Windows, additional locations are searched (e.g., C:\\Program Files\\ffmpeg).
See the documentation for the full list.
"""
global FFMPEG_BIN, FFPROBE_BIN, FFMPEG_VER
has_ffmpeg = ffmpeg_path is not None
has_ffprobe = ffprobe_path is not None
has_ffdir = has_ffmpeg and _path.isdir(ffmpeg_path)
if (has_ffmpeg != has_ffprobe) and (not has_ffdir or has_ffprobe):
raise ValueError(
"Either specify paths of both ffmpeg and ffprobe or a path to the directory containing both."
)
if has_ffdir:
ext = ".exe" if _os_name == "nt" else ""
ffdir = ffmpeg_path
ffmpeg_path = _path.join(ffdir, f"ffmpeg{ext}")
ffprobe_path = _path.join(ffdir, f"ffprobe{ext}")
if has_ffmpeg:
if not which(ffmpeg_path):
raise ValueError(
f"ffmpeg not found in {ffdir}"
if has_ffdir
else f"ffmpeg executable not found or {ffmpeg_path}"
)
elif not which(ffprobe_path):
raise ValueError(
f"ffprobe not found in {ffdir}"
if has_ffdir
else f"ffprobe executable not found or {ffprobe_path}"
)
FFMPEG_BIN = ffmpeg_path
FFPROBE_BIN = ffprobe_path
else:
res = plugins.get_hook().finder()
if res is None:
raise RuntimeError("Failed to auto-detect ffmpeg and ffprobe executable.")
FFMPEG_BIN, FFPROBE_BIN = res
if FFMPEG_BIN:
ver = versions()["version"]
m = re.match(r"\d+(?:\.\d+(?:\d+)?)?", ver)
FFMPEG_VER = Version(m[0]) if m else "nightly"
else:
FFMPEG_VER = Version("0.dev")
return FFMPEG_BIN, FFPROBE_BIN, FFMPEG_VER
def ffmpeg(args, sp_run=None, *sp_args, **other_sp_args):
"""just run ffmpeg without bells-n-whistles
:param args: FFmpeg command arguments without `ffmpeg`
:type args: str or Sequence[str]
:param sp_run: command runner, defaults to subprocess.run
:param sp_run: Callable, optional
:param *sp_args: sp_run arguments
:type *sp_args: tuple, optional
:param **other_sp_args: sp_run keyword arguments
:type **other_sp_args: dict, optional
:returns: sp_run output
:rtype: subprocess.CompletedProcess or subprocess.Popen or others
"""
if isinstance(args, str):
args = shlex.split(args)
logger.debug(shlex_join(args))
try:
assert FFMPEG_BIN is not None
return (sp_run or run)((FFMPEG_BIN, *args), *sp_args, **other_sp_args)
except (FileNotFoundError, AssertionError):
raise FFmpegNotFound()
def ffprobe(args, sp_run=None, *sp_args, **other_sp_args):
"""just run ffprobe without bells-n-whistles
:param sp_run: command runner, defaults to subprocess.run
:param sp_run: Callable, optional
:param *sp_args: sp_run arguments
:type *sp_args: tuple, optional
:param **other_sp_args: sp_run keyword arguments
:type **other_sp_args: dict, optional
:returns: sp_run output
:rtype: subprocess.CompletedProcess or subprocess.Popen or others
"""
if isinstance(args, str):
args = shlex.split(args)
logger.debug(shlex_join(args))
try:
assert FFMPEG_BIN is not None
return (sp_run or run)((FFPROBE_BIN, *args), *sp_args, **other_sp_args)
except (FileNotFoundError, AssertionError):
raise FFmpegNotFound()
def versions():
"""Get FFmpeg version and configuration information
:return: versions of ffmpeg and its av libraries as well as build configuration
:rtype: dict
================== ==== =========================================
key type description
================== ==== =========================================
'version' str FFmpeg version
'configuration' list list of build configuration options
'library_versions' dict version numbers of dependent av libraries
================== ==== =========================================
"""
s = ffmpeg(
["-version"], stdout=PIPE, universal_newlines=True, encoding="utf-8"
).stdout.splitlines()
v = dict(version=re.match(r"ffmpeg version (\S+)", s[0])[1])
i = 2 if s[1].startswith("built with") else 1
if s[i].startswith("configuration:"):
v["configuration"] = sorted([m[1] for m in re.finditer(r"\s--(\S+)", s[i])])
i += 1
lv = None
for l in s[i:]:
m = re.match(r"(\S+)\s+(.+?) /", l)
if m:
if lv is None:
lv = v["library_versions"] = {}
lv[m[1]] = m[2].replace(" ", "")
return v
def check_version(ver, cond=None):
"""check FFmpeg version against the given version for the specified condition
:param ver: desired version string
:type ver: str
:param cond: condition, defaults to None (">=)
:type cond: "==", "!=", "<", "<=", ">", ">=", optional
:return: True if condition is met
:rtype: bool
Note "nightly" builds are assumed to be the latest.
"""
ver_nightly = ver == "nightly"
# ffmpeg version is a nightly (assumed the latest)
if FFMPEG_VER == "nightly":
return {
"==": ver_nightly,
"!=": not ver_nightly,
"<": False,
"<=": ver_nightly,
">": not ver_nightly,
">=": True,
}[cond or ">="]
# ffmpeg version is a release compared to nightly
if ver_nightly:
return {
"==": False,
"!=": True,
"<": True,
"<=": True,
">": False,
">=": False,
}[cond or ">="]
# both are releases
return {
"==": FFMPEG_VER.__eq__,
"!=": FFMPEG_VER.__ne__,
"<": FFMPEG_VER.__lt__,
"<=": FFMPEG_VER.__le__,
">": FFMPEG_VER.__gt__,
">=": FFMPEG_VER.__ge__,
}[cond or ">="](Version(ver))