Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Features:
- Use zero-copy for Packet init from buffer data by :gh-user:`WyattBlue` in (:pr:`2199`).
- Expose AVIndexEntry by :gh-user:`Queuecumber` in (:pr:`2136`).
- Preserving hardware memory during cuvid decoding, exporting/importing via dlpack by :gh-user:`WyattBlue` in (:pr:`2155`).
- Add enumerate_input_devices and enumerate_output_devices API by :gh-user:`WyattBlue` in (:pr:`2174`).

Fixes:

Expand Down
4 changes: 4 additions & 0 deletions av/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from av.codec.codec import Codec, codecs_available
from av.codec.context import CodecContext
from av.container import open
from av.device import DeviceInfo, enumerate_input_devices, enumerate_output_devices
from av.format import ContainerFormat, formats_available
from av.packet import Packet
from av.error import * # noqa: F403; This is limited to exception types.
Expand All @@ -44,6 +45,9 @@
"codecs_available",
"CodecContext",
"open",
"DeviceInfo",
"enumerate_input_devices",
"enumerate_output_devices",
"ContainerFormat",
"formats_available",
"Packet",
Expand Down
6 changes: 0 additions & 6 deletions av/_core.pxd
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
cdef extern from "libavdevice/avdevice.h" nogil:
cdef int avdevice_version()
cdef char* avdevice_configuration()
cdef char* avdevice_license()
void avdevice_register_all()

cdef extern from "libswscale/swscale.h" nogil:
cdef int swscale_version()
cdef char* swscale_configuration()
Expand Down
8 changes: 4 additions & 4 deletions av/_core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cython
import cython.cimports.libav as lib

avdevice_register_all()
lib.avdevice_register_all()

