Skip to content

Commit 55782ee

Browse files
WyattBlueclaude
andcommitted
Add enumerate_input_devices and enumerate_output_devices API
Uses avdevice_list_input_sources/avdevice_list_output_sinks (the modern FFmpeg API) with a log-capture fallback for formats like avfoundation that log devices instead of implementing get_device_list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ccf04ed commit 55782ee

8 files changed

Lines changed: 245 additions & 10 deletions

File tree

av/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from av.codec.codec import Codec, codecs_available
1919
from av.codec.context import CodecContext
2020
from av.container import open
21+
from av.device import DeviceInfo, enumerate_input_devices, enumerate_output_devices
2122
from av.format import ContainerFormat, formats_available
2223
from av.packet import Packet
2324
from av.error import * # noqa: F403; This is limited to exception types.
@@ -44,6 +45,9 @@
4445
"codecs_available",
4546
"CodecContext",
4647
"open",
48+
"DeviceInfo",
49+
"enumerate_input_devices",
50+
"enumerate_output_devices",
4751
"ContainerFormat",
4852
"formats_available",
4953
"Packet",

av/_core.pxd

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
cdef extern from "libavdevice/avdevice.h" nogil:
2-
cdef int avdevice_version()
3-
cdef char* avdevice_configuration()
4-
cdef char* avdevice_license()
5-
void avdevice_register_all()
6-
71
cdef extern from "libswscale/swscale.h" nogil:
82
cdef int swscale_version()
93
cdef char* swscale_configuration()

av/device.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import re
2+
3+
import cython
4+
import cython.cimports.libav as lib
5+
from cython.cimports.av.error import err_check
6+
7+
8+
class DeviceInfo:
9+
"""Information about an input or output device.
10+
11+
:param str name: The device identifier, for use as the first argument to :func:`av.open`.
12+
:param str description: Human-readable description of the device.
13+
:param bool is_default: Whether this is the default device.
14+
:param list media_types: Media types this device provides, e.g. ``["video"]``, ``["audio"]``,
15+
or ``["video", "audio"]``.
16+
17+
"""
18+
19+
name: str
20+
description: str
21+
is_default: bool
22+
media_types: list[str]
23+
24+
def __init__(
25+
self,
26+
name: str,
27+
description: str,
28+
is_default: bool,
29+
media_types: list[str],
30+
) -> None:
31+
self.name = name
32+
self.description = description
33+
self.is_default = is_default
34+
self.media_types = media_types
35+
36+
def __repr__(self) -> str:
37+
default = " (default)" if self.is_default else ""
38+
return f"<av.DeviceInfo {self.name!r} {self.description!r}{default}>"
39+
40+
41+
@cython.cfunc
42+
def _build_device_list(device_list: cython.pointer[lib.AVDeviceInfoList]) -> list:
43+
devices: list = []
44+
i: cython.int
45+
j: cython.int
46+
device_info: cython.pointer[lib.AVDeviceInfo]
47+
mt: lib.AVMediaType
48+
s: cython.p_const_char
49+
50+
for i in range(device_list.nb_devices):
51+
device_info = device_list.devices[i]
52+
53+
media_types: list = []
54+
for j in range(device_info.nb_media_types):
55+
mt = device_info.media_types[j]
56+
s = lib.av_get_media_type_string(mt)
57+
if s:
58+
media_types.append(s.decode())
59+
60+
devices.append(
61+
DeviceInfo(
62+
name=device_info.device_name.decode()
63+
if device_info.device_name
64+
else "",
65+
description=device_info.device_description.decode()
66+
if device_info.device_description
67+
else "",
68+
is_default=(i == device_list.default_device),
69+
media_types=media_types,
70+
)
71+
)
72+
73+
return devices
74+
75+
76+
def _enumerate_via_log_fallback(format_name: str) -> list[DeviceInfo]:
77+
"""Fallback for formats (e.g. avfoundation) that log devices instead of
78+
implementing get_device_list. Opens the format with list_devices=1 and
79+
parses the INFO log output."""
80+
from av import logging as avlogging
81+
82+
fmt: cython.pointer[cython.const[lib.AVInputFormat]] = lib.av_find_input_format(
83+
format_name
84+
)
85+
86+
opts: cython.pointer[lib.AVDictionary] = cython.NULL
87+
lib.av_dict_set(cython.address(opts), b"list_devices", b"1", 0)
88+
89+
ctx: cython.pointer[lib.AVFormatContext] = cython.NULL
90+
91+
# Temporarily enable INFO logging so Capture receives device list messages.
92+
old_level = avlogging.get_level()
93+
avlogging.set_level(avlogging.INFO)
94+
devices: list[DeviceInfo] = []
95+
try:
96+
with avlogging.Capture() as logs:
97+
lib.avformat_open_input(cython.address(ctx), b"", fmt, cython.address(opts))
98+
if ctx:
99+
lib.avformat_close_input(cython.address(ctx))
100+
101+
current_media_type = "video"
102+
for _level, _name, message in logs:
103+
message = message.strip()
104+
if "video devices" in message.lower():
105+
current_media_type = "video"
106+
elif "audio devices" in message.lower():
107+
current_media_type = "audio"
108+
else:
109+
m = re.match(r"\[(\d+)\] (.+)", message)
110+
if m:
111+
devices.append(
112+
DeviceInfo(
113+
name=m.group(1),
114+
description=m.group(2),
115+
is_default=False,
116+
media_types=[current_media_type],
117+
)
118+
)
119+
finally:
120+
avlogging.set_level(old_level)
121+
lib.av_dict_free(cython.address(opts))
122+
123+
return devices
124+
125+
126+
def enumerate_input_devices(format_name: str) -> list[DeviceInfo]:
127+
"""List the available input devices for a given format.
128+
129+
:param str format_name: The format name, e.g. ``"avfoundation"``, ``"dshow"``, ``"v4l2"``.
130+
:rtype: list[DeviceInfo]
131+
:raises ValueError: If *format_name* is not a known input format.
132+
:raises av.FFmpegError: If the device does not support enumeration.
133+
134+
Example::
135+
136+
for device in av.enumerate_input_devices("avfoundation"):
137+
print(device.name, device.description)
138+
139+
"""
140+
fmt: cython.pointer[cython.const[lib.AVInputFormat]] = lib.av_find_input_format(
141+
format_name
142+
)
143+
if not fmt:
144+
raise ValueError(f"no such input format: {format_name!r}")
145+
146+
device_list: cython.pointer[lib.AVDeviceInfoList] = cython.NULL
147+
try:
148+
err_check(
149+
lib.avdevice_list_input_sources(
150+
fmt, cython.NULL, cython.NULL, cython.address(device_list)
151+
)
152+
)
153+
return _build_device_list(device_list)
154+
except NotImplementedError:
155+
# Format doesn't implement get_device_list (e.g. avfoundation).
156+
# Fall back to opening with list_devices=1 and parsing the log output.
157+
return _enumerate_via_log_fallback(format_name)
158+
finally:
159+
lib.avdevice_free_list_devices(cython.address(device_list))
160+
161+
162+
def enumerate_output_devices(format_name: str) -> list[DeviceInfo]:
163+
"""List the available output devices for a given format.
164+
165+
:param str format_name: The format name, e.g. ``"audiotoolbox"``.
166+
:rtype: list[DeviceInfo]
167+
:raises ValueError: If *format_name* is not a known output format.
168+
:raises av.FFmpegError: If the device does not support enumeration.
169+
170+
"""
171+
fmt: cython.pointer[cython.const[lib.AVOutputFormat]] = lib.av_guess_format(
172+
format_name, cython.NULL, cython.NULL
173+
)
174+
if not fmt:
175+
raise ValueError(f"no such output format: {format_name!r}")
176+
177+
device_list: cython.pointer[lib.AVDeviceInfoList] = cython.NULL
178+
err_check(
179+
lib.avdevice_list_output_sinks(
180+
fmt, cython.NULL, cython.NULL, cython.address(device_list)
181+
)
182+
)
183+
184+
try:
185+
return _build_device_list(device_list)
186+
finally:
187+
lib.avdevice_free_list_devices(cython.address(device_list))

