{"id":21053,"url":"https://patchwork.libcamera.org/api/1.1/patches/21053/?format=json","web_url":"https://patchwork.libcamera.org/patch/21053/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/1.1/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20240829195703.1456614-6-chenghaoyang@chromium.org>","date":"2024-08-29T19:47:44","name":"[v10,5/7] libcamera: virtual: Read config and register cameras based on the config","commit_ref":null,"pull_url":null,"state":"superseded","archived":false,"hash":"fd63b6d2deb872e072e25b42e80bb40899933c25","submitter":{"id":117,"url":"https://patchwork.libcamera.org/api/1.1/people/117/?format=json","name":"Cheng-Hao Yang","email":"chenghaoyang@chromium.org"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/21053/mbox/","series":[{"id":4547,"url":"https://patchwork.libcamera.org/api/1.1/series/4547/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=4547","date":"2024-08-29T19:47:39","name":"Add VirtualPipelineHandler","version":10,"mbox":"https://patchwork.libcamera.org/series/4547/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/21053/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/21053/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 9EF22C32D5\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 29 Aug 2024 19:57:24 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id DCBC7634D5;\n\tThu, 29 Aug 2024 21:57:22 +0200 (CEST)","from mail-wm1-x336.google.com (mail-wm1-x336.google.com\n\t[IPv6:2a00:1450:4864:20::336])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 20B0763461\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 29 Aug 2024 21:57:11 +0200 (CEST)","by mail-wm1-x336.google.com with SMTP id\n\t5b1f17b1804b1-42bb8c6e250so5816135e9.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 29 Aug 2024 12:57:11 -0700 (PDT)","from chenghaoyang-germany.c.googlers.com.com\n\t(161.126.77.34.bc.googleusercontent.com. [34.77.126.161])\n\tby smtp.gmail.com with ESMTPSA id\n\t5b1f17b1804b1-42ba7b4271fsm54602805e9.29.2024.08.29.12.57.09\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tThu, 29 Aug 2024 12:57:09 -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=\"b7PYxXoA\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=chromium.org; s=google; t=1724961430; x=1725566230;\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=ZZmdvzRgpzfypR6+SodM748cVYFeImRRBgndtuYIC78=;\n\tb=b7PYxXoAY/akwdyNOzG6hgXH3jwUslk+7ZNaAZfPYJdrXiUXYPe5B1cikT2pKsnGt+\n\tDP9HixWcqvkQLHrUF5JUrOkwRQTRmrh8JjMcw+i7gsC9BINJ0aUFfjRNkkXF+oYLvMYk\n\t1VdDLqs8gANeJgaQkTXqFjmczz0ZSfAMVf7Wg=","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1724961430; x=1725566230;\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=ZZmdvzRgpzfypR6+SodM748cVYFeImRRBgndtuYIC78=;\n\tb=CdAVpw9wUiWpEzik3XHvnPyFqs6WAb9RJR5d7Wrt9v5N9BZtYD71Hvs7EBH4v5oGw6\n\tkNa6IvtXLZeje1ScR/+xhil8wHbZcjUwkVxX1UTuXY+noB27O5cQSj1z1gw+mERA3cHf\n\tS2vBFMN+kQdjaZNIiaKYtsYWjbzjE/c3Chw1bg2DntVNtfM4IQyzcg0tQdYAwhw/WEQr\n\t2HL+TXOJI0vedfaRQ6ksV5/s1hse4KxlvSkg/fqSDUdmNSJl3KpxQ1kBIFYNvu7nFR6R\n\tBv2mGdTRSk1EcMTD1iWN+zKBns4FANJRxHzuByKpZ8hD6jQ6EuuEkxmDn3PislMsQG3o\n\tCdfg==","X-Gm-Message-State":"AOJu0YxbQWzYyC57kJo8qKm5gvquLgNwNNTvEcE6hsUl5EqlHbhsJJZY\n\tUi8nugaMEop0acEaC32C9blYnr9zd+QvLXecp3Oa7amHskQBJPeieDLEGdJ4E5oZGSWTbBOVNV7\n\tkog==","X-Google-Smtp-Source":"AGHT+IFgCBXtpdsldoM4G6XLcRH54Ztxl6ON7o3OSHG001dT8LeueGXQnE6E919AMOKYizLCLVTXOA==","X-Received":"by 2002:a05:600c:4586:b0:42a:b62c:8c86 with SMTP id\n\t5b1f17b1804b1-42bb02f95b6mr33567215e9.32.1724961430361; \n\tThu, 29 Aug 2024 12:57:10 -0700 (PDT)","From":"Harvey Yang <chenghaoyang@chromium.org>","To":"libcamera-devel@lists.libcamera.org","Cc":"Konami Shu <konamiz@google.com>, Harvey Yang <chenghaoyang@chromium.org>,\n\tYunke Cao <yunkec@chromium.org>, Tomasz Figa <tfiga@chromium.org>","Subject":"[PATCH v10 5/7] libcamera: virtual: Read config and register cameras\n\tbased on the config","Date":"Thu, 29 Aug 2024 19:47:44 +0000","Message-ID":"<20240829195703.1456614-6-chenghaoyang@chromium.org>","X-Mailer":"git-send-email 2.46.0.469.g59c65b2a67-goog","In-Reply-To":"<20240829195703.1456614-1-chenghaoyang@chromium.org>","References":"<20240829195703.1456614-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":"From: Konami Shu <konamiz@google.com>\n\nThis 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. Only Test Patterns are supported now, while the next\npatches will add generators to read from real images.\n\nSigned-off-by: Konami Shu <konamiz@google.com>\nCo-developed-by: Harvey Yang <chenghaoyang@chromium.org>\nCo-developed-by: Yunke Cao <yunkec@chromium.org>\nCo-developed-by: Tomasz Figa <tfiga@chromium.org>\n---\n src/libcamera/pipeline/virtual/README.md      |  43 ++++\n .../pipeline/virtual/data/virtual.yaml        |  37 ++++\n .../pipeline/virtual/frame_generator.h        |   3 +-\n src/libcamera/pipeline/virtual/meson.build    |   1 +\n src/libcamera/pipeline/virtual/parser.cpp     | 191 ++++++++++++++++++\n src/libcamera/pipeline/virtual/parser.h       |  42 ++++\n src/libcamera/pipeline/virtual/virtual.cpp    |  56 ++---\n src/libcamera/pipeline/virtual/virtual.h      |   1 +\n 8 files changed, 345 insertions(+), 29 deletions(-)\n create mode 100644 src/libcamera/pipeline/virtual/README.md\n create mode 100644 src/libcamera/pipeline/virtual/data/virtual.yaml\n create mode 100644 src/libcamera/pipeline/virtual/parser.cpp\n create mode 100644 src/libcamera/pipeline/virtual/parser.h","diff":"diff --git a/src/libcamera/pipeline/virtual/README.md b/src/libcamera/pipeline/virtual/README.md\nnew file mode 100644\nindex 000000000..ff1e8a5f9\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/README.md\n@@ -0,0 +1,43 @@\n+# Virtual Pipeline Handler\n+\n+Virtual pipeline handler emulates fake external camera(s) on ChromeOS for testing.\n+\n+## Parse config file and register cameras\n+\n+- The config file is located at `src/libcamera/pipeline/virtual/data/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 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) : List of supported resolution and frame rates of the emulated camera\n+    - `width` (`unsigned int`, default=1920): Width of the window resolution. 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 rate (per second). The list has to be two values of the lower bound and the upper bound of the frame rate.\n+- `test_pattern` (`string`): Which test pattern to use as frames. The options are \"bars\", \"lines\".\n+- `location` (`string`, default=\"front\"): The location of the camera. Support \"front\" and \"back\". This is displayed in qcam camera selection window but this does not change the output.\n+- `model` (`string`, default=\"Unknown\"): The model name of the camera. This is displayed in qcam camera selection window but this does not change the output.\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 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 nullptr. The camera will be skipped.\n+3. Parse each property and register the data.\n+    - `parseSupportedFormats()`: Parses `supported_formats` in the config, which contains resolutions and frame rates.\n+    - `parseTestPattern()`: Parses `test_pattern` 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/data/virtual.yaml b/src/libcamera/pipeline/virtual/data/virtual.yaml\nnew file mode 100644\nindex 000000000..317beb828\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/data/virtual.yaml\n@@ -0,0 +1,37 @@\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: \"front\"\n+  model: \"Virtual Video Device\"\n+\"Virtual1\":\n+  supported_formats:\n+  - width: 800\n+    height: 600\n+    frame_rates:\n+    - 30\n+    - 60\n+  test_pattern: \"bars\"\n+  location: \"back\"\n+  model: \"Virtual Video Device1\"\n+\"Virtual2\":\n+  supported_formats:\n+  - width: 400\n+    height: 300\n+  test_pattern: \"lines\"\n+  location: \"front\"\n+  model: \"Virtual Video Device2\"\n+\"Virtual3\":\n+  test_pattern: \"bars\"\ndiff --git a/src/libcamera/pipeline/virtual/frame_generator.h b/src/libcamera/pipeline/virtual/frame_generator.h\nindex d8727b8f3..f68a795fb 100644\n--- a/src/libcamera/pipeline/virtual/frame_generator.h\n+++ b/src/libcamera/pipeline/virtual/frame_generator.h\n@@ -19,8 +19,7 @@ public:\n \n \tvirtual void configure(const Size &size) = 0;\n \n-\tvirtual void generateFrame(const Size &size,\n-\t\t\t\t   const FrameBuffer *buffer) = 0;\n+\tvirtual void generateFrame(const Size &size, const FrameBuffer *buffer) = 0;\n \n protected:\n \tFrameGenerator() {}\ndiff --git a/src/libcamera/pipeline/virtual/meson.build b/src/libcamera/pipeline/virtual/meson.build\nindex 0c537777f..d72ac5be7 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+    'parser.cpp',\n     'test_pattern_generator.cpp',\n     'virtual.cpp',\n ])\ndiff --git a/src/libcamera/pipeline/virtual/parser.cpp b/src/libcamera/pipeline/virtual/parser.cpp\nnew file mode 100644\nindex 000000000..e579a3f7c\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/parser.cpp\n@@ -0,0 +1,191 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Google Inc.\n+ *\n+ * parser.cpp - Virtual cameras helper to parse config file\n+ */\n+\n+#include \"parser.h\"\n+\n+#include <memory>\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+Parser::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->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(int64_t(1000000 / data->supportedResolutions_[0].frameRates[1]),\n+\t\t\t\t    int64_t(1000000 / data->supportedResolutions_[0].frameRates[0]));\n+\t\tdata->controlInfo_ = ControlInfoMap(std::move(controls), controls::controls);\n+\t\tconfigurations.push_back(std::move(data));\n+\t}\n+\treturn configurations;\n+}\n+\n+std::unique_ptr<VirtualCameraData>\n+Parser::parseCameraConfigData(const YamlObject &cameraConfigData,\n+\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 (parseTestPattern(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 Parser::parseSupportedFormats(const YamlObject &cameraConfigData,\n+\t\t\t\t  std::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<int> 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() != 2) {\n+\t\t\t\t\tLOG(Virtual, Error) << \"frame_rates needs to be the two edge values of a range\";\n+\t\t\t\t\treturn -EINVAL;\n+\t\t\t\t}\n+\t\t\t\tif (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\tframeRates.push_back(frameRatesList.value()[0]);\n+\t\t\t\tframeRates.push_back(frameRatesList.value()[1]);\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 Parser::parseTestPattern(const YamlObject &cameraConfigData, VirtualCameraData *data)\n+{\n+\tstd::string testPattern = cameraConfigData[\"test_pattern\"].get<std::string>(\"\");\n+\n+\t/* Default value is \"bars\" */\n+\tif (testPattern == \"bars\") {\n+\t\tdata->testPattern_ = TestPattern::ColorBars;\n+\t} else if (testPattern == \"lines\") {\n+\t\tdata->testPattern_ = TestPattern::DiagonalLines;\n+\t} else {\n+\t\tLOG(Virtual, Error) << \"Test pattern: \" << testPattern\n+\t\t\t\t    << \"is not supported\";\n+\t\treturn -EINVAL;\n+\t}\n+\treturn 0;\n+}\n+\n+int Parser::parseLocation(const YamlObject &cameraConfigData, VirtualCameraData *data)\n+{\n+\tstd::string location = cameraConfigData[\"location\"].get<std::string>(\"\");\n+\n+\t/* Default value is properties::CameraLocationFront */\n+\tif (location == \"front\" || location == \"\") {\n+\t\tdata->properties_.set(properties::Location,\n+\t\t\t\t      properties::CameraLocationFront);\n+\t} else if (location == \"back\") {\n+\t\tdata->properties_.set(properties::Location,\n+\t\t\t\t      properties::CameraLocationBack);\n+\t} else {\n+\t\tLOG(Virtual, Error) << \"location: \" << location\n+\t\t\t\t    << \" is not supported\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int Parser::parseModel(const YamlObject &cameraConfigData, VirtualCameraData *data)\n+{\n+\tstd::string model = cameraConfigData[\"model\"].get<std::string>(\"\");\n+\n+\t/* Default value is \"Unknown\" */\n+\tif (model == \"\")\n+\t\tdata->properties_.set(properties::Model, \"Unknown\");\n+\telse\n+\t\tdata->properties_.set(properties::Model, model);\n+\n+\treturn 0;\n+}\n+\n+} /* namespace libcamera */\ndiff --git a/src/libcamera/pipeline/virtual/parser.h b/src/libcamera/pipeline/virtual/parser.h\nnew file mode 100644\nindex 000000000..09c3c56b8\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/parser.h\n@@ -0,0 +1,42 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Google Inc.\n+ *\n+ * parser.h - 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 Parser\n+{\n+public:\n+\tParser() {}\n+\t~Parser() = default;\n+\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 parseTestPattern(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/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp\nindex 690d72a22..c196a56aa 100644\n--- a/src/libcamera/pipeline/virtual/virtual.cpp\n+++ b/src/libcamera/pipeline/virtual/virtual.cpp\n@@ -24,6 +24,9 @@\n #include \"libcamera/internal/camera.h\"\n #include \"libcamera/internal/formats.h\"\n #include \"libcamera/internal/pipeline_handler.h\"\n+#include \"libcamera/internal/yaml_parser.h\"\n+\n+#include \"parser.h\"\n \n namespace libcamera {\n \n@@ -55,6 +58,9 @@ VirtualCameraData::VirtualCameraData(PipelineHandler *pipe,\n \t\tmaxResolutionSize_ = std::max(maxResolutionSize_, resolution.size);\n \t}\n \n+\tproperties_.set(properties::PixelArrayActiveAreas,\n+\t\t\t{ Rectangle(maxResolutionSize_) });\n+\n \t/* \\todo Support multiple streams and pass multi_stream_test */\n \tstreamConfigs_.resize(kMaxStream);\n }\n@@ -242,37 +248,33 @@ 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-\tdata->controlInfo_ = ControlInfoMap(std::move(controls), controls::controls);\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-\t/* Create and register the camera. */\n-\tstd::set<Stream *> streams;\n-\tfor (auto &streamConfig : data->streamConfigs_)\n-\t\tstreams.insert(&streamConfig.stream);\n+\tParser 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-\tconst std::string id = \"Virtual0\";\n-\tstd::shared_ptr<Camera> camera = Camera::create(std::move(data), id, streams);\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->id_;\n+\t\tstd::shared_ptr<Camera> camera = Camera::create(std::move(data), id, streams);\n \n-\tinitFrameGenerator(camera.get());\n+\t\tinitFrameGenerator(camera.get());\n \n-\tregisterCamera(std::move(camera));\n+\t\tregisterCamera(std::move(camera));\n+\t}\n \n \treturn true;\n }\ndiff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h\nindex 79c951a06..5daa960b1 100644\n--- a/src/libcamera/pipeline/virtual/virtual.h\n+++ b/src/libcamera/pipeline/virtual/virtual.h\n@@ -39,6 +39,7 @@ public:\n \n \t~VirtualCameraData() = default;\n \n+\tstd::string id_;\n \tTestPattern testPattern_;\n \n \tconst std::vector<Resolution> supportedResolutions_;\n","prefixes":["v10","5/7"]}