# Exports.
time_base = lib.AV_TIME_BASE
Expand Down Expand Up @@ -41,9 +41,9 @@ def decode_version(v):
license=lib.avformat_license(),
),
"libavdevice": dict(
version=decode_version(avdevice_version()),
configuration=avdevice_configuration(),
license=avdevice_license(),
version=decode_version(lib.avdevice_version()),
configuration=lib.avdevice_configuration(),
license=lib.avdevice_license(),
),
"libavfilter": dict(
version=decode_version(lib.avfilter_version()),
Expand Down
187 changes: 187 additions & 0 deletions av/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import re

import cython
import cython.cimports.libav as lib
from cython.cimports.av.error import err_check


class DeviceInfo:
"""Information about an input or output device.

:param str name: The device identifier, for use as the first argument to :func:`av.open`.
:param str description: Human-readable description of the device.
:param bool is_default: Whether this is the default device.
:param list media_types: Media types this device provides, e.g. ``["video"]``, ``["audio"]``,
or ``["video", "audio"]``.

"""

name: str
description: str
is_default: bool
media_types: list[str]

def __init__(
self,
name: str,
description: str,
is_default: bool,
media_types: list[str],
) -> None:
self.name = name
self.description = description
self.is_default = is_default
self.media_types = media_types

def __repr__(self) -> str:
default = " (default)" if self.is_default else ""
return f"<av.DeviceInfo {self.name!r} {self.description!r}{default}>"


@cython.cfunc
def _build_device_list(device_list: cython.pointer[lib.AVDeviceInfoList]) -> list:
devices: list = []
i: cython.int
j: cython.int
device_info: cython.pointer[lib.AVDeviceInfo]
mt: lib.AVMediaType
s: cython.p_const_char

for i in range(device_list.nb_devices):
device_info = device_list.devices[i]

media_types: list = []
for j in range(device_info.nb_media_types):
mt = device_info.media_types[j]
s = lib.av_get_media_type_string(mt)
if s:
media_types.append(s.decode())

devices.append(
DeviceInfo(
name=device_info.device_name.decode()
if device_info.device_name
else "",
description=device_info.device_description.decode()
if device_info.device_description
else "",
is_default=(i == device_list.default_device),
media_types=media_types,
)
)

return devices


def _enumerate_via_log_fallback(format_name: str) -> list[DeviceInfo]:
"""Fallback for formats (e.g. avfoundation) that log devices instead of
implementing get_device_list. Opens the format with list_devices=1 and
parses the INFO log output."""
from av import logging as avlogging

fmt: cython.pointer[cython.const[lib.AVInputFormat]] = lib.av_find_input_format(
format_name
)

opts: cython.pointer[lib.AVDictionary] = cython.NULL
lib.av_dict_set(cython.address(opts), b"list_devices", b"1", 0)

ctx: cython.pointer[lib.AVFormatContext] = cython.NULL

# Temporarily enable INFO logging so Capture receives device list messages.
old_level = avlogging.get_level()
avlogging.set_level(avlogging.INFO)
devices: list[DeviceInfo] = []
try:
with avlogging.Capture() as logs:
lib.avformat_open_input(cython.address(ctx), b"", fmt, cython.address(opts))
if ctx:
lib.avformat_close_input(cython.address(ctx))

current_media_type = "video"
for _level, _name, message in logs:
message = message.strip()
if "video devices" in message.lower():
current_media_type = "video"
elif "audio devices" in message.lower():
current_media_type = "audio"
else:
m = re.match(r"\[(\d+)\] (.+)", message)
if m:
devices.append(
DeviceInfo(
name=m.group(1),
description=m.group(2),
is_default=False,
media_types=[current_media_type],
)
)
finally:
avlogging.set_level(old_level)
lib.av_dict_free(cython.address(opts))

return devices


def enumerate_input_devices(format_name: str) -> list[DeviceInfo]:
"""List the available input devices for a given format.

:param str format_name: The format name, e.g. ``"avfoundation"``, ``"dshow"``, ``"v4l2"``.
:rtype: list[DeviceInfo]
:raises ValueError: If *format_name* is not a known input format.
:raises av.FFmpegError: If the device does not support enumeration.

Example::

for device in av.enumerate_input_devices("avfoundation"):
print(device.name, device.description)

"""
fmt: cython.pointer[cython.const[lib.AVInputFormat]] = lib.av_find_input_format(
format_name
)
if not fmt:
raise ValueError(f"no such input format: {format_name!r}")

device_list: cython.pointer[lib.AVDeviceInfoList] = cython.NULL
try:
err_check(
lib.avdevice_list_input_sources(
fmt, cython.NULL, cython.NULL, cython.address(device_list)
)
)
return _build_device_list(device_list)
except NotImplementedError:
# Format doesn't implement get_device_list (e.g. avfoundation).
# Fall back to opening with list_devices=1 and parsing the log output.
return _enumerate_via_log_fallback(format_name)
finally:
lib.avdevice_free_list_devices(cython.address(device_list))


def enumerate_output_devices(format_name: str) -> list[DeviceInfo]:
"""List the available output devices for a given format.

:param str format_name: The format name, e.g. ``"audiotoolbox"``.
:rtype: list[DeviceInfo]
:raises ValueError: If *format_name* is not a known output format.
:raises av.FFmpegError: If the device does not support enumeration.

"""
fmt: cython.pointer[cython.const[lib.AVOutputFormat]] = lib.av_guess_format(
format_name, cython.NULL, cython.NULL
)
if not fmt:
raise ValueError(f"no such output format: {format_name!r}")

device_list: cython.pointer[lib.AVDeviceInfoList] = cython.NULL
err_check(
lib.avdevice_list_output_sinks(
fmt, cython.NULL, cython.NULL, cython.address(device_list)
)
)

try:
return _build_device_list(device_list)
finally:
lib.avdevice_free_list_devices(cython.address(device_list))
19 changes: 19 additions & 0 deletions av/device.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
__all__ = ("DeviceInfo", "enumerate_input_devices", "enumerate_output_devices")

class DeviceInfo:
name: str
description: str
is_default: bool
media_types: list[str]

def __init__(
self,
name: str,
description: str,
is_default: bool,
media_types: list[str],
) -> None: ...
def __repr__(self) -> str: ...

def enumerate_input_devices(format_name: str) -> list[DeviceInfo]: ...
def enumerate_output_devices(format_name: str) -> list[DeviceInfo]: ...
4 changes: 2 additions & 2 deletions examples/basics/record_facecam.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

"""
This is written for MacOS. Other platforms will need to init `input_` differently.
You may need to change the file "0". Use this command to list all devices:
You may need to change the file "0". Use this API to list all devices:

ffmpeg -f avfoundation -list_devices true -i ""
av.enumerate_input_devices("avfoundation")

"""

Expand Down
4 changes: 2 additions & 2 deletions examples/basics/record_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

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

Expand Down
30 changes: 30 additions & 0 deletions include/avdevice.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
cdef extern from "libavdevice/avdevice.h" nogil:
cdef int avdevice_version()
cdef char* avdevice_configuration()
cdef char* avdevice_license()
void avdevice_register_all()

cdef struct AVDeviceInfo:
char *device_name
char *device_description
int nb_media_types
AVMediaType *media_types

cdef struct AVDeviceInfoList:
AVDeviceInfo **devices
int nb_devices
int default_device

int avdevice_list_input_sources(
const AVInputFormat *device,
const char *device_name,
AVDictionary *device_options,
AVDeviceInfoList **device_list,
)
int avdevice_list_output_sinks(
const AVOutputFormat *device,
const char *device_name,
AVDictionary *device_options,
AVDeviceInfoList **device_list,
)
void avdevice_free_list_devices(AVDeviceInfoList **device_list)
1 change: 1 addition & 0 deletions include/libav.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ include "avutil.pxd"
include "avcodec.pxd"
include "avformat.pxd"
include "avfilter.pxd"
include "avdevice.pxd"
66 changes: 66 additions & 0 deletions tests/test_device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import sys

import pytest

import av.error
from av.device import DeviceInfo, enumerate_input_devices, enumerate_output_devices


def test_device_info_attributes() -> None:
d = DeviceInfo("0", "FaceTime HD Camera", True, ["video"])
assert d.name == "0"
assert d.description == "FaceTime HD Camera"
assert d.is_default is True
assert d.media_types == ["video"]


def test_device_info_repr_default() -> None:
d = DeviceInfo("0", "FaceTime HD Camera", True, ["video"])
assert repr(d) == "<av.DeviceInfo '0' 'FaceTime HD Camera' (default)>"


def test_device_info_repr_non_default() -> None:
d = DeviceInfo("1", "Built-in Microphone", False, ["audio"])
assert repr(d) == "<av.DeviceInfo '1' 'Built-in Microphone'>"


def test_enumerate_input_devices_unknown_format() -> None:
with pytest.raises(ValueError, match="no such input format"):
enumerate_input_devices("not_a_real_format_xyz")


def test_enumerate_output_devices_unknown_format() -> None:
with pytest.raises(ValueError, match="no such output format"):
enumerate_output_devices("not_a_real_format_xyz")


def _assert_valid_device_list(devices: list[DeviceInfo]) -> None:
assert isinstance(devices, list)
for device in devices:
assert isinstance(device, DeviceInfo)
assert isinstance(device.name, str)
assert isinstance(device.description, str)
assert isinstance(device.is_default, bool)
assert isinstance(device.media_types, list)
assert all(isinstance(mt, str) for mt in device.media_types)


@pytest.mark.skipif(sys.platform != "darwin", reason="avfoundation is macOS only")
def test_enumerate_input_devices_avfoundation() -> None:
_assert_valid_device_list(enumerate_input_devices("avfoundation"))


@pytest.mark.skipif(sys.platform != "linux", reason="v4l2 is Linux only")
def test_enumerate_input_devices_v4l2() -> None:
try:
_assert_valid_device_list(enumerate_input_devices("video4linux2"))
except av.error.OSError:
pytest.skip("v4l2 device enumeration not available")


@pytest.mark.skipif(sys.platform != "win32", reason="dshow is Windows only")
def test_enumerate_input_devices_dshow() -> None:
try:
_assert_valid_device_list(enumerate_input_devices("dshow"))
except av.error.OSError:
pytest.skip("dshow device enumeration not available")
Loading