-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathdevices.py
More file actions
348 lines (282 loc) · 10.6 KB
/
devices.py
File metadata and controls
348 lines (282 loc) · 10.6 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
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
"""I/O Device Enumeration Module
This module allows input and output hardware devices to be enumerated in the same fashion as the
streams of media containers. For example, instead of specifying DirectShow hardware by
```
url = 'video="WebCam":audio="Microphone"'
```
You can specify them as
```
url = 'v:0|a:0'
```
"""
import logging
logger = logging.getLogger("ffmpegio")
from ffmpegio.path import ffmpeg
from subprocess import PIPE, DEVNULL
from . import plugins
import re
SOURCES = {}
SINKS = {}
def scan():
"""scans the system for input/output hardware
This function must be called by user to enable device enumeration in
ffmpegio. Also, none of functions in `ffmpegio.devices` module will return
meaningful outputs until `scan` is called. Likewise, `scan()` must
run again after a change in hardware to reflect the change.
The devices are enumerated according to the outputs of outputs
`ffmpeg -sources` and `ffmpeg -sinks` calls for the devices supporting
this fairly new FFmpeg interface. Additional hardware configurations
are detected by registered plugins with hooks `device_source_api` or
`device_sink_api`.
Currently Supported Devices
---------------------------
Windows: dshow
Mac: tbd
Linux: tbd
"""
global SOURCES, SINKS
def get_devices(dev_type):
out = ffmpeg(
f"-{dev_type}",
stderr=DEVNULL,
stdout=PIPE,
universal_newlines=True,
)
logger.debug(f"ffmpeg -{dev_type}")
logger.debug(out.stdout)
src_spans = [
[m[1], *m.span()]
for m in re.finditer(fr"Auto-detected {dev_type} for (.+?):\n", out.stdout)
]
for i in range(len(src_spans) - 1):
src_spans[i][1] = src_spans[i][2]
src_spans[i][2] = src_spans[i + 1][1]
src_spans[-1][1] = src_spans[-1][2]
src_spans[-1][2] = len(out.stdout)
def parse(log):
# undoing print_device_list() in fftools/cmdutils.c
if log.startswith(f"Cannot list {dev_type}"):
return None
devices = {}
counts = {"audio": 0, "video": 0}
for m in re.finditer(r"([ *]) (.+?) \[(.+?)\] \((.+?)\)\n", log):
info = {
"name": m[2],
"description": m[3],
"is_default": m[1] == "*" or None,
}
media_types = m[4].split(",")
for media_type in media_types:
if media_type in ("video", "audio"):
spec = f"{media_type[0]}:{counts[media_type]}"
counts[media_type] += 1
devices[spec] = {**info, "media_type": media_type}
return devices
return [(name, parse(out.stdout[i0:i1])) for name, i0, i1 in src_spans]
def gather_device_info(dev_type, hook):
plugin_devices = {
name: api for name, api in getattr(plugins.get_hook(), hook)()
}
devs = {}
for key, devlist in get_devices(dev_type):
names = key.split(",") # may have alias
name = names[0] # plugin must be defined for the base name
if name in plugin_devices:
info = plugin_devices[name]
if devlist is not None:
info["list"] = devlist
elif "scan" in info:
info["list"] = info["scan"]()
else:
info = {"list": devlist} if devlist else None
if info is not None:
for name in names:
devs[name] = info
return devs
SOURCES = gather_device_info("sources", "device_source_api")
SINKS = gather_device_info("sinks", "device_sink_api")
def _list_devices(devs, dev, mtype, return_nested):
if mtype:
mtype = mtype[0]
return (
{
d: {
k: v["name"]
for k, v in devs.get(d, {}).get("list", {}).items()
if not mtype or mtype == k[0]
}
for d in (devs.keys() if dev is None else [dev])
}
if return_nested or dev
else {
(d, k): v["name"]
for d in (devs.keys() if dev is None else [dev])
for k, v in devs.get(d, {}).get("list", {}).items()
if not mtype or mtype == k[0]
}
)
def list_sources(dev=None, mtype=None, return_nested=False):
"""list enumerated source hardware devices
:param dev: ffmpeg device name, defaults to None
:type dev: str, optional
:param mtype: media type, defaults to None
:type dev: "video", "audio", optional
:param return_nested: True to return results in nested dict, defaults to False
:type return_nested: bool, optional
:returns: dict of names of supported hardware, keyed by a tuple of the device name and enumeration,
or nested dicts. If dev is specified, dict of enumerated hardware devices and their names
:rtype: dict(tuple(str,str),str) or dict(str,dict(str,str)) or dict(str,str)
"""
devs = _list_devices(SOURCES, dev, mtype, return_nested)
return devs[dev] if dev else devs
def list_sinks(dev=None, mtype=None, return_nested=False):
"""list enumerated sink hardware devices
:param dev: ffmpeg device name, default to None
:type dev: str, optional
:param mtype: media type, default to None
:type dev: "video", "audio", optional
:param return_nested: True to return results in nested dict, defaults to False
:type return_nested: bool, optional
:returns: dict of names of supported hardware, keyed by a tuple of the device name and enumeration,
or nested dicts. If dev is specified, dict of enumerated hardware devices and their names
:rtype: dict(tuple(str,str),str) or dict(str,dict(str,str)) or dict(str,str)
"""
devs = _list_devices(SINKS, dev, mtype, return_nested)
return devs[dev] if dev else devs
def _get_dev(device, dev_type):
try:
devices = dev_type and {"source": SOURCES, "sink": SINKS}[dev_type]
except:
raise ValueError(f'Unknown dev_type: {dev_type} (must be "source" or "sink") ')
try:
if devices:
return devices[device]
else:
try:
return SOURCES[device]
except:
return SINKS[device]
except:
raise ValueError(f"Unknown/unenumerated device: {device}")
def get_source_info(device, enum):
"""get source information
:param device: device name
:type device: str
:param enum: hardware enumeration
:type enum: str
:return: info dict with keys: name, description, and is_default
:rtype: dict[str,str]
"""
info = _get_dev(device, "source")
try:
return {k: info["list"][enum][k] for k in ("name", "description", "is_default")}
except:
raise ValueError(f"Source device {device}:{enum} is not found ")
def get_sink_info(device, enum):
"""get sink information
:param device: device name
:type device: str
:param enum: hardware enumeration
:type enum: str
:return: info dict with keys: name, description, and is_default
:rtype: dict[str,str]
"""
info = _get_dev(device, "sink")
try:
return {k: info["list"][enum][k] for k in ("name", "description", "is_default")}
except:
raise ValueError(f"Sink device {device}:{enum} is not found ")
# TODO find_source() and find_sink() given device name or description
def list_source_options(device, enum):
"""list supported options of enumerated source hardware
:param device: device name
:type device: str
:param enum: hardware specifier, e.g., v:0, a:0
:type enum: str
:return: list of supported option combinations. If option values are tuple
it indicates the min and max range of the option value.
:rtype: list[dict]
"""
dev = _get_dev(device, "source")
try:
list_options = dev["list_options"]
except:
raise ValueError(f"No options to list")
return list_options(dev["list"][enum])
def list_sink_options(device, enum):
"""list supported options of enumerated sink hardware
:param device: device name
:type device: str
:param enum: hardware specifier, e.g., v:0, a:0
:type enum: str
:return: list of supported option combinations. If option values are tuple
it indicates the min and max range of the option value.
:rtype: list[dict]
"""
info = _get_dev(device, "sink")
try:
list_options = info["list_options"]
except:
raise ValueError(f"No options to list")
return list_options("sink", enum)
def _resolve(devs, url, opts):
try:
# try to get device name
try:
f = opts["f"]
_opts = opts
_url = url
except:
# use the first part of the device name
f, _url = url.split(":", 1)
try:
_opts = {**opts, "f": f}
except:
_opts = {"f": f}
# try to get device info
dev = devs[f]
assert dev is not None
except:
# not a device or unknown device
return url, opts
# find device names
try:
enums = {enum for enum in _url.split("|")}
infos = [dev["list"][enum] for enum in enums]
except:
# unknown enumeration (possibly the actual name)
return url, opts
try:
# if device-specific resolver is available, use it
return dev["resolve"](infos), _opts
except:
# only allow single stream
if len(infos) > 1:
raise ValueError(f"{f} only supports 1 enumerated hardware device per url")
return infos[0]["name"], opts
def resolve_source(url, opts):
"""resolve source enumeration
:param url: input url, possibly device enum
:type url: str
:param opts: input options
:type opts: dict
:return: possibly modified url and opts
:rtype: tuple[str,dict]
This function is called by `ffmpeg.compose()` to convert
device enumeration back to url expected by ffmpeg
The device name (-f) could be provided via opts['f'] or encoded as a
part of enumeration
"""
return _resolve(SOURCES, url, opts)
def resolve_sink(url, opts):
"""resolve sink enumeration
:param url: output url, possibly device enum
:type url: str
:param opts: output options
:type opts: dict
:return: possibly modified url and opts
:rtype: tuple[str,dict]
This function is called by `ffmpeg.compose()` to convert
device enumeration back to url expected by ffmpeg
"""
return _resolve(SINKS, url, opts)