Show a patch.

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

{
    "id": 21198,
    "url": "https://patchwork.libcamera.org/api/patches/21198/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/21198/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/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": "<20240907143110.2210711-7-chenghaoyang@google.com>",
    "date": "2024-09-07T14:28:31",
    "name": "[v11,6/7] libcamera: virtual: Add ImageFrameGenerator",
    "commit_ref": null,
    "pull_url": null,
    "state": "superseded",
    "archived": false,
    "hash": "30475a76abed15440275dde7e885de88fd34b695",
    "submitter": {
        "id": 117,
        "url": "https://patchwork.libcamera.org/api/people/117/?format=api",
        "name": "Cheng-Hao Yang",
        "email": "chenghaoyang@chromium.org"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/21198/mbox/",
    "series": [
        {
            "id": 4567,
            "url": "https://patchwork.libcamera.org/api/series/4567/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4567",
            "date": "2024-09-07T14:28:25",
            "name": "Add VirtualPipelineHandler",
            "version": 11,
            "mbox": "https://patchwork.libcamera.org/series/4567/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/21198/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/21198/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 65A83C3259\n\tfor <parsemail@patchwork.libcamera.org>;\n\tSat,  7 Sep 2024 14:31:37 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id D7CFA634F4;\n\tSat,  7 Sep 2024 16:31:36 +0200 (CEST)",
            "from mail-pl1-x631.google.com (mail-pl1-x631.google.com\n\t[IPv6:2607:f8b0:4864:20::631])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id AF79D63505\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSat,  7 Sep 2024 16:31:28 +0200 (CEST)",
            "by mail-pl1-x631.google.com with SMTP id\n\td9443c01a7336-20688fbaeafso29019865ad.0\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSat, 07 Sep 2024 07:31:28 -0700 (PDT)",
            "from chenghaoyang-low.c.googlers.com.com\n\t(0.223.81.34.bc.googleusercontent.com. [34.81.223.0])\n\tby smtp.gmail.com with ESMTPSA id\n\td9443c01a7336-20710e33974sm9208935ad.104.2024.09.07.07.31.24\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tSat, 07 Sep 2024 07:31:25 -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=\"dOxadJ4/\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=chromium.org; s=google; t=1725719487; x=1726324287;\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=K3Nj5TdQ66fojNPaNK9VCVEWEMA+f1mXeZ/KyGmrgnQ=;\n\tb=dOxadJ4/pncHsx96Lv25PZ08Yr42qfS/95UD1x+y77XdwObzVSDqXbSNIZ8bYVbdG/\n\tggO+DN6qhTAX1n1evWPB/Au5YmOhN/xlIIeXtQp0CX9gSYnpLMmH13ew8w+fIY7d6QWG\n\tav/Azzx+Bg2bm+aChkLpOMqbfoivRomYc2btU=",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1725719487; x=1726324287;\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=K3Nj5TdQ66fojNPaNK9VCVEWEMA+f1mXeZ/KyGmrgnQ=;\n\tb=rXNt7XaNPgLzRhrxwRbVkLcsyFxe4RUJoPniC0c38wEn0wCb47r0yLoA/g+Ozxhlk2\n\tRFF2vQlYSGKuLGsntNBkfuxyL8lCTAzSmgfXqto4A1NKDVakXugceGB2BBTv1H784RKG\n\tJ5aIae/hmvXqyiMNamhZoc8VuPH2+o8jo2mmRcDkeJbbUvoXPZVQP0lrt79blR5SAnA9\n\tsykRmPUkvqvpDlUGPyu/DM9OdlU1ULsyk6rkDuw2dRJS9dDsjlQ8BiOtk7XXGxIffatG\n\te1+Yt4w1bqieiOiBKvwAJ5cGMsyWbbexWYcNoBVBBX8W8L1zJPDw0fF5AbDV3PlvQkA2\n\t9vjw==",
        "X-Gm-Message-State": "AOJu0YzsP0gt9ac1icr5kNUok7EoNh9kUdHjLtv008645sZjmtSxV37W\n\tV7x0FKO63KiMdVGgflCclSbsjzuZTgqdWNHuLtnbgj5c5/ZLWQDRGEmAFMzoa4HinMSlqd74zVf\n\ti1g==",
        "X-Google-Smtp-Source": "AGHT+IHJgwUUReG3lrt4HGuK/DmeyPW296pAru+ql5a7ToR5xynjo+Lk9USp14qWK/U1MW5LufQG+A==",
        "X-Received": "by 2002:a17:902:e886:b0:202:3432:230c with SMTP id\n\td9443c01a7336-206f04e943fmr64132685ad.18.1725719486499; \n\tSat, 07 Sep 2024 07:31:26 -0700 (PDT)",
        "From": "Harvey Yang <chenghaoyang@chromium.org>",
        "X-Google-Original-From": "Harvey Yang <chenghaoyang@google.com>",
        "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 v11 6/7] libcamera: virtual: Add ImageFrameGenerator",
        "Date": "Sat,  7 Sep 2024 14:28:31 +0000",
        "Message-ID": "<20240907143110.2210711-7-chenghaoyang@google.com>",
        "X-Mailer": "git-send-email 2.46.0.469.g59c65b2a67-goog",
        "In-Reply-To": "<20240907143110.2210711-1-chenghaoyang@google.com>",
        "References": "<20240907143110.2210711-1-chenghaoyang@google.com>",
        "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\nBesides TestPatternGenerator, this patch adds ImageFrameGenerator that\nloads real images (jpg / jpeg for now) as the source and generates\nscaled frames.\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      |   9 +-\n .../virtual/image_frame_generator.cpp         | 178 ++++++++++++++++++\n .../pipeline/virtual/image_frame_generator.h  |  54 ++++++\n src/libcamera/pipeline/virtual/meson.build    |   4 +\n src/libcamera/pipeline/virtual/parser.cpp     |  76 +++++++-\n src/libcamera/pipeline/virtual/parser.h       |   2 +\n src/libcamera/pipeline/virtual/utils.h        |  17 ++\n src/libcamera/pipeline/virtual/virtual.cpp    |  60 ++++--\n src/libcamera/pipeline/virtual/virtual.h      |  24 ++-\n 9 files changed, 389 insertions(+), 35 deletions(-)\n create mode 100644 src/libcamera/pipeline/virtual/image_frame_generator.cpp\n create mode 100644 src/libcamera/pipeline/virtual/image_frame_generator.h\n create mode 100644 src/libcamera/pipeline/virtual/utils.h",
    "diff": "diff --git a/src/libcamera/pipeline/virtual/README.md b/src/libcamera/pipeline/virtual/README.md\nindex ef80bb48..18c8341b 100644\n--- a/src/libcamera/pipeline/virtual/README.md\n+++ b/src/libcamera/pipeline/virtual/README.md\n@@ -16,7 +16,13 @@ Each camera block is a dictionary, containing the following keys:\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). If the list contains one value, it's the lower bound and the upper bound. If the list contains two values, the first is the lower bound and the second is the upper bound. No other number of values is allowed.\n-- `test_pattern` (`string`): Which test pattern to use as frames. The options are \"bars\", \"lines\".\n+- `test_pattern` (`string`): Which test pattern to use as frames. The options are \"bars\", \"lines\". Cannot be set with `frames`.\n+- `frames` (dictionary):\n+  - `path` (`string`): Path to an image, or path to a directory of a series of images. Cannot be set with `test_pattern`.\n+    - The test patterns are \"bars\" which means color bars, and \"lines\" which means diagonal lines.\n+    - The path to an image has \".jpg\" extension.\n+    - The path to a directory ends with \"/\". The name of the images in the directory are \"{n}.jpg\" with {n} is the sequence of images starting with 0.\n+  - `scale_mode`(`string`, default=\"fill\"): Scale mode when the frames are images. The only scale mode supported now is \"fill\". This does not affect the scale mode for now.\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@@ -37,6 +43,7 @@ This is the procedure of the Parser class:\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+    - `parseFrame()`: Parses `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.\ndiff --git a/src/libcamera/pipeline/virtual/image_frame_generator.cpp b/src/libcamera/pipeline/virtual/image_frame_generator.cpp\nnew file mode 100644\nindex 00000000..db3efe15\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/image_frame_generator.cpp\n@@ -0,0 +1,178 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Google Inc.\n+ *\n+ * image_frame_generator.cpp - Derived class of FrameGenerator for\n+ * generating frames from images\n+ */\n+\n+#include \"image_frame_generator.h\"\n+\n+#include <filesystem>\n+#include <memory>\n+#include <string>\n+\n+#include <libcamera/base/file.h>\n+#include <libcamera/base/log.h>\n+\n+#include <libcamera/framebuffer.h>\n+\n+#include \"libcamera/internal/mapped_framebuffer.h\"\n+\n+#include \"libyuv/convert.h\"\n+#include \"libyuv/scale.h\"\n+\n+namespace libcamera {\n+\n+LOG_DECLARE_CATEGORY(Virtual)\n+\n+/*\n+ * Factory function to create an ImageFrameGenerator object.\n+ * Read the images and convert them to buffers in NV12 format.\n+ * Store the pointers to the buffers to a list (imageFrameDatas)\n+ */\n+std::unique_ptr<ImageFrameGenerator>\n+ImageFrameGenerator::create(ImageFrames &imageFrames)\n+{\n+\tstd::unique_ptr<ImageFrameGenerator> imageFrameGenerator =\n+\t\tstd::make_unique<ImageFrameGenerator>();\n+\timageFrameGenerator->imageFrames_ = &imageFrames;\n+\n+\t/*\n+         * For each file in the directory, load the image,\n+         * 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+\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+\t\t\t\t\t    << \": \" << strerror(file.error());\n+\t\t\treturn nullptr;\n+\t\t}\n+\n+\t\t/* Read the image file to data */\n+\t\tauto fileSize = file.size();\n+\t\tauto buffer = std::make_unique<uint8_t[]>(file.size());\n+\t\tif (file.read({ buffer.get(), static_cast<size_t>(fileSize) }) != fileSize) {\n+\t\t\tLOG(Virtual, Error) << \"Failed to read file \" << file.fileName()\n+\t\t\t\t\t    << \": \" << strerror(file.error());\n+\t\t\treturn nullptr;\n+\t\t}\n+\n+\t\t/* Get the width and height of the image */\n+\t\tint width, height;\n+\t\tif (libyuv::MJPGSize(buffer.get(), fileSize, &width, &height)) {\n+\t\t\tLOG(Virtual, Error) << \"Failed to get the size of the image file: \"\n+\t\t\t\t\t    << file.fileName();\n+\t\t\treturn nullptr;\n+\t\t}\n+\n+\t\t/* Convert to NV12 and write the data to tmpY and tmpUV */\n+\t\tunsigned int halfWidth = (width + 1) / 2;\n+\t\tunsigned int halfHeight = (height + 1) / 2;\n+\t\tstd::unique_ptr<uint8_t[]> dstY =\n+\t\t\tstd::make_unique<uint8_t[]>(width * height);\n+\t\tstd::unique_ptr<uint8_t[]> dstUV =\n+\t\t\tstd::make_unique<uint8_t[]>(halfWidth * halfHeight * 2);\n+\t\tint ret = libyuv::MJPGToNV12(buffer.get(), fileSize,\n+\t\t\t\t\t     dstY.get(), width, dstUV.get(),\n+\t\t\t\t\t     width, width, height, width, height);\n+\t\tif (ret != 0)\n+\t\t\tLOG(Virtual, Error) << \"MJPGToNV12() failed with \" << ret;\n+\n+\t\timageFrameGenerator->imageFrameDatas_.emplace_back(\n+\t\t\tImageFrameData{ std::move(dstY), std::move(dstUV),\n+\t\t\t\t\tSize(width, height) });\n+\t}\n+\n+\treturn imageFrameGenerator;\n+}\n+\n+/* Scale the buffers for image frames. */\n+void ImageFrameGenerator::configure(const Size &size)\n+{\n+\t/* Reset the source images to prevent multiple configuration calls */\n+\tscaledFrameDatas_.clear();\n+\tframeCount_ = 0;\n+\tparameter_ = 0;\n+\n+\tfor (unsigned int i = 0; i < imageFrames_->number.value_or(1); 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+\t\tstd::unique_ptr<uint8_t[]> scaledY =\n+\t\t\tstd::make_unique<uint8_t[]>(size.width * size.height);\n+\t\tstd::unique_ptr<uint8_t[]> scaledUV =\n+\t\t\tstd::make_unique<uint8_t[]>(halfSizeWidth * halfSizeHeight * 2);\n+\t\tauto &src = imageFrameDatas_[i];\n+\n+\t\t/*\n+\t\t * \\todo Some platforms might enforce stride due to GPU, like\n+\t\t * ChromeOS ciri (64). The weight needs to be a multiple of\n+\t\t * the stride to work properly for now.\n+\t\t */\n+\t\tlibyuv::NV12Scale(src.Y.get(), src.size.width,\n+\t\t\t\t  src.UV.get(), src.size.width,\n+\t\t\t\t  src.size.width, src.size.height,\n+\t\t\t\t  scaledY.get(), size.width, scaledUV.get(), size.width,\n+\t\t\t\t  size.width, size.height, libyuv::FilterMode::kFilterBilinear);\n+\n+\t\tscaledFrameDatas_.emplace_back(\n+\t\t\tImageFrameData{ std::move(scaledY), std::move(scaledUV), size });\n+\t}\n+}\n+\n+void ImageFrameGenerator::generateFrame(const Size &size, const FrameBuffer *buffer)\n+{\n+\t/* Don't do anything when the list of buffers is empty*/\n+\tASSERT(!scaledFrameDatas_.empty());\n+\n+\tMappedFrameBuffer mappedFrameBuffer(buffer, MappedFrameBuffer::MapFlag::Write);\n+\n+\tauto planes = mappedFrameBuffer.planes();\n+\n+\t/* Make sure the frameCount does not over the number of images */\n+\tframeCount_ %= imageFrames_->number.value_or(1);\n+\n+\t/* Write the scaledY and scaledUV to the mapped frame buffer */\n+\tlibyuv::NV12Copy(scaledFrameDatas_[frameCount_].Y.get(), size.width,\n+\t\t\t scaledFrameDatas_[frameCount_].UV.get(), size.width, planes[0].begin(),\n+\t\t\t size.width, planes[1].begin(), size.width,\n+\t\t\t size.width, size.height);\n+\n+\t/* proceed an image every 4 frames */\n+\t/* \\todo read the parameter_ from the configuration file? */\n+\tparameter_++;\n+\tif (parameter_ % 4 == 0)\n+\t\tframeCount_++;\n+}\n+\n+/**\n+ * \\var ImageFrameGenerator::imageFrameDatas_\n+ * \\brief List of pointers to the not scaled image buffers\n+ */\n+\n+/**\n+ * \\var ImageFrameGenerator::scaledFrameDatas_\n+ * \\brief List of pointers to the scaled image buffers\n+ */\n+\n+/**\n+ * \\var ImageFrameGenerator::imageFrames_\n+ * \\brief Pointer to the imageFrames_ in VirtualCameraData\n+ */\n+\n+/**\n+ * \\var ImageFrameGenerator::parameter_\n+ * \\brief Speed parameter. Change to the next image every parameter_ frames\n+ */\n+\n+} /* namespace libcamera */\ndiff --git a/src/libcamera/pipeline/virtual/image_frame_generator.h b/src/libcamera/pipeline/virtual/image_frame_generator.h\nnew file mode 100644\nindex 00000000..4ad8aad2\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/image_frame_generator.h\n@@ -0,0 +1,54 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Google Inc.\n+ *\n+ * image_frame_generator.h - Derived class of FrameGenerator for\n+ * generating frames from images\n+ */\n+\n+#pragma once\n+\n+#include <filesystem>\n+#include <memory>\n+#include <optional>\n+#include <stdint.h>\n+#include <sys/types.h>\n+\n+#include \"frame_generator.h\"\n+\n+namespace libcamera {\n+\n+enum class ScaleMode : char {\n+\tFill = 0,\n+};\n+\n+/* Frame configuration provided by the config file */\n+struct ImageFrames {\n+\tstd::filesystem::path path;\n+\tScaleMode scaleMode;\n+\tstd::optional<unsigned int> number;\n+};\n+\n+class ImageFrameGenerator : public FrameGenerator\n+{\n+public:\n+\tstatic std::unique_ptr<ImageFrameGenerator> create(ImageFrames &imageFrames);\n+\n+private:\n+\tstruct ImageFrameData {\n+\t\tstd::unique_ptr<uint8_t[]> Y;\n+\t\tstd::unique_ptr<uint8_t[]> UV;\n+\t\tSize size;\n+\t};\n+\n+\tvoid configure(const Size &size) override;\n+\tvoid generateFrame(const Size &size, const FrameBuffer *buffer) override;\n+\n+\tstd::vector<ImageFrameData> imageFrameDatas_;\n+\tstd::vector<ImageFrameData> scaledFrameDatas_;\n+\tImageFrames *imageFrames_;\n+\tunsigned int frameCount_;\n+\tunsigned int parameter_;\n+};\n+\n+} /* namespace libcamera */\ndiff --git a/src/libcamera/pipeline/virtual/meson.build b/src/libcamera/pipeline/virtual/meson.build\nindex d72ac5be..395919b3 100644\n--- a/src/libcamera/pipeline/virtual/meson.build\n+++ b/src/libcamera/pipeline/virtual/meson.build\n@@ -1,9 +1,13 @@\n # SPDX-License-Identifier: CC0-1.0\n \n libcamera_internal_sources += files([\n+    'image_frame_generator.cpp',\n     'parser.cpp',\n     'test_pattern_generator.cpp',\n     'virtual.cpp',\n ])\n \n+libjpeg = dependency('libjpeg', required : false)\n+\n libcamera_deps += [libyuv_dep]\n+libcamera_deps += [libjpeg]\ndiff --git a/src/libcamera/pipeline/virtual/parser.cpp b/src/libcamera/pipeline/virtual/parser.cpp\nindex d861a52a..5076e71c 100644\n--- a/src/libcamera/pipeline/virtual/parser.cpp\n+++ b/src/libcamera/pipeline/virtual/parser.cpp\n@@ -52,12 +52,12 @@ Parser::parseConfigFile(File &file, PipelineHandler *pipe)\n \t\t\tcontinue;\n \t\t}\n \n-\t\tdata->id_ = cameraId;\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(int64_t(1000000 / data->supportedResolutions_[0].frameRates[1]),\n-\t\t\t\t    int64_t(1000000 / data->supportedResolutions_[0].frameRates[0]));\n+\t\t\tControlInfo(int64_t(1000000 / data->config_.resolutions[0].frameRates[1]),\n+\t\t\t\t    int64_t(1000000 / data->config_.resolutions[0].frameRates[0]));\n \t\tdata->controlInfo_ = ControlInfoMap(std::move(controls), controls::controls);\n \t\tconfigurations.push_back(std::move(data));\n \t}\n@@ -75,7 +75,8 @@ Parser::parseCameraConfigData(const YamlObject &cameraConfigData,\n \tstd::unique_ptr<VirtualCameraData> data =\n \t\tstd::make_unique<VirtualCameraData>(pipe, resolutions);\n \n-\tif (parseTestPattern(cameraConfigData, data.get()))\n+\tif (parseTestPattern(cameraConfigData, data.get()) &&\n+\t    parseFrame(cameraConfigData, data.get()))\n \t\treturn nullptr;\n \n \tif (parseLocation(cameraConfigData, data.get()))\n@@ -148,16 +149,75 @@ int Parser::parseTestPattern(const YamlObject &cameraConfigData, VirtualCameraDa\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\tdata->config_.frame = TestPattern::ColorBars;\n \t} else if (testPattern == \"lines\") {\n-\t\tdata->testPattern_ = TestPattern::DiagonalLines;\n+\t\tdata->config_.frame = TestPattern::DiagonalLines;\n \t} else {\n-\t\tLOG(Virtual, Error) << \"Test pattern: \" << testPattern\n+\t\tLOG(Virtual, Debug) << \"Test pattern: \" << testPattern\n \t\t\t\t    << \"is not supported\";\n \t\treturn -EINVAL;\n \t}\n+\n+\treturn 0;\n+}\n+\n+int Parser::parseFrame(const YamlObject &cameraConfigData, VirtualCameraData *data)\n+{\n+\tconst YamlObject &frames = cameraConfigData[\"frames\"];\n+\n+\t/* When there is no frames provided in the config file, use color bar test pattern */\n+\tif (frames.size() == 0) {\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+\tstd::string path = frames[\"path\"].get<std::string>(\"\");\n+\n+\tScaleMode scaleMode;\n+\tif (auto ext = std::filesystem::path(path).extension();\n+\t    ext == \".jpg\" || ext == \".jpeg\") {\n+\t\tif (parseScaleMode(frames, &scaleMode))\n+\t\t\treturn -EINVAL;\n+\t\tdata->config_.frame = ImageFrames{ path, scaleMode, std::nullopt };\n+\t} else if (std::filesystem::is_directory(std::filesystem::symlink_status(path))) {\n+\t\tif (parseScaleMode(frames, &scaleMode))\n+\t\t\treturn -EINVAL;\n+\n+\t\tusing std::filesystem::directory_iterator;\n+\t\tunsigned int numOfFiles = std::distance(directory_iterator(path), directory_iterator{});\n+\t\tif (numOfFiles == 0) {\n+\t\t\tLOG(Virtual, Error) << \"Empty directory\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\t\tdata->config_.frame = ImageFrames{ path, scaleMode, numOfFiles };\n+\t} else {\n+\t\tLOG(Virtual, Error) << \"Frame: \" << path << \" is not supported\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int Parser::parseScaleMode(\n+\tconst YamlObject &framesConfigData, ScaleMode *scaleMode)\n+{\n+\tstd::string mode = framesConfigData[\"scale_mode\"].get<std::string>(\"\");\n+\n+\t/* Default value is fill */\n+\tif (mode == \"fill\" || mode == \"\") {\n+\t\t*scaleMode = ScaleMode::Fill;\n+\t} else {\n+\t\tLOG(Virtual, Error) << \"scaleMode: \" << mode\n+\t\t\t\t    << \" is not supported\";\n+\t\treturn -EINVAL;\n+\t}\n+\n \treturn 0;\n }\n \ndiff --git a/src/libcamera/pipeline/virtual/parser.h b/src/libcamera/pipeline/virtual/parser.h\nindex 09c3c56b..f65616e3 100644\n--- a/src/libcamera/pipeline/virtual/parser.h\n+++ b/src/libcamera/pipeline/virtual/parser.h\n@@ -35,8 +35,10 @@ private:\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 parseFrame(const YamlObject &cameraConfigData, VirtualCameraData *data);\n \tint parseLocation(const YamlObject &cameraConfigData, VirtualCameraData *data);\n \tint parseModel(const YamlObject &cameraConfigData, VirtualCameraData *data);\n+\tint parseScaleMode(const YamlObject &framesConfigData, ScaleMode *scaleMode);\n };\n \n } /* namespace libcamera */\ndiff --git a/src/libcamera/pipeline/virtual/utils.h b/src/libcamera/pipeline/virtual/utils.h\nnew file mode 100644\nindex 00000000..43a14d4b\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/utils.h\n@@ -0,0 +1,17 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Google Inc.\n+ *\n+ * utils.h - Utility types for Virtual Pipeline Handler\n+ */\n+\n+namespace libcamera {\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+} /* namespace libcamera */\ndiff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp\nindex 55bc30df..98aed412 100644\n--- a/src/libcamera/pipeline/virtual/virtual.cpp\n+++ b/src/libcamera/pipeline/virtual/virtual.cpp\n@@ -27,6 +27,7 @@\n #include \"libcamera/internal/yaml_parser.h\"\n \n #include \"parser.h\"\n+#include \"utils.h\"\n \n namespace libcamera {\n \n@@ -49,17 +50,18 @@ uint64_t currentTimestamp()\n \n VirtualCameraData::VirtualCameraData(PipelineHandler *pipe,\n \t\t\t\t     std::vector<Resolution> supportedResolutions)\n-\t: Camera::Private(pipe), supportedResolutions_(std::move(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 = std::move(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(maxResolutionSize_) });\n+\t\t\t{ Rectangle(config_.maxResolutionSize) });\n \n \t/* \\todo Support multiple streams and pass multi_stream_test */\n \tstreamConfigs_.resize(kMaxStream);\n@@ -87,7 +89,7 @@ CameraConfiguration::Status VirtualCameraConfiguration::validate()\n \n \tfor (StreamConfiguration &cfg : config_) {\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@@ -102,7 +104,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}\n \n@@ -145,11 +147,11 @@ PipelineHandlerVirtual::generateConfiguration(Camera *camera,\n \tfor (const StreamRole role : roles) {\n \t\tstd::map<PixelFormat, std::vector<SizeRange>> streamFormats;\n \t\tPixelFormat pixelFormat = formats::NV12;\n-\t\tstreamFormats[pixelFormat] = { { data->minResolutionSize_, data->maxResolutionSize_ } };\n+\t\tstreamFormats[pixelFormat] = { { data->config_.minResolutionSize, 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\tswitch (role) {\n@@ -181,6 +183,7 @@ int PipelineHandlerVirtual::configure(Camera *camera,\n \tVirtualCameraData *data = cameraData(camera);\n \tfor (size_t i = 0; i < config->size(); ++i) {\n \t\tconfig->at(i).setStream(&data->streamConfigs_[i].stream);\n+\t\t/* Start reading the images/generating test patterns */\n \t\tdata->streamConfigs_[i].frameGenerator->configure(\n \t\t\tdata->streamConfigs_[i].stream.configuration().size);\n \t}\n@@ -274,10 +277,14 @@ bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator\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::string id = data->config_.id;\n \t\tstd::shared_ptr<Camera> camera = Camera::create(std::move(data), id, streams);\n \n-\t\tinitFrameGenerator(camera.get());\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 \t\tregisterCamera(std::move(camera));\n \t}\n@@ -285,15 +292,30 @@ bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator\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 8830e00f..efa97e88 100644\n--- a/src/libcamera/pipeline/virtual/virtual.h\n+++ b/src/libcamera/pipeline/virtual/virtual.h\n@@ -8,6 +8,8 @@\n #pragma once\n \n #include <memory>\n+#include <string>\n+#include <variant>\n #include <vector>\n \n #include <libcamera/base/file.h>\n@@ -16,10 +18,14 @@\n #include \"libcamera/internal/dma_buf_allocator.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@@ -33,18 +39,22 @@ public:\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  std::vector<Resolution> supportedResolutions);\n \n \t~VirtualCameraData() = default;\n \n-\tstd::string id_;\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@@ -89,7 +99,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",
    "prefixes": [
        "v11",
        "6/7"
    ]
}