diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:28:17 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:28:17 +0000 |
commit | 7a46c07230b8d8108c0e8e80df4522d0ac116538 (patch) | |
tree | d483300dab478b994fe199a5d19d18d74153718a /spa/plugins/v4l2 | |
parent | Initial commit. (diff) | |
download | pipewire-0bfb2679f751193be0325ef92c84c3863d22ac84.tar.xz pipewire-0bfb2679f751193be0325ef92c84c3863d22ac84.zip |
Adding upstream version 0.3.65.upstream/0.3.65upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'spa/plugins/v4l2')
-rw-r--r-- | spa/plugins/v4l2/meson.build | 10 | ||||
-rw-r--r-- | spa/plugins/v4l2/v4l2-device.c | 291 | ||||
-rw-r--r-- | spa/plugins/v4l2/v4l2-source.c | 1055 | ||||
-rw-r--r-- | spa/plugins/v4l2/v4l2-udev.c | 754 | ||||
-rw-r--r-- | spa/plugins/v4l2/v4l2-utils.c | 1743 | ||||
-rw-r--r-- | spa/plugins/v4l2/v4l2.c | 59 | ||||
-rw-r--r-- | spa/plugins/v4l2/v4l2.h | 49 |
7 files changed, 3961 insertions, 0 deletions
diff --git a/spa/plugins/v4l2/meson.build b/spa/plugins/v4l2/meson.build new file mode 100644 index 0000000..648583f --- /dev/null +++ b/spa/plugins/v4l2/meson.build @@ -0,0 +1,10 @@ +v4l2_sources = ['v4l2.c', + 'v4l2-device.c', + 'v4l2-udev.c', + 'v4l2-source.c'] + +v4l2lib = shared_library('spa-v4l2', + v4l2_sources, + dependencies : [ spa_dep, libudev_dep, libinotify_dep ], + install : true, + install_dir : spa_plugindir / 'v4l2') diff --git a/spa/plugins/v4l2/v4l2-device.c b/spa/plugins/v4l2/v4l2-device.c new file mode 100644 index 0000000..53fb033 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-device.c @@ -0,0 +1,291 @@ +/* Spa V4l2 Source + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stddef.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> + +#include <linux/videodev2.h> + +#include <spa/support/plugin.h> +#include <spa/support/log.h> +#include <spa/support/loop.h> +#include <spa/utils/keys.h> +#include <spa/utils/names.h> +#include <spa/utils/string.h> +#include <spa/node/node.h> +#include <spa/pod/builder.h> +#include <spa/monitor/device.h> +#include <spa/monitor/utils.h> +#include <spa/param/param.h> + +#include "v4l2.h" + +#define NAME "v4l2-device" + +static const char default_device[] = "/dev/video0"; + +struct props { + char device[64]; + char product_id[6]; + char vendor_id[6]; + int device_fd; +}; + +static void reset_props(struct props *props) +{ + strncpy(props->device, default_device, 64); +} + +struct impl { + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + + struct props props; + + struct spa_hook_list hooks; + + struct spa_v4l2_device dev; +}; + +static int emit_info(struct impl *this, bool full) +{ + int res; + struct spa_dict_item items[12]; + uint32_t n_items = 0; + struct spa_device_info info; + struct spa_param_info params[2]; + char path[128], version[16], capabilities[16], device_caps[16]; + + if ((res = spa_v4l2_open(&this->dev, this->props.device)) < 0) + return res; + + info = SPA_DEVICE_INFO_INIT(); + + info.change_mask = SPA_DEVICE_CHANGE_MASK_PROPS; + +#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value) + snprintf(path, sizeof(path), "v4l2:%s", this->props.device); + ADD_ITEM(SPA_KEY_OBJECT_PATH, path); + ADD_ITEM(SPA_KEY_DEVICE_API, "v4l2"); + ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Video/Device"); + if (this->props.product_id[0]) + ADD_ITEM(SPA_KEY_DEVICE_PRODUCT_ID, this->props.product_id); + if (this->props.vendor_id[0]) + ADD_ITEM(SPA_KEY_DEVICE_VENDOR_ID, this->props.vendor_id); + ADD_ITEM(SPA_KEY_API_V4L2_PATH, (char *)this->props.device); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_DRIVER, (char *)this->dev.cap.driver); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_CARD, (char *)this->dev.cap.card); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_BUS_INFO, (char *)this->dev.cap.bus_info); + snprintf(version, sizeof(version), "%u.%u.%u", + (this->dev.cap.version >> 16) & 0xFF, + (this->dev.cap.version >> 8) & 0xFF, + (this->dev.cap.version) & 0xFF); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_VERSION, version); + snprintf(capabilities, sizeof(capabilities), "%08x", this->dev.cap.capabilities); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_CAPABILITIES, capabilities); + snprintf(device_caps, sizeof(device_caps), "%08x", this->dev.cap.device_caps); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_DEVICE_CAPS, device_caps); +#undef ADD_ITEM + info.props = &SPA_DICT_INIT(items, n_items); + + info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ); + params[1] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_WRITE); + info.n_params = 0; + info.params = params; + + spa_device_emit_info(&this->hooks, &info); + + if (spa_v4l2_is_capture(&this->dev)) { + struct spa_device_object_info oinfo; + + oinfo = SPA_DEVICE_OBJECT_INFO_INIT(); + oinfo.type = SPA_TYPE_INTERFACE_Node; + oinfo.factory_name = SPA_NAME_API_V4L2_SOURCE; + oinfo.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + oinfo.props = &SPA_DICT_INIT(items, n_items); + + spa_device_emit_object_info(&this->hooks, 0, &oinfo); + } + + spa_v4l2_close(&this->dev); + + return 0; +} + +static int impl_add_listener(void *object, + struct spa_hook *listener, + const struct spa_device_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + int res = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + if (events->info || events->object_info) + res = emit_info(this, true); + + spa_hook_list_join(&this->hooks, &save); + + return res; +} + +static int impl_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_device_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + return -ENOTSUP; +} + +static int impl_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + return -ENOTSUP; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_add_listener, + .sync = impl_sync, + .enum_params = impl_enum_params, + .set_param = impl_set_param, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear, this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + spa_hook_list_init(&this->hooks); + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + this->dev.log = this->log; + this->dev.fd = -1; + + reset_props(&this->props); + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_V4L2_PATH))) + strncpy(this->props.device, str, 63); + if (info && (str = spa_dict_lookup(info, SPA_KEY_DEVICE_PRODUCT_ID))) + strncpy(this->props.product_id, str, 5); + if (info && (str = spa_dict_lookup(info, SPA_KEY_DEVICE_VENDOR_ID))) + strncpy(this->props.vendor_id, str, 5); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + + return 1; +} + +const struct spa_handle_factory spa_v4l2_device_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_V4L2_DEVICE, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/v4l2/v4l2-source.c b/spa/plugins/v4l2/v4l2-source.c new file mode 100644 index 0000000..b6eae49 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-source.c @@ -0,0 +1,1055 @@ +/* Spa V4l2 Source + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stddef.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> + +#include <linux/videodev2.h> + +#include <spa/support/plugin.h> +#include <spa/support/log.h> +#include <spa/support/loop.h> +#include <spa/utils/list.h> +#include <spa/utils/keys.h> +#include <spa/utils/names.h> +#include <spa/utils/string.h> +#include <spa/monitor/device.h> +#include <spa/node/node.h> +#include <spa/node/io.h> +#include <spa/node/utils.h> +#include <spa/node/keys.h> +#include <spa/param/video/format-utils.h> +#include <spa/param/param.h> +#include <spa/param/latency-utils.h> +#include <spa/pod/filter.h> +#include <spa/control/control.h> + +#include "v4l2.h" + +static const char default_device[] = "/dev/video0"; + +struct props { + char device[64]; + char device_name[128]; + int device_fd; +}; + +static void reset_props(struct props *props) +{ + strncpy(props->device, default_device, 64); +} + +#define MAX_BUFFERS 32 + +#define BUFFER_FLAG_OUTSTANDING (1<<0) +#define BUFFER_FLAG_ALLOCATED (1<<1) +#define BUFFER_FLAG_MAPPED (1<<2) + +struct buffer { + uint32_t id; + uint32_t flags; + struct spa_list link; + struct spa_buffer *outbuf; + struct spa_meta_header *h; + struct v4l2_buffer v4l2_buffer; + void *ptr; +}; + +#define MAX_CONTROLS 64 + +struct control { + uint32_t id; + uint32_t ctrl_id; + double value; +}; + +struct port { + struct impl *impl; + + bool alloc_buffers; + bool have_expbuf; + + bool next_fmtdesc; + struct v4l2_fmtdesc fmtdesc; + bool next_frmsize; + struct v4l2_frmsizeenum frmsize; + struct v4l2_frmivalenum frmival; + + bool have_format; + struct spa_video_info current_format; + struct spa_fraction rate; + + struct spa_v4l2_device dev; + + bool have_query_ext_ctrl; + struct v4l2_format fmt; + enum v4l2_buf_type type; + enum v4l2_memory memtype; + + struct control controls[MAX_CONTROLS]; + uint32_t n_controls; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + struct spa_list queue; + + struct spa_source source; + + uint64_t info_all; + struct spa_port_info info; + struct spa_io_buffers *io; + struct spa_io_sequence *control; +#define PORT_PropInfo 0 +#define PORT_EnumFormat 1 +#define PORT_Meta 2 +#define PORT_IO 3 +#define PORT_Format 4 +#define PORT_Buffers 5 +#define PORT_Latency 6 +#define N_PORT_PARAMS 7 + struct spa_param_info params[N_PORT_PARAMS]; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + + uint64_t info_all; + struct spa_node_info info; +#define NODE_PropInfo 0 +#define NODE_Props 1 +#define NODE_EnumFormat 2 +#define NODE_Format 3 +#define N_NODE_PARAMS 4 + struct spa_param_info params[N_NODE_PARAMS]; + struct props props; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + struct port out_ports[1]; + + struct spa_io_position *position; + struct spa_io_clock *clock; + + struct spa_latency_info latency[2]; +}; + +#define CHECK_PORT(this,direction,port_id) ((direction) == SPA_DIRECTION_OUTPUT && (port_id) == 0) + +#define GET_OUT_PORT(this,p) (&this->out_ports[p]) +#define GET_PORT(this,d,p) GET_OUT_PORT(this,p) + +#include "v4l2-utils.c" + +static int port_get_format(struct port *port, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct spa_pod_frame f; + + if (!port->have_format) + return -EIO; + if (index > 0) + return 0; + + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, SPA_PARAM_Format); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(port->current_format.media_type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(port->current_format.media_subtype), + 0); + + switch (port->current_format.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_format, SPA_POD_Id(port->current_format.info.raw.format), + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format.info.raw.size), + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.raw.framerate), + 0); + break; + case SPA_MEDIA_SUBTYPE_mjpg: + case SPA_MEDIA_SUBTYPE_jpeg: + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format.info.mjpg.size), + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.mjpg.framerate), + 0); + break; + case SPA_MEDIA_SUBTYPE_h264: + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format.info.h264.size), + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.h264.framerate), + 0); + break; + default: + return -EIO; + } + + *param = spa_pod_builder_pop(builder, &f); + + return 1; +} + + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device), + SPA_PROP_INFO_description, SPA_POD_String("The V4L2 device"), + SPA_PROP_INFO_type, SPA_POD_String(p->device)); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName), + SPA_PROP_INFO_description, SPA_POD_String("The V4L2 device name"), + SPA_PROP_INFO_type, SPA_POD_String(p->device_name)); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceFd), + SPA_PROP_INFO_description, SPA_POD_String("The V4L2 fd"), + SPA_PROP_INFO_type, SPA_POD_Int(p->device_fd)); + break; + default: + return spa_v4l2_enum_controls(this, seq, result.index - 3, num, filter); + } + break; + } + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_device, SPA_POD_String(p->device), + SPA_PROP_deviceName, SPA_POD_String(p->device_name), + SPA_PROP_deviceFd, SPA_POD_Int(p->device_fd)); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_EnumFormat: + return spa_v4l2_enum_format(this, seq, start, num, filter); + case SPA_PARAM_Format: + if((res = port_get_format(GET_OUT_PORT(this, 0), + result.index, filter, ¶m, &b)) <= 0) + return res; + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + struct spa_pod_object *obj = (struct spa_pod_object *) param; + struct spa_pod_prop *prop; + + if (param == NULL) { + reset_props(p); + return 0; + } + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_PROP_device: + strncpy(p->device, + (char *)SPA_POD_CONTENTS(struct spa_pod_string, &prop->value), + sizeof(p->device)-1); + break; + default: + spa_v4l2_set_control(this, prop->key, prop); + break; + } + } + + break; + } + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = GET_OUT_PORT(this, 0); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_ParamBegin: + if ((res = spa_v4l2_open(&port->dev, this->props.device)) < 0) + return res; + break; + case SPA_NODE_COMMAND_ParamEnd: + if (port->have_format) + return 0; + if ((res = spa_v4l2_close(&port->dev)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Start: + { + if (!port->have_format) { + spa_log_error(this->log, "no format"); + return -EIO; + } + if (port->n_buffers == 0) { + spa_log_error(this->log, "no buffers"); + return -EIO; + } + + if ((res = spa_v4l2_stream_on(this)) < 0) + return res; + break; + } + case SPA_NODE_COMMAND_Pause: + case SPA_NODE_COMMAND_Suspend: + if ((res = spa_v4l2_stream_off(this)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + + return 0; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_DEVICE_API, "v4l2" }, + { SPA_KEY_MEDIA_CLASS, "Video/Source" }, + { SPA_KEY_MEDIA_ROLE, "Camera" }, + { SPA_KEY_NODE_DRIVER, "true" }, +}; + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_OUTPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, GET_OUT_PORT(this, 0), true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_node_add_port(void *object, + enum spa_direction direction, + uint32_t port_id, const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, + enum spa_direction direction, + uint32_t port_id) +{ + return -ENOTSUP; +} + +static int impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + return spa_v4l2_enum_controls(this, seq, start, num, filter); + + case SPA_PARAM_EnumFormat: + return spa_v4l2_enum_format(this, seq, start, num, filter); + + case SPA_PARAM_Format: + if((res = port_get_format(port, result.index, filter, ¶m, &b)) <= 0) + return res; + break; + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(4, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(port->fmt.fmt.pix.sizeimage), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->fmt.fmt.pix.bytesperline)); + break; + + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock))); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Control), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_sequence))); + break; + default: + return 0; + } + break; + case SPA_PARAM_Latency: + switch (result.index) { + case 0: case 1: + param = spa_latency_build(&b, id, &this->latency[result.index]); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + struct spa_video_info info; + int res; + + if (port->have_format) { + spa_v4l2_stream_off(this); + spa_v4l2_clear_buffers(this); + } + if (format == NULL) { + if (!port->have_format) + return 0; + + port->have_format = false; + port->dev.have_format = false; + spa_v4l2_close(&port->dev); + goto done; + } else { + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_video) { + spa_log_error(this->log, "media type must be video"); + return -EINVAL; + } + + switch (info.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + if (spa_format_video_raw_parse(format, &info.info.raw) < 0) { + spa_log_error(this->log, "can't parse video raw"); + return -EINVAL; + } + break; + case SPA_MEDIA_SUBTYPE_mjpg: + if (spa_format_video_mjpg_parse(format, &info.info.mjpg) < 0) + return -EINVAL; + break; + case SPA_MEDIA_SUBTYPE_h264: + if (spa_format_video_h264_parse(format, &info.info.h264) < 0) + return -EINVAL; + break; + default: + return -EINVAL; + } + } + + if (port->have_format && !SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_TEST_ONLY)) { + port->have_format = false; + } + + if ((res = spa_v4l2_set_format(this, &info, flags)) < 0) + return res; + + if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_TEST_ONLY)) { + port->current_format = info; + port->have_format = true; + } + + done: + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READ); + } else { + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, 0); + } + emit_port_info(this, port, false); + emit_node_info(this, false); + + return 0; +} + +static int impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + int res; + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(object != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(object, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_PARAM_Latency: + { + struct spa_latency_info info; + if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (direction == info.direction) + return -EINVAL; + + this->latency[info.direction] = info; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[PORT_Latency].flags ^= SPA_PARAM_INFO_SERIAL; + emit_port_info(this, port, false); + break; + } + case SPA_PARAM_Format: + res = port_set_format(object, port, flags, param); + break; + default: + res = -ENOENT; + } + return res; +} + +static int impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + if (port->n_buffers) { + spa_v4l2_stream_off(this); + if ((res = spa_v4l2_clear_buffers(this)) < 0) + return res; + } + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + if (buffers == NULL) + return 0; + + if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) { + res = spa_v4l2_alloc_buffers(this, buffers, n_buffers); + } else { + res = spa_v4l2_use_buffers(this, buffers, n_buffers); + } + return res; +} + +static int impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + case SPA_IO_Control: + port->control = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, + uint32_t port_id, + uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(port_id == 0, -EINVAL); + + port = GET_OUT_PORT(this, port_id); + + spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL); + + res = spa_v4l2_buffer_recycle(this, buffer_id); + + return res; +} + +static int process_control(struct impl *this, struct spa_pod_sequence *control) +{ + struct spa_pod_control *c; + + SPA_POD_SEQUENCE_FOREACH(control, c) { + switch (c->type) { + case SPA_CONTROL_Properties: + { + struct spa_pod_prop *prop; + struct spa_pod_object *obj = (struct spa_pod_object *) &c->value; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + spa_v4l2_set_control(this, prop->key, prop); + } + break; + } + default: + break; + } + } + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + int res; + struct spa_io_buffers *io; + struct port *port; + struct buffer *b; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = GET_OUT_PORT(this, 0); + if ((io = port->io) == NULL) + return -EIO; + + if (port->control) + process_control(this, &port->control->sequence); + + spa_log_trace(this->log, "%p; status %d", this, io->status); + + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + if (io->buffer_id < port->n_buffers) { + if ((res = spa_v4l2_buffer_recycle(this, io->buffer_id)) < 0) + return res; + + io->buffer_id = SPA_ID_INVALID; + } + + if (spa_list_is_empty(&port->queue)) + return SPA_STATUS_OK; + + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + + spa_log_trace(this->log, "%p: dequeue buffer %d", this, b->id); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + struct port *port; + int res; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + v4l2_log_topic_init(this->log); + + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data_loop is needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + spa_hook_list_init(&this->hooks); + + this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[NODE_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, 0); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + reset_props(&this->props); + + port = GET_OUT_PORT(this, 0); + port->impl = this; + spa_list_init(&port->queue); + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + port->params[PORT_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READ); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + port->alloc_buffers = true; + port->have_expbuf = true; + port->have_query_ext_ctrl = true; + port->dev.log = this->log; + port->dev.fd = -1; + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_V4L2_PATH))) { + strncpy(this->props.device, str, 63); + if ((res = spa_v4l2_open(&port->dev, this->props.device)) < 0) + return res; + spa_v4l2_close(&port->dev); + } + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + + return 1; +} + +const struct spa_handle_factory spa_v4l2_source_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_V4L2_SOURCE, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/v4l2/v4l2-udev.c b/spa/plugins/v4l2/v4l2-udev.c new file mode 100644 index 0000000..e7f9265 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-udev.c @@ -0,0 +1,754 @@ +/* Spa V4l2 udev monitor + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stddef.h> +#include <stdio.h> +#include <unistd.h> +#include <limits.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/inotify.h> +#include <fcntl.h> + +#include <libudev.h> + +#include <spa/support/log.h> +#include <spa/utils/type.h> +#include <spa/utils/keys.h> +#include <spa/utils/names.h> +#include <spa/utils/result.h> +#include <spa/utils/string.h> +#include <spa/support/loop.h> +#include <spa/support/plugin.h> +#include <spa/monitor/device.h> +#include <spa/monitor/utils.h> + +#define NAME "v4l2-udev" + +#define MAX_DEVICES 64 + +#define ACTION_ADD 0 +#define ACTION_REMOVE 1 +#define ACTION_DISABLE 2 + +struct device { + uint32_t id; + struct udev_device *dev; + unsigned int accessible:1; + unsigned int ignored:1; + unsigned int emitted:1; +}; + +struct impl { + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + struct spa_loop *main_loop; + + struct spa_hook_list hooks; + + uint64_t info_all; + struct spa_device_info info; + + struct udev *udev; + struct udev_monitor *umonitor; + + struct device devices[MAX_DEVICES]; + uint32_t n_devices; + + struct spa_source source; + struct spa_source notify; +}; + +static int impl_udev_open(struct impl *this) +{ + if (this->udev == NULL) { + this->udev = udev_new(); + if (this->udev == NULL) + return -ENOMEM; + } + return 0; +} + +static int impl_udev_close(struct impl *this) +{ + if (this->udev != NULL) + udev_unref(this->udev); + this->udev = NULL; + return 0; +} + +static struct device *add_device(struct impl *this, uint32_t id, struct udev_device *dev) +{ + struct device *device; + + if (this->n_devices >= MAX_DEVICES) + return NULL; + device = &this->devices[this->n_devices++]; + spa_zero(*device); + device->id = id; + udev_device_ref(dev); + device->dev = dev; + return device; +} + +static struct device *find_device(struct impl *this, uint32_t id) +{ + uint32_t i; + for (i = 0; i < this->n_devices; i++) { + if (this->devices[i].id == id) + return &this->devices[i]; + } + return NULL; +} + +static void remove_device(struct impl *this, struct device *device) +{ + udev_device_unref(device->dev); + *device = this->devices[--this->n_devices]; +} + +static void clear_devices(struct impl *this) +{ + uint32_t i; + for (i = 0; i < this->n_devices; i++) + udev_device_unref(this->devices[i].dev); + this->n_devices = 0; +} + +static uint32_t get_device_id(struct impl *this, struct udev_device *dev) +{ + const char *str; + + if ((str = udev_device_get_devnode(dev)) == NULL) + return SPA_ID_INVALID; + + if (!(str = strrchr(str, '/'))) + return SPA_ID_INVALID; + + if (strlen(str) <= 6 || strncmp(str, "/video", 6) != 0) + return SPA_ID_INVALID; + + return atoi(str + 6); +} + +static int dehex(char x) +{ + if (x >= '0' && x <= '9') + return x - '0'; + if (x >= 'A' && x <= 'F') + return x - 'A' + 10; + if (x >= 'a' && x <= 'f') + return x - 'a' + 10; + return -1; +} + +static void unescape(const char *src, char *dst) +{ + const char *s; + char *d; + int h1 = 0, h2 = 0; + enum { TEXT, BACKSLASH, EX, FIRST } state = TEXT; + + for (s = src, d = dst; *s; s++) { + switch (state) { + case TEXT: + if (*s == '\\') + state = BACKSLASH; + else + *(d++) = *s; + break; + + case BACKSLASH: + if (*s == 'x') + state = EX; + else { + *(d++) = '\\'; + *(d++) = *s; + state = TEXT; + } + break; + + case EX: + h1 = dehex(*s); + if (h1 < 0) { + *(d++) = '\\'; + *(d++) = 'x'; + *(d++) = *s; + state = TEXT; + } else + state = FIRST; + break; + + case FIRST: + h2 = dehex(*s); + if (h2 < 0) { + *(d++) = '\\'; + *(d++) = 'x'; + *(d++) = *(s-1); + *(d++) = *s; + } else + *(d++) = (char) (h1 << 4) | h2; + state = TEXT; + break; + } + } + switch (state) { + case TEXT: + break; + case BACKSLASH: + *(d++) = '\\'; + break; + case EX: + *(d++) = '\\'; + *(d++) = 'x'; + break; + case FIRST: + *(d++) = '\\'; + *(d++) = 'x'; + *(d++) = *(s-1); + break; + } + *d = 0; +} + +static int emit_object_info(struct impl *this, struct device *device) +{ + struct spa_device_object_info info; + uint32_t id = device->id; + struct udev_device *dev = device->dev; + const char *str; + struct spa_dict_item items[20]; + uint32_t n_items = 0; + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + + info.type = SPA_TYPE_INTERFACE_Device; + info.factory_name = SPA_NAME_API_V4L2_DEVICE; + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS | + SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + info.flags = 0; + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ENUM_API,"udev"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "v4l2"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Video/Device"); + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_V4L2_PATH, udev_device_get_devnode(dev)); + + if ((str = udev_device_get_property_value(dev, "USEC_INITIALIZED")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PLUGGED_USEC, str); + + str = udev_device_get_property_value(dev, "ID_PATH"); + if (!(str && *str)) + str = udev_device_get_syspath(dev); + if (str && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_PATH, str); + } + if ((str = udev_device_get_devpath(dev)) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SYSFS_PATH, str); + } + if ((str = udev_device_get_property_value(dev, "ID_ID")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_ID, str); + } + if ((str = udev_device_get_property_value(dev, "ID_BUS")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, str); + } + if ((str = udev_device_get_property_value(dev, "SUBSYSTEM")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SUBSYSTEM, str); + } + if ((str = udev_device_get_property_value(dev, "ID_VENDOR_ID")) && *str) { + int32_t val; + if (spa_atoi32(str, &val, 16)) { + char *dec = alloca(12); /* 0xffff is max */ + snprintf(dec, 12, "0x%04x", val); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, dec); + } + } + str = udev_device_get_property_value(dev, "ID_VENDOR_FROM_DATABASE"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_VENDOR_ENC"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_VENDOR"); + } else { + char *t = alloca(strlen(str) + 1); + unescape(str, t); + str = t; + } + } + if (str && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_NAME, str); + } + if ((str = udev_device_get_property_value(dev, "ID_MODEL_ID")) && *str) { + int32_t val; + if (spa_atoi32(str, &val, 16)) { + char *dec = alloca(12); /* 0xffff is max */ + snprintf(dec, 12, "0x%04x", val); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, dec); + } + } + + str = udev_device_get_property_value(dev, "ID_MODEL_FROM_DATABASE"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_MODEL_ENC"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_MODEL"); + if (!(str && *str)) + str = udev_device_get_property_value(dev, "ID_V4L_PRODUCT"); + } else { + char *t = alloca(strlen(str) + 1); + unescape(str, t); + str = t; + } + } + if (str && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_NAME, str); + + if ((str = udev_device_get_property_value(dev, "ID_SERIAL")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SERIAL, str); + } + if ((str = udev_device_get_property_value(dev, "ID_V4L_CAPABILITIES")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_CAPABILITIES, str); + } + info.props = &SPA_DICT_INIT(items, n_items); + spa_device_emit_object_info(&this->hooks, id, &info); + device->emitted = true; + + return 1; +} + +static bool check_access(struct impl *this, struct device *device) +{ + char path[128]; + + snprintf(path, sizeof(path), "/dev/video%u", device->id); + device->accessible = access(path, R_OK|W_OK) >= 0; + spa_log_debug(this->log, "%s accessible:%u", path, device->accessible); + + return device->accessible; +} + +static void process_device(struct impl *this, uint32_t action, struct udev_device *dev) +{ + uint32_t id; + struct device *device; + bool emitted; + + if ((id = get_device_id(this, dev)) == SPA_ID_INVALID) + return; + + device = find_device(this, id); + if (device && device->ignored) + return; + + switch (action) { + case ACTION_ADD: + if (device == NULL) + device = add_device(this, id, dev); + if (device == NULL) + return; + if (!check_access(this, device)) + return; + emit_object_info(this, device); + break; + + case ACTION_REMOVE: + if (device == NULL) + return; + emitted = device->emitted; + remove_device(this, device); + if (emitted) + spa_device_emit_object_info(&this->hooks, id, NULL); + break; + + case ACTION_DISABLE: + if (device == NULL) + return; + if (device->emitted) { + device->emitted = false; + spa_device_emit_object_info(&this->hooks, id, NULL); + } + break; + } +} + +static int stop_inotify(struct impl *this) +{ + if (this->notify.fd == -1) + return 0; + spa_log_info(this->log, "stop inotify"); + spa_loop_remove_source(this->main_loop, &this->notify); + close(this->notify.fd); + this->notify.fd = -1; + return 0; +} + +static void impl_on_notify_events(struct spa_source *source) +{ + bool deleted = false; + struct impl *this = source->data; + union { + unsigned char name[sizeof(struct inotify_event) + NAME_MAX + 1]; + struct inotify_event e; /* for appropriate alignment */ + } buf; + + while (true) { + ssize_t len; + const struct inotify_event *event; + void *p, *e; + + len = read(source->fd, &buf, sizeof(buf)); + if (len < 0 && errno != EAGAIN) + break; + if (len <= 0) + break; + + e = SPA_PTROFF(&buf, len, void); + + for (p = &buf; p < e; + p = SPA_PTROFF(p, sizeof(struct inotify_event) + event->len, void)) { + unsigned int id; + struct device *device; + + event = (const struct inotify_event *) p; + + if ((event->mask & IN_ATTRIB)) { + bool access; + if (sscanf(event->name, "video%u", &id) != 1) + continue; + if ((device = find_device(this, id)) == NULL) + continue; + access = check_access(this, device); + if (access && !device->emitted) + process_device(this, ACTION_ADD, device->dev); + else if (!access && device->emitted) + process_device(this, ACTION_DISABLE, device->dev); + } + /* /dev/ might have been removed */ + if ((event->mask & (IN_DELETE_SELF | IN_MOVE_SELF))) + deleted = true; + } + } + if (deleted) + stop_inotify(this); +} + +static int start_inotify(struct impl *this) +{ + int res, notify_fd; + + if (this->notify.fd != -1) + return 0; + + if ((notify_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK)) < 0) + return -errno; + + res = inotify_add_watch(notify_fd, "/dev", + IN_ATTRIB | IN_CLOSE_WRITE | IN_DELETE_SELF | IN_MOVE_SELF); + if (res < 0) { + res = -errno; + close(notify_fd); + + if (res == -ENOENT) { + spa_log_debug(this->log, "/dev/ does not exist yet"); + return 0; + } + spa_log_error(this->log, "inotify_add_watch() failed: %s", spa_strerror(res)); + return res; + } + spa_log_info(this->log, "start inotify"); + this->notify.func = impl_on_notify_events; + this->notify.data = this; + this->notify.fd = notify_fd; + this->notify.mask = SPA_IO_IN | SPA_IO_ERR; + + spa_loop_add_source(this->main_loop, &this->notify); + + return 0; +} + +static void impl_on_fd_events(struct spa_source *source) +{ + struct impl *this = source->data; + struct udev_device *dev; + const char *action; + + dev = udev_monitor_receive_device(this->umonitor); + if (dev == NULL) + return; + + if ((action = udev_device_get_action(dev)) == NULL) + action = "change"; + + spa_log_debug(this->log, "action %s", action); + + start_inotify(this); + + if (spa_streq(action, "add") || + spa_streq(action, "change")) { + process_device(this, ACTION_ADD, dev); + } else if (spa_streq(action, "remove")) { + process_device(this, ACTION_REMOVE, dev); + } + udev_device_unref(dev); +} + +static int start_monitor(struct impl *this) +{ + int res; + + if (this->umonitor != NULL) + return 0; + + this->umonitor = udev_monitor_new_from_netlink(this->udev, "udev"); + if (this->umonitor == NULL) + return -ENOMEM; + + udev_monitor_filter_add_match_subsystem_devtype(this->umonitor, + "video4linux", NULL); + udev_monitor_enable_receiving(this->umonitor); + + this->source.func = impl_on_fd_events; + this->source.data = this; + this->source.fd = udev_monitor_get_fd(this->umonitor); + this->source.mask = SPA_IO_IN | SPA_IO_ERR; + + spa_log_debug(this->log, "monitor %p", this->umonitor); + spa_loop_add_source(this->main_loop, &this->source); + + if ((res = start_inotify(this)) < 0) + return res; + + return 0; +} + +static int stop_monitor(struct impl *this) +{ + if (this->umonitor == NULL) + return 0; + + clear_devices (this); + + spa_loop_remove_source(this->main_loop, &this->source); + udev_monitor_unref(this->umonitor); + this->umonitor = NULL; + + stop_inotify(this); + + return 0; +} + +static int enum_devices(struct impl *this) +{ + struct udev_enumerate *enumerate; + struct udev_list_entry *devices; + + enumerate = udev_enumerate_new(this->udev); + if (enumerate == NULL) + return -ENOMEM; + + udev_enumerate_add_match_subsystem(enumerate, "video4linux"); + udev_enumerate_scan_devices(enumerate); + + for (devices = udev_enumerate_get_list_entry(enumerate); devices; + devices = udev_list_entry_get_next(devices)) { + struct udev_device *dev; + + dev = udev_device_new_from_syspath(this->udev, udev_list_entry_get_name(devices)); + if (dev == NULL) + continue; + + process_device(this, ACTION_ADD, dev); + + udev_device_unref(dev); + } + udev_enumerate_unref(enumerate); + + return 0; +} + +static const struct spa_dict_item device_info_items[] = { + { SPA_KEY_DEVICE_API, "udev" }, + { SPA_KEY_DEVICE_NICK, "v4l2-udev" }, + { SPA_KEY_API_UDEV_MATCH, "video4linux" }, +}; + +static void emit_device_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(device_info_items); + spa_device_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void impl_hook_removed(struct spa_hook *hook) +{ + struct impl *this = hook->priv; + if (spa_hook_list_is_empty(&this->hooks)) { + stop_monitor(this); + impl_udev_close(this); + } +} + +static int +impl_device_add_listener(void *object, struct spa_hook *listener, + const struct spa_device_events *events, void *data) +{ + int res; + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + if ((res = impl_udev_open(this)) < 0) + return res; + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_device_info(this, true); + + if ((res = enum_devices(this)) < 0) + return res; + + if ((res = start_monitor(this)) < 0) + return res; + + spa_hook_list_join(&this->hooks, &save); + + listener->removed = impl_hook_removed; + listener->priv = this; + + return 0; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_device_add_listener, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + stop_monitor(this); + impl_udev_close(this); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + this->notify.fd = -1; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + + if (this->main_loop == NULL) { + spa_log_error(this->log, "a main-loop is needed"); + return -EINVAL; + } + spa_hook_list_init(&this->hooks); + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + + this->info = SPA_DEVICE_INFO_INIT(); + this->info_all = SPA_DEVICE_CHANGE_MASK_FLAGS | + SPA_DEVICE_CHANGE_MASK_PROPS; + this->info.flags = 0; + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + return 1; +} + +const struct spa_handle_factory spa_v4l2_udev_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_V4L2_ENUM_UDEV, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/v4l2/v4l2-utils.c b/spa/plugins/v4l2/v4l2-utils.c new file mode 100644 index 0000000..8ffda13 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-utils.c @@ -0,0 +1,1743 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <sched.h> +#include <errno.h> +#include <sys/ioctl.h> +#include <sys/mman.h> +#include <poll.h> + +#include <spa/utils/result.h> + +static int xioctl(int fd, int request, void *arg) +{ + int err; + + do { + err = ioctl(fd, request, arg); + } while (err == -1 && errno == EINTR); + + return err; +} + +int spa_v4l2_open(struct spa_v4l2_device *dev, const char *path) +{ + struct stat st; + int err; + + if (dev->fd != -1) + return 0; + + if (path == NULL) { + spa_log_error(dev->log, "Device property not set"); + return -EIO; + } + + spa_log_info(dev->log, "device is '%s'", path); + + dev->fd = open(path, O_RDWR | O_NONBLOCK, 0); + if (dev->fd == -1) { + err = errno; + spa_log_error(dev->log, "Cannot open '%s': %d, %s", + path, err, strerror(err)); + goto error; + } + + if (fstat(dev->fd, &st) < 0) { + err = errno; + spa_log_error(dev->log, "Cannot identify '%s': %d, %s", + path, err, strerror(err)); + goto error_close; + } + + if (!S_ISCHR(st.st_mode)) { + spa_log_error(dev->log, "%s is no device", path); + err = ENODEV; + goto error_close; + } + + if (xioctl(dev->fd, VIDIOC_QUERYCAP, &dev->cap) < 0) { + err = errno; + spa_log_error(dev->log, "'%s' QUERYCAP: %m", path); + goto error_close; + } + snprintf(dev->path, sizeof(dev->path), "%s", path); + return 0; + +error_close: + close(dev->fd); + dev->fd = -1; +error: + return -err; +} + +int spa_v4l2_is_capture(struct spa_v4l2_device *dev) +{ + uint32_t caps = dev->cap.capabilities; + if ((caps & V4L2_CAP_DEVICE_CAPS)) + caps = dev->cap.device_caps; + return (caps & V4L2_CAP_VIDEO_CAPTURE) == V4L2_CAP_VIDEO_CAPTURE; +} + +int spa_v4l2_close(struct spa_v4l2_device *dev) +{ + if (dev->fd == -1) + return 0; + + if (dev->active || dev->have_format) + return 0; + + spa_log_info(dev->log, "close '%s'", dev->path); + + if (close(dev->fd)) + spa_log_warn(dev->log, "close: %m"); + + dev->fd = -1; + return 0; +} + +static int spa_v4l2_buffer_recycle(struct impl *this, uint32_t buffer_id) +{ + struct port *port = &this->out_ports[0]; + struct buffer *b = &port->buffers[buffer_id]; + struct spa_v4l2_device *dev = &port->dev; + int err; + + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) + return 0; + + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUTSTANDING); + spa_log_trace(this->log, "v4l2 %p: recycle buffer %d", this, buffer_id); + + if (xioctl(dev->fd, VIDIOC_QBUF, &b->v4l2_buffer) < 0) { + err = errno; + spa_log_error(this->log, "'%s' VIDIOC_QBUF: %m", this->props.device); + return -err; + } + + return 0; +} + +static int spa_v4l2_clear_buffers(struct impl *this) +{ + struct port *port = &this->out_ports[0]; + struct v4l2_requestbuffers reqbuf; + uint32_t i; + + if (port->n_buffers == 0) + return 0; + + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b; + struct spa_data *d; + + b = &port->buffers[i]; + d = b->outbuf->datas; + + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) { + spa_log_debug(this->log, "queueing outstanding buffer %p", b); + spa_v4l2_buffer_recycle(this, i); + } + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_MAPPED)) { + munmap(b->ptr, d[0].maxsize); + } + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_ALLOCATED)) { + spa_log_debug(this->log, "close %d", (int) d[0].fd); + close(d[0].fd); + } + d[0].type = SPA_ID_INVALID; + } + + spa_zero(reqbuf); + reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + reqbuf.memory = port->memtype; + reqbuf.count = 0; + + if (xioctl(port->dev.fd, VIDIOC_REQBUFS, &reqbuf) < 0) { + spa_log_warn(this->log, "VIDIOC_REQBUFS: %m"); + } + port->n_buffers = 0; + + return 0; +} + + +struct format_info { + uint32_t fourcc; + uint32_t format; + uint32_t media_type; + uint32_t media_subtype; +}; + +#define VIDEO SPA_MEDIA_TYPE_video +#define IMAGE SPA_MEDIA_TYPE_image + +#define RAW SPA_MEDIA_SUBTYPE_raw + +#define BAYER SPA_MEDIA_SUBTYPE_bayer +#define MJPG SPA_MEDIA_SUBTYPE_mjpg +#define JPEG SPA_MEDIA_SUBTYPE_jpeg +#define DV SPA_MEDIA_SUBTYPE_dv +#define MPEGTS SPA_MEDIA_SUBTYPE_mpegts +#define H264 SPA_MEDIA_SUBTYPE_h264 +#define H263 SPA_MEDIA_SUBTYPE_h263 +#define MPEG1 SPA_MEDIA_SUBTYPE_mpeg1 +#define MPEG2 SPA_MEDIA_SUBTYPE_mpeg2 +#define MPEG4 SPA_MEDIA_SUBTYPE_mpeg4 +#define XVID SPA_MEDIA_SUBTYPE_xvid +#define VC1 SPA_MEDIA_SUBTYPE_vc1 +#define VP8 SPA_MEDIA_SUBTYPE_vp8 + +#define FORMAT_UNKNOWN SPA_VIDEO_FORMAT_UNKNOWN +#define FORMAT_ENCODED SPA_VIDEO_FORMAT_ENCODED +#define FORMAT_RGB15 SPA_VIDEO_FORMAT_RGB15 +#define FORMAT_BGR15 SPA_VIDEO_FORMAT_BGR15 +#define FORMAT_RGB16 SPA_VIDEO_FORMAT_RGB16 +#define FORMAT_BGR SPA_VIDEO_FORMAT_BGR +#define FORMAT_RGB SPA_VIDEO_FORMAT_RGB +#define FORMAT_BGRA SPA_VIDEO_FORMAT_BGRA +#define FORMAT_BGRx SPA_VIDEO_FORMAT_BGRx +#define FORMAT_ARGB SPA_VIDEO_FORMAT_ARGB +#define FORMAT_xRGB SPA_VIDEO_FORMAT_xRGB +#define FORMAT_GRAY8 SPA_VIDEO_FORMAT_GRAY8 +#define FORMAT_GRAY16_LE SPA_VIDEO_FORMAT_GRAY16_LE +#define FORMAT_GRAY16_BE SPA_VIDEO_FORMAT_GRAY16_BE +#define FORMAT_YVU9 SPA_VIDEO_FORMAT_YVU9 +#define FORMAT_YV12 SPA_VIDEO_FORMAT_YV12 +#define FORMAT_YUY2 SPA_VIDEO_FORMAT_YUY2 +#define FORMAT_YVYU SPA_VIDEO_FORMAT_YVYU +#define FORMAT_UYVY SPA_VIDEO_FORMAT_UYVY +#define FORMAT_Y42B SPA_VIDEO_FORMAT_Y42B +#define FORMAT_Y41B SPA_VIDEO_FORMAT_Y41B +#define FORMAT_YUV9 SPA_VIDEO_FORMAT_YUV9 +#define FORMAT_I420 SPA_VIDEO_FORMAT_I420 +#define FORMAT_NV12 SPA_VIDEO_FORMAT_NV12 +#define FORMAT_NV12_64Z32 SPA_VIDEO_FORMAT_NV12_64Z32 +#define FORMAT_NV21 SPA_VIDEO_FORMAT_NV21 +#define FORMAT_NV16 SPA_VIDEO_FORMAT_NV16 +#define FORMAT_NV61 SPA_VIDEO_FORMAT_NV61 +#define FORMAT_NV24 SPA_VIDEO_FORMAT_NV24 + +static const struct format_info format_info[] = { + /* RGB formats */ + {V4L2_PIX_FMT_RGB332, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_ARGB555, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_XRGB555, FORMAT_RGB15, VIDEO, RAW}, + {V4L2_PIX_FMT_ARGB555X, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_XRGB555X, FORMAT_BGR15, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB565, FORMAT_RGB16, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB565X, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_BGR666, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_BGR24, FORMAT_BGR, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB24, FORMAT_RGB, VIDEO, RAW}, + {V4L2_PIX_FMT_ABGR32, FORMAT_BGRA, VIDEO, RAW}, + {V4L2_PIX_FMT_XBGR32, FORMAT_BGRx, VIDEO, RAW}, + {V4L2_PIX_FMT_ARGB32, FORMAT_ARGB, VIDEO, RAW}, + {V4L2_PIX_FMT_XRGB32, FORMAT_xRGB, VIDEO, RAW}, + + /* Deprecated Packed RGB Image Formats (alpha ambiguity) */ + {V4L2_PIX_FMT_RGB444, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB555, FORMAT_RGB15, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB555X, FORMAT_BGR15, VIDEO, RAW}, + {V4L2_PIX_FMT_BGR32, FORMAT_BGRx, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB32, FORMAT_xRGB, VIDEO, RAW}, + + /* Grey formats */ + {V4L2_PIX_FMT_GREY, FORMAT_GRAY8, VIDEO, RAW}, + {V4L2_PIX_FMT_Y4, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_Y6, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_Y10, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_Y12, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_Y16, FORMAT_GRAY16_LE, VIDEO, RAW}, + {V4L2_PIX_FMT_Y16_BE, FORMAT_GRAY16_BE, VIDEO, RAW}, + {V4L2_PIX_FMT_Y10BPACK, FORMAT_UNKNOWN, VIDEO, RAW}, + + /* Palette formats */ + {V4L2_PIX_FMT_PAL8, FORMAT_UNKNOWN, VIDEO, RAW}, + + /* Chrominance formats */ + {V4L2_PIX_FMT_UV8, FORMAT_UNKNOWN, VIDEO, RAW}, + + /* Luminance+Chrominance formats */ + {V4L2_PIX_FMT_YVU410, FORMAT_YVU9, VIDEO, RAW}, + {V4L2_PIX_FMT_YVU420, FORMAT_YV12, VIDEO, RAW}, + {V4L2_PIX_FMT_YVU420M, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUYV, FORMAT_YUY2, VIDEO, RAW}, + {V4L2_PIX_FMT_YYUV, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YVYU, FORMAT_YVYU, VIDEO, RAW}, + {V4L2_PIX_FMT_UYVY, FORMAT_UYVY, VIDEO, RAW}, + {V4L2_PIX_FMT_VYUY, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV422P, FORMAT_Y42B, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV411P, FORMAT_Y41B, VIDEO, RAW}, + {V4L2_PIX_FMT_Y41P, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV444, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV555, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV565, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV32, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV410, FORMAT_YUV9, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV420, FORMAT_I420, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV420M, FORMAT_I420, VIDEO, RAW}, + {V4L2_PIX_FMT_HI240, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_HM12, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_M420, FORMAT_UNKNOWN, VIDEO, RAW}, + + /* two planes -- one Y, one Cr + Cb interleaved */ + {V4L2_PIX_FMT_NV12, FORMAT_NV12, VIDEO, RAW}, + {V4L2_PIX_FMT_NV12M, FORMAT_NV12, VIDEO, RAW}, + {V4L2_PIX_FMT_NV12MT, FORMAT_NV12_64Z32, VIDEO, RAW}, + {V4L2_PIX_FMT_NV12MT_16X16, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_NV21, FORMAT_NV21, VIDEO, RAW}, + {V4L2_PIX_FMT_NV21M, FORMAT_NV21, VIDEO, RAW}, + {V4L2_PIX_FMT_NV16, FORMAT_NV16, VIDEO, RAW}, + {V4L2_PIX_FMT_NV16M, FORMAT_NV16, VIDEO, RAW}, + {V4L2_PIX_FMT_NV61, FORMAT_NV61, VIDEO, RAW}, + {V4L2_PIX_FMT_NV61M, FORMAT_NV61, VIDEO, RAW}, + {V4L2_PIX_FMT_NV24, FORMAT_NV24, VIDEO, RAW}, + {V4L2_PIX_FMT_NV42, FORMAT_UNKNOWN, VIDEO, RAW}, + + /* Bayer formats - see http://www.siliconimaging.com/RGB%20Bayer.htm */ + {V4L2_PIX_FMT_SBGGR8, FORMAT_UNKNOWN, VIDEO, BAYER}, + {V4L2_PIX_FMT_SGBRG8, FORMAT_UNKNOWN, VIDEO, BAYER}, + {V4L2_PIX_FMT_SGRBG8, FORMAT_UNKNOWN, VIDEO, BAYER}, + {V4L2_PIX_FMT_SRGGB8, FORMAT_UNKNOWN, VIDEO, BAYER}, + + /* compressed formats */ + {V4L2_PIX_FMT_MJPEG, FORMAT_ENCODED, VIDEO, MJPG}, + {V4L2_PIX_FMT_JPEG, FORMAT_ENCODED, VIDEO, MJPG}, + {V4L2_PIX_FMT_PJPG, FORMAT_ENCODED, VIDEO, MJPG}, + {V4L2_PIX_FMT_DV, FORMAT_ENCODED, VIDEO, DV}, + {V4L2_PIX_FMT_MPEG, FORMAT_ENCODED, VIDEO, MPEGTS}, + {V4L2_PIX_FMT_H264, FORMAT_ENCODED, VIDEO, H264}, + {V4L2_PIX_FMT_H264_NO_SC, FORMAT_ENCODED, VIDEO, H264}, + {V4L2_PIX_FMT_H264_MVC, FORMAT_ENCODED, VIDEO, H264}, + {V4L2_PIX_FMT_H263, FORMAT_ENCODED, VIDEO, H263}, + {V4L2_PIX_FMT_MPEG1, FORMAT_ENCODED, VIDEO, MPEG1}, + {V4L2_PIX_FMT_MPEG2, FORMAT_ENCODED, VIDEO, MPEG2}, + {V4L2_PIX_FMT_MPEG4, FORMAT_ENCODED, VIDEO, MPEG4}, + {V4L2_PIX_FMT_XVID, FORMAT_ENCODED, VIDEO, XVID}, + {V4L2_PIX_FMT_VC1_ANNEX_G, FORMAT_ENCODED, VIDEO, VC1}, + {V4L2_PIX_FMT_VC1_ANNEX_L, FORMAT_ENCODED, VIDEO, VC1}, + {V4L2_PIX_FMT_VP8, FORMAT_ENCODED, VIDEO, VP8}, + + /* Vendor-specific formats */ + {V4L2_PIX_FMT_WNVA, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_SN9C10X, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_PWC1, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_PWC2, FORMAT_UNKNOWN, VIDEO, RAW}, +}; + +static const struct format_info *fourcc_to_format_info(uint32_t fourcc) +{ + SPA_FOR_EACH_ELEMENT_VAR(format_info, i) { + if (i->fourcc == fourcc) + return i; + } + return NULL; +} + +#if 0 +static const struct format_info *video_format_to_format_info(uint32_t format) +{ + SPA_FOR_EACH_ELEMENT_VAR(format_info, i) { + if (i->format == format) + return i; + } + return NULL; +} +#endif + +static const struct format_info *find_format_info_by_media_type(uint32_t type, + uint32_t subtype, + uint32_t format, + int startidx) +{ + size_t i; + + for (i = startidx; i < SPA_N_ELEMENTS(format_info); i++) { + const struct format_info *fi = &format_info[i]; + if ((fi->media_type == type) && + (fi->media_subtype == subtype) && + (format == 0 || fi->format == format)) + return fi; + } + return NULL; +} + +static uint32_t +enum_filter_format(uint32_t media_type, int32_t media_subtype, + const struct spa_pod *filter, uint32_t index) +{ + uint32_t video_format = 0; + + switch (media_type) { + case SPA_MEDIA_TYPE_video: + case SPA_MEDIA_TYPE_image: + if (media_subtype == SPA_MEDIA_SUBTYPE_raw) { + const struct spa_pod_prop *p; + const struct spa_pod *val; + uint32_t n_values, choice; + const uint32_t *values; + + if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_format))) + return SPA_VIDEO_FORMAT_UNKNOWN; + + val = spa_pod_get_values(&p->value, &n_values, &choice); + + if (val->type != SPA_TYPE_Id) + return SPA_VIDEO_FORMAT_UNKNOWN; + + values = SPA_POD_BODY(val); + + if (choice == SPA_CHOICE_None) { + if (index == 0) + video_format = values[0]; + } else { + if (index + 1 < n_values) + video_format = values[index + 1]; + } + } else { + if (index == 0) + video_format = SPA_VIDEO_FORMAT_ENCODED; + } + } + return video_format; +} + +static bool +filter_framesize(struct v4l2_frmsizeenum *frmsize, + const struct spa_rectangle *min, + const struct spa_rectangle *max, + const struct spa_rectangle *step) +{ + if (frmsize->type == V4L2_FRMSIZE_TYPE_DISCRETE) { + if (frmsize->discrete.width < min->width || + frmsize->discrete.height < min->height || + frmsize->discrete.width > max->width || + frmsize->discrete.height > max->height) { + return false; + } + } else if (frmsize->type == V4L2_FRMSIZE_TYPE_CONTINUOUS || + frmsize->type == V4L2_FRMSIZE_TYPE_STEPWISE) { + /* FIXME, use LCM */ + frmsize->stepwise.step_width *= step->width; + frmsize->stepwise.step_height *= step->height; + + if (frmsize->stepwise.max_width < min->width || + frmsize->stepwise.max_height < min->height || + frmsize->stepwise.min_width > max->width || + frmsize->stepwise.min_height > max->height) + return false; + + frmsize->stepwise.min_width = SPA_MAX(frmsize->stepwise.min_width, min->width); + frmsize->stepwise.min_height = SPA_MAX(frmsize->stepwise.min_height, min->height); + frmsize->stepwise.max_width = SPA_MIN(frmsize->stepwise.max_width, max->width); + frmsize->stepwise.max_height = SPA_MIN(frmsize->stepwise.max_height, max->height); + } else + return false; + + return true; +} + +static int compare_fraction(struct v4l2_fract *f1, const struct spa_fraction *f2) +{ + uint64_t n1, n2; + + /* fractions are reduced when set, so we can quickly see if they're equal */ + if (f1->denominator == f2->num && f1->numerator == f2->denom) + return 0; + + /* extend to 64 bits */ + n1 = ((int64_t) f1->denominator) * f2->denom; + n2 = ((int64_t) f1->numerator) * f2->num; + if (n1 < n2) + return -1; + return 1; +} + +static bool +filter_framerate(struct v4l2_frmivalenum *frmival, + const struct spa_fraction *min, + const struct spa_fraction *max, + const struct spa_fraction *step) +{ + if (frmival->type == V4L2_FRMIVAL_TYPE_DISCRETE) { + if (compare_fraction(&frmival->discrete, min) < 0 || + compare_fraction(&frmival->discrete, max) > 0) + return false; + } else if (frmival->type == V4L2_FRMIVAL_TYPE_CONTINUOUS || + frmival->type == V4L2_FRMIVAL_TYPE_STEPWISE) { + /* FIXME, use LCM */ + frmival->stepwise.step.denominator *= step->num; + frmival->stepwise.step.numerator *= step->denom; + + if (compare_fraction(&frmival->stepwise.max, min) < 0 || + compare_fraction(&frmival->stepwise.min, max) > 0) + return false; + + if (compare_fraction(&frmival->stepwise.min, min) < 0) { + frmival->stepwise.min.denominator = min->num; + frmival->stepwise.min.numerator = min->denom; + } + if (compare_fraction(&frmival->stepwise.max, max) > 0) { + frmival->stepwise.max.denominator = max->num; + frmival->stepwise.max.numerator = max->denom; + } + } else + return false; + + return true; +} + +#define FOURCC_ARGS(f) (f)&0x7f,((f)>>8)&0x7f,((f)>>16)&0x7f,((f)>>24)&0x7f + +static int +spa_v4l2_enum_format(struct impl *this, int seq, + uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct port *port = &this->out_ports[0]; + int res, n_fractions; + const struct format_info *info; + struct spa_pod_choice *choice; + uint32_t filter_media_type, filter_media_subtype, video_format; + struct spa_v4l2_device *dev = &port->dev; + uint8_t buffer[1024]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[2]; + struct spa_result_node_params result; + uint32_t count = 0; + + if ((res = spa_v4l2_open(dev, this->props.device)) < 0) + return res; + + result.id = SPA_PARAM_EnumFormat; + result.next = start; + + if (result.next == 0) { + spa_zero(port->fmtdesc); + port->fmtdesc.index = 0; + port->fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + port->next_fmtdesc = true; + spa_zero(port->frmsize); + port->next_frmsize = true; + spa_zero(port->frmival); + } + + if (filter) { + if ((res = spa_format_parse(filter, &filter_media_type, &filter_media_subtype)) < 0) + return res; + } + + if (false) { + next_fmtdesc: + port->fmtdesc.index++; + port->next_fmtdesc = true; + } + + next: + result.index = result.next++; + + while (port->next_fmtdesc) { + if (filter) { + struct v4l2_format fmt; + + video_format = enum_filter_format(filter_media_type, + filter_media_subtype, + filter, port->fmtdesc.index); + + if (video_format == SPA_VIDEO_FORMAT_UNKNOWN) + goto enum_end; + + info = find_format_info_by_media_type(filter_media_type, + filter_media_subtype, + video_format, 0); + if (info == NULL) + goto next_fmtdesc; + + port->fmtdesc.pixelformat = info->fourcc; + + spa_zero(fmt); + fmt.type = port->fmtdesc.type; + fmt.fmt.pix.pixelformat = info->fourcc; + fmt.fmt.pix.field = V4L2_FIELD_ANY; + fmt.fmt.pix.width = 0; + fmt.fmt.pix.height = 0; + + if ((res = xioctl(dev->fd, VIDIOC_TRY_FMT, &fmt)) < 0) { + spa_log_debug(this->log, "'%s' VIDIOC_TRY_FMT %08x: %m", + this->props.device, info->fourcc); + goto next_fmtdesc; + } + if (fmt.fmt.pix.pixelformat != info->fourcc) { + spa_log_debug(this->log, "'%s' VIDIOC_TRY_FMT wanted %.4s gave %.4s", + this->props.device, (char*)&info->fourcc, + (char*)&fmt.fmt.pix.pixelformat); + goto next_fmtdesc; + } + + } else { + if ((res = xioctl(dev->fd, VIDIOC_ENUM_FMT, &port->fmtdesc)) < 0) { + if (errno == EINVAL) + goto enum_end; + + res = -errno; + spa_log_error(this->log, "'%s' VIDIOC_ENUM_FMT: %m", + this->props.device); + goto exit; + } + } + port->next_fmtdesc = false; + port->frmsize.index = 0; + port->frmsize.pixel_format = port->fmtdesc.pixelformat; + port->next_frmsize = true; + } + if (!(info = fourcc_to_format_info(port->fmtdesc.pixelformat))) + goto next_fmtdesc; + + next_frmsize: + while (port->next_frmsize) { + if (filter) { + const struct spa_pod_prop *p; + struct spa_pod *val; + uint32_t n_vals, choice; + + /* check if we have a fixed frame size */ + if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_size))) + goto do_frmsize; + + val = spa_pod_get_values(&p->value, &n_vals, &choice); + if (val->type != SPA_TYPE_Rectangle) + goto enum_end; + + if (choice == SPA_CHOICE_None) { + const struct spa_rectangle *values = SPA_POD_BODY(val); + + if (port->frmsize.index > 0) + goto next_fmtdesc; + + port->frmsize.type = V4L2_FRMSIZE_TYPE_DISCRETE; + port->frmsize.discrete.width = values[0].width; + port->frmsize.discrete.height = values[0].height; + goto have_size; + } + } + do_frmsize: + if ((res = xioctl(dev->fd, VIDIOC_ENUM_FRAMESIZES, &port->frmsize)) < 0) { + if (errno == EINVAL) + goto next_fmtdesc; + + res = -errno; + spa_log_error(this->log, "'%s' VIDIOC_ENUM_FRAMESIZES: %m", + this->props.device); + goto exit; + } + if (filter) { + static const struct spa_rectangle step = {1, 1}; + + const struct spa_rectangle *values; + const struct spa_pod_prop *p; + struct spa_pod *val; + uint32_t choice, i, n_values; + + /* check if we have a fixed frame size */ + if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_size))) + goto have_size; + + val = spa_pod_get_values(&p->value, &n_values, &choice); + if (val->type != SPA_TYPE_Rectangle) + goto have_size; + + values = SPA_POD_BODY_CONST(val); + + if (choice == SPA_CHOICE_Range && n_values > 2) { + if (filter_framesize(&port->frmsize, &values[1], &values[2], &step)) + goto have_size; + } else if (choice == SPA_CHOICE_Step && n_values > 3) { + if (filter_framesize(&port->frmsize, &values[1], &values[2], &values[3])) + goto have_size; + } else if (choice == SPA_CHOICE_Enum) { + for (i = 1; i < n_values; i++) { + if (filter_framesize(&port->frmsize, &values[i], &values[i], &step)) + goto have_size; + } + } + /* nothing matches the filter, get next frame size */ + port->frmsize.index++; + continue; + } + + have_size: + if (port->frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) { + /* we have a fixed size, use this to get the frame intervals */ + port->frmival.index = 0; + port->frmival.pixel_format = port->frmsize.pixel_format; + port->frmival.width = port->frmsize.discrete.width; + port->frmival.height = port->frmsize.discrete.height; + port->next_frmsize = false; + } else if (port->frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS || + port->frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE) { + /* we have a non fixed size, fix to something sensible to get the + * framerate */ + port->frmival.index = 0; + port->frmival.pixel_format = port->frmsize.pixel_format; + port->frmival.width = port->frmsize.stepwise.min_width; + port->frmival.height = port->frmsize.stepwise.min_height; + port->next_frmsize = false; + } else { + port->frmsize.index++; + } + } + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(&b, + SPA_FORMAT_mediaType, SPA_POD_Id(info->media_type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(info->media_subtype), + 0); + + if (info->media_subtype == SPA_MEDIA_SUBTYPE_raw) { + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_format, 0); + spa_pod_builder_id(&b, info->format); + } + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_size, 0); + if (port->frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) { + spa_pod_builder_rectangle(&b, + port->frmsize.discrete.width, + port->frmsize.discrete.height); + } else if (port->frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS || + port->frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE) { + spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(&b, &f[1]); + + spa_pod_builder_rectangle(&b, + port->frmsize.stepwise.min_width, + port->frmsize.stepwise.min_height); + spa_pod_builder_rectangle(&b, + port->frmsize.stepwise.min_width, + port->frmsize.stepwise.min_height); + spa_pod_builder_rectangle(&b, + port->frmsize.stepwise.max_width, + port->frmsize.stepwise.max_height); + + if (port->frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS) { + choice->body.type = SPA_CHOICE_Range; + } else { + choice->body.type = SPA_CHOICE_Step; + spa_pod_builder_rectangle(&b, + port->frmsize.stepwise.max_width, + port->frmsize.stepwise.max_height); + } + spa_pod_builder_pop(&b, &f[1]); + } + + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_framerate, 0); + + n_fractions = 0; + + spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(&b, &f[1]); + port->frmival.index = 0; + + while (true) { + if ((res = xioctl(dev->fd, VIDIOC_ENUM_FRAMEINTERVALS, &port->frmival)) < 0) { + res = -errno; + if (errno == EINVAL) { + port->frmsize.index++; + port->next_frmsize = true; + if (port->frmival.index == 0) + goto next_frmsize; + break; + } + spa_log_error(this->log, "'%s' VIDIOC_ENUM_FRAMEINTERVALS: %m", + this->props.device); + goto exit; + } + if (filter) { + static const struct spa_fraction step = {1, 1}; + + const struct spa_fraction *values; + const struct spa_pod_prop *p; + struct spa_pod *val; + uint32_t i, n_values, choice; + + if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_framerate))) + goto have_framerate; + + val = spa_pod_get_values(&p->value, &n_values, &choice); + + if (val->type != SPA_TYPE_Fraction) + goto enum_end; + + values = SPA_POD_BODY(val); + + switch (choice) { + case SPA_CHOICE_None: + if (filter_framerate(&port->frmival, &values[0], &values[0], &step)) + goto have_framerate; + break; + + case SPA_CHOICE_Range: + if (n_values > 2 && filter_framerate(&port->frmival, &values[1], &values[2], &step)) + goto have_framerate; + break; + + case SPA_CHOICE_Step: + if (n_values > 3 && filter_framerate(&port->frmival, &values[1], &values[2], &values[3])) + goto have_framerate; + break; + + case SPA_CHOICE_Enum: + for (i = 1; i < n_values; i++) { + if (filter_framerate(&port->frmival, &values[i], &values[i], &step)) + goto have_framerate; + } + break; + default: + break; + } + port->frmival.index++; + continue; + } + + have_framerate: + + if (port->frmival.type == V4L2_FRMIVAL_TYPE_DISCRETE) { + choice->body.type = SPA_CHOICE_Enum; + if (n_fractions == 0) + spa_pod_builder_fraction(&b, + port->frmival.discrete.denominator, + port->frmival.discrete.numerator); + spa_pod_builder_fraction(&b, + port->frmival.discrete.denominator, + port->frmival.discrete.numerator); + port->frmival.index++; + } else if (port->frmival.type == V4L2_FRMIVAL_TYPE_CONTINUOUS || + port->frmival.type == V4L2_FRMIVAL_TYPE_STEPWISE) { + if (n_fractions == 0) + spa_pod_builder_fraction(&b, 25, 1); + spa_pod_builder_fraction(&b, + port->frmival.stepwise.min.denominator, + port->frmival.stepwise.min.numerator); + spa_pod_builder_fraction(&b, + port->frmival.stepwise.max.denominator, + port->frmival.stepwise.max.numerator); + + if (port->frmival.type == V4L2_FRMIVAL_TYPE_CONTINUOUS) { + choice->body.type = SPA_CHOICE_Range; + } else { + choice->body.type = SPA_CHOICE_Step; + spa_pod_builder_fraction(&b, + port->frmival.stepwise.step.denominator, + port->frmival.stepwise.step.numerator); + } + + port->frmsize.index++; + port->next_frmsize = true; + break; + } + n_fractions++; + } + if (n_fractions <= 1) + choice->body.type = SPA_CHOICE_None; + + spa_pod_builder_pop(&b, &f[1]); + result.param = spa_pod_builder_pop(&b, &f[0]); + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + enum_end: + res = 0; + exit: + spa_v4l2_close(dev); + return res; +} + +static int spa_v4l2_set_format(struct impl *this, struct spa_video_info *format, uint32_t flags) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + int res, cmd; + struct v4l2_format reqfmt, fmt; + struct v4l2_streamparm streamparm; + const struct format_info *info = NULL; + uint32_t video_format; + struct spa_rectangle *size = NULL; + struct spa_fraction *framerate = NULL; + bool match; + + spa_zero(fmt); + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + spa_zero(streamparm); + streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + switch (format->media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + video_format = format->info.raw.format; + size = &format->info.raw.size; + framerate = &format->info.raw.framerate; + break; + case SPA_MEDIA_SUBTYPE_mjpg: + case SPA_MEDIA_SUBTYPE_jpeg: + video_format = SPA_VIDEO_FORMAT_ENCODED; + size = &format->info.mjpg.size; + framerate = &format->info.mjpg.framerate; + break; + case SPA_MEDIA_SUBTYPE_h264: + video_format = SPA_VIDEO_FORMAT_ENCODED; + size = &format->info.h264.size; + framerate = &format->info.h264.framerate; + break; + default: + video_format = SPA_VIDEO_FORMAT_ENCODED; + break; + } + + info = find_format_info_by_media_type(format->media_type, + format->media_subtype, video_format, 0); + if (info == NULL || size == NULL || framerate == NULL) { + spa_log_error(this->log, "unknown media type %d %d %d", format->media_type, + format->media_subtype, video_format); + return -EINVAL; + } + + fmt.fmt.pix.pixelformat = info->fourcc; + fmt.fmt.pix.field = V4L2_FIELD_ANY; + fmt.fmt.pix.width = size->width; + fmt.fmt.pix.height = size->height; + streamparm.parm.capture.timeperframe.numerator = framerate->denom; + streamparm.parm.capture.timeperframe.denominator = framerate->num; + + spa_log_debug(this->log, "set %.4s %dx%d %d/%d", (char *)&fmt.fmt.pix.pixelformat, + fmt.fmt.pix.width, fmt.fmt.pix.height, + streamparm.parm.capture.timeperframe.denominator, + streamparm.parm.capture.timeperframe.numerator); + + reqfmt = fmt; + + if ((res = spa_v4l2_open(dev, this->props.device)) < 0) + return res; + + cmd = (flags & SPA_NODE_PARAM_FLAG_TEST_ONLY) ? VIDIOC_TRY_FMT : VIDIOC_S_FMT; + if (xioctl(dev->fd, cmd, &fmt) < 0) { + res = -errno; + spa_log_error(this->log, "'%s' VIDIOC_S_FMT: %m", + this->props.device); + return res; + } + + /* some cheap USB cam's won't accept any change */ + if (xioctl(dev->fd, VIDIOC_S_PARM, &streamparm) < 0) + spa_log_warn(this->log, "VIDIOC_S_PARM: %m"); + + match = (reqfmt.fmt.pix.pixelformat == fmt.fmt.pix.pixelformat && + reqfmt.fmt.pix.width == fmt.fmt.pix.width && + reqfmt.fmt.pix.height == fmt.fmt.pix.height); + + if (!match && !SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST)) { + spa_log_error(this->log, "wanted %.4s %dx%d, got %.4s %dx%d", + (char *)&reqfmt.fmt.pix.pixelformat, + reqfmt.fmt.pix.width, reqfmt.fmt.pix.height, + (char *)&fmt.fmt.pix.pixelformat, + fmt.fmt.pix.width, fmt.fmt.pix.height); + return -EINVAL; + } + + if (flags & SPA_NODE_PARAM_FLAG_TEST_ONLY) + return match ? 0 : 1; + + spa_log_info(this->log, "'%s' got %.4s %dx%d %d/%d", + dev->path, (char *)&fmt.fmt.pix.pixelformat, + fmt.fmt.pix.width, fmt.fmt.pix.height, + streamparm.parm.capture.timeperframe.denominator, + streamparm.parm.capture.timeperframe.numerator); + + dev->have_format = true; + size->width = fmt.fmt.pix.width; + size->height = fmt.fmt.pix.height; + port->rate.denom = framerate->num = streamparm.parm.capture.timeperframe.denominator; + port->rate.num = framerate->denom = streamparm.parm.capture.timeperframe.numerator; + + port->fmt = fmt; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_RATE; + port->info.flags = (port->alloc_buffers ? SPA_PORT_FLAG_CAN_ALLOC_BUFFERS : 0) | + SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + port->info.rate = SPA_FRACTION(port->rate.num, port->rate.denom); + + return match ? 0 : 1; +} + +static int query_ext_ctrl_ioctl(struct port *port, struct v4l2_query_ext_ctrl *qctrl) +{ + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_queryctrl qc; + int res; + + if (port->have_query_ext_ctrl) { + res = xioctl(dev->fd, VIDIOC_QUERY_EXT_CTRL, qctrl); + if (errno != ENOTTY) + return res; + port->have_query_ext_ctrl = false; + } + spa_zero(qc); + qc.id = qctrl->id; + res = xioctl(dev->fd, VIDIOC_QUERYCTRL, &qc); + if (res == 0) { + qctrl->type = qc.type; + memcpy(qctrl->name, qc.name, sizeof(qctrl->name)); + qctrl->minimum = qc.minimum; + if (qc.type == V4L2_CTRL_TYPE_BITMASK) { + qctrl->maximum = (__u32)qc.maximum; + qctrl->default_value = (__u32)qc.default_value; + } else { + qctrl->maximum = qc.maximum; + qctrl->default_value = qc.default_value; + } + qctrl->step = qc.step; + qctrl->flags = qc.flags; + qctrl->elems = 1; + qctrl->nr_of_dims = 0; + memset(qctrl->dims, 0, sizeof(qctrl->dims)); + switch (qctrl->type) { + case V4L2_CTRL_TYPE_INTEGER64: + qctrl->elem_size = sizeof(__s64); + break; + case V4L2_CTRL_TYPE_STRING: + qctrl->elem_size = qc.maximum + 1; + break; + default: + qctrl->elem_size = sizeof(__s32); + break; + } + memset(qctrl->reserved, 0, sizeof(qctrl->reserved)); + } + qctrl->id = qc.id; + return res; +} + +static struct { + uint32_t v4l2_id; + uint32_t spa_id; +} control_map[] = { + { V4L2_CID_BRIGHTNESS, SPA_PROP_brightness }, + { V4L2_CID_CONTRAST, SPA_PROP_contrast }, + { V4L2_CID_SATURATION, SPA_PROP_saturation }, + { V4L2_CID_HUE, SPA_PROP_hue }, + { V4L2_CID_GAMMA, SPA_PROP_gamma }, + { V4L2_CID_EXPOSURE, SPA_PROP_exposure }, + { V4L2_CID_GAIN, SPA_PROP_gain }, + { V4L2_CID_SHARPNESS, SPA_PROP_sharpness }, +}; + +static uint32_t control_to_prop_id(struct impl *impl, uint32_t control_id) +{ + SPA_FOR_EACH_ELEMENT_VAR(control_map, c) { + if (c->v4l2_id == control_id) + return c->spa_id; + } + return SPA_PROP_START_CUSTOM + control_id; +} + +static uint32_t prop_id_to_control(struct impl *impl, uint32_t prop_id) +{ + SPA_FOR_EACH_ELEMENT_VAR(control_map, c) { + if (c->spa_id == prop_id) + return c->v4l2_id; + } + if (prop_id >= SPA_PROP_START_CUSTOM) + return prop_id - SPA_PROP_START_CUSTOM; + return SPA_ID_INVALID; +} + +static int +spa_v4l2_enum_controls(struct impl *this, int seq, + uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_query_ext_ctrl queryctrl; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint32_t prop_id, ctrl_id; + uint8_t buffer[1024]; + int res; + const unsigned next_fl = V4L2_CTRL_FLAG_NEXT_CTRL | V4L2_CTRL_FLAG_NEXT_COMPOUND; + struct spa_pod_frame f[2]; + struct spa_result_node_params result; + uint32_t count = 0; + + if ((res = spa_v4l2_open(dev, this->props.device)) < 0) + return res; + + result.id = SPA_PARAM_PropInfo; + result.next = start; + next: + result.index = result.next; + + spa_zero(queryctrl); + + if (result.next == 0) { + result.next |= next_fl; + port->n_controls = 0; + } + + queryctrl.id = result.next; + spa_log_debug(this->log, "test control %08x", queryctrl.id); + + if (query_ext_ctrl_ioctl(port, &queryctrl) != 0) { + if (errno == ENOTTY) + goto enum_end; + if (errno == EINVAL) { + if (queryctrl.id != next_fl) + goto enum_end; + + if (result.next & next_fl) + result.next = V4L2_CID_USER_BASE; + else if (result.next >= V4L2_CID_USER_BASE && result.next < V4L2_CID_LASTP1) + result.next++; + else if (result.next >= V4L2_CID_LASTP1) + result.next = V4L2_CID_PRIVATE_BASE; + else + goto enum_end; + goto next; + } + res = -errno; + spa_log_error(this->log, "'%s' VIDIOC_QUERYCTRL: %m", this->props.device); + spa_v4l2_close(dev); + return res; + } + if (result.next & next_fl) + result.next = queryctrl.id | next_fl; + else + result.next++; + + if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) + goto next; + + if (port->n_controls >= MAX_CONTROLS) + goto enum_end; + + ctrl_id = queryctrl.id & ~next_fl; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + prop_id = control_to_prop_id(this, ctrl_id); + + port->controls[port->n_controls].id = prop_id; + port->controls[port->n_controls].ctrl_id = ctrl_id; + port->controls[port->n_controls].value = queryctrl.default_value; + + spa_log_debug(this->log, "Control '%s' %d %d", queryctrl.name, prop_id, ctrl_id); + + port->n_controls++; + + switch (queryctrl.type) { + case V4L2_CTRL_TYPE_INTEGER: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_id, SPA_POD_Id(prop_id), + SPA_PROP_INFO_type, SPA_POD_CHOICE_STEP_Int( + (int32_t)queryctrl.default_value, + (int32_t)queryctrl.minimum, + (int32_t)queryctrl.maximum, + (int32_t)queryctrl.step), + SPA_PROP_INFO_description, SPA_POD_String(queryctrl.name)); + break; + case V4L2_CTRL_TYPE_BOOLEAN: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_id, SPA_POD_Id(prop_id), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool((bool)queryctrl.default_value), + SPA_PROP_INFO_description, SPA_POD_String(queryctrl.name)); + break; + case V4L2_CTRL_TYPE_MENU: + { + struct v4l2_querymenu querymenu; + struct spa_pod_builder_state state; + + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo); + spa_pod_builder_add(&b, + SPA_PROP_INFO_id, SPA_POD_Id(prop_id), + SPA_PROP_INFO_type, SPA_POD_CHOICE_ENUM_Int(1, (int32_t)queryctrl.default_value), + SPA_PROP_INFO_description, SPA_POD_String(queryctrl.name), + 0); + + spa_zero(querymenu); + querymenu.id = queryctrl.id; + + spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0); + + spa_pod_builder_get_state(&b, &state); + spa_pod_builder_push_struct(&b, &f[1]); + for (querymenu.index = queryctrl.minimum; + querymenu.index <= queryctrl.maximum; + querymenu.index++) { + if (xioctl(dev->fd, VIDIOC_QUERYMENU, &querymenu) == 0) { + spa_pod_builder_int(&b, querymenu.index); + spa_pod_builder_string(&b, (const char *)querymenu.name); + } + } + if (spa_pod_builder_pop(&b, &f[1]) == NULL) { + spa_log_warn(this->log, "can't create Control '%s' overflow %d", + queryctrl.name, b.state.offset); + spa_pod_builder_reset(&b, &state); + spa_pod_builder_none(&b); + } + param = spa_pod_builder_pop(&b, &f[0]); + break; + } + case V4L2_CTRL_TYPE_INTEGER_MENU: + case V4L2_CTRL_TYPE_BITMASK: + case V4L2_CTRL_TYPE_BUTTON: + case V4L2_CTRL_TYPE_INTEGER64: + case V4L2_CTRL_TYPE_STRING: + default: + goto next; + + } + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + enum_end: + res = 0; + spa_v4l2_close(dev); + return res; +} + +static int +spa_v4l2_set_control(struct impl *this, uint32_t id, + const struct spa_pod_prop *prop) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_control control; + int res; + + spa_zero(control); + control.id = prop_id_to_control(this, prop->key); + if (control.id == SPA_ID_INVALID) + return -ENOENT; + + if ((res = spa_v4l2_open(dev, this->props.device)) < 0) + return res; + + switch (SPA_POD_TYPE(&prop->value)) { + case SPA_TYPE_Bool: + { + bool val; + if ((res = spa_pod_get_bool(&prop->value, &val)) < 0) + goto done; + control.value = val; + break; + } + case SPA_TYPE_Int: + { + int32_t val; + if ((res = spa_pod_get_int(&prop->value, &val)) < 0) + goto done; + control.value = val; + break; + } + default: + res = -EINVAL; + goto done; + } + if (xioctl(dev->fd, VIDIOC_S_CTRL, &control) < 0) { + res = -errno; + goto done; + } + + res = 0; + +done: + spa_v4l2_close(dev); + return res; +} + +static int mmap_read(struct impl *this) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_buffer buf; + struct buffer *b; + struct spa_data *d; + int64_t pts; + + spa_zero(buf); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = port->memtype; + + if (xioctl(dev->fd, VIDIOC_DQBUF, &buf) < 0) + return -errno; + + pts = SPA_TIMEVAL_TO_NSEC(&buf.timestamp); + spa_log_trace(this->log, "v4l2 %p: have output %d", this, buf.index); + + if (this->clock) { + this->clock->nsec = pts; + this->clock->rate = port->rate; + this->clock->position = buf.sequence; + this->clock->duration = 1; + this->clock->delay = 0; + this->clock->rate_diff = 1.0; + this->clock->next_nsec = pts + 1000000000LL / port->rate.denom; + } + + b = &port->buffers[buf.index]; + if (b->h) { + b->h->flags = 0; + if (buf.flags & V4L2_BUF_FLAG_ERROR) + b->h->flags |= SPA_META_HEADER_FLAG_CORRUPTED; + b->h->offset = 0; + b->h->seq = buf.sequence; + b->h->pts = pts; + b->h->dts_offset = 0; + } + + d = b->outbuf->datas; + d[0].chunk->offset = 0; + d[0].chunk->size = buf.bytesused; + d[0].chunk->stride = port->fmt.fmt.pix.bytesperline; + d[0].chunk->flags = 0; + if (buf.flags & V4L2_BUF_FLAG_ERROR) + d[0].chunk->flags |= SPA_CHUNK_FLAG_CORRUPTED; + + spa_list_append(&port->queue, &b->link); + return 0; +} + +static void v4l2_on_fd_events(struct spa_source *source) +{ + struct impl *this = source->data; + struct spa_io_buffers *io; + struct port *port = &this->out_ports[0]; + struct buffer *b; + + if (source->rmask & SPA_IO_ERR) { + struct port *port = &this->out_ports[0]; + spa_log_error(this->log, "'%p' error %08x", this->props.device, source->rmask); + if (port->source.loop) + spa_loop_remove_source(this->data_loop, &port->source); + return; + } + + if (!(source->rmask & SPA_IO_IN)) { + spa_log_warn(this->log, "v4l2 %p: spurious wakeup %d", this, source->rmask); + return; + } + + if (mmap_read(this) < 0) + return; + + if (spa_list_is_empty(&port->queue)) + return; + + io = port->io; + if (io == NULL) { + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + spa_v4l2_buffer_recycle(this, b->id); + } + else if (io->status != SPA_STATUS_HAVE_DATA) { + if (io->buffer_id < port->n_buffers) + spa_v4l2_buffer_recycle(this, io->buffer_id); + + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + spa_log_trace(this->log, "v4l2 %p: now queued %d", this, b->id); + } + spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA); +} + +static int spa_v4l2_use_buffers(struct impl *this, struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_requestbuffers reqbuf; + unsigned int i; + struct spa_data *d; + + if (n_buffers > 0) { + d = buffers[0]->datas; + + if (d[0].type == SPA_DATA_MemFd || + (d[0].type == SPA_DATA_MemPtr && d[0].data != NULL)) { + port->memtype = V4L2_MEMORY_USERPTR; + } else if (d[0].type == SPA_DATA_DmaBuf) { + port->memtype = V4L2_MEMORY_DMABUF; + } else { + spa_log_error(this->log, "can't use buffers of type %d", d[0].type); + return -EINVAL; + } + } + + spa_zero(reqbuf); + reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + reqbuf.memory = port->memtype; + reqbuf.count = n_buffers; + + if (xioctl(dev->fd, VIDIOC_REQBUFS, &reqbuf) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_REQBUFS %m", this->props.device); + return -errno; + } + spa_log_debug(this->log, "got %d buffers", reqbuf.count); + if (reqbuf.count < n_buffers) { + spa_log_error(this->log, "'%s' can't allocate enough buffers %d < %d", + this->props.device, reqbuf.count, n_buffers); + return -ENOMEM; + } + + for (i = 0; i < reqbuf.count; i++) { + struct buffer *b; + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->flags = BUFFER_FLAG_OUTSTANDING; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + spa_log_debug(this->log, "import buffer %p", buffers[i]); + + if (buffers[i]->n_datas < 1) { + spa_log_error(this->log, "invalid memory on buffer %p", buffers[i]); + return -EINVAL; + } + d = buffers[i]->datas; + + spa_zero(b->v4l2_buffer); + b->v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + b->v4l2_buffer.memory = port->memtype; + b->v4l2_buffer.index = i; + + if (port->memtype == V4L2_MEMORY_USERPTR) { + if (d[0].data == NULL) { + void *data; + + data = mmap(NULL, + d[0].maxsize, + PROT_READ | PROT_WRITE, MAP_SHARED, + d[0].fd, + d[0].mapoffset); + if (data == MAP_FAILED) + return -errno; + + b->ptr = data; + SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED); + } + else + b->ptr = d[0].data; + + b->v4l2_buffer.m.userptr = (unsigned long) b->ptr; + b->v4l2_buffer.length = d[0].maxsize; + } + else if (port->memtype == V4L2_MEMORY_DMABUF) { + b->v4l2_buffer.m.fd = d[0].fd; + } + else { + spa_log_error(this->log, "invalid port memory %d", + port->memtype); + return -EIO; + } + + spa_v4l2_buffer_recycle(this, i); + } + port->n_buffers = reqbuf.count; + + return 0; +} + +static int +mmap_init(struct impl *this, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_requestbuffers reqbuf; + unsigned int i; + bool use_expbuf = false; + + port->memtype = V4L2_MEMORY_MMAP; + + spa_zero(reqbuf); + reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + reqbuf.memory = port->memtype; + reqbuf.count = n_buffers; + + if (xioctl(dev->fd, VIDIOC_REQBUFS, &reqbuf) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_REQBUFS: %m", this->props.device); + return -errno; + } + + spa_log_debug(this->log, "got %d buffers", reqbuf.count); + + if (reqbuf.count < n_buffers) { + spa_log_error(this->log, "'%s' can't allocate enough buffers (%d < %d)", + this->props.device, reqbuf.count, n_buffers); + return -ENOMEM; + } + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d; + + if (buffers[i]->n_datas < 1) { + spa_log_error(this->log, "invalid buffer data"); + return -EINVAL; + } + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->flags = BUFFER_FLAG_OUTSTANDING; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + spa_zero(b->v4l2_buffer); + b->v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + b->v4l2_buffer.memory = port->memtype; + b->v4l2_buffer.index = i; + + if (xioctl(dev->fd, VIDIOC_QUERYBUF, &b->v4l2_buffer) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_QUERYBUF: %m", this->props.device); + return -errno; + } + + if (b->v4l2_buffer.flags & V4L2_BUF_FLAG_QUEUED) { + /* some drivers can give us an already queued buffer. */ + spa_log_warn(this->log, "buffer %d was already queued", i); + n_buffers = i; + break; + } + + d = buffers[i]->datas; + d[0].mapoffset = 0; + d[0].maxsize = b->v4l2_buffer.length; + d[0].chunk->offset = 0; + d[0].chunk->size = 0; + d[0].chunk->stride = port->fmt.fmt.pix.bytesperline; + d[0].chunk->flags = 0; + + spa_log_debug(this->log, "data types %08x", d[0].type); + + if (port->have_expbuf && + d[0].type != SPA_ID_INVALID && + (d[0].type & ((1u << SPA_DATA_DmaBuf)|(1u<<SPA_DATA_MemFd)))) { + struct v4l2_exportbuffer expbuf; + + spa_zero(expbuf); + expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + expbuf.index = i; + expbuf.flags = O_CLOEXEC | O_RDONLY; + if (xioctl(dev->fd, VIDIOC_EXPBUF, &expbuf) < 0) { + if (errno == ENOTTY || errno == EINVAL) { + spa_log_debug(this->log, "'%s' VIDIOC_EXPBUF not supported: %m", + this->props.device); + port->have_expbuf = false; + goto fallback; + } + spa_log_error(this->log, "'%s' VIDIOC_EXPBUF: %m", this->props.device); + return -errno; + } + if (d[0].type & (1u<<SPA_DATA_DmaBuf)) + d[0].type = SPA_DATA_DmaBuf; + else + d[0].type = SPA_DATA_MemFd; + d[0].flags = SPA_DATA_FLAG_READABLE; + d[0].fd = expbuf.fd; + d[0].data = NULL; + SPA_FLAG_SET(b->flags, BUFFER_FLAG_ALLOCATED); + spa_log_debug(this->log, "EXPBUF fd:%d", expbuf.fd); + use_expbuf = true; + } else if (d[0].type & (1u << SPA_DATA_MemPtr)) { +fallback: + d[0].type = SPA_DATA_MemPtr; + d[0].flags = SPA_DATA_FLAG_READABLE; + d[0].fd = -1; + d[0].mapoffset = b->v4l2_buffer.m.offset; + d[0].data = mmap(NULL, + b->v4l2_buffer.length, + PROT_READ, MAP_SHARED, + dev->fd, + b->v4l2_buffer.m.offset); + if (d[0].data == MAP_FAILED) { + spa_log_error(this->log, "'%s' mmap: %m", this->props.device); + return -errno; + } + b->ptr = d[0].data; + SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED); + spa_log_debug(this->log, "mmap offset:%u data:%p", d[0].mapoffset, b->ptr); + use_expbuf = false; + } else { + spa_log_error(this->log, "unsupported data type:%08x", d[0].type); + return -ENOTSUP; + } + spa_v4l2_buffer_recycle(this, i); + } + spa_log_info(this->log, "%s: have %u buffers using %s", dev->path, n_buffers, + use_expbuf ? "EXPBUF" : "MMAP"); + + port->n_buffers = n_buffers; + + return 0; +} + +static int userptr_init(struct impl *this) +{ + return -ENOTSUP; +} + +static int read_init(struct impl *this) +{ + return -ENOTSUP; +} + +static int +spa_v4l2_alloc_buffers(struct impl *this, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + int res; + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + + if (port->n_buffers > 0) + return -EIO; + + if (dev->cap.capabilities & V4L2_CAP_STREAMING) { + if ((res = mmap_init(this, buffers, n_buffers)) < 0) + if ((res = userptr_init(this)) < 0) + return res; + } else if (dev->cap.capabilities & V4L2_CAP_READWRITE) { + if ((res = read_init(this)) < 0) + return res; + } else { + spa_log_error(this->log, "invalid capabilities %08x", + dev->cap.capabilities); + return -EIO; + } + + return 0; +} + +static int spa_v4l2_stream_on(struct impl *this) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + enum v4l2_buf_type type; + + if (dev->fd == -1) + return -EIO; + + if (!dev->have_format) + return -EIO; + + if (dev->active) + return 0; + + spa_log_debug(this->log, "starting"); + + type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (xioctl(dev->fd, VIDIOC_STREAMON, &type) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_STREAMON: %m", this->props.device); + return -errno; + } + + port->source.func = v4l2_on_fd_events; + port->source.data = this; + port->source.fd = dev->fd; + port->source.mask = SPA_IO_IN | SPA_IO_ERR; + port->source.rmask = 0; + spa_loop_add_source(this->data_loop, &port->source); + + dev->active = true; + + return 0; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct port *port = user_data; + if (port->source.loop) + spa_loop_remove_source(loop, &port->source); + return 0; +} + +static int spa_v4l2_stream_off(struct impl *this) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + enum v4l2_buf_type type; + uint32_t i; + + if (!dev->active) + return 0; + + if (dev->fd == -1) + return -EIO; + + spa_log_debug(this->log, "stopping"); + + spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, port); + + type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (xioctl(dev->fd, VIDIOC_STREAMOFF, &type) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_STREAMOFF: %m", this->props.device); + return -errno; + } + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b; + + b = &port->buffers[i]; + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) { + if (xioctl(dev->fd, VIDIOC_QBUF, &b->v4l2_buffer) < 0) + spa_log_warn(this->log, "VIDIOC_QBUF: %s", strerror(errno)); + } + } + spa_list_init(&port->queue); + dev->active = false; + + return 0; +} diff --git a/spa/plugins/v4l2/v4l2.c b/spa/plugins/v4l2/v4l2.c new file mode 100644 index 0000000..77fb4ce --- /dev/null +++ b/spa/plugins/v4l2/v4l2.c @@ -0,0 +1,59 @@ +/* Spa V4l2 support + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <errno.h> + +#include <spa/support/plugin.h> +#include <spa/support/log.h> + +extern const struct spa_handle_factory spa_v4l2_source_factory; +extern const struct spa_handle_factory spa_v4l2_udev_factory; +extern const struct spa_handle_factory spa_v4l2_device_factory; + +struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.v4l2"); +struct spa_log_topic *v4l2_log_topic = &log_topic; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_v4l2_source_factory; + break; + case 1: + *factory = &spa_v4l2_udev_factory; + break; + case 2: + *factory = &spa_v4l2_device_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/v4l2/v4l2.h b/spa/plugins/v4l2/v4l2.h new file mode 100644 index 0000000..e2293c7 --- /dev/null +++ b/spa/plugins/v4l2/v4l2.h @@ -0,0 +1,49 @@ +/* Spa V4l2 support + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <errno.h> + +#include <spa/support/log.h> + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT v4l2_log_topic +extern struct spa_log_topic *v4l2_log_topic; + +static inline void v4l2_log_topic_init(struct spa_log *log) +{ + spa_log_topic_init(log, v4l2_log_topic); +} + +struct spa_v4l2_device { + struct spa_log *log; + int fd; + struct v4l2_capability cap; + unsigned int active:1; + unsigned int have_format:1; + char path[64]; +}; + +int spa_v4l2_open(struct spa_v4l2_device *dev, const char *path); +int spa_v4l2_close(struct spa_v4l2_device *dev); +int spa_v4l2_is_capture(struct spa_v4l2_device *dev); |