libcamera: internal: Add MediaPipeline helper

Provide a MediaPipeline class to help identifing and managing pipelines across
a MediaDevice graph.

Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Reviewed-by: Umang Jain <umang.jain@ideasonboard.com>
Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com>
This commit is contained in:
Kieran Bingham 2025-04-02 16:39:17 +09:00 committed by Paul Elder
parent 0785f5f99a
commit f1721c2f9f
4 changed files with 365 additions and 0 deletions

View file

@ -0,0 +1,59 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2024, Ideas on Board Oy
*
* Media pipeline support
*/
#pragma once
#include <list>
#include <string>
#include <libcamera/base/log.h>
namespace libcamera {
class CameraSensor;
class MediaEntity;
class MediaLink;
class MediaPad;
struct V4L2SubdeviceFormat;
class MediaPipeline
{
public:
int init(MediaEntity *source, std::string_view sink);
int initLinks();
int configure(CameraSensor *sensor, V4L2SubdeviceFormat *);
private:
struct Entity {
/* The media entity, always valid. */
MediaEntity *entity;
/*
* Whether or not the entity is a subdev that supports the
* routing API.
*/
bool supportsRouting;
/*
* The local sink pad connected to the upstream entity, null for
* the camera sensor at the beginning of the pipeline.
*/
const MediaPad *sink;
/*
* The local source pad connected to the downstream entity, null
* for the video node at the end of the pipeline.
*/
const MediaPad *source;
/*
* The link on the source pad, to the downstream entity, null
* for the video node at the end of the pipeline.
*/
MediaLink *sourceLink;
};
std::list<Entity> entities_;
};
} /* namespace libcamera */

View file

@ -32,6 +32,7 @@ libcamera_internal_headers = files([
'matrix.h', 'matrix.h',
'media_device.h', 'media_device.h',
'media_object.h', 'media_object.h',
'media_pipeline.h',
'pipeline_handler.h', 'pipeline_handler.h',
'process.h', 'process.h',
'pub_key.h', 'pub_key.h',

View file

@ -0,0 +1,304 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2024, Ideas on Board Oy
*
* Media pipeline support
*/
#include "libcamera/internal/media_pipeline.h"
#include <algorithm>
#include <errno.h>
#include <queue>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <linux/media.h>
#include <libcamera/base/log.h>
#include "libcamera/internal/camera_sensor.h"
#include "libcamera/internal/media_device.h"
#include "libcamera/internal/media_object.h"
#include "libcamera/internal/v4l2_subdevice.h"
/**
* \file media_pipeline.h
* \brief Provide a representation of a pipeline of devices using the Media
* Controller.
*/
namespace libcamera {
LOG_DEFINE_CATEGORY(MediaPipeline)
/**
* \class MediaPipeline
* \brief The MediaPipeline represents a set of entities that together form a
* data path for stream data.
*
* A MediaPipeline instance is constructed from a sink and a source between
* two entities in a media graph.
*/
/**
* \brief Retrieve all source pads connected to a sink pad through active routes
*
* Examine the entity using the V4L2 Subdevice Routing API to collect all the
* source pads which are connected with an active route to the sink pad.
*
* \return A vector of source MediaPads
*/
static std::vector<const MediaPad *> routedSourcePads(MediaPad *sink)
{
MediaEntity *entity = sink->entity();
std::unique_ptr<V4L2Subdevice> subdev =
std::make_unique<V4L2Subdevice>(entity);
int ret = subdev->open();
if (ret < 0)
return {};
V4L2Subdevice::Routing routing = {};
ret = subdev->getRouting(&routing, V4L2Subdevice::ActiveFormat);
if (ret < 0)
return {};
std::vector<const MediaPad *> pads;
for (const V4L2Subdevice::Route &route : routing) {
if (sink->index() != route.sink.pad ||
!(route.flags & V4L2_SUBDEV_ROUTE_FL_ACTIVE))
continue;
const MediaPad *pad = entity->getPadByIndex(route.source.pad);
if (!pad) {
LOG(MediaPipeline, Error)
<< "Entity " << entity->name()
<< " has invalid route source pad "
<< route.source.pad;
return {};
}
pads.push_back(pad);
}
return pads;
}
/**
* \brief Find the path from source to sink
*
* Starting from a source entity, determine the shortest path to the target
* described by \a sink.
*
* If \a sink can not be found, or a route from source to sink can not be
* achieved an error of -ENOLINK will be returned.
*
* When successful, the MediaPipeline will internally store the representation
* of entities and links to describe the path between the two entities.
*
* \return 0 on success, a negative errno otherwise
*/
int MediaPipeline::init(MediaEntity *source, std::string_view sink)
{
/*
* Find the shortest path between from the Camera Sensor and the
* target entity.
*/
std::unordered_set<MediaEntity *> visited;
std::queue<std::tuple<MediaEntity *, MediaPad *>> queue;
/* Remember at each entity where we came from. */
std::unordered_map<MediaEntity *, Entity> parents;
MediaEntity *entity = nullptr;
MediaEntity *target = nullptr;
MediaPad *sinkPad;
queue.push({ source, nullptr });
while (!queue.empty()) {
std::tie(entity, sinkPad) = queue.front();
queue.pop();
/* Found the target device. */
if (entity->name() == sink) {
LOG(MediaPipeline, Debug)
<< "Found Pipeline target " << entity->name();
target = entity;
break;
}
visited.insert(entity);
/*
* Add direct downstream entities to the search queue. If the
* current entity supports the subdev internal routing API,
* restrict the search to downstream entities reachable through
* active routes.
*/
std::vector<const MediaPad *> pads;
bool supportsRouting = false;
if (sinkPad) {
pads = routedSourcePads(sinkPad);
if (!pads.empty())
supportsRouting = true;
}
if (pads.empty()) {
for (const MediaPad *pad : entity->pads()) {
if (!(pad->flags() & MEDIA_PAD_FL_SOURCE))
continue;
pads.push_back(pad);
}
}
for (const MediaPad *pad : pads) {
for (MediaLink *link : pad->links()) {
MediaEntity *next = link->sink()->entity();
if (visited.find(next) == visited.end()) {
queue.push({ next, link->sink() });
Entity e{ entity, supportsRouting, sinkPad, pad, link };
parents.insert({ next, e });
}
}
}
}
if (!target) {
LOG(MediaPipeline, Error)
<< "Failed to connect " << source->name()
<< " to " << sink;
return -ENOLINK;
}
/*
* With the parents, we can follow back our way from the capture device
* to the sensor. Store all the entities in the pipeline, from the
* camera sensor to the video node, in entities_.
*/
entities_.push_front({ entity, false, sinkPad, nullptr, nullptr });
for (auto it = parents.find(entity); it != parents.end();
it = parents.find(entity)) {
const Entity &e = it->second;
entities_.push_front(e);
entity = e.entity;
}
LOG(MediaPipeline, Info)
<< "Found pipeline: "
<< utils::join(entities_, " -> ",
[](const Entity &e) {
std::string s = "[";
if (e.sink)
s += std::to_string(e.sink->index()) + "|";
s += e.entity->name();
if (e.source)
s += "|" + std::to_string(e.source->index());
s += "]";
return s;
});
return 0;
}
/**
* \brief Initialise and enable all links through the MediaPipeline
* \return 0 on success, or a negative errno otherwise
*/
int MediaPipeline::initLinks()
{
int ret = 0;
MediaLink *sinkLink = nullptr;
for (Entity &e : entities_) {
/* Sensor entities have no connected sink. */
if (!sinkLink) {
sinkLink = e.sourceLink;
continue;
}
LOG(MediaPipeline, Debug) << "Enabling : " << *sinkLink;
if (!(sinkLink->flags() & MEDIA_LNK_FL_ENABLED)) {
ret = sinkLink->setEnabled(true);
if (ret < 0)
return ret;
}
sinkLink = e.sourceLink;
}
return ret;
}
/**
* \brief Configure the entities of this MediaPipeline
*
* Propagate formats through each of the entities of the Pipeline, validating
* that each one was not adjusted by the driver from the desired format.
*
* \return 0 on success or a negative errno otherwise
*/
int MediaPipeline::configure(CameraSensor *sensor, V4L2SubdeviceFormat *format)
{
int ret;
for (const Entity &e : entities_) {
/* The sensor is configured through the CameraSensor */
if (!e.sourceLink)
break;
MediaLink *link = e.sourceLink;
MediaPad *source = link->source();
MediaPad *sink = link->sink();
/* 'format' already contains the sensor configuration */
if (source->entity() != sensor->entity()) {
/* \todo Add MediaDevice cache to reduce FD pressure */
V4L2Subdevice subdev(source->entity());
ret = subdev.open();
if (ret)
return ret;
ret = subdev.getFormat(source->index(), format);
if (ret < 0)
return ret;
}
V4L2SubdeviceFormat sourceFormat = *format;
/* \todo Add MediaDevice cache to reduce FD pressure */
V4L2Subdevice subdev(sink->entity());
ret = subdev.open();
if (ret)
return ret;
ret = subdev.setFormat(sink->index(), format);
if (ret < 0)
return ret;
if (format->code != sourceFormat.code ||
format->size != sourceFormat.size) {
LOG(MediaPipeline, Debug)
<< "Source '" << *source
<< " produces " << sourceFormat
<< ", sink '" << *sink
<< " requires " << *format;
return -EINVAL;
}
LOG(MediaPipeline, Debug)
<< "Link " << *link << " configured with format "
<< *format;
}
return 0;
}
} /* namespace libcamera */

View file

@ -43,6 +43,7 @@ libcamera_internal_sources = files([
'matrix.cpp', 'matrix.cpp',
'media_device.cpp', 'media_device.cpp',
'media_object.cpp', 'media_object.cpp',
'media_pipeline.cpp',
'pipeline_handler.cpp', 'pipeline_handler.cpp',
'process.cpp', 'process.cpp',
'pub_key.cpp', 'pub_key.cpp',