<div dir="ltr"><div dir="ltr">Hi Jacopo,</div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Sat, Aug 31, 2024 at 8:44 PM Jacopo Mondi <<a href="mailto:jacopo.mondi@ideasonboard.com" target="_blank">jacopo.mondi@ideasonboard.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">Hi<br>
<br>
On Thu, Aug 29, 2024 at 09:57:58PM GMT, Cheng-Hao Yang wrote:<br>
> Thanks for the review, Jacopo!<br>
><br>
> On Tue, Aug 27, 2024 at 5:43 PM Jacopo Mondi <<a href="mailto:jacopo.mondi@ideasonboard.com" target="_blank">jacopo.mondi@ideasonboard.com</a>><br>
> wrote:<br>
><br>
> > Hi Harvey<br>
> ><br>
> > On Tue, Aug 20, 2024 at 04:23:34PM GMT, Harvey Yang wrote:<br>
> > > From: Harvey Yang <<a href="mailto:chenghaoyang@chromium.org" target="_blank">chenghaoyang@chromium.org</a>><br>
> > ><br>
> > > Add VirtualPipelineHandler for more unit tests and verfiy libcamera<br>
> > > infrastructure works on devices without using hardware cameras.<br>
> > ><br>
> > > Signed-off-by: Harvey Yang <<a href="mailto:chenghaoyang@chromium.org" target="_blank">chenghaoyang@chromium.org</a>><br>
> > > ---<br>
> > >  meson.build                                |   1 +<br>
> > >  meson_options.txt                          |   3 +-<br>
> > >  src/libcamera/pipeline/virtual/meson.build |   5 +<br>
> > >  src/libcamera/pipeline/virtual/virtual.cpp | 251 +++++++++++++++++++++<br>
> > >  src/libcamera/pipeline/virtual/virtual.h   |  78 +++++++<br>
> ><br>
> > Do you expect other components to include this header in future ? If<br>
> > not, you can move its content to the .cpp file I guess<br>
> ><br>
> ><br>
> Actually `virtual/parser.h` needs to include it to return VirtualCameraData<br>
> when parsing the yaml config file [1]. Does that count :p?<br>
<br>
I guess it does :)<br>
<br>
><br>
> [1]: <a href="https://patchwork.libcamera.org/patch/20971/" rel="noreferrer" target="_blank">https://patchwork.libcamera.org/patch/20971/</a><br>
><br>
> >  5 files changed, 337 insertions(+), 1 deletion(-)<br>
> > >  create mode 100644 src/libcamera/pipeline/virtual/meson.build<br>
> > >  create mode 100644 src/libcamera/pipeline/virtual/virtual.cpp<br>
> > >  create mode 100644 src/libcamera/pipeline/virtual/virtual.h<br>
> > ><br>
> > > diff --git a/meson.build b/meson.build<br>
> > > index f946eba94..3cad3249a 100644<br>
> > > --- a/meson.build<br>
> > > +++ b/meson.build<br>
> > > @@ -222,6 +222,7 @@ pipelines_support = {<br>
> > >      'simple':       arch_arm,<br>
> > >      'uvcvideo':     ['any'],<br>
> > >      'vimc':         ['test'],<br>
> > > +    'virtual':      ['test'],<br>
> > >  }<br>
> > ><br>
> > >  if pipelines.contains('all')<br>
> > > diff --git a/meson_options.txt b/meson_options.txt<br>
> > > index 7aa412491..c91cd241a 100644<br>
> > > --- a/meson_options.txt<br>
> > > +++ b/meson_options.txt<br>
> > > @@ -53,7 +53,8 @@ option('pipelines',<br>
> > >              'rpi/vc4',<br>
> > >              'simple',<br>
> > >              'uvcvideo',<br>
> > > -            'vimc'<br>
> > > +            'vimc',<br>
> > > +            'virtual'<br>
> > >          ],<br>
> > >          description : 'Select which pipeline handlers to build. If this<br>
> > is set to "auto", all the pipelines applicable to the target architecture<br>
> > will be built. If this is set to "all", all the pipelines will be built. If<br>
> > both are selected then "all" will take precedence.')<br>
> > ><br>
> > > diff --git a/src/libcamera/pipeline/virtual/meson.build<br>
> > b/src/libcamera/pipeline/virtual/meson.build<br>
> > > new file mode 100644<br>
> > > index 000000000..ba7ff754e<br>
> > > --- /dev/null<br>
> > > +++ b/src/libcamera/pipeline/virtual/meson.build<br>
> > > @@ -0,0 +1,5 @@<br>
> > > +# SPDX-License-Identifier: CC0-1.0<br>
> > > +<br>
> > > +libcamera_sources += files([<br>
> > > +    'virtual.cpp',<br>
> > > +])<br>
> > > diff --git a/src/libcamera/pipeline/virtual/virtual.cpp<br>
> > b/src/libcamera/pipeline/virtual/virtual.cpp<br>
> > > new file mode 100644<br>
> > > index 000000000..74eb8c7ad<br>
> > > --- /dev/null<br>
> > > +++ b/src/libcamera/pipeline/virtual/virtual.cpp<br>
> > > @@ -0,0 +1,251 @@<br>
> > > +/* SPDX-License-Identifier: LGPL-2.1-or-later */<br>
> > > +/*<br>
> > > + * Copyright (C) 2023, Google Inc.<br>
> > > + *<br>
> > > + * virtual.cpp - Pipeline handler for virtual cameras<br>
> > > + */<br>
> > > +<br>
> ><br>
> > You should include the header for the standard library construct you<br>
> > use. I see vectors, maps, unique_ptrs etc<br>
> ><br>
> Done, please check again.<br>
><br>
><br>
> ><br>
> > > +#include "virtual.h"<br>
> > > +<br>
> > > +#include <libcamera/base/log.h><br>
> > > +<br>
> > > +#include <libcamera/camera.h><br>
> > > +#include <libcamera/control_ids.h><br>
> > > +#include <libcamera/controls.h><br>
> > > +#include <libcamera/formats.h><br>
> > > +#include <libcamera/property_ids.h><br>
> > > +<br>
> > > +#include "libcamera/internal/camera.h"<br>
> ><br>
> > The internal header includes the public one by definition<br>
> ><br>
> Ack. Removed the public one.<br>
><br>
><br>
> ><br>
> > > +#include "libcamera/internal/formats.h"<br>
> ><br>
> > This doesn't as <libcamera/formats.h> is generated. I wonder if it<br>
> > should.<br>
> ><br>
> Keeping `#include <libcamera/formats.h>`.<br>
><br>
><br>
> ><br>
> > > +#include "libcamera/internal/pipeline_handler.h"<br>
> > > +<br>
> > > +namespace libcamera {<br>
> > > +<br>
> > > +LOG_DEFINE_CATEGORY(Virtual)<br>
> > > +<br>
> > > +namespace {<br>
> > > +<br>
> > > +uint64_t currentTimestamp()<br>
> > > +{<br>
> > > +     struct timespec ts;<br>
> > > +     if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {<br>
> > > +             LOG(Virtual, Error) << "Get clock time fails";<br>
> > > +             return 0;<br>
> > > +     }<br>
> > > +<br>
> > > +     return ts.tv_sec * 1'000'000'000LL + ts.tv_nsec;<br>
> > > +}<br>
> ><br>
> > Could <a href="https://en.cppreference.com/w/cpp/chrono/steady_clock/now" rel="noreferrer" target="_blank">https://en.cppreference.com/w/cpp/chrono/steady_clock/now</a> save<br>
> > you a custom function ?<br>
> ><br>
> > In example:<br>
> ><br>
> >         const auto now = std::chrono::steady_clock::now();<br>
> >         auto nsecs =<br>
> > std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch());<br>
> >         std::cout << nsecs.count();<br>
> ><br>
> > should give you the time in nanoseconds since system boot (if I got it<br>
> > right)<br>
> ><br>
> ><br>
> > > +<br>
> > > +} // namespace<br>
> ><br>
> > nit: /* namespace */<br>
> > here and in other places<br>
> ><br>
> > Done<br>
><br>
><br>
> > > +<br>
> > ><br>
> > +VirtualCameraConfiguration::VirtualCameraConfiguration(VirtualCameraData<br>
> > *data)<br>
> > > +     : CameraConfiguration(), data_(data)<br>
> > > +{<br>
> > > +}<br>
> > > +<br>
> > > +CameraConfiguration::Status VirtualCameraConfiguration::validate()<br>
> > > +{<br>
> > > +     Status status = Valid;<br>
> > > +<br>
> > > +     if (config_.empty()) {<br>
> > > +             LOG(Virtual, Error) << "Empty config";<br>
> > > +             return Invalid;<br>
> > > +     }<br>
> > > +<br>
> > > +     /* Currently only one stream is supported */<br>
> > > +     if (config_.size() > 1) {<br>
> > > +             config_.resize(1);<br>
> > > +             status = Adjusted;<br>
> > > +     }<br>
> > > +<br>
> > > +     Size maxSize;<br>
> > > +     for (const auto &resolution : data_->supportedResolutions_)<br>
> > > +             maxSize = std::max(maxSize, resolution.size);<br>
> > > +<br>
> > > +     for (StreamConfiguration &cfg : config_) {<br>
> ><br>
> > you only have config, or in the next patches will this be augmented ?<br>
> ><br>
> > Do you mean that I should check `sensorConfig` or `orientation` as well?<br>
><br>
<br>
No I meant I was assuming the virtual pipline works with a single<br>
stream by design. But seeing your replies to the next patches makes me<br>
think my assumption was wrong.<br>
<br>
> > +             bool found = false;<br>
> > > +             for (const auto &resolution :<br>
> > data_->supportedResolutions_) {<br>
> > > +                     if (resolution.size.width == cfg.size.width &&<br>
> > > +                         resolution.size.height == cfg.size.height) {<br>
> > > +                             found = true;<br>
> > > +                             break;<br>
> > > +                     }<br>
> > > +             }<br>
> > > +<br>
> > > +             if (!found) {<br>
> > > +                     cfg.size = maxSize;<br>
> > > +                     status = Adjusted;<br>
> > > +             }<br>
> ><br>
> > so it's either the exact resolution or the biggest available one ?<br>
> ><br>
> > As you have a single config it's easy to get the closest one to the<br>
> > requested size, if it doesn't match exactly one of the supported<br>
> > resolutions.<br>
> ><br>
><br>
> Hmm, I think it's a bit hard to define the "closest". Do we compare<br>
> the area size directly? Do we prefer a size that has both larger or<br>
> the same height and width?<br>
><br>
<br>
Good question, it's generally a pipeline decision (which is not<br>
optimal, as we should aim to a unified behaviour among pipelines).<br>
<br>
As this is for testing, I think it's fine to keep what you have, but<br>
could you add a comment to highlight this implementation decision ?<br>
<br></blockquote><div><br></div><div>Sure, will be updated in v11. Please take another look.</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
><br>
> ><br>
> > > +<br>
> > > +             const PixelFormatInfo &info =<br>
> > PixelFormatInfo::info(cfg.pixelFormat);<br>
> > > +             cfg.stride = info.stride(cfg.size.width, 0, 1);<br>
> > > +             cfg.frameSize = info.frameSize(cfg.size, 1);<br>
> > > +<br>
> > > +             cfg.setStream(const_cast<Stream *>(&data_->stream_));<br>
> > > +<br>
> > > +             cfg.bufferCount = VirtualCameraConfiguration::kBufferCount;<br>
> > > +     }<br>
> > > +<br>
> > > +     return status;<br>
> > > +}<br>
> > > +<br>
> > > +PipelineHandlerVirtual::PipelineHandlerVirtual(CameraManager *manager)<br>
> > > +     : PipelineHandler(manager)<br>
> > > +{<br>
> > > +}<br>
> > > +<br>
> > > +std::unique_ptr<CameraConfiguration><br>
> > > +PipelineHandlerVirtual::generateConfiguration(Camera *camera,<br>
> > > +                                           Span<const StreamRole> roles)<br>
> > > +{<br>
> > > +     VirtualCameraData *data = cameraData(camera);<br>
> > > +     auto config =<br>
> > > +             std::make_unique<VirtualCameraConfiguration>(data);<br>
> > > +<br>
> > > +     if (roles.empty())<br>
> > > +             return config;<br>
> > > +<br>
> > > +     Size minSize, sensorResolution;<br>
> > > +     for (const auto &resolution : data->supportedResolutions_) {<br>
> > > +             if (minSize.isNull() || minSize > resolution.size)<br>
> > > +                     minSize = resolution.size;<br>
> > > +<br>
> > > +             sensorResolution = std::max(sensorResolution,<br>
> > resolution.size);<br>
> ><br>
> > As you do this min/max search in a few places, why not doing it when<br>
> > you construct  data->supportedResolutions_ once ?<br>
> ><br>
> Added in VirtualCameraData.<br>
><br>
><br>
> ><br>
> > > +     }<br>
> > > +<br>
> > > +     for (const StreamRole role : roles) {<br>
> ><br>
> > If the pipeline handler works with a single stream, you should only<br>
> > consider the first role maybe<br>
> ><br>
> Actually I think there's no reason to only support one Stream in<br>
> Virtual Pipeline Handler. The raw stream doesn't seem to be<br>
> properly supported in the following patches though. I think we<br>
> should drop the support of raw.<br>
><br>
<br>
I assumed (maybe from a previous discussion) you were going to have a<br>
single stream. I'm sorry, I was wrong.<br></blockquote><div><br></div><div>Nah, I prepared for multiple streams, but haven't enabled it yet :p.</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
<br>
Please drop RAW, yes.<br></blockquote><div><br></div><div>Done in v10.</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
<br>
><br>
> ><br>
> > > +             std::map<PixelFormat, std::vector<SizeRange>><br>
> > streamFormats;<br>
> > > +             unsigned int bufferCount;<br>
> > > +             PixelFormat pixelFormat;<br>
> > > +<br>
> > > +             switch (role) {<br>
> > > +             case StreamRole::StillCapture:<br>
> > > +                     pixelFormat = formats::NV12;<br>
> > > +                     bufferCount =<br>
> > VirtualCameraConfiguration::kBufferCount;<br>
> > > +                     streamFormats[pixelFormat] = { { minSize,<br>
> > sensorResolution } };<br>
> ><br>
> > bufferCount and streamFormats can be assigned outsize of the cases,<br>
> > they're the same for all roles<br>
> ><br>
> Done<br>
><br>
><br>
> ><br>
> > > +<br>
> > > +                     break;<br>
> > > +<br>
> > > +             case StreamRole::Raw: {<br>
> > > +                     /* \todo check */<br>
> > > +                     pixelFormat = formats::SBGGR10;<br>
> > > +                     bufferCount =<br>
> > VirtualCameraConfiguration::kBufferCount;<br>
> > > +                     streamFormats[pixelFormat] = { { minSize,<br>
> > sensorResolution } };<br>
> > > +<br>
> > > +                     break;<br>
> > > +             }<br>
> > > +<br>
> > > +             case StreamRole::Viewfinder:<br>
> > > +             case StreamRole::VideoRecording: {<br>
> > > +                     pixelFormat = formats::NV12;<br>
> > > +                     bufferCount =<br>
> > VirtualCameraConfiguration::kBufferCount;<br>
> > > +                     streamFormats[pixelFormat] = { { minSize,<br>
> > sensorResolution } };<br>
> > > +<br>
> > > +                     break;<br>
> > > +             }<br>
> > > +<br>
> > > +             default:<br>
> > > +                     LOG(Virtual, Error)<br>
> > > +                             << "Requested stream role not supported: "<br>
> > << role;<br>
> > > +                     config.reset();<br>
> > > +                     return config;<br>
> > > +             }<br>
> > > +<br>
> > > +             StreamFormats formats(streamFormats);<br>
> > > +             StreamConfiguration cfg(formats);<br>
> > > +             cfg.size = sensorResolution;<br>
> > > +             cfg.pixelFormat = pixelFormat;<br>
> > > +             cfg.bufferCount = bufferCount;<br>
> > > +             config->addConfiguration(cfg);<br>
> > > +     }<br>
> > > +<br>
> > > +     if (config->validate() == CameraConfiguration::Invalid)<br>
> > > +             config.reset();<br>
> > > +<br>
> > > +     return config;<br>
> > > +}<br>
> > > +<br>
> > > +int PipelineHandlerVirtual::configure(<br>
> > > +     [[maybe_unused]] Camera *camera,<br>
> > > +     [[maybe_unused]] CameraConfiguration *config)<br>
> ><br>
> > int PipelineHandlerVirtual::configure([[maybe_unused]] Camera *camera,<br>
> >                                      [[maybe_unused]] CameraConfiguration<br>
> > *config)<br>
> ><br>
> > Done<br>
><br>
><br>
> > > +{<br>
> > > +     // Nothing to be done.<br>
> > > +     return 0;<br>
> > > +}<br>
> > > +<br>
> > > +int PipelineHandlerVirtual::exportFrameBuffers(<br>
> > > +     [[maybe_unused]] Camera *camera,<br>
> > > +     Stream *stream,<br>
> > > +     std::vector<std::unique_ptr<FrameBuffer>> *buffers)<br>
> ><br>
> > int PipelineHandlerVirtual::exportFrameBuffers([[maybe_unused]] Camera<br>
> > *camera,<br>
> >                                                Stream *stream,<br>
> ><br>
> >  std::vector<std::unique_ptr<FrameBuffer>> *buffers)<br>
> ><br>
> > if you prefer<br>
> ><br>
> > Done<br>
><br>
><br>
> > > +{<br>
> > > +     if (!dmaBufAllocator_.isValid())<br>
> > > +             return -ENOBUFS;<br>
> > > +<br>
> > > +     const StreamConfiguration &config = stream->configuration();<br>
> > > +<br>
> > > +     auto info = PixelFormatInfo::info(config.pixelFormat);<br>
> > > +<br>
> > > +     std::vector<std::size_t> planeSizes;<br>
> > > +     for (size_t i = 0; i < info.planes.size(); ++i)<br>
> > > +             planeSizes.push_back(info.planeSize(config.size, i));<br>
> > > +<br>
> > > +     return dmaBufAllocator_.exportBuffers(config.bufferCount,<br>
> > planeSizes, buffers);<br>
> ><br>
> > ah that's probably why you return count from<br>
> > DmaBufferAllocator::exportBuffers()<br>
> ><br>
> Exactly :)<br>
><br>
><br>
> ><br>
> > > +}<br>
> > > +<br>
> > > +int PipelineHandlerVirtual::start([[maybe_unused]] Camera *camera,<br>
> > > +                               [[maybe_unused]] const ControlList<br>
> > *controls)<br>
> > > +{<br>
> > > +     /* \todo Start reading the virtual video if any. */<br>
> > > +     return 0;<br>
> > > +}<br>
> > > +<br>
> > > +void PipelineHandlerVirtual::stopDevice([[maybe_unused]] Camera *camera)<br>
> > > +{<br>
> > > +     /* \todo Reset the virtual video if any. */<br>
> > > +}<br>
> > > +<br>
> > > +int PipelineHandlerVirtual::queueRequestDevice([[maybe_unused]] Camera<br>
> > *camera,<br>
> > > +                                            Request *request)<br>
> > > +{<br>
> > > +     /* \todo Read from the virtual video if any. */<br>
> > > +     for (auto it : request->buffers())<br>
> > > +             completeBuffer(request, it.second);<br>
> > > +<br>
> > > +     request->metadata().set(controls::SensorTimestamp,<br>
> > currentTimestamp());<br>
> > > +     completeRequest(request);<br>
> > > +<br>
> > > +     return 0;<br>
> > > +}<br>
> > > +<br>
> > > +bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator<br>
> > *enumerator)<br>
> > > +{<br>
> > > +     /* \todo Add virtual cameras according to a config file. */<br>
> > > +<br>
> > > +     std::unique_ptr<VirtualCameraData> data =<br>
> > std::make_unique<VirtualCameraData>(this);<br>
> > > +<br>
> > > +     data->supportedResolutions_.resize(2);<br>
> > > +     data->supportedResolutions_[0] = { .size = Size(1920, 1080),<br>
> > .frame_rates = { 30 } };<br>
> > > +     data->supportedResolutions_[1] = { .size = Size(1280, 720),<br>
> > .frame_rates = { 30, 60 } };<br>
> > > +<br>
> > > +     data->properties_.set(properties::Location,<br>
> > properties::CameraLocationFront);<br>
> > > +     data->properties_.set(properties::Model, "Virtual Video Device");<br>
> > > +     data->properties_.set(properties::PixelArrayActiveAreas, {<br>
> > Rectangle(Size(1920, 1080)) });<br>
> > > +<br>
> > > +     /* \todo Set FrameDurationLimits based on config. */<br>
> > > +     ControlInfoMap::Map controls;<br>
> > > +     int64_t min_frame_duration = 30, max_frame_duration = 60;<br>
> ><br>
> > doesn't match the above frame rates and it should be expressed in<br>
> > microseconds. I would suggest for this patch to set both framerates to<br>
> > 30 and initialize FrameDurationLimits with {33333, 333333}<br>
> ><br>
> > It's not a big deal however, it will be replaced later in the series<br>
> ><br>
> ><br>
> Done, thanks!<br>
><br>
><br>
> ><br>
> > > +     controls[&controls::FrameDurationLimits] =<br>
> > ControlInfo(min_frame_duration, max_frame_duration);<br>
> > > +     data->controlInfo_ = ControlInfoMap(std::move(controls),<br>
> > controls::controls);<br>
> > > +<br>
> > > +     /* Create and register the camera. */<br>
> > > +     std::set<Stream *> streams{ &data->stream_ };<br>
> > > +     const std::string id = "Virtual0";<br>
> > > +     std::shared_ptr<Camera> camera = Camera::create(std::move(data),<br>
> > id, streams);<br>
> > > +     registerCamera(std::move(camera));<br>
> > > +<br>
> > > +     return false; // Prevent infinite loops for now<br>
> ><br>
> > I presume this will also be changed in next patches...<br>
> ><br>
> Updated in this patch, with a static boolean though.<br>
><br>
><br>
> ><br>
> > > +}<br>
> > > +<br>
> > > +REGISTER_PIPELINE_HANDLER(PipelineHandlerVirtual, "virtual")<br>
> > > +<br>
> > > +} /* namespace libcamera */<br>
> > > diff --git a/src/libcamera/pipeline/virtual/virtual.h<br>
> > b/src/libcamera/pipeline/virtual/virtual.h<br>
> > > new file mode 100644<br>
> > > index 000000000..6fc6b34d8<br>
> > > --- /dev/null<br>
> > > +++ b/src/libcamera/pipeline/virtual/virtual.h<br>
> > > @@ -0,0 +1,78 @@<br>
> > > +/* SPDX-License-Identifier: LGPL-2.1-or-later */<br>
> > > +/*<br>
> > > + * Copyright (C) 2023, Google Inc.<br>
> > > + *<br>
> > > + * virtual.h - Pipeline handler for virtual cameras<br>
> > > + */<br>
> > > +<br>
> > > +#pragma once<br>
> > > +<br>
> > > +#include <libcamera/base/file.h><br>
> > > +<br>
> > > +#include "libcamera/internal/camera.h"<br>
> > > +#include "libcamera/internal/dma_buf_allocator.h"<br>
> > > +#include "libcamera/internal/pipeline_handler.h"<br>
> > > +<br>
> > > +namespace libcamera {<br>
> > > +<br>
> > > +class VirtualCameraData : public Camera::Private<br>
> > > +{<br>
> > > +public:<br>
> > > +     struct Resolution {<br>
> > > +             Size size;<br>
> > > +             std::vector<int> frame_rates;<br>
> > > +     };<br>
> > > +     VirtualCameraData(PipelineHandler *pipe)<br>
> > > +             : Camera::Private(pipe)<br>
> > > +     {<br>
> > > +     }<br>
> > > +<br>
> > > +     ~VirtualCameraData() = default;<br>
> > > +<br>
> > > +     std::vector<Resolution> supportedResolutions_;<br>
> > > +<br>
> > > +     Stream stream_;<br>
> > > +};<br>
> > > +<br>
> > > +class VirtualCameraConfiguration : public CameraConfiguration<br>
> > > +{<br>
> > > +public:<br>
> > > +     static constexpr unsigned int kBufferCount = 4;<br>
> > > +<br>
> > > +     VirtualCameraConfiguration(VirtualCameraData *data);<br>
> > > +<br>
> > > +     Status validate() override;<br>
> > > +<br>
> > > +private:<br>
> > > +     const VirtualCameraData *data_;<br>
> > > +};<br>
> > > +<br>
> > > +class PipelineHandlerVirtual : public PipelineHandler<br>
> > > +{<br>
> > > +public:<br>
> > > +     PipelineHandlerVirtual(CameraManager *manager);<br>
> > > +<br>
> > > +     std::unique_ptr<CameraConfiguration> generateConfiguration(Camera<br>
> > *camera,<br>
> > > +<br>
> > Span<const StreamRole> roles) override;<br>
> > > +     int configure(Camera *camera, CameraConfiguration *config)<br>
> > override;<br>
> > > +<br>
> > > +     int exportFrameBuffers(Camera *camera, Stream *stream,<br>
> > > +                            std::vector<std::unique_ptr<FrameBuffer>><br>
> > *buffers) override;<br>
> > > +<br>
> > > +     int start(Camera *camera, const ControlList *controls) override;<br>
> > > +     void stopDevice(Camera *camera) override;<br>
> > > +<br>
> > > +     int queueRequestDevice(Camera *camera, Request *request) override;<br>
> > > +<br>
> > > +     bool match(DeviceEnumerator *enumerator) override;<br>
> > > +<br>
> > > +private:<br>
> > > +     VirtualCameraData *cameraData(Camera *camera)<br>
> > > +     {<br>
> > > +             return static_cast<VirtualCameraData *>(camera->_d());<br>
> > > +     }<br>
> > > +<br>
> > > +     DmaBufAllocator dmaBufAllocator_;<br>
> > > +};<br>
> > > +<br>
> > > +} // namespace libcamera<br>
> > > --<br>
> > > 2.46.0.184.g6999bdac58-goog<br>
> > ><br>
> ><br>
</blockquote></div></div>