av/device.pyi

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
__all__ = ("DeviceInfo", "enumerate_input_devices", "enumerate_output_devices")
2+
3+
class DeviceInfo:
4+
name: str
5+
description: str
6+
is_default: bool
7+
media_types: list[str]
8+
9+
def __init__(
10+
self,
11+
name: str,
12+
description: str,
13+
is_default: bool,
14+
media_types: list[str],
15+
) -> None: ...
16+
def __repr__(self) -> str: ...
17+
18+
def enumerate_input_devices(format_name: str) -> list[DeviceInfo]: ...
19+
def enumerate_output_devices(format_name: str) -> list[DeviceInfo]: ...

examples/basics/record_facecam.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
"""
77
This is written for MacOS. Other platforms will need to init `input_` differently.
8-
You may need to change the file "0". Use this command to list all devices:
8+
You may need to change the file "0". Use this API to list all devices:
99
10-
ffmpeg -f avfoundation -list_devices true -i ""
10+
av.enumerate_input_devices("avfoundation")
1111
1212
"""
1313

examples/basics/record_screen.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
"""
66
This is written for MacOS. Other platforms will need a different file, format pair.
7-
You may need to change the file "1". Use this command to list all devices:
7+
You may need to change the file "1". Use this API to list all devices:
88
9-
ffmpeg -f avfoundation -list_devices true -i ""
9+
av.enumerate_input_devices("avfoundation")
1010
1111
"""
1212

include/avdevice.pxd

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
cdef extern from "libavdevice/avdevice.h" nogil:
2+
cdef int avdevice_version()
3+
cdef char* avdevice_configuration()
4+
cdef char* avdevice_license()
5+
void avdevice_register_all()
6+
7+
cdef struct AVDeviceInfo:
8+
char *device_name
9+
char *device_description
10+
int nb_media_types
11+
AVMediaType *media_types
12+
13+
cdef struct AVDeviceInfoList:
14+
AVDeviceInfo **devices
15+
int nb_devices
16+
int default_device
17+
18+
int avdevice_list_input_sources(
19+
const AVInputFormat *device,
20+
const char *device_name,
21+
AVDictionary *device_options,
22+
AVDeviceInfoList **device_list,
23+
)
24+
int avdevice_list_output_sinks(
25+
const AVOutputFormat *device,
26+
const char *device_name,
27+
AVDictionary *device_options,
28+
AVDeviceInfoList **device_list,
29+
)
30+
void avdevice_free_list_devices(AVDeviceInfoList **device_list)

include/libav.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ include "avutil.pxd"
22
include "avcodec.pxd"
33
include "avformat.pxd"
44
include "avfilter.pxd"
5+
include "avdevice.pxd"

0 commit comments

Comments
 (0)