/* * Media device discovery library * * Copyright (C) 2012 Ideas on board SPRL * * Contact: Laurent Pinchart * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #define HAVE_LIBUDEV #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_LIBUDEV #include #endif #include #include "media-enumerate.h" struct media_enumerate { unsigned int refcount; char **syspaths; struct media_device **devices; unsigned int devices_count; unsigned int devices_max; #ifdef HAVE_LIBUDEV struct udev *udev; #endif }; static int media_enumerate_add_device(struct media_enumerate *media_enum, const char *syspath, struct media_device *media) { if (media_enum->devices_count == media_enum->devices_max) { struct media_device **devices; char **syspaths; unsigned int count; count = media_enum->devices_max * 2; count = count < 16 ? 16 : count; devices = realloc(media_enum->devices, count * sizeof *devices); if (devices == NULL) return -ENOMEM; media_enum->devices = devices; syspaths = realloc(media_enum->syspaths, count * sizeof *syspaths); if (syspaths == NULL) return -ENOMEM; media_enum->syspaths = syspaths; media_enum->devices_max = count; } media_enum->devices[media_enum->devices_count] = media; media_enum->syspaths[media_enum->devices_count] = strdup(syspath); media_enum->devices_count++; return 0; } typedef int(*media_enumerate_callback)(struct media_enumerate *, const char *, const char *, dev_t); enum media_enumerate_bus_type { BUS_TYPE_UNKNOWN, BUS_TYPE_PCI, BUS_TYPE_USB, }; static enum media_enumerate_bus_type media_enumerate_get_physical_bus(const char *syspath) { char *modalias; char type[4]; int ret; int fd; modalias = malloc(strlen(syspath) + 10); if (modalias == NULL) return BUS_TYPE_UNKNOWN; sprintf(modalias, "%s/modalias", syspath); fd = open(modalias, O_RDONLY); free(modalias); if (fd == -1) return BUS_TYPE_UNKNOWN; ret = read(fd, type, 3); close(fd); if (ret != 3) return BUS_TYPE_UNKNOWN; type[3] = '\0'; if (!strcmp(type, "pci")) return BUS_TYPE_PCI; else if (!strcmp(type, "usb")) return BUS_TYPE_USB; else return BUS_TYPE_UNKNOWN; } static char *media_enumerate_get_physical_parent(const char *syspath) { enum media_enumerate_bus_type bus_type; char *phys; char *p; phys = realpath(syspath, NULL); if (phys == NULL) return NULL; p = strstr(phys, "media"); if (p) *(p - 1) = '\0'; p = strstr(phys, "sound"); if (p) *(p - 1) = '\0'; p = strstr(phys, "video4linux"); if (p) *(p - 1) = '\0'; bus_type = media_enumerate_get_physical_bus(phys); switch (bus_type) { case BUS_TYPE_PCI: /* Remove the PCI function from the path if present. */ p = strrchr(phys, '.'); if (p) *p = '\0'; break; case BUS_TYPE_USB: /* Remove the USB interface from the path if present. */ p = strrchr(phys, '/'); if (p && strchr(p, ':')) *p = '\0'; break; default: break; } return phys; } #ifdef HAVE_LIBUDEV /* ----------------------------------------------------------------------------- * udev helpers */ static int media_enumerate_udev_scan_one(struct media_enumerate *media_enum, const char *syspath, media_enumerate_callback callback) { struct udev_device *dev; const char *devnode; char *devpath = NULL; dev_t devnum; int ret = -ENODEV; dev = udev_device_new_from_syspath(media_enum->udev, syspath); if (dev == NULL) return -ENODEV; devnode = udev_device_get_devnode(dev); if (devnode == NULL) goto done; devpath = media_enumerate_get_physical_parent(syspath); if (devpath == NULL) goto done; devnum = udev_device_get_devnum(dev); ret = callback(media_enum, devpath, devnode, devnum); done: udev_device_unref(dev); free(devpath); return ret; } static int media_enumerate_udev_scan(struct media_enumerate *media_enum, const char *subsystem, const char *type, media_enumerate_callback callback) { struct udev_enumerate *udev_enum = NULL; struct udev_list_entry *devs; struct udev_list_entry *dev; unsigned int len; int ret; udev_enum = udev_enumerate_new(media_enum->udev); if (udev_enum == NULL) { ret = -ENOMEM; goto done; } ret = udev_enumerate_add_match_subsystem(udev_enum, subsystem); if (ret < 0) goto done; ret = udev_enumerate_scan_devices(udev_enum); if (ret < 0) goto done; devs = udev_enumerate_get_list_entry(udev_enum); if (devs == NULL) goto done; len = strlen(type); udev_list_entry_foreach(dev, devs) { const char *syspath; const char *p; syspath = udev_list_entry_get_name(dev); p = strrchr(syspath, '/'); if (p == NULL || strncmp(p + 1, type, len)) continue; media_enumerate_udev_scan_one(media_enum, syspath, callback); } ret = 0; done: udev_enumerate_unref(udev_enum); return ret; } #else /* ----------------------------------------------------------------------------- * sysfs helpers */ static int media_enumerate_sysfs_scan_one(struct media_enumerate *media_enum, const char *sysbase, const char *prefix, int idx, media_enumerate_callback callback) { unsigned int major; unsigned int minor; struct stat devstat; unsigned int len; char *devname; char *devpath; char dev[20]; char *end; int ret; int fd; /* Read the device major and minor from sysfs. */ len = strlen(sysbase) + strlen(prefix) + 17; devname = malloc(len); if (devname == NULL) return -ENOMEM; sprintf(devname, "%s/%s%u", sysbase, prefix, idx); devpath = media_enumerate_get_physical_parent(devname); if (devpath == NULL) goto done; sprintf(devname, "%s/%s%u/dev", sysbase, prefix, idx); fd = open(devname, O_RDONLY); if (fd == -1) { ret = -ENODEV; goto done; } ret = read(fd, dev, sizeof dev - 1); close(fd); if (ret < 0) { ret = -ENODEV; goto done; } dev[ret] = '\0'; major = strtoul(dev, &end, 10); if (*end != ':') { ret = -ENODEV; goto done; } minor = strtoul(end + 1, &end, 10); if (!isspace(*end) && end != '\0') { ret = -ENODEV; goto done; } /* Verify that the device node exists. udev might have reordered the * device nodes, make sure the major/minor match as a sanity check. */ if (!strcmp(prefix, "controlC")) sprintf(devname, "/dev/snd/%s%u", prefix, idx); else sprintf(devname, "/dev/%s%u", prefix, idx); ret = stat(devname, &devstat); if (ret < 0) { ret = -ENODEV; goto done; } if (major(devstat.st_rdev) != major || minor(devstat.st_rdev) != minor) { ret = -ENODEV; goto done; } ret = callback(media_enum, devpath, devname, makedev(major, minor)); done: free(devname); free(devpath); return ret; } static int media_enumerate_sysfs_scan(struct media_enumerate *media_enum, const char *sysbase, const char *prefix, media_enumerate_callback callback) { struct dirent *ent; unsigned int len; char *end; DIR *dir; len = strlen(prefix); dir = opendir(sysbase); if (dir == NULL) return -ENODEV; while ((ent = readdir(dir)) != NULL) { unsigned int idx; if (strncmp(ent->d_name, prefix, len)) continue; idx = strtoul(ent->d_name + len, &end, 10); if (*end != '\0') continue; media_enumerate_sysfs_scan_one(media_enum, sysbase, prefix, idx, callback); } closedir(dir); return 0; } #endif /* ----------------------------------------------------------------------------- * Media devices enumeration */ static int media_enumerate_match_one_media(struct media_enumerate *media_enum, const char *syspath, const char *devname, dev_t devnum __attribute__((__unused__))) { struct media_device *media; int ret; media = media_device_new(devname); if (media == NULL) return -ENODEV; ret = media_device_enumerate(media); if (ret < 0) { media_device_unref(media); return -ENODEV; } return media_enumerate_add_device(media_enum, syspath, media); } static int media_enumerate_scan_media(struct media_enumerate *media_enum) { #ifdef HAVE_LIBUDEV return media_enumerate_udev_scan(media_enum, "media", "media", media_enumerate_match_one_media); #else return media_enumerate_sysfs_scan(media_enum, "/sys/bus/media/devices", "media", media_enumerate_match_one_media); #endif /* HAVE_LIBUDEV */ } /* ----------------------------------------------------------------------------- * Video devices enumeration */ static int media_enumerate_match_one_video(struct media_enumerate *media_enum, const char *syspath, const char *devname, dev_t devnum) { struct media_entity_desc entity; struct media_device_info info; struct v4l2_capability cap; struct media_device *media; unsigned int i, j; int ret; int fd; /* Verify whether the video device node is part of an already enumerated * media device. */ for (i = 0; i < media_enum->devices_count; ++i) { unsigned int nents; media = media_enum->devices[i]; nents = media_get_entities_count(media); for (j = 0; j < nents; ++j) { struct media_entity *entity = media_get_entity(media, j); const struct media_entity_desc *info = media_entity_get_info(entity); dev_t dev; if (info->type != MEDIA_ENT_T_DEVNODE_V4L) continue; dev = makedev(info->v4l.major, info->v4l.minor); if (dev == devnum) return 0; } } fd = open(devname, O_RDWR); if (fd < 0) return -ENODEV; memset(&cap, 0, sizeof cap); ret = ioctl(fd, VIDIOC_QUERYCAP, &cap); close(fd); if (ret < 0) return -ENODEV; memset(&info, 0, sizeof info); memcpy(info.driver, cap.driver, sizeof info.driver); memcpy(info.model, cap.card, sizeof info.model); memcpy(info.bus_info, cap.bus_info, sizeof info.bus_info); info.driver_version = cap.version; media = media_device_new_emulated(&info); if (media == NULL) return -ENOMEM; memset(&entity, 0, sizeof entity); memcpy(entity.name, cap.card, sizeof entity.name); entity.type = MEDIA_ENT_T_DEVNODE_V4L; entity.flags = MEDIA_ENT_FL_DEFAULT; entity.v4l.major = major(devnum); entity.v4l.minor = minor(devnum); ret = media_device_add_entity(media, &entity, devname); if (ret < 0) { media_device_unref(media); return ret; } return media_enumerate_add_device(media_enum, syspath, media); } static int media_enumerate_scan_video(struct media_enumerate *media_enum) { #ifdef HAVE_LIBUDEV return media_enumerate_udev_scan(media_enum, "video4linux", "video", media_enumerate_match_one_video); #else return media_enumerate_sysfs_scan(media_enum, "/sys/class/video4linux", "video", media_enumerate_match_one_video); #endif /* HAVE_LIBUDEV */ } /* ----------------------------------------------------------------------------- * Audio devices enumeration */ static int media_enumerate_match_one_audio(struct media_enumerate *media_enum, const char *syspath, const char *devname, dev_t devnum __attribute__((__unused__))) { struct media_entity_desc entity; struct media_device *parent = NULL; struct media_device *media; unsigned int card; unsigned int i, j; char *endp; /* Verify whether the sound card is part of an already enumerated media * device. */ card = strtoul(strrchr(devname, '/') + strlen("/controlC"), &endp, 10); if (*endp != '\0') return -EINVAL; for (i = 0; i < media_enum->devices_count; ++i) { unsigned int nents; media = media_enum->devices[i]; nents = media_get_entities_count(media); if (!strcmp(media_enum->syspaths[i], syspath)) parent = media; for (j = 0; j < nents; ++j) { struct media_entity *entity = media_get_entity(media, j); const struct media_entity_desc *info = media_entity_get_info(entity); if (info->type != MEDIA_ENT_T_DEVNODE_ALSA) continue; if (info->alsa.card == card) return 0; } } if (!parent) return 0; memset(&entity, 0, sizeof entity); entity.type = MEDIA_ENT_T_DEVNODE_ALSA; entity.flags = MEDIA_ENT_FL_DEFAULT; snprintf(entity.name, sizeof entity.name, "ALSA %u/%u/%u", card, 0, 0); entity.alsa.card = card; entity.alsa.device = 0; entity.alsa.subdevice = 0; return media_device_add_entity(parent, &entity, devname); } static int media_enumerate_scan_audio(struct media_enumerate *media_enum) { #ifdef HAVE_LIBUDEV return media_enumerate_udev_scan(media_enum, "sound", "control", media_enumerate_match_one_audio); #else return media_enumerate_sysfs_scan(media_enum, "/sys/class/sound", "controlC", media_enumerate_match_one_audio); #endif /* HAVE_LIBUDEV */ } /* -------------------------------------------------------------------------- */ struct media_enumerate *media_enumerate_new(void) { struct media_enumerate *media_enum; media_enum = calloc(1, sizeof *media_enum); if (media_enum == NULL) return NULL; media_enum->refcount = 1; #ifdef HAVE_LIBUDEV media_enum->udev = udev_new(); if (media_enum->udev == NULL) { printf("Unable to get udev context\n"); media_enumerate_unref(media_enum); return NULL; } #endif return media_enum; } struct media_enumerate *media_enumerate_ref(struct media_enumerate *media_enum) { media_enum->refcount++; return media_enum; } void media_enumerate_unref(struct media_enumerate *media_enum) { unsigned int i; media_enum->refcount--; if (media_enum->refcount > 0) return; for (i = 0; i < media_enum->devices_count; ++i) { media_device_unref(media_enum->devices[i]); free(media_enum->syspaths[i]); } #ifdef HAVE_LIBUDEV udev_unref(media_enum->udev); #endif free(media_enum->devices); free(media_enum->syspaths); free(media_enum); } int media_enumerate_scan(struct media_enumerate *media_enum) { media_enumerate_scan_media(media_enum); media_enumerate_scan_video(media_enum); media_enumerate_scan_audio(media_enum); return 0; } struct media_device **media_enumerate_get_devices(struct media_enumerate *media_enum) { return media_enum->devices; } unsigned int media_enumerate_get_devices_count(struct media_enumerate *media_enum) { return media_enum->devices_count; }