|
| 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)) |
0 commit comments