From patchwork Mon Oct 14 09:59:33 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jacopo Mondi X-Patchwork-Id: 21613 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 83C00C32F8 for ; Mon, 14 Oct 2024 10:00:04 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id E7BA565381; Mon, 14 Oct 2024 12:00:00 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="cDT2QZt+"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 621EE65371 for ; Mon, 14 Oct 2024 11:59:57 +0200 (CEST) Received: from ideasonboard.com (unknown [5.77.95.224]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 4A11296C; Mon, 14 Oct 2024 11:58:16 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1728899896; bh=IRF0KJndvCvXsWyz+gzVS0tD7jlOURmOkBmhut9Wra4=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=cDT2QZt+GlkBTRizpLTWhq38kQgd++YAclMq+FWHrwFjNx1iGiz4NrGXbebMSyO+j XKGO/TohxrQpzo6CjjnZP71MF/cs1yLBm62vIQs5ioX3O88e1PDxVv7qMBg5Usz8e7 TpPORW42kSRJyIIiztByvm3VketzlL5qaksoCXpo= From: Jacopo Mondi To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Paul Elder Subject: [RFC 1/4] pipeline: Add support for dumping capture script and metadata Date: Mon, 14 Oct 2024 11:59:33 +0200 Message-ID: <20241014095937.24924-2-jacopo.mondi@ideasonboard.com> X-Mailer: git-send-email 2.46.2 In-Reply-To: <20241014095937.24924-1-jacopo.mondi@ideasonboard.com> References: <20241014095937.24924-1-jacopo.mondi@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: Paul Elder Add support for dumping capture scripts and metadata. The capture scripts can then be fed into the cam application and a capture can thus be "replayed". Metadata can also be dumped. Camera configuration is also dumped to the capture script. The cam application currently does not support loading configuration from the capture script, but support for that will be added in a subsequent patch. These can be enabled by a new environment variable. Signed-off-by: Paul Elder --- include/libcamera/internal/camera.h | 3 + include/libcamera/internal/pipeline_handler.h | 16 ++++ src/libcamera/camera.cpp | 13 +++ src/libcamera/pipeline_handler.cpp | 93 ++++++++++++++++++- 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/include/libcamera/internal/camera.h b/include/libcamera/internal/camera.h index 0add0428bb5d..a42f03d4c755 100644 --- a/include/libcamera/internal/camera.h +++ b/include/libcamera/internal/camera.h @@ -19,6 +19,8 @@ namespace libcamera { +enum class Orientation; + class CameraControlValidator; class PipelineHandler; class Stream; @@ -65,6 +67,7 @@ private: std::string id_; std::set streams_; std::set activeStreams_; + Orientation orientation_; bool disconnected_; std::atomic state_; diff --git a/include/libcamera/internal/pipeline_handler.h b/include/libcamera/internal/pipeline_handler.h index 0d38080369c5..fb3914185a01 100644 --- a/include/libcamera/internal/pipeline_handler.h +++ b/include/libcamera/internal/pipeline_handler.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,8 @@ namespace libcamera { +enum class Orientation; + class Camera; class CameraConfiguration; class CameraManager; @@ -68,6 +71,9 @@ public: CameraManager *cameraManager() const { return manager_; } + void dumpConfiguration(const std::set &streams, + const Orientation &orientation); + protected: void registerCamera(std::shared_ptr camera); void hotplugMediaDevice(MediaDevice *media); @@ -81,6 +87,11 @@ protected: CameraManager *manager_; private: + enum DumpMode { + Controls, + Metadata, + }; + void unlockMediaDevices(); void mediaDeviceDisconnected(MediaDevice *media); @@ -89,6 +100,8 @@ private: void doQueueRequest(Request *request); void doQueueRequests(); + void dumpRequest(Request *request, DumpMode mode); + std::vector> mediaDevices_; std::vector> cameras_; @@ -97,6 +110,9 @@ private: const char *name_; unsigned int useCount_; + std::ostream *dumpCaptureScript_; + std::ostream *dumpMetadata_; + friend class PipelineHandlerFactoryBase; }; diff --git a/src/libcamera/camera.cpp b/src/libcamera/camera.cpp index 7507e9ddae77..9d28b1202397 100644 --- a/src/libcamera/camera.cpp +++ b/src/libcamera/camera.cpp @@ -1215,6 +1215,9 @@ int Camera::configure(CameraConfiguration *config) d->activeStreams_.insert(stream); } + /* TODO Save sensor configuration for dumping it to capture script */ + d->orientation_ = config->orientation; + d->setState(Private::CameraConfigured); return 0; @@ -1356,6 +1359,16 @@ int Camera::start(const ControlList *controls) ASSERT(d->requestSequence_ == 0); + /* + * Invoke method in blocking mode to avoid the risk of writing after + * streaming has started. + * This needs to be here as PipelineHandler::start is a virtual function + * so it is impractical to add the dumping there. + * TODO Pass the sensor configuration, once it is supported + */ + d->pipe_->invokeMethod(&PipelineHandler::dumpConfiguration, + ConnectionTypeBlocking, d->activeStreams_, d->orientation_); + ret = d->pipe_->invokeMethod(&PipelineHandler::start, ConnectionTypeBlocking, this, controls); if (ret) diff --git a/src/libcamera/pipeline_handler.cpp b/src/libcamera/pipeline_handler.cpp index e5940469127e..7002b4323bdd 100644 --- a/src/libcamera/pipeline_handler.cpp +++ b/src/libcamera/pipeline_handler.cpp @@ -8,6 +8,7 @@ #include "libcamera/internal/pipeline_handler.h" #include +#include #include #include @@ -68,14 +69,36 @@ LOG_DEFINE_CATEGORY(Pipeline) * through the PipelineHandlerFactoryBase::create() function. */ PipelineHandler::PipelineHandler(CameraManager *manager) - : manager_(manager), useCount_(0) + : manager_(manager), useCount_(0), + dumpCaptureScript_(nullptr), dumpMetadata_(nullptr) { + /* TODO Print notification that we're dumping capture script */ + const char *file = utils::secure_getenv("LIBCAMERA_DUMP_CAPTURE_SCRIPT"); + if (!file) + return; + + dumpCaptureScript_ = new std::ofstream(file); + + /* + * Metadata needs to go into a separate file because otherwise it'll + * flood the capture script + */ + dumpMetadata_ = new std::ofstream(std::string(file) + ".metadata"); + std::string str = "frames:\n"; + dumpMetadata_->write(str.c_str(), str.size()); + dumpMetadata_->flush(); } PipelineHandler::~PipelineHandler() { for (std::shared_ptr media : mediaDevices_) media->release(); + + if (dumpCaptureScript_) + delete dumpCaptureScript_; + + if (dumpMetadata_) + delete dumpMetadata_; } /** @@ -464,6 +487,8 @@ void PipelineHandler::doQueueRequest(Request *request) request->_d()->sequence_ = data->requestSequence_++; + dumpRequest(request, DumpMode::Controls); + if (request->_d()->cancelled_) { completeRequest(request); return; @@ -555,6 +580,8 @@ void PipelineHandler::completeRequest(Request *request) request->_d()->complete(); + dumpRequest(request, DumpMode::Metadata); + Camera::Private *data = camera->_d(); while (!data->queuedRequests_.empty()) { @@ -758,6 +785,70 @@ void PipelineHandler::disconnect() * \return The CameraManager for this pipeline handler */ +void PipelineHandler::dumpConfiguration(const std::set &streams, + const Orientation &orientation) +{ + if (!dumpCaptureScript_) + return; + + std::stringstream ss; + ss << "configuration:" << std::endl; + ss << " orientation: " << orientation << std::endl; + + /* TODO Dump Sensor configuration */ + + ss << " streams:" << std::endl; + for (const auto &stream : streams) { + const StreamConfiguration &streamConfig = stream->configuration(); + ss << " - pixelFormat: " << streamConfig.pixelFormat << std::endl; + ss << " size: " << streamConfig.size << std::endl; + ss << " stride: " << streamConfig.stride << std::endl; + ss << " frameSize: " << streamConfig.frameSize << std::endl; + ss << " bufferCount: " << streamConfig.bufferCount << std::endl; + if (streamConfig.colorSpace) + ss << " colorSpace: " << streamConfig.colorSpace->toString() << std::endl; + } + + dumpCaptureScript_->write(ss.str().c_str(), ss.str().size()); + + std::string str = "frames:\n"; + dumpCaptureScript_->write(str.c_str(), str.size()); + dumpCaptureScript_->flush(); +} + +void PipelineHandler::dumpRequest(Request *request, DumpMode mode) +{ + ControlList &controls = + mode == DumpMode::Controls ? request->controls() + : request->metadata(); + std::ostream *output = + mode == DumpMode::Controls ? dumpCaptureScript_ + : dumpMetadata_; + + if (!output || controls.empty()) + return; + + std::stringstream ss; + /* TODO Figure out PFC */ + ss << " - " << request->sequence() << ":" << std::endl; + + const ControlIdMap *idMap = controls.idMap(); + for (const auto &pair : controls) { + const ControlId *ctrlId = idMap->at(pair.first); + /* TODO Prettify enums (probably by upgrading ControlValue::toString()) */ + ss << " " << ctrlId->name() << ": " << pair.second.toString() << std::endl; + } + + /* + * TODO Investigate the overhead of flushing this frequently + * Controls aren't going to be queued too frequently so it should be + * fine to dump controls every frame. Metadata on the other hand needs + * to be investigated. + */ + output->write(ss.str().c_str(), ss.str().size()); + output->flush(); +} + /** * \class PipelineHandlerFactoryBase * \brief Base class for pipeline handler factories From patchwork Mon Oct 14 09:59:34 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jacopo Mondi X-Patchwork-Id: 21614 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id C79EFC326C for ; Mon, 14 Oct 2024 10:00:06 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id E4D5265383; Mon, 14 Oct 2024 12:00:01 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="ISMwRoyO"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 3CDB265371 for ; Mon, 14 Oct 2024 11:59:58 +0200 (CEST) Received: from ideasonboard.com (unknown [5.77.95.224]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 117458BE; Mon, 14 Oct 2024 11:58:16 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1728899897; bh=bE3dig4hKkrj14JN455AaVX1K8ENqB8xqqOqAdnaBdc=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=ISMwRoyOeStQQs1iodu6EHiCp9AlgwUqHtj96WIUmCSl0ZRt/E4D8/rKSvWtbn9Tw HrUhWCn4q4Z18zqSCYS9VjQKYvK2xSMpKzCqWvwpivMEoxMrJXgEKEGxaDilk0qaOM liVSxEwxM2+XbZJjkoMD64H4VZ0GmK4JpKwZih9I= From: Jacopo Mondi To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Paul Elder Subject: [RFC 2/4] apps: cam: Add support for loading configuration from capture script Date: Mon, 14 Oct 2024 11:59:34 +0200 Message-ID: <20241014095937.24924-3-jacopo.mondi@ideasonboard.com> X-Mailer: git-send-email 2.46.2 In-Reply-To: <20241014095937.24924-1-jacopo.mondi@ideasonboard.com> References: <20241014095937.24924-1-jacopo.mondi@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: Paul Elder Add support to the cam application for loading the camera configuration from a capture script. These are not expected to be written by hand, but rather dumped via the LIBCAMERA_DUMP_CAPTURE_SCRIPT environment variable. If any configuration options are specified by command line parameters, those will take precedence. Signed-off-by: Paul Elder --- src/apps/cam/camera_session.cpp | 22 +++-- src/apps/cam/capture_script.cpp | 164 ++++++++++++++++++++++++++++++++ src/apps/cam/capture_script.h | 8 ++ 3 files changed, 184 insertions(+), 10 deletions(-) diff --git a/src/apps/cam/camera_session.cpp b/src/apps/cam/camera_session.cpp index edc49b875450..6f1d8b58870f 100644 --- a/src/apps/cam/camera_session.cpp +++ b/src/apps/cam/camera_session.cpp @@ -70,6 +70,18 @@ CameraSession::CameraSession(CameraManager *cm, return; } + if (options_.isSet(OptCaptureScript)) { + std::string scriptName = options_[OptCaptureScript].toString(); + script_ = std::make_unique(camera_, scriptName); + if (!script_->valid()) { + std::cerr << "Invalid capture script '" << scriptName + << "'" << std::endl; + return; + } + + script_->populateConfiguration(config.get()); + } + if (options_.isSet(OptOrientation)) { std::string orientOpt = options_[OptOrientation].toString(); static const std::map orientations{ @@ -119,16 +131,6 @@ CameraSession::CameraSession(CameraManager *cm, } #endif - if (options_.isSet(OptCaptureScript)) { - std::string scriptName = options_[OptCaptureScript].toString(); - script_ = std::make_unique(camera_, scriptName); - if (!script_->valid()) { - std::cerr << "Invalid capture script '" << scriptName - << "'" << std::endl; - return; - } - } - switch (config->validate()) { case CameraConfiguration::Valid: break; diff --git a/src/apps/cam/capture_script.cpp b/src/apps/cam/capture_script.cpp index fc1dfa75f2d4..7f166f45657e 100644 --- a/src/apps/cam/capture_script.cpp +++ b/src/apps/cam/capture_script.cpp @@ -7,6 +7,7 @@ #include "capture_script.h" +#include #include #include #include @@ -162,6 +163,10 @@ int CaptureScript::parseScript(FILE *script) ret = parseFrames(); if (ret) return ret; + } else if (section == "configuration") { + ret = parseConfiguration(); + if (ret) + return ret; } else { std::cerr << "Unsupported section '" << section << "'" << std::endl; @@ -322,6 +327,165 @@ int CaptureScript::parseControl(EventPtr event, ControlList &controls) return 0; } +int CaptureScript::parseOrientation(EventPtr event) +{ + static const std::map orientations{ + { "Rotate0", libcamera::Orientation::Rotate0 }, + { "Rotate0Mirror", libcamera::Orientation::Rotate0Mirror }, + { "Rotate180", libcamera::Orientation::Rotate180 }, + { "Rotate180Mirror", libcamera::Orientation::Rotate180Mirror }, + { "Rotate90Mirror", libcamera::Orientation::Rotate90Mirror }, + { "Rotate270", libcamera::Orientation::Rotate270 }, + { "Rotate270Mirror", libcamera::Orientation::Rotate270Mirror }, + { "Rotate90", libcamera::Orientation::Rotate90 }, + }; + + std::string orientation = eventScalarValue(event); + + auto it = orientations.find(orientation); + if (it == orientations.end()) { + std::cerr << "Invalid orientation '" << orientation + << "' in capture script" << std::endl; + return -EINVAL; + } + + orientation_ = it->second; + + return 0; +} + +int CaptureScript::parseStream(EventPtr event, unsigned int index) +{ + if (!checkEvent(event, YAML_MAPPING_START_EVENT)) + return -EINVAL; + + StreamConfiguration config; + while (1) { + event = nextEvent(); + if (!event) + return -EINVAL; + if (event->type == YAML_MAPPING_END_EVENT) + break; + + std::string key = eventScalarValue(event); + + event = nextEvent(); + if (!event) + return -EINVAL; + if (event->type == YAML_MAPPING_END_EVENT) + break; + + std::string value = eventScalarValue(event); + + if (key == "pixelFormat") { + config.pixelFormat = libcamera::PixelFormat::fromString(value); + } else if (key == "size") { + unsigned int split = value.find("x"); + if (split == std::string::npos) { + std::cerr << "Invalid size '" << value + << "' in stream configuration " + << index << std::endl; + } + + std::string width = value.substr(0, split); + std::string height = value.substr(split + 1); + config.size = Size(std::stoi(width), std::stoi(height)); + } else if (key == "stride") { + config.stride = std::stoi(value); + } else if (key == "frameSize") { + config.frameSize = std::stoi(value); + } else if (key == "bufferCount") { + config.bufferCount = std::stoi(value); + } else if (key == "colorSpace") { + config.colorSpace = libcamera::ColorSpace::fromString(value); + } else { + std::cerr << "Unknown key-value pair '" + << key << "': '" << value + << "' in stream configuration " + << index << std::endl; + return -EINVAL; + } + } + + streamConfigs_.push_back(config); + + return 0; +} + +int CaptureScript::parseStreams(EventPtr event) +{ + if (!checkEvent(event, YAML_SEQUENCE_START_EVENT)) + return -EINVAL; + + unsigned int index = 0; + while (1) { + event = nextEvent(); + if (!event) + return -EINVAL; + if (event->type == YAML_SEQUENCE_END_EVENT) + return 0; + + if (event->type == YAML_MAPPING_START_EVENT) { + parseStream(std::move(event), index++); + continue; + } else { + std::cerr << "UNKNOWN TYPE" << std::endl; + return -EINVAL; + } + } + + return 0; +} + +int CaptureScript::parseConfiguration() +{ + int ret; + + EventPtr event = nextEvent(YAML_MAPPING_START_EVENT); + if (!event) + return -EINVAL; + + while (1) { + event = nextEvent(); + if (!event) + return -EINVAL; + if (event->type == YAML_MAPPING_END_EVENT) + break; + + std::string key = eventScalarValue(event); + + event = nextEvent(); + if (!event) + return -EINVAL; + if (event->type == YAML_MAPPING_END_EVENT) + break; + + /* TODO Load sensor configuration */ + if (key == "orientation") { + ret = parseOrientation(std::move(event)); + if (ret) + return ret; + } else if (key == "streams") { + ret = parseStreams(std::move(event)); + if (ret) + return ret; + } + } + + return 0; +} + +void CaptureScript::populateConfiguration(CameraConfiguration *configuration) const +{ + if (!configuration) + return; + + configuration->orientation = orientation_; + + for (unsigned int i = 0; i < streamConfigs_.size(); i++) + (*configuration)[i] = streamConfigs_[i]; +} + std::string CaptureScript::parseScalar() { EventPtr event = nextEvent(YAML_SCALAR_EVENT); diff --git a/src/apps/cam/capture_script.h b/src/apps/cam/capture_script.h index 294b920363ba..4ba862d742cf 100644 --- a/src/apps/cam/capture_script.h +++ b/src/apps/cam/capture_script.h @@ -26,6 +26,7 @@ public: const libcamera::ControlList &frameControls(unsigned int frame); + void populateConfiguration(libcamera::CameraConfiguration *configuration) const; private: struct EventDeleter { void operator()(yaml_event_t *event) const @@ -43,6 +44,9 @@ private: unsigned int loop_; bool valid_; + libcamera::Orientation orientation_; + std::vector streamConfigs_; + EventPtr nextEvent(yaml_event_type_t expectedType = YAML_NO_EVENT); bool checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const; static std::string eventScalarValue(const EventPtr &event); @@ -55,6 +59,10 @@ private: int parseFrames(); int parseFrame(EventPtr event); int parseControl(EventPtr event, libcamera::ControlList &controls); + int parseConfiguration(); + int parseOrientation(EventPtr event); + int parseStreams(EventPtr event); + int parseStream(EventPtr event, unsigned int index); libcamera::ControlValue parseScalarControl(const libcamera::ControlId *id, const std::string repr); From patchwork Mon Oct 14 09:59:35 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jacopo Mondi X-Patchwork-Id: 21615 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id E5F51C32F9 for ; Mon, 14 Oct 2024 10:00:08 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 7A9F665382; Mon, 14 Oct 2024 12:00:04 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="tiNZANuT"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id E657C65371 for ; Mon, 14 Oct 2024 11:59:58 +0200 (CEST) Received: from ideasonboard.com (unknown [5.77.95.224]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id DAC0F96C; Mon, 14 Oct 2024 11:58:17 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1728899898; bh=vEHRonUu2eK2IZq2zVA946KYB5yCGqNw1e5khveH3dM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=tiNZANuTXBvgA4nT4ny0yGc8qxhFpWHO05h96eMopYAmAd4+a3dUwR1gg9sjA8GWX vHRgUcvibN2rNtDahEvZgnBmfSTdp9uMPt5BLn4ZDZxDE6mLMKbzOQyXnKMJds4KGi ypgt5GFquTHFWojRaCUoZ0wVZ++8eAtuMa+P2Bh0= From: Jacopo Mondi To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi Subject: [RFC 3/4] libcamera: Implement YamlEmitter Date: Mon, 14 Oct 2024 11:59:35 +0200 Message-ID: <20241014095937.24924-4-jacopo.mondi@ideasonboard.com> X-Mailer: git-send-email 2.46.2 In-Reply-To: <20241014095937.24924-1-jacopo.mondi@ideasonboard.com> References: <20241014095937.24924-1-jacopo.mondi@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Signed-off-by: Jacopo Mondi --- include/libcamera/internal/meson.build | 1 + include/libcamera/internal/yaml_emitter.h | 172 +++++++++ src/libcamera/meson.build | 1 + src/libcamera/yaml_emitter.cpp | 427 ++++++++++++++++++++++ 4 files changed, 601 insertions(+) create mode 100644 include/libcamera/internal/yaml_emitter.h create mode 100644 src/libcamera/yaml_emitter.cpp diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build index 1c5eef9cab80..7533b075fde2 100644 --- a/include/libcamera/internal/meson.build +++ b/include/libcamera/internal/meson.build @@ -41,6 +41,7 @@ libcamera_internal_headers = files([ 'v4l2_pixelformat.h', 'v4l2_subdevice.h', 'v4l2_videodevice.h', + 'yaml_emitter.h', 'yaml_parser.h', ]) diff --git a/include/libcamera/internal/yaml_emitter.h b/include/libcamera/internal/yaml_emitter.h new file mode 100644 index 000000000000..3fe05f97de70 --- /dev/null +++ b/include/libcamera/internal/yaml_emitter.h @@ -0,0 +1,172 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board Oy + * + * libcamera YAML emitter helper + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include + +namespace libcamera { + +class YamlDict; +class YamlEvent; +class YamlList; +class YamlRoot; +class YamlScalar; + +class YamlEmitter final +{ +public: + ~YamlEmitter() = default; + + static std::unique_ptr root(std::string_view path); + + int emit(YamlEvent *event); + +private: + LIBCAMERA_DISABLE_COPY(YamlEmitter) + + class Emitter + { + public: + Emitter() = default; + ~Emitter(); + + void init(File *file); + + int emit(YamlEvent *event); + + private: + void logError(); + + yaml_emitter_t emitter_; + }; + + YamlEmitter() = default; + + void init(); + + std::unique_ptr file_; + Emitter emitter_; +}; + +class YamlOutput +{ +public: + virtual ~YamlOutput() = default; + + void close() + { + closed_ = true; + } + + std::unique_ptr scalar(); + std::unique_ptr dict(); + std::unique_ptr list(); + +protected: + YamlOutput(YamlEmitter *emitter) + : emitter_(emitter), closed_(false) + { + } + + int emitScalar(std::string_view scalar); + int emitMappingStart(); + int emitMappingEnd(); + int emitSequenceStart(); + int emitSequenceEnd(); + + YamlEmitter *emitter_; + + bool closed_; +}; + +class YamlRoot : public YamlOutput +{ +public: + ~YamlRoot(); + void close() {} + + std::unique_ptr list(); + std::unique_ptr dict(); + void scalar(std::string_view scalar); + +private: + friend class YamlEmitter; + + YamlRoot(YamlEmitter *emitter) + : YamlOutput(emitter) + { + emitterRoot_ = std::unique_ptr(emitter); + } + + std::unique_ptr emitterRoot_; +}; + +class YamlScalar : public YamlOutput +{ +public: + ~YamlScalar() = default; + + void close() {} + + void operator=(const Orientation &orientation); + void operator=(std::string_view scalar); + +private: + friend class YamlOutput; + + YamlScalar(YamlEmitter *emitter); +}; + +class YamlList : public YamlOutput +{ +public: + ~YamlList(); + + void close(); + + std::unique_ptr list(); + std::unique_ptr dict(); + void scalar(std::string_view scalar); + +private: + friend class YamlOutput; + + YamlList(YamlEmitter *emitter); +}; + +class YamlDict : public YamlOutput, + private std::unordered_map> +{ +public: + using Map = std::unordered_map; + + ~YamlDict(); + + void close(); + + std::unique_ptr list(std::string_view key); + std::unique_ptr dict(std::string_view key); + + YamlScalar &operator[](const Map::key_type &key); + +private: + friend class YamlOutput; + + YamlDict(YamlEmitter *emitter); +}; + +} /* namespace libcamera */ diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build index aa9ab0291854..5b8af4103085 100644 --- a/src/libcamera/meson.build +++ b/src/libcamera/meson.build @@ -51,6 +51,7 @@ libcamera_internal_sources = files([ 'v4l2_pixelformat.cpp', 'v4l2_subdevice.cpp', 'v4l2_videodevice.cpp', + 'yaml_emitter.cpp', 'yaml_parser.cpp', ]) diff --git a/src/libcamera/yaml_emitter.cpp b/src/libcamera/yaml_emitter.cpp new file mode 100644 index 000000000000..1f7651ffcb24 --- /dev/null +++ b/src/libcamera/yaml_emitter.cpp @@ -0,0 +1,427 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board Oy + * + * libcamera YAML emitter helper + */ + +#include "libcamera/internal/yaml_emitter.h" + +#include + +/** + * \file yaml_emitter.h + * \brief A YAML emitter helper + * + * The YAML Emitter helpers allows users to emit output in YAML format. + * + * To emit YAML users of this classes should create a root node with + * + * \code + std::string filePath("..."); + auto root = YamlEmitter::root(filePath); + \endcode + * + * A YamlRoot implements RAII-style handling of YAML sequence and document + * events handling. Creating a YamlRoot emits the STREAM_START and DOC_START + * events. Once a YamlRoot gets deleted the DOC_SEND and STREAM_END events + * gets automatically deleted. + * + * Once a root node has been created it is possible to populate it with + * scalars, list or dictionaries. + * + * YamlList and YamlDict can only be created wrapped in unique_ptr<> instances, + * to implement a RAII-style handling of YAML of lists and dictionaries. + * Creating a YamlList and a YamlDict emits the YAML sequence and mapping start + * events. Once an instance gets deleted, the sequence and mapping gets + * automatically emitted. If a list of dictionary needs to be closed before + * it gets deleted, it can be explicitly closed with the close() function. + * + * A YamlScalar is a simpler object that can be assigned to different types + * to emit them as strings. + */ + +namespace libcamera { + +LOG_DEFINE_CATEGORY(YamlEmitter) + +namespace { + +int yamlWrite(void *data, unsigned char *buffer, size_t size) +{ + File *file = static_cast(data); + + Span buf{ buffer, size }; + ssize_t ret = file->write(buf); + if (ret < 0) { + ret = errno; + LOG(YamlEmitter, Error) << "Write error : " << strerror(ret); + return 0; + } + + return 1; +} + +} /* namespace */ + +/** + * \class YamlEvent + * + * Class that represents a yaml_event that automatically cleans-up on + * destruction. + */ +class YamlEvent +{ +public: + static std::unique_ptr create(); + + const yaml_event_t *event() const + { + return &event_; + } + + yaml_event_t *event() + { + return &event_; + } + +private: + YamlEvent() = default; + + yaml_event_t event_; +}; + +std::unique_ptr YamlEvent::create() +{ + struct Deleter : std::default_delete { + void operator()(YamlEvent *yamlEvent) + { + yaml_event_delete(yamlEvent->event()); + } + }; + + return std::unique_ptr(new YamlEvent(), Deleter()); +} + +/** + * \class YamlEmitter + * + * Yaml Emitter entry point. Allows to create a YamlRoot object that users + * can populate. + */ + +/** + * \brief Create a YamlRoot that applications can start populating with YamlOutput + * \param[in] path The YAML output file path + * \return A unique pointer to a YamlRoot + */ +std::unique_ptr YamlEmitter::root(std::string_view path) +{ + YamlEmitter *emitter = new YamlEmitter(); + + std::string filePath(path); + emitter->file_ = std::make_unique(filePath); + emitter->file_->open(File::OpenModeFlag::WriteOnly); + + emitter->init(); + + YamlRoot *root = new YamlRoot(emitter); + return std::unique_ptr(root); +} + +/** + * \brief Emit a YamlEvent + */ +int YamlEmitter::emit(YamlEvent *event) +{ + return emitter_.emit(event); +} + +void YamlEmitter::init() +{ + emitter_.init(file_.get()); + + std::unique_ptr event = YamlEvent::create(); + + yaml_stream_start_event_initialize(event->event(), YAML_UTF8_ENCODING); + emitter_.emit(event.get()); + + yaml_document_start_event_initialize(event->event(), NULL, NULL, + NULL, 0); + emitter_.emit(event.get()); +} + +/** + * \class YamlEmitter::Emitter + * + * yaml_emitter_t wrapper. Initialize the yaml_emitter_t on creation allows + * YamlOutput classes to emit events. + */ + +YamlEmitter::Emitter::~Emitter() +{ + yaml_emitter_delete(&emitter_); +} + +void YamlEmitter::Emitter::init(File *file) +{ + yaml_emitter_initialize(&emitter_); + yaml_emitter_set_output(&emitter_, yamlWrite, file); +} + +void YamlEmitter::Emitter::logError() +{ + switch (emitter_.error) { + case YAML_MEMORY_ERROR: + LOG(YamlEmitter, Error) + << "Memory error: Not enough memory for emitting"; + break; + + case YAML_WRITER_ERROR: + LOG(YamlEmitter, Error) + << "Writer error: " << emitter_.problem; + break; + + case YAML_EMITTER_ERROR: + LOG(YamlEmitter, Error) + << "Emitter error: " << emitter_.problem; + break; + + default: + LOG(YamlEmitter, Error) << "Internal problem"; + break; + } +} + +int YamlEmitter::Emitter::emit(YamlEvent *event) +{ + int ret = yaml_emitter_emit(&emitter_, event->event()); + if (!ret) { + logError(); + return -EINVAL; + } + + return 0; +} + +/** + * \class YamlOutput + * + * The YamlOutput base class. From this class YamlScalar, YamlList and YamlDict + * are derived. + * + * The YamlOutput class provides functions to create a scalar, a list or a + * dictionary. + * + * The class cannot be instantiated directly by applications. + */ + +std::unique_ptr YamlOutput::scalar() +{ + return std::unique_ptr(new YamlScalar(emitter_)); +} + +std::unique_ptr YamlOutput::dict() +{ + return std::unique_ptr(new YamlDict(emitter_)); +} + +std::unique_ptr YamlOutput::list() +{ + return std::unique_ptr(new YamlList(emitter_)); +} + +int YamlOutput::emitScalar(std::string_view scalar) +{ + std::unique_ptr event = YamlEvent::create(); + + const unsigned char *value = reinterpret_cast + (scalar.data()); + yaml_scalar_event_initialize(event->event(), NULL, NULL, value, + scalar.length(), true, false, + YAML_PLAIN_SCALAR_STYLE); + return emitter_->emit(event.get()); +} + +int YamlOutput::emitMappingStart() +{ + std::unique_ptr event = YamlEvent::create(); + yaml_mapping_start_event_initialize(event->event(), NULL, NULL, + true, YAML_BLOCK_MAPPING_STYLE); + return emitter_->emit(event.get()); +} + +int YamlOutput::emitMappingEnd() +{ + std::unique_ptr event = YamlEvent::create(); + yaml_mapping_end_event_initialize(event->event()); + return emitter_->emit(event.get()); +} + +int YamlOutput::emitSequenceStart() +{ + std::unique_ptr event = YamlEvent::create(); + yaml_sequence_start_event_initialize(event->event(), NULL, NULL, true, + YAML_BLOCK_SEQUENCE_STYLE); + return emitter_->emit(event.get()); +} + +int YamlOutput::emitSequenceEnd() +{ + std::unique_ptr event = YamlEvent::create(); + yaml_sequence_end_event_initialize(event->event()); + return emitter_->emit(event.get()); +} + +/** + * \class YamlRoot + * + * Yaml root node. A root node can be populated with a scalar, a list or a dict. + */ + +YamlRoot::~YamlRoot() +{ + std::unique_ptr event = YamlEvent::create(); + + yaml_document_end_event_initialize(event->event(), 0); + emitterRoot_->emit(event.get()); + + yaml_stream_end_event_initialize(event->event()); + emitterRoot_->emit(event.get()); +} + +std::unique_ptr YamlRoot::dict() +{ + emitMappingStart(); + + return YamlOutput::dict(); +} + +std::unique_ptr YamlRoot::list() +{ + emitSequenceStart(); + + return YamlOutput::list(); +} + +/** + * \class YamlScalar + * + * A YamlScalar can be assigned to an std::string_view and other libcamera + * types to emit them as YAML scalars. + */ + +YamlScalar::YamlScalar(YamlEmitter *emitter) + : YamlOutput(emitter) +{ +} + +void YamlScalar::operator=(const libcamera::Orientation &orientation) +{ + std::stringstream o; + o << orientation; + + emitScalar(o.str()); +} + +void YamlScalar::operator=(std::string_view scalar) +{ + emitScalar(scalar); +} + +/** + * \class YamlList + * + * A YamlList can be populated with scalar and allows to create other lists + * and dictionaries. + */ + +YamlList::YamlList(YamlEmitter *emitter) + : YamlOutput(emitter) +{ +} + +YamlList::~YamlList() +{ + if (!closed_) + close(); +} + +void YamlList::close() +{ + emitSequenceEnd(); + YamlOutput::close(); +} + +void YamlList::scalar(std::string_view scalar) +{ + emitScalar(scalar); +} + +std::unique_ptr YamlList::list() +{ + emitSequenceStart(); + + return YamlOutput::list(); +} + +std::unique_ptr YamlList::dict() +{ + emitMappingStart(); + + return YamlOutput::dict(); +} + +/** + * \class YamlDict + * + * A YamlDict derives an unordered_map that maps strings to YAML scalar. + * + * A YamlDict can be populated with scalars using operator[] and allows to + * create other lists and dictionaries. + */ + +YamlDict::YamlDict(YamlEmitter *emitter) + : YamlOutput(emitter) +{ +} + +YamlDict::~YamlDict() +{ + if (!closed_) + close(); + + clear(); +} + +void YamlDict::close() +{ + emitMappingEnd(); + YamlOutput::close(); +} + +std::unique_ptr YamlDict::list(std::string_view key) +{ + emitScalar(key); + emitSequenceStart(); + + return YamlOutput::list(); +} + +std::unique_ptr YamlDict::dict(std::string_view key) +{ + emitScalar(key); + emitMappingStart(); + + return YamlOutput::dict(); +} + +YamlScalar &YamlDict::operator[](const YamlDict::Map::key_type &key) +{ + emplace(key, YamlOutput::scalar()); + emitScalar(key); + + return *at(key); +} + +} /* namespace libcamera */ From patchwork Mon Oct 14 09:59:36 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jacopo Mondi X-Patchwork-Id: 21616 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 8D4D8C32F8 for ; Mon, 14 Oct 2024 10:00:10 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 4F6736538E; Mon, 14 Oct 2024 12:00:06 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="ob6HMijX"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 3ED316537B for ; Mon, 14 Oct 2024 12:00:00 +0200 (CEST) Received: from ideasonboard.com (unknown [5.77.95.224]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 849828BE; Mon, 14 Oct 2024 11:58:18 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1728899899; bh=oXrkN3on37pRu2/oNtO4qLlJzaW8X6MHVA/ZT7ap/KA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=ob6HMijXxQ/xuQcQawIjayCr466GvGB5aCZhLimfLOMbSMnOr3LCI28koPfA0Iavr xSQteQfwgyXjVyUFhQzDWLGyIIddOZVA0sMmSfw9yZwds2o71F4k5tTMz2WGYnFxuO p8pT7qMbQkWqjNk/R+gwEAAw04M3udexZcjlCuf0= From: Jacopo Mondi To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi Subject: [RFC 4/4] libcamera: pipeline_handler: Use YamlEmitter Date: Mon, 14 Oct 2024 11:59:36 +0200 Message-ID: <20241014095937.24924-5-jacopo.mondi@ideasonboard.com> X-Mailer: git-send-email 2.46.2 In-Reply-To: <20241014095937.24924-1-jacopo.mondi@ideasonboard.com> References: <20241014095937.24924-1-jacopo.mondi@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Signed-off-by: Jacopo Mondi --- include/libcamera/internal/pipeline_handler.h | 11 ++- src/libcamera/pipeline_handler.cpp | 94 +++++++++---------- 2 files changed, 54 insertions(+), 51 deletions(-) diff --git a/include/libcamera/internal/pipeline_handler.h b/include/libcamera/internal/pipeline_handler.h index fb3914185a01..a10d7b1fab8c 100644 --- a/include/libcamera/internal/pipeline_handler.h +++ b/include/libcamera/internal/pipeline_handler.h @@ -19,6 +19,8 @@ #include #include +#include "libcamera/internal/yaml_emitter.h" + namespace libcamera { enum class Orientation; @@ -110,8 +112,13 @@ private: const char *name_; unsigned int useCount_; - std::ostream *dumpCaptureScript_; - std::ostream *dumpMetadata_; + std::unique_ptr controlsEmitter_; + std::unique_ptr controlsDict_; + std::unique_ptr controlsList_; + + std::unique_ptr metadataEmitter_; + std::unique_ptr metadataDict_; + std::unique_ptr metadataList_; friend class PipelineHandlerFactoryBase; }; diff --git a/src/libcamera/pipeline_handler.cpp b/src/libcamera/pipeline_handler.cpp index 7002b4323bdd..82868a8122ce 100644 --- a/src/libcamera/pipeline_handler.cpp +++ b/src/libcamera/pipeline_handler.cpp @@ -69,36 +69,31 @@ LOG_DEFINE_CATEGORY(Pipeline) * through the PipelineHandlerFactoryBase::create() function. */ PipelineHandler::PipelineHandler(CameraManager *manager) - : manager_(manager), useCount_(0), - dumpCaptureScript_(nullptr), dumpMetadata_(nullptr) + : manager_(manager), useCount_(0) { /* TODO Print notification that we're dumping capture script */ const char *file = utils::secure_getenv("LIBCAMERA_DUMP_CAPTURE_SCRIPT"); if (!file) return; - dumpCaptureScript_ = new std::ofstream(file); + std::string filePath(file); + controlsEmitter_ = YamlEmitter::root(filePath); + controlsDict_ = controlsEmitter_->dict(); /* * Metadata needs to go into a separate file because otherwise it'll * flood the capture script */ - dumpMetadata_ = new std::ofstream(std::string(file) + ".metadata"); - std::string str = "frames:\n"; - dumpMetadata_->write(str.c_str(), str.size()); - dumpMetadata_->flush(); + std::string metadataFilePath = filePath + ".metadata"; + metadataEmitter_ = YamlEmitter::root(metadataFilePath); + metadataDict_ = metadataEmitter_->dict(); + metadataList_ = metadataDict_->list("frames"); } PipelineHandler::~PipelineHandler() { for (std::shared_ptr media : mediaDevices_) media->release(); - - if (dumpCaptureScript_) - delete dumpCaptureScript_; - - if (dumpMetadata_) - delete dumpMetadata_; } /** @@ -788,65 +783,66 @@ void PipelineHandler::disconnect() void PipelineHandler::dumpConfiguration(const std::set &streams, const Orientation &orientation) { - if (!dumpCaptureScript_) - return; + auto configurationDict = controlsDict_->dict("configuration"); - std::stringstream ss; - ss << "configuration:" << std::endl; - ss << " orientation: " << orientation << std::endl; + (*configurationDict)["orientation"] = orientation; /* TODO Dump Sensor configuration */ - ss << " streams:" << std::endl; + auto streamsList = configurationDict->list("streams"); + for (const auto &stream : streams) { const StreamConfiguration &streamConfig = stream->configuration(); - ss << " - pixelFormat: " << streamConfig.pixelFormat << std::endl; - ss << " size: " << streamConfig.size << std::endl; - ss << " stride: " << streamConfig.stride << std::endl; - ss << " frameSize: " << streamConfig.frameSize << std::endl; - ss << " bufferCount: " << streamConfig.bufferCount << std::endl; + auto yamlStream = streamsList->dict(); + + (*yamlStream)["pixelformat"] = streamConfig.pixelFormat.toString(); + (*yamlStream)["size"] = streamConfig.size.toString(); + (*yamlStream)["stride"] = std::to_string(streamConfig.stride); + (*yamlStream)["frameSize"] = std::to_string(streamConfig.frameSize); + (*yamlStream)["bufferCount"] = std::to_string(streamConfig.bufferCount); + if (streamConfig.colorSpace) - ss << " colorSpace: " << streamConfig.colorSpace->toString() << std::endl; + (*yamlStream)["colorSpace"] = + streamConfig.colorSpace->toString(); } - dumpCaptureScript_->write(ss.str().c_str(), ss.str().size()); + streamsList->close(); + configurationDict->close(); - std::string str = "frames:\n"; - dumpCaptureScript_->write(str.c_str(), str.size()); - dumpCaptureScript_->flush(); + controlsList_ = controlsDict_->list("frames"); } void PipelineHandler::dumpRequest(Request *request, DumpMode mode) { - ControlList &controls = - mode == DumpMode::Controls ? request->controls() - : request->metadata(); - std::ostream *output = - mode == DumpMode::Controls ? dumpCaptureScript_ - : dumpMetadata_; - - if (!output || controls.empty()) + ControlList &controls = mode == DumpMode::Controls ? request->controls() + : request->metadata(); + if (controls.empty()) return; - std::stringstream ss; + std::unique_ptr yamlFrame; + if (mode == DumpMode::Controls) { + if (!controlsEmitter_) + return; + + yamlFrame = controlsList_->dict(); + } else { + if (!metadataEmitter_) + return; + + yamlFrame = metadataList_->dict(); + } + + auto yamlCtrls = yamlFrame->dict(std::to_string(request->sequence())); + /* TODO Figure out PFC */ - ss << " - " << request->sequence() << ":" << std::endl; const ControlIdMap *idMap = controls.idMap(); for (const auto &pair : controls) { const ControlId *ctrlId = idMap->at(pair.first); + /* TODO Prettify enums (probably by upgrading ControlValue::toString()) */ - ss << " " << ctrlId->name() << ": " << pair.second.toString() << std::endl; + (*yamlCtrls)[ctrlId->name()] = pair.second.toString(); } - - /* - * TODO Investigate the overhead of flushing this frequently - * Controls aren't going to be queued too frequently so it should be - * fine to dump controls every frame. Metadata on the other hand needs - * to be investigated. - */ - output->write(ss.str().c_str(), ss.str().size()); - output->flush(); } /**