Show a patch.

GET /api/1.1/patches/21730/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 21730,
    "url": "https://patchwork.libcamera.org/api/1.1/patches/21730/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/21730/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/1.1/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20241022074544.3790451-7-chenghaoyang@chromium.org>",
    "date": "2024-10-22T07:43:42",
    "name": "[v16,6/7] libcamera: virtual: Read config and register cameras based on the config",
    "commit_ref": null,
    "pull_url": null,
    "state": "accepted",
    "archived": false,
    "hash": "80207e1438d8490b011e8e9af120a37b623d3190",
    "submitter": {
        "id": 117,
        "url": "https://patchwork.libcamera.org/api/1.1/people/117/?format=api",
        "name": "Cheng-Hao Yang",
        "email": "chenghaoyang@chromium.org"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/21730/mbox/",
    "series": [
        {
            "id": 4733,
            "url": "https://patchwork.libcamera.org/api/1.1/series/4733/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4733",
            "date": "2024-10-22T07:43:36",
            "name": "Add VirtualPipelineHandler",
            "version": 16,
            "mbox": "https://patchwork.libcamera.org/series/4733/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/21730/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/21730/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<libcamera-devel-bounces@lists.libcamera.org>",
        "X-Original-To": "parsemail@patchwork.libcamera.org",
        "Delivered-To": "parsemail@patchwork.libcamera.org",
        "Received": [
            "from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 1D88FC330B\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 22 Oct 2024 07:46:11 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 47F386539A;\n\tTue, 22 Oct 2024 09:46:10 +0200 (CEST)",
            "from mail-pf1-x430.google.com (mail-pf1-x430.google.com\n\t[IPv6:2607:f8b0:4864:20::430])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id A0F3565395\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 22 Oct 2024 09:46:03 +0200 (CEST)",
            "by mail-pf1-x430.google.com with SMTP id\n\td2e1a72fcca58-71e681bc315so3410231b3a.0\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 22 Oct 2024 00:46:03 -0700 (PDT)",
            "from chenghaoyang-low.c.googlers.com.com\n\t(27.247.221.35.bc.googleusercontent.com. [35.221.247.27])\n\tby smtp.gmail.com with ESMTPSA id\n\td2e1a72fcca58-71ec13d75aesm4091024b3a.124.2024.10.22.00.45.59\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tTue, 22 Oct 2024 00:46:00 -0700 (PDT)"
        ],
        "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=chromium.org header.i=@chromium.org\n\theader.b=\"k61L7dFC\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=chromium.org; s=google; t=1729583162; x=1730187962;\n\tdarn=lists.libcamera.org; \n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=c+7zVqSVqFsZ2Uv3Yyr1EYXopNA61/gFb3Mlrt0VqjQ=;\n\tb=k61L7dFCi96ebddgcV6rDP0oXJpcnhDWXqecg+F5Qhjk5Pwuj5h2xLQgHKwdn6KTjb\n\tU3XK9y3yF6IiQD717uYhj8F72xqIbq3DhHf2Snci2MNB6heiw8ZDql48QNnw2Wk7wRTm\n\tABtd9pYyaqMt+2teRdnGMUBWQq2sWV1alzINc=",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1729583162; x=1730187962;\n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc\n\t:subject:date:message-id:reply-to;\n\tbh=c+7zVqSVqFsZ2Uv3Yyr1EYXopNA61/gFb3Mlrt0VqjQ=;\n\tb=Yv4KS2v1rBkvAsdNdaawJxPx00O40L52OX57eJZUn/NhxM81wN4TDasoeudbbaHrKQ\n\tSYpbTsQMXAOpOnGQxXm1uMkKjC4YyW5ppDUxnF7ReUzwuHW+v3EOSgjUoI3YQdoqrRvE\n\tnbG6L45Aon8QQbZXhWyLFIjGQHnfvylKjNM5tS+XfwyR2KZPkj/PyKqaPU5uINP3Jcmm\n\t1ef8M/FL6uICYLAlh+rldW7NZi+Vv8cZLQRwMGLWtvotzszXWOABO+S1w6Vq7lJ7Qf+K\n\t/NljNctsfn6HUb0ozPP57YQXkT4giM4TNXSIhM2r4dcTUuHw5uq1tV2JcsvBqRRt//+n\n\tyoag==",
        "X-Gm-Message-State": "AOJu0YxcPAvVyTd/OB8vTtCkv13VSicSrT3UEcDnE5YDMw9/BuG3oNz8\n\tJZGVpkgpZJZoX1W0WtvoNPT9Qw+r0eyHSB/+rIp8OZomBuUtfS9iBFklMNNQNe6Jwrc9i0QZOww\n\t=",
        "X-Google-Smtp-Source": "AGHT+IG2aLlz3dUWL7HmPudTyw2dMpmoEzU2wk8S4GGxP+nj948p1ZvNLEJZhCDxrNaO32op7+mBdA==",
        "X-Received": "by 2002:a05:6a00:22c2:b0:71e:4fb7:3a87 with SMTP id\n\td2e1a72fcca58-71edc1f7871mr4395923b3a.13.1729583161374; \n\tTue, 22 Oct 2024 00:46:01 -0700 (PDT)",
        "From": "Harvey Yang <chenghaoyang@chromium.org>",
        "To": "libcamera-devel@lists.libcamera.org",
        "Cc": "Harvey Yang <chenghaoyang@chromium.org>, Konami Shu <konamiz@google.com>,\n\tYunke Cao <yunkec@chromium.org>, Tomasz Figa <tfiga@chromium.org>,\n\tKieran Bingham <kieran.bingham@ideasonboard.com>",
        "Subject": "[PATCH v16 6/7] libcamera: virtual: Read config and register cameras\n\tbased on the config",
        "Date": "Tue, 22 Oct 2024 07:43:42 +0000",
        "Message-ID": "<20241022074544.3790451-7-chenghaoyang@chromium.org>",
        "X-Mailer": "git-send-email 2.47.0.105.g07ac214952-goog",
        "In-Reply-To": "<20241022074544.3790451-1-chenghaoyang@chromium.org>",
        "References": "<20241022074544.3790451-1-chenghaoyang@chromium.org>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "X-BeenThere": "libcamera-devel@lists.libcamera.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "<libcamera-devel.lists.libcamera.org>",
        "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>",
        "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>",
        "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>",
        "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>",
        "Errors-To": "libcamera-devel-bounces@lists.libcamera.org",
        "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"
    },
    "content": "This patch introduces the configuration file for Virtual Pipeline\nHandler. The config file is written in yaml, and the format is\ndocumented in `README.md`.\n\nThe config file will define the camera with IDs, supported formats and\nimage sources, etc. In the default config file, only Test Patterns are\nused. Developers can use real images loading if desired.\n\nSigned-off-by: Konami Shu <konamiz@google.com>\nCo-developed-by: Harvey Yang <chenghaoyang@chromium.org>\nSigned-off-by: Harvey Yang <chenghaoyang@chromium.org>\nCo-developed-by: Yunke Cao <yunkec@chromium.org>\nSigned-off-by: Yunke Cao <yunkec@chromium.org>\nCo-developed-by: Tomasz Figa <tfiga@chromium.org>\nSigned-off-by: Tomasz Figa <tfiga@chromium.org>\nReviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>\n---\n src/libcamera/pipeline/virtual/README.md      |  65 +++++\n .../pipeline/virtual/config_parser.cpp        | 260 ++++++++++++++++++\n .../pipeline/virtual/config_parser.h          |  39 +++\n .../pipeline/virtual/data/virtual.yaml        |  36 +++\n .../virtual/image_frame_generator.cpp         |  16 +-\n .../pipeline/virtual/image_frame_generator.h  |   5 +-\n src/libcamera/pipeline/virtual/meson.build    |   1 +\n src/libcamera/pipeline/virtual/virtual.cpp    | 126 +++++----\n src/libcamera/pipeline/virtual/virtual.h      |  23 +-\n 9 files changed, 501 insertions(+), 70 deletions(-)\n create mode 100644 src/libcamera/pipeline/virtual/README.md\n create mode 100644 src/libcamera/pipeline/virtual/config_parser.cpp\n create mode 100644 src/libcamera/pipeline/virtual/config_parser.h\n create mode 100644 src/libcamera/pipeline/virtual/data/virtual.yaml",
    "diff": "diff --git a/src/libcamera/pipeline/virtual/README.md b/src/libcamera/pipeline/virtual/README.md\nnew file mode 100644\nindex 000000000..a9f39c151\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/README.md\n@@ -0,0 +1,65 @@\n+# Virtual Pipeline Handler\n+\n+Virtual pipeline handler emulates fake external camera(s) for testing.\n+\n+## Parse config file and register cameras\n+\n+- A sample config file is located at `src/libcamera/pipeline/virtual/data/virtual.yaml`.\n+- If libcamera is installed, the config file should be installed at\n+  `share/libcamera/pipeline/virtual/virtual.yaml`.\n+\n+### Config File Format\n+The config file contains the information about cameras' properties to register.\n+The config file should be a yaml file with dictionary of the cameraIds\n+associated with their properties as top level. The default value will be applied\n+when any property is empty.\n+\n+Each camera block is a dictionary, containing the following keys:\n+- `supported_formats` (list of `VirtualCameraData::Resolution`, optional):\n+  List of supported resolution and frame rates of the emulated camera\n+    - `width` (`unsigned int`, default=1920): Width of the window resolution.\n+      This needs to be even.\n+    - `height` (`unsigned int`, default=1080): Height of the window resolution.\n+    - `frame_rates` (list of `int`, default=`[30,60]` ): Range of the frame\n+      rate (per second). If the list contains one value, it's the lower bound\n+      and the upper bound. If the list contains two values, the first is the\n+      lower bound and the second is the upper bound. No other number of values\n+      is allowed.\n+- `test_pattern` (`string`): Which test pattern to use as frames. The options\n+  are \"bars\", \"lines\". Cannot be set with `frames`.\n+  - The test patterns are \"bars\" which means color bars, and \"lines\" which means\n+    diagonal lines.\n+- `frames` (dictionary):\n+  - `path` (`string`): Path to an image, or path to a directory of a series of\n+    images. Cannot be set with `test_pattern`.\n+    - The path to an image has \".jpg\" extension.\n+    - The path to a directory ends with \"/\". The name of the images in the\n+      directory are \"{n}.jpg\" with {n} is the sequence of images starting with 0.\n+- `location` (`string`, default=\"front\"): The location of the camera. Support\n+  \"CameraLocationFront\", \"CameraLocationBack\", and \"CameraLocationExternal\".\n+- `model` (`string`, default=\"Unknown\"): The model name of the camera.\n+\n+Check `data/virtual.yaml` as the sample config file.\n+\n+### Implementation\n+\n+`Parser` class provides methods to parse the config file to register cameras\n+in Virtual Pipeline Handler. `parseConfigFile()` is exposed to use in\n+Virtual Pipeline Handler.\n+\n+This is the procedure of the Parser class:\n+1. `parseConfigFile()` parses the config file to `YamlObject` using `YamlParser::parse()`.\n+    - Parse the top level of config file which are the camera ids and look into\n+      each camera properties.\n+2. For each camera, `parseCameraConfigData()` returns a camera with the configuration.\n+    - The methods in the next step fill the data with the pointer to the Camera object.\n+    - If the config file contains invalid configuration, this method returns\n+      nullptr. The camera will be skipped.\n+3. Parse each property and register the data.\n+    - `parseSupportedFormats()`: Parses `supported_formats` in the config, which\n+      contains resolutions and frame rates.\n+    - `parseFrameGenerator()`: Parses `test_pattern` or `frames` in the config.\n+    - `parseLocation()`: Parses `location` in the config.\n+    - `parseModel()`: Parses `model` in the config.\n+4. Back to `parseConfigFile()` and append the camera configuration.\n+5. Returns a list of camera configurations.\ndiff --git a/src/libcamera/pipeline/virtual/config_parser.cpp b/src/libcamera/pipeline/virtual/config_parser.cpp\nnew file mode 100644\nindex 000000000..0cbfe39bf\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/config_parser.cpp\n@@ -0,0 +1,260 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Google Inc.\n+ *\n+ * Virtual cameras helper to parse config file\n+ */\n+\n+#include \"config_parser.h\"\n+\n+#include <string.h>\n+#include <utility>\n+\n+#include <libcamera/base/log.h>\n+\n+#include <libcamera/control_ids.h>\n+#include <libcamera/property_ids.h>\n+\n+#include \"libcamera/internal/pipeline_handler.h\"\n+#include \"libcamera/internal/yaml_parser.h\"\n+\n+#include \"virtual.h\"\n+\n+namespace libcamera {\n+\n+LOG_DECLARE_CATEGORY(Virtual)\n+\n+std::vector<std::unique_ptr<VirtualCameraData>>\n+ConfigParser::parseConfigFile(File &file, PipelineHandler *pipe)\n+{\n+\tstd::vector<std::unique_ptr<VirtualCameraData>> configurations;\n+\n+\tstd::unique_ptr<YamlObject> cameras = YamlParser::parse(file);\n+\tif (!cameras) {\n+\t\tLOG(Virtual, Error) << \"Failed to pass config file.\";\n+\t\treturn configurations;\n+\t}\n+\n+\tif (!cameras->isDictionary()) {\n+\t\tLOG(Virtual, Error) << \"Config file is not a dictionary at the top level.\";\n+\t\treturn configurations;\n+\t}\n+\n+\t/* Look into the configuration of each camera */\n+\tfor (const auto &[cameraId, cameraConfigData] : cameras->asDict()) {\n+\t\tstd::unique_ptr<VirtualCameraData> data =\n+\t\t\tparseCameraConfigData(cameraConfigData, pipe);\n+\t\t/* Parse configData to data */\n+\t\tif (!data) {\n+\t\t\t/* Skip the camera if it has invalid config */\n+\t\t\tLOG(Virtual, Error) << \"Failed to parse config of the camera: \"\n+\t\t\t\t\t    << cameraId;\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tdata->config_.id = cameraId;\n+\t\tControlInfoMap::Map controls;\n+\t\t/* todo: Check which resolution's frame rate to be reported */\n+\t\tcontrols[&controls::FrameDurationLimits] =\n+\t\t\tControlInfo(1000000 / data->config_.resolutions[0].frameRates[1],\n+\t\t\t\t    1000000 / data->config_.resolutions[0].frameRates[0]);\n+\n+\t\tstd::vector<ControlValue> supportedFaceDetectModes{\n+\t\t\tstatic_cast<int32_t>(controls::draft::FaceDetectModeOff),\n+\t\t};\n+\t\tcontrols[&controls::draft::FaceDetectMode] = ControlInfo(supportedFaceDetectModes);\n+\n+\t\tdata->controlInfo_ = ControlInfoMap(std::move(controls), controls::controls);\n+\t\tconfigurations.push_back(std::move(data));\n+\t}\n+\n+\treturn configurations;\n+}\n+\n+std::unique_ptr<VirtualCameraData>\n+ConfigParser::parseCameraConfigData(const YamlObject &cameraConfigData,\n+\t\t\t\t    PipelineHandler *pipe)\n+{\n+\tstd::vector<VirtualCameraData::Resolution> resolutions;\n+\tif (parseSupportedFormats(cameraConfigData, &resolutions))\n+\t\treturn nullptr;\n+\n+\tstd::unique_ptr<VirtualCameraData> data =\n+\t\tstd::make_unique<VirtualCameraData>(pipe, resolutions);\n+\n+\tif (parseFrameGenerator(cameraConfigData, data.get()))\n+\t\treturn nullptr;\n+\n+\tif (parseLocation(cameraConfigData, data.get()))\n+\t\treturn nullptr;\n+\n+\tif (parseModel(cameraConfigData, data.get()))\n+\t\treturn nullptr;\n+\n+\treturn data;\n+}\n+\n+int ConfigParser::parseSupportedFormats(const YamlObject &cameraConfigData,\n+\t\t\t\t\tstd::vector<VirtualCameraData::Resolution> *resolutions)\n+{\n+\tif (cameraConfigData.contains(\"supported_formats\")) {\n+\t\tconst YamlObject &supportedResolutions = cameraConfigData[\"supported_formats\"];\n+\n+\t\tfor (const YamlObject &supportedResolution : supportedResolutions.asList()) {\n+\t\t\tunsigned int width = supportedResolution[\"width\"].get<unsigned int>(1920);\n+\t\t\tunsigned int height = supportedResolution[\"height\"].get<unsigned int>(1080);\n+\t\t\tif (width == 0 || height == 0) {\n+\t\t\t\tLOG(Virtual, Error) << \"Invalid width or/and height\";\n+\t\t\t\treturn -EINVAL;\n+\t\t\t}\n+\t\t\tif (width % 2 != 0) {\n+\t\t\t\tLOG(Virtual, Error) << \"Invalid width: width needs to be even\";\n+\t\t\t\treturn -EINVAL;\n+\t\t\t}\n+\n+\t\t\tstd::vector<int64_t> frameRates;\n+\t\t\tif (supportedResolution.contains(\"frame_rates\")) {\n+\t\t\t\tauto frameRatesList =\n+\t\t\t\t\tsupportedResolution[\"frame_rates\"].getList<int>();\n+\t\t\t\tif (!frameRatesList || (frameRatesList->size() != 1 &&\n+\t\t\t\t\t\t\tframeRatesList->size() != 2)) {\n+\t\t\t\t\tLOG(Virtual, Error) << \"Invalid frame_rates: either one or two values\";\n+\t\t\t\t\treturn -EINVAL;\n+\t\t\t\t}\n+\n+\t\t\t\tif (frameRatesList->size() == 2 &&\n+\t\t\t\t    frameRatesList.value()[0] > frameRatesList.value()[1]) {\n+\t\t\t\t\tLOG(Virtual, Error) << \"frame_rates's first value(lower bound)\"\n+\t\t\t\t\t\t\t    << \" is higher than the second value(upper bound)\";\n+\t\t\t\t\treturn -EINVAL;\n+\t\t\t\t}\n+\t\t\t\t/*\n+                                 * Push the min and max framerates. A\n+                                 * single rate is duplicated.\n+                                 */\n+\t\t\t\tframeRates.push_back(frameRatesList.value().front());\n+\t\t\t\tframeRates.push_back(frameRatesList.value().back());\n+\t\t\t} else {\n+\t\t\t\tframeRates.push_back(30);\n+\t\t\t\tframeRates.push_back(60);\n+\t\t\t}\n+\n+\t\t\tresolutions->emplace_back(\n+\t\t\t\tVirtualCameraData::Resolution{ Size{ width, height },\n+\t\t\t\t\t\t\t       frameRates });\n+\t\t}\n+\t} else {\n+\t\tresolutions->emplace_back(\n+\t\t\tVirtualCameraData::Resolution{ Size{ 1920, 1080 },\n+\t\t\t\t\t\t       { 30, 60 } });\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int ConfigParser::parseFrameGenerator(const YamlObject &cameraConfigData, VirtualCameraData *data)\n+{\n+\tconst std::string testPatternKey = \"test_pattern\";\n+\tconst std::string framesKey = \"frames\";\n+\tif (cameraConfigData.contains(testPatternKey)) {\n+\t\tif (cameraConfigData.contains(framesKey)) {\n+\t\t\tLOG(Virtual, Error) << \"A camera should use either \"\n+\t\t\t\t\t    << testPatternKey << \" or \" << framesKey;\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tauto testPattern = cameraConfigData[testPatternKey].get<std::string>(\"\");\n+\n+\t\tif (testPattern == \"bars\") {\n+\t\t\tdata->config_.frame = TestPattern::ColorBars;\n+\t\t} else if (testPattern == \"lines\") {\n+\t\t\tdata->config_.frame = TestPattern::DiagonalLines;\n+\t\t} else {\n+\t\t\tLOG(Virtual, Debug) << \"Test pattern: \" << testPattern\n+\t\t\t\t\t    << \" is not supported\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\treturn 0;\n+\t}\n+\n+\tconst YamlObject &frames = cameraConfigData[framesKey];\n+\n+\t/* When there is no frames provided in the config file, use color bar test pattern */\n+\tif (!frames) {\n+\t\tdata->config_.frame = TestPattern::ColorBars;\n+\t\treturn 0;\n+\t}\n+\n+\tif (!frames.isDictionary()) {\n+\t\tLOG(Virtual, Error) << \"'frames' is not a dictionary.\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tauto path = frames[\"path\"].get<std::string>();\n+\n+\tif (!path) {\n+\t\tLOG(Virtual, Error) << \"Test pattern or path should be specified.\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tstd::vector<std::filesystem::path> files;\n+\n+\tswitch (std::filesystem::symlink_status(*path).type()) {\n+\tcase std::filesystem::file_type::regular:\n+\t\tfiles.push_back(*path);\n+\t\tbreak;\n+\n+\tcase std::filesystem::file_type::directory:\n+\t\tfor (const auto &dentry : std::filesystem::directory_iterator{ *path }) {\n+\t\t\tif (dentry.is_regular_file())\n+\t\t\t\tfiles.push_back(dentry.path());\n+\t\t}\n+\n+\t\tstd::sort(files.begin(), files.end(), [](const auto &a, const auto &b) {\n+\t\t\treturn ::strverscmp(a.c_str(), b.c_str()) < 0;\n+\t\t});\n+\n+\t\tif (files.empty()) {\n+\t\t\tLOG(Virtual, Error) << \"Directory has no files: \" << *path;\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\t\tbreak;\n+\n+\tdefault:\n+\t\tLOG(Virtual, Error) << \"Frame: \" << *path << \" is not supported\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tdata->config_.frame = ImageFrames{ std::move(files) };\n+\n+\treturn 0;\n+}\n+\n+int ConfigParser::parseLocation(const YamlObject &cameraConfigData, VirtualCameraData *data)\n+{\n+\tstd::string location = cameraConfigData[\"location\"].get<std::string>(\"CameraLocationFront\");\n+\n+\t/* Default value is properties::CameraLocationFront */\n+\tauto it = properties::LocationNameValueMap.find(location);\n+\tif (it == properties::LocationNameValueMap.end()) {\n+\t\tLOG(Virtual, Error)\n+\t\t\t<< \"location: \" << location << \" is not supported\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tdata->properties_.set(properties::Location, it->second);\n+\n+\treturn 0;\n+}\n+\n+int ConfigParser::parseModel(const YamlObject &cameraConfigData, VirtualCameraData *data)\n+{\n+\tstd::string model = cameraConfigData[\"model\"].get<std::string>(\"Unknown\");\n+\n+\tdata->properties_.set(properties::Model, model);\n+\n+\treturn 0;\n+}\n+\n+} /* namespace libcamera */\ndiff --git a/src/libcamera/pipeline/virtual/config_parser.h b/src/libcamera/pipeline/virtual/config_parser.h\nnew file mode 100644\nindex 000000000..d2000de9c\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/config_parser.h\n@@ -0,0 +1,39 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Google Inc.\n+ *\n+ * Virtual cameras helper to parse config file\n+ */\n+\n+#pragma once\n+\n+#include <memory>\n+#include <vector>\n+\n+#include <libcamera/base/file.h>\n+\n+#include \"libcamera/internal/pipeline_handler.h\"\n+#include \"libcamera/internal/yaml_parser.h\"\n+\n+#include \"virtual.h\"\n+\n+namespace libcamera {\n+\n+class ConfigParser\n+{\n+public:\n+\tstd::vector<std::unique_ptr<VirtualCameraData>>\n+\tparseConfigFile(File &file, PipelineHandler *pipe);\n+\n+private:\n+\tstd::unique_ptr<VirtualCameraData>\n+\tparseCameraConfigData(const YamlObject &cameraConfigData, PipelineHandler *pipe);\n+\n+\tint parseSupportedFormats(const YamlObject &cameraConfigData,\n+\t\t\t\t  std::vector<VirtualCameraData::Resolution> *resolutions);\n+\tint parseFrameGenerator(const YamlObject &cameraConfigData, VirtualCameraData *data);\n+\tint parseLocation(const YamlObject &cameraConfigData, VirtualCameraData *data);\n+\tint parseModel(const YamlObject &cameraConfigData, VirtualCameraData *data);\n+};\n+\n+} /* namespace libcamera */\ndiff --git a/src/libcamera/pipeline/virtual/data/virtual.yaml b/src/libcamera/pipeline/virtual/data/virtual.yaml\nnew file mode 100644\nindex 000000000..20471bb94\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/data/virtual.yaml\n@@ -0,0 +1,36 @@\n+# SPDX-License-Identifier: CC0-1.0\n+%YAML 1.1\n+---\n+\"Virtual0\":\n+  supported_formats:\n+  - width: 1920\n+    height: 1080\n+    frame_rates:\n+    - 30\n+    - 60\n+  - width: 1680\n+    height: 1050\n+    frame_rates:\n+    - 70\n+    - 80\n+  test_pattern: \"lines\"\n+  location: \"CameraLocationFront\"\n+  model: \"Virtual Video Device\"\n+\"Virtual1\":\n+  supported_formats:\n+  - width: 800\n+    height: 600\n+    frame_rates:\n+    - 60\n+  test_pattern: \"bars\"\n+  location: \"CameraLocationBack\"\n+  model: \"Virtual Video Device1\"\n+\"Virtual2\":\n+  supported_formats:\n+  - width: 400\n+    height: 300\n+  test_pattern: \"lines\"\n+  location: \"CameraLocationFront\"\n+  model: \"Virtual Video Device2\"\n+\"Virtual3\":\n+  test_pattern: \"bars\"\ndiff --git a/src/libcamera/pipeline/virtual/image_frame_generator.cpp b/src/libcamera/pipeline/virtual/image_frame_generator.cpp\nindex e140969c8..2baef5880 100644\n--- a/src/libcamera/pipeline/virtual/image_frame_generator.cpp\n+++ b/src/libcamera/pipeline/virtual/image_frame_generator.cpp\n@@ -39,15 +39,7 @@ ImageFrameGenerator::create(ImageFrames &imageFrames)\n \t * For each file in the directory, load the image,\n \t * convert it to NV12, and store the pointer.\n \t */\n-\tfor (unsigned int i = 0; i < imageFrames.number.value_or(1); i++) {\n-\t\tstd::filesystem::path path;\n-\t\tif (!imageFrames.number)\n-\t\t\t/* If the path is to an image */\n-\t\t\tpath = imageFrames.path;\n-\t\telse\n-\t\t\t/* If the path is to a directory */\n-\t\t\tpath = imageFrames.path / (std::to_string(i) + \".jpg\");\n-\n+\tfor (std::filesystem::path path : imageFrames.files) {\n \t\tFile file(path);\n \t\tif (!file.open(File::OpenModeFlag::ReadOnly)) {\n \t\t\tLOG(Virtual, Error) << \"Failed to open image file \" << file.fileName()\n@@ -87,6 +79,8 @@ ImageFrameGenerator::create(ImageFrames &imageFrames)\n \t\t\t\t\tSize(width, height) });\n \t}\n \n+\tASSERT(!imageFrameGenerator->imageFrameDatas_.empty());\n+\n \treturn imageFrameGenerator;\n }\n \n@@ -103,7 +97,7 @@ void ImageFrameGenerator::configure(const Size &size)\n \tframeIndex_ = 0;\n \tparameter_ = 0;\n \n-\tfor (unsigned int i = 0; i < imageFrames_->number.value_or(1); i++) {\n+\tfor (unsigned int i = 0; i < imageFrameDatas_.size(); i++) {\n \t\t/* Scale the imageFrameDatas_ to scaledY and scaledUV */\n \t\tunsigned int halfSizeWidth = (size.width + 1) / 2;\n \t\tunsigned int halfSizeHeight = (size.height + 1) / 2;\n@@ -138,7 +132,7 @@ int ImageFrameGenerator::generateFrame(const Size &size, const FrameBuffer *buff\n \tauto planes = mappedFrameBuffer.planes();\n \n \t/* Loop only around the number of images available */\n-\tframeIndex_ %= imageFrames_->number.value_or(1);\n+\tframeIndex_ %= imageFrameDatas_.size();\n \n \t/* Write the scaledY and scaledUV to the mapped frame buffer */\n \tlibyuv::NV12Copy(scaledFrameDatas_[frameIndex_].Y.get(), size.width,\ndiff --git a/src/libcamera/pipeline/virtual/image_frame_generator.h b/src/libcamera/pipeline/virtual/image_frame_generator.h\nindex e072a47b8..42a077ba8 100644\n--- a/src/libcamera/pipeline/virtual/image_frame_generator.h\n+++ b/src/libcamera/pipeline/virtual/image_frame_generator.h\n@@ -9,9 +9,9 @@\n \n #include <filesystem>\n #include <memory>\n-#include <optional>\n #include <stdint.h>\n #include <sys/types.h>\n+#include <vector>\n \n #include \"frame_generator.h\"\n \n@@ -19,8 +19,7 @@ namespace libcamera {\n \n /* Frame configuration provided by the config file */\n struct ImageFrames {\n-\tstd::filesystem::path path;\n-\tstd::optional<unsigned int> number;\n+\tstd::vector<std::filesystem::path> files;\n };\n \n class ImageFrameGenerator : public FrameGenerator\ndiff --git a/src/libcamera/pipeline/virtual/meson.build b/src/libcamera/pipeline/virtual/meson.build\nindex bb38c193c..4786fe2e0 100644\n--- a/src/libcamera/pipeline/virtual/meson.build\n+++ b/src/libcamera/pipeline/virtual/meson.build\n@@ -1,6 +1,7 @@\n # SPDX-License-Identifier: CC0-1.0\n \n libcamera_internal_sources += files([\n+    'config_parser.cpp',\n     'image_frame_generator.cpp',\n     'test_pattern_generator.cpp',\n     'virtual.cpp',\ndiff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp\nindex 04fb08bb2..cec8a85bf 100644\n--- a/src/libcamera/pipeline/virtual/virtual.cpp\n+++ b/src/libcamera/pipeline/virtual/virtual.cpp\n@@ -36,6 +36,9 @@\n #include \"libcamera/internal/formats.h\"\n #include \"libcamera/internal/framebuffer.h\"\n #include \"libcamera/internal/pipeline_handler.h\"\n+#include \"libcamera/internal/yaml_parser.h\"\n+\n+#include \"pipeline/virtual/config_parser.h\"\n \n namespace libcamera {\n \n@@ -54,6 +57,13 @@ uint64_t currentTimestamp()\n \n } /* namespace */\n \n+template<class... Ts>\n+struct overloaded : Ts... {\n+\tusing Ts::operator()...;\n+};\n+template<class... Ts>\n+overloaded(Ts...) -> overloaded<Ts...>;\n+\n class VirtualCameraConfiguration : public CameraConfiguration\n {\n public:\n@@ -95,7 +105,7 @@ private:\n \t\treturn static_cast<VirtualCameraData *>(camera->_d());\n \t}\n \n-\tvoid initFrameGenerator(Camera *camera);\n+\tbool initFrameGenerator(Camera *camera);\n \n \tDmaBufAllocator dmaBufAllocator_;\n \n@@ -104,15 +114,19 @@ private:\n \n VirtualCameraData::VirtualCameraData(PipelineHandler *pipe,\n \t\t\t\t     const std::vector<Resolution> &supportedResolutions)\n-\t: Camera::Private(pipe), supportedResolutions_(supportedResolutions)\n+\t: Camera::Private(pipe)\n {\n-\tfor (const auto &resolution : supportedResolutions_) {\n-\t\tif (minResolutionSize_.isNull() || minResolutionSize_ > resolution.size)\n-\t\t\tminResolutionSize_ = resolution.size;\n+\tconfig_.resolutions = supportedResolutions;\n+\tfor (const auto &resolution : config_.resolutions) {\n+\t\tif (config_.minResolutionSize.isNull() || config_.minResolutionSize > resolution.size)\n+\t\t\tconfig_.minResolutionSize = resolution.size;\n \n-\t\tmaxResolutionSize_ = std::max(maxResolutionSize_, resolution.size);\n+\t\tconfig_.maxResolutionSize = std::max(config_.maxResolutionSize, resolution.size);\n \t}\n \n+\tproperties_.set(properties::PixelArrayActiveAreas,\n+\t\t\t{ Rectangle(config_.maxResolutionSize) });\n+\n \t/* \\todo Support multiple streams and pass multi_stream_test */\n \tstreamConfigs_.resize(kMaxStream);\n }\n@@ -140,7 +154,7 @@ CameraConfiguration::Status VirtualCameraConfiguration::validate()\n \tfor (StreamConfiguration &cfg : config_) {\n \t\tbool adjusted = false;\n \t\tbool found = false;\n-\t\tfor (const auto &resolution : data_->supportedResolutions_) {\n+\t\tfor (const auto &resolution : data_->config_.resolutions) {\n \t\t\tif (resolution.size.width == cfg.size.width &&\n \t\t\t    resolution.size.height == cfg.size.height) {\n \t\t\t\tfound = true;\n@@ -155,7 +169,7 @@ CameraConfiguration::Status VirtualCameraConfiguration::validate()\n \t\t\t * Defining the default logic in PipelineHandler to\n \t\t\t * find the closest resolution would be nice.\n \t\t\t */\n-\t\t\tcfg.size = data_->maxResolutionSize_;\n+\t\t\tcfg.size = data_->config_.maxResolutionSize;\n \t\t\tstatus = Adjusted;\n \t\t\tadjusted = true;\n \t\t}\n@@ -224,12 +238,12 @@ PipelineHandlerVirtual::generateConfiguration(Camera *camera,\n \n \t\tstd::map<PixelFormat, std::vector<SizeRange>> streamFormats;\n \t\tPixelFormat pixelFormat = formats::NV12;\n-\t\tstreamFormats[pixelFormat] = { { data->minResolutionSize_,\n-\t\t\t\t\t\t data->maxResolutionSize_ } };\n+\t\tstreamFormats[pixelFormat] = { { data->config_.minResolutionSize,\n+\t\t\t\t\t\t data->config_.maxResolutionSize } };\n \t\tStreamFormats formats(streamFormats);\n \t\tStreamConfiguration cfg(formats);\n \t\tcfg.pixelFormat = pixelFormat;\n-\t\tcfg.size = data->maxResolutionSize_;\n+\t\tcfg.size = data->config_.maxResolutionSize;\n \t\tcfg.bufferCount = VirtualCameraConfiguration::kBufferCount;\n \n \t\tconfig->addConfiguration(cfg);\n@@ -246,6 +260,7 @@ int PipelineHandlerVirtual::configure(Camera *camera,\n \tVirtualCameraData *data = cameraData(camera);\n \tfor (auto [i, c] : utils::enumerate(*config)) {\n \t\tc.setStream(&data->streamConfigs_[i].stream);\n+\t\t/* Start reading the images/generating test patterns */\n \t\tdata->streamConfigs_[i].frameGenerator->configure(c.size);\n \t}\n \n@@ -315,56 +330,67 @@ bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator\n \n \tcreated_ = true;\n \n-\t/* \\todo Add virtual cameras according to a config file. */\n-\n-\tstd::vector<VirtualCameraData::Resolution> supportedResolutions;\n-\tsupportedResolutions.resize(2);\n-\tsupportedResolutions[0] = { .size = Size(1920, 1080), .frameRates = { 30 } };\n-\tsupportedResolutions[1] = { .size = Size(1280, 720), .frameRates = { 30 } };\n-\n-\tstd::unique_ptr<VirtualCameraData> data =\n-\t\tstd::make_unique<VirtualCameraData>(this, supportedResolutions);\n-\n-\tdata->properties_.set(properties::Location, properties::CameraLocationFront);\n-\tdata->properties_.set(properties::Model, \"Virtual Video Device\");\n-\tdata->properties_.set(properties::PixelArrayActiveAreas, { Rectangle(Size(1920, 1080)) });\n-\n-\t/* \\todo Set FrameDurationLimits based on config. */\n-\tControlInfoMap::Map controls;\n-\tint64_t min_frame_duration = 33333, max_frame_duration = 33333;\n-\tcontrols[&controls::FrameDurationLimits] = ControlInfo(min_frame_duration, max_frame_duration);\n-\tstd::vector<ControlValue> supportedFaceDetectModes{\n-\t\tstatic_cast<int32_t>(controls::draft::FaceDetectModeOff),\n-\t};\n-\tcontrols[&controls::draft::FaceDetectMode] = ControlInfo(supportedFaceDetectModes);\n-\tdata->controlInfo_ = ControlInfoMap(std::move(controls), controls::controls);\n-\n-\t/* Create and register the camera. */\n-\tstd::set<Stream *> streams;\n-\tfor (auto &streamConfig : data->streamConfigs_)\n-\t\tstreams.insert(&streamConfig.stream);\n+\tFile file(configurationFile(\"virtual\", \"virtual.yaml\"));\n+\tbool isOpen = file.open(File::OpenModeFlag::ReadOnly);\n+\tif (!isOpen) {\n+\t\tLOG(Virtual, Error) << \"Failed to open config file: \" << file.fileName();\n+\t\treturn false;\n+\t}\n \n-\tconst std::string id = \"Virtual0\";\n-\tstd::shared_ptr<Camera> camera = Camera::create(std::move(data), id, streams);\n+\tConfigParser parser;\n+\tauto configData = parser.parseConfigFile(file, this);\n+\tif (configData.size() == 0) {\n+\t\tLOG(Virtual, Error) << \"Failed to parse any cameras from the config file: \"\n+\t\t\t\t    << file.fileName();\n+\t\treturn false;\n+\t}\n \n-\tinitFrameGenerator(camera.get());\n+\t/* Configure and register cameras with configData */\n+\tfor (auto &data : configData) {\n+\t\tstd::set<Stream *> streams;\n+\t\tfor (auto &streamConfig : data->streamConfigs_)\n+\t\t\tstreams.insert(&streamConfig.stream);\n+\t\tstd::string id = data->config_.id;\n+\t\tstd::shared_ptr<Camera> camera = Camera::create(std::move(data), id, streams);\n+\n+\t\tif (!initFrameGenerator(camera.get())) {\n+\t\t\tLOG(Virtual, Error) << \"Failed to initialize frame \"\n+\t\t\t\t\t    << \"generator for camera: \" << id;\n+\t\t\tcontinue;\n+\t\t}\n \n-\tregisterCamera(std::move(camera));\n+\t\tregisterCamera(std::move(camera));\n+\t}\n \n \tresetCreated_ = true;\n \n \treturn true;\n }\n \n-void PipelineHandlerVirtual::initFrameGenerator(Camera *camera)\n+bool PipelineHandlerVirtual::initFrameGenerator(Camera *camera)\n {\n \tauto data = cameraData(camera);\n-\tfor (auto &streamConfig : data->streamConfigs_) {\n-\t\tif (data->testPattern_ == TestPattern::DiagonalLines)\n-\t\t\tstreamConfig.frameGenerator = std::make_unique<DiagonalLinesGenerator>();\n-\t\telse\n-\t\t\tstreamConfig.frameGenerator = std::make_unique<ColorBarsGenerator>();\n-\t}\n+\tauto &frame = data->config_.frame;\n+\tstd::visit(overloaded{\n+\t\t\t   [&](TestPattern &testPattern) {\n+\t\t\t\t   for (auto &streamConfig : data->streamConfigs_) {\n+\t\t\t\t\t   if (testPattern == TestPattern::DiagonalLines)\n+\t\t\t\t\t\t   streamConfig.frameGenerator = std::make_unique<DiagonalLinesGenerator>();\n+\t\t\t\t\t   else\n+\t\t\t\t\t\t   streamConfig.frameGenerator = std::make_unique<ColorBarsGenerator>();\n+\t\t\t\t   }\n+\t\t\t   },\n+\t\t\t   [&](ImageFrames &imageFrames) {\n+\t\t\t\t   for (auto &streamConfig : data->streamConfigs_)\n+\t\t\t\t\t   streamConfig.frameGenerator = ImageFrameGenerator::create(imageFrames);\n+\t\t\t   } },\n+\t\t   frame);\n+\n+\tfor (auto &streamConfig : data->streamConfigs_)\n+\t\tif (!streamConfig.frameGenerator)\n+\t\t\treturn false;\n+\n+\treturn true;\n }\n \n REGISTER_PIPELINE_HANDLER(PipelineHandlerVirtual, \"virtual\")\ndiff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h\nindex 297b6b672..92ad7d4a9 100644\n--- a/src/libcamera/pipeline/virtual/virtual.h\n+++ b/src/libcamera/pipeline/virtual/virtual.h\n@@ -7,6 +7,8 @@\n \n #pragma once\n \n+#include <string>\n+#include <variant>\n #include <vector>\n \n #include <libcamera/geometry.h>\n@@ -15,10 +17,14 @@\n #include \"libcamera/internal/camera.h\"\n #include \"libcamera/internal/pipeline_handler.h\"\n \n+#include \"frame_generator.h\"\n+#include \"image_frame_generator.h\"\n #include \"test_pattern_generator.h\"\n \n namespace libcamera {\n \n+using VirtualFrame = std::variant<TestPattern, ImageFrames>;\n+\n class VirtualCameraData : public Camera::Private\n {\n public:\n@@ -26,23 +32,28 @@ public:\n \n \tstruct Resolution {\n \t\tSize size;\n-\t\tstd::vector<int> frameRates;\n+\t\tstd::vector<int64_t> frameRates;\n \t};\n \tstruct StreamConfig {\n \t\tStream stream;\n \t\tstd::unique_ptr<FrameGenerator> frameGenerator;\n \t};\n+\t/* The config file is parsed to the Configuration struct */\n+\tstruct Configuration {\n+\t\tstd::string id;\n+\t\tstd::vector<Resolution> resolutions;\n+\t\tVirtualFrame frame;\n+\n+\t\tSize maxResolutionSize;\n+\t\tSize minResolutionSize;\n+\t};\n \n \tVirtualCameraData(PipelineHandler *pipe,\n \t\t\t  const std::vector<Resolution> &supportedResolutions);\n \n \t~VirtualCameraData() = default;\n \n-\tTestPattern testPattern_ = TestPattern::ColorBars;\n-\n-\tconst std::vector<Resolution> supportedResolutions_;\n-\tSize maxResolutionSize_;\n-\tSize minResolutionSize_;\n+\tConfiguration config_;\n \n \tstd::vector<StreamConfig> streamConfigs_;\n };\n",
    "prefixes": [
        "v16",
        "6/7"
    ]
}