From patchwork Fri Oct 4 12:05:16 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Paul Elder X-Patchwork-Id: 21519 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 C2682C3257 for ; Fri, 4 Oct 2024 12:05:39 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 5AD716352B; Fri, 4 Oct 2024 14:05:39 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="MF410xBo"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 53C4B63524 for ; Fri, 4 Oct 2024 14:05:34 +0200 (CEST) Received: from neptunite.flets-east.jp (unknown [IPv6:2404:7a81:160:2100:e3ca:2180:ae9b:1941]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 6CD6E4C9; Fri, 4 Oct 2024 14:03:59 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1728043440; bh=IPGZftDfOzCXfNQGGjhfWMnYl0kgQs31aaPrb8Jxi+E=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=MF410xBoqZs0+1vA+18Yf2vzTaMUgABSsFpe7bNw0uNB41i6oMRxaqDNPPPWzYyWi vb9Mj442b0xG/iC13jwsqLjnCNIughJIYj2eLolVAex5FVpedHvSxZ7DHZbXBgaZnP 8MtsQcAAjR7TPSkHimV8IL4z/L+VLWC9X3pNqkR4= From: Paul Elder To: libcamera-devel@lists.libcamera.org Cc: Paul Elder Subject: [PATCH v2 1/2] pipeline: Add support for dumping capture script and metadata Date: Fri, 4 Oct 2024 21:05:16 +0900 Message-Id: <20241004120517.3572281-2-paul.elder@ideasonboard.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20241004120517.3572281-1-paul.elder@ideasonboard.com> References: <20241004120517.3572281-1-paul.elder@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" 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 --- Changes in v2: - clean up code - add support for creating new dump file when restarting capture - document the environment variables --- Documentation/environment_variables.rst | 26 ++++++ include/libcamera/internal/camera.h | 2 + include/libcamera/internal/pipeline_handler.h | 19 +++++ src/libcamera/camera.cpp | 13 +++ src/libcamera/pipeline_handler.cpp | 85 ++++++++++++++++++- 5 files changed, 144 insertions(+), 1 deletion(-) diff --git a/Documentation/environment_variables.rst b/Documentation/environment_variables.rst index 7da9883a8380..b0448d387847 100644 --- a/Documentation/environment_variables.rst +++ b/Documentation/environment_variables.rst @@ -29,6 +29,32 @@ LIBCAMERA_IPA_CONFIG_PATH Example value: ``${HOME}/.libcamera/share/ipa:/opt/libcamera/vendor/share/ipa`` +LIBCAMERA_DUMP_CAPTURE_SCRIPT + The custom destination for capture script dump output. + + The precensce of this environment variable enables capture script dumping. + All controls that are set for each request will be dumped into the file + specified by the environment variable as a capture script, which can later + be fed into the cam application to replay a control sequence. + + The file that is written to will be suffixed with a number indicating the + number of capture. That is, if the capture is stopped and started again, a + new capture script will be dumped with the suffix incremented. + + Example value: ``/home/{user}/capture_script.yaml`` + +LIBCAMERA_DUMP_METADATA + The custom destination for metadata dump output. + + This is similar to LIBCAMERA_DUMP_CAPTURE_SCRIPT, except instead of a + capture script with controls for each frame, the dump will consist of all + metadata that was returned for every frame. + + Also similar to LIBCAMERA_DUMP_CAPTURE_SCRIPT, there will be a number suffix + added to the filename of the dump. + + Example value: ``/home/{user}/metadata_dump.yaml`` + LIBCAMERA_IPA_FORCE_ISOLATION When set to a non-empty string, force process isolation of all IPA modules. diff --git a/include/libcamera/internal/camera.h b/include/libcamera/internal/camera.h index 0add0428bb5d..b029d85901a7 100644 --- a/include/libcamera/internal/camera.h +++ b/include/libcamera/internal/camera.h @@ -16,6 +16,7 @@ #include #include +#include namespace libcamera { @@ -65,6 +66,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..f31aced71331 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, + Orientation orientation); + protected: void registerCamera(std::shared_ptr camera); void hotplugMediaDevice(MediaDevice *media); @@ -81,6 +87,11 @@ protected: CameraManager *manager_; private: + enum class 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,12 @@ private: const char *name_; unsigned int useCount_; + unsigned int captureCount_; + std::string fileCapture_; + std::string fileMetadata_; + std::unique_ptr dumpCaptureScript_; + std::unique_ptr dumpMetadata_; + friend class PipelineHandlerFactoryBase; }; diff --git a/src/libcamera/camera.cpp b/src/libcamera/camera.cpp index a86f552a47bc..1282f99d839b 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..4df64ac90bdf 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,8 +69,10 @@ LOG_DEFINE_CATEGORY(Pipeline) * through the PipelineHandlerFactoryBase::create() function. */ PipelineHandler::PipelineHandler(CameraManager *manager) - : manager_(manager), useCount_(0) + : manager_(manager), useCount_(0), captureCount_(0) { + fileCapture_ = utils::secure_getenv("LIBCAMERA_DUMP_CAPTURE_SCRIPT"); + fileMetadata_ = utils::secure_getenv("LIBCAMERA_DUMP_METADATA"); } PipelineHandler::~PipelineHandler() @@ -464,6 +467,8 @@ void PipelineHandler::doQueueRequest(Request *request) request->_d()->sequence_ = data->requestSequence_++; + dumpRequest(request, DumpMode::Controls); + if (request->_d()->cancelled_) { completeRequest(request); return; @@ -555,6 +560,8 @@ void PipelineHandler::completeRequest(Request *request) request->_d()->complete(); + dumpRequest(request, DumpMode::Metadata); + Camera::Private *data = camera->_d(); while (!data->queuedRequests_.empty()) { @@ -758,6 +765,82 @@ void PipelineHandler::disconnect() * \return The CameraManager for this pipeline handler */ +void PipelineHandler::dumpConfiguration(const std::set &streams, + Orientation orientation) +{ + captureCount_++; + + /* These need to be done here in case capture is restarted */ + if (!fileCapture_.empty()) { + std::string file = fileCapture_ + "." + std::to_string(captureCount_); + LOG(Pipeline, Info) << "Dumping capture script to " << file; + dumpCaptureScript_ = std::make_unique(file); + } + + /* + * Metadata needs to go into a separate file because otherwise it'll + * flood the capture script + */ + if (!fileMetadata_.empty()) { + std::string file = fileMetadata_ + "." + std::to_string(captureCount_); + LOG(Pipeline, Info) << "Dumping metadata to " << file; + dumpMetadata_ = std::make_unique(file); + *dumpMetadata_ << "frames:" << std::endl; + dumpMetadata_->flush(); + } + + if (!dumpCaptureScript_) + return; + + std::ostream &output = *dumpCaptureScript_; + + output << "configuration:" << std::endl; + output << " orientation: " << orientation << std::endl; + + /* \todo Dump Sensor configuration */ + + output << " streams:" << std::endl; + for (const auto &stream : streams) { + const StreamConfiguration &streamConfig = stream->configuration(); + output << " - pixelFormat: " << streamConfig.pixelFormat << std::endl; + output << " size: " << streamConfig.size << std::endl; + output << " stride: " << streamConfig.stride << std::endl; + output << " frameSize: " << streamConfig.frameSize << std::endl; + output << " bufferCount: " << streamConfig.bufferCount << std::endl; + if (streamConfig.colorSpace) { + output << " colorSpace: " << streamConfig.colorSpace->toString() << std::endl; + } + } + + output << "frames:" << std::endl; + 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_.get() + : dumpMetadata_.get(); + + if (!output || controls.empty()) + return; + + /* \todo Figure out PFC */ + *output << " - " << request->sequence() << ":" << std::endl; + + const ControlIdMap *idMap = controls.idMap(); + for (const auto &pair : controls) { + const ControlId *ctrlId = idMap->at(pair.first); + *output << " " << ctrlId->name() << ": " << pair.second.toString() << std::endl; + } + + /* \todo Investigate the overhead of flushing this frequently */ + output->flush(); +} + /** * \class PipelineHandlerFactoryBase * \brief Base class for pipeline handler factories From patchwork Fri Oct 4 12:05:17 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Paul Elder X-Patchwork-Id: 21520 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 27409C3257 for ; Fri, 4 Oct 2024 12:05:42 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id DB83163512; Fri, 4 Oct 2024 14:05:40 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="hzxQUl+z"; 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 AE0DB62C92 for ; Fri, 4 Oct 2024 14:05:35 +0200 (CEST) Received: from neptunite.flets-east.jp (unknown [IPv6:2404:7a81:160:2100:e3ca:2180:ae9b:1941]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 0AABB59C; Fri, 4 Oct 2024 14:04:00 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1728043442; bh=93ebda6kJkLq8Nx5UezhNbPfWynaFqu/KPp6y3YAOTA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=hzxQUl+zglvFBHvUPA1KQYcLjgfxst1SIdHTBq2O3jDjzEIlsSh2fuKE9otp2Puay BqML9cVR3YHkY2vtp+wkkTtOXry6UJcGx1MVuV7QNHwXmFaXaoYw3Oo1pC4svge+49 BKrwnwkAn59a91ZT/gVv+mIvd655MAj4NKaqgqrU= From: Paul Elder To: libcamera-devel@lists.libcamera.org Cc: Paul Elder Subject: [PATCH v2 2/2] apps: cam: Add support for loading configuration from capture script Date: Fri, 4 Oct 2024 21:05:17 +0900 Message-Id: <20241004120517.3572281-3-paul.elder@ideasonboard.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20241004120517.3572281-1-paul.elder@ideasonboard.com> References: <20241004120517.3572281-1-paul.elder@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" Add support to the cam application for loading the camera configuration from a capture script. These can be written manually, or dumped from a capture session 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 Reviewed-by: Laurent Pinchart --- Changes in v2: - clean up code --- src/apps/cam/camera_session.cpp | 26 +++-- src/apps/cam/capture_script.cpp | 163 ++++++++++++++++++++++++++++++++ src/apps/cam/capture_script.h | 9 ++ src/apps/cam/main.cpp | 4 +- 4 files changed, 191 insertions(+), 11 deletions(-) diff --git a/src/apps/cam/camera_session.cpp b/src/apps/cam/camera_session.cpp index edc49b875450..695b313c3779 100644 --- a/src/apps/cam/camera_session.cpp +++ b/src/apps/cam/camera_session.cpp @@ -70,6 +70,22 @@ CameraSession::CameraSession(CameraManager *cm, return; } + /* + * Parse the capture script first to populate the configuration, and + * let command line arguments take precedence. + */ + 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); + } + if (options_.isSet(OptOrientation)) { std::string orientOpt = options_[OptOrientation].toString(); static const std::map orientations{ @@ -119,16 +135,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..a4298f99c4d8 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 @@ -158,6 +159,10 @@ int CaptureScript::parseScript(FILE *script) ret = parseProperties(); if (ret) return ret; + } else if (section == "configuration") { + ret = parseConfiguration(); + if (ret) + return ret; } else if (section == "frames") { ret = parseFrames(); if (ret) @@ -229,6 +234,156 @@ int CaptureScript::parseProperties() 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; +} + +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::parseStreams(EventPtr event) +{ + int ret; + + 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) { + if ((ret = parseStream(std::move(event), index++)) < 0) + return ret; + } else { + std::cerr << "Unknown type" << std::endl; + return -EINVAL; + } + } + + 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); + + std::string value = parseScalar(); + if (value.empty()) + return -EINVAL; + + 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::parseFrames() { EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT); @@ -322,6 +477,14 @@ int CaptureScript::parseControl(EventPtr event, ControlList &controls) return 0; } +void CaptureScript::populateConfiguration(CameraConfiguration &configuration) const +{ + 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..4fa3447d156f 100644 --- a/src/apps/cam/capture_script.h +++ b/src/apps/cam/capture_script.h @@ -26,6 +26,8 @@ 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 +45,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); @@ -52,6 +57,10 @@ private: int parseProperties(); int parseProperty(); + int parseConfiguration(); + int parseOrientation(EventPtr event); + int parseStreams(EventPtr event); + int parseStream(EventPtr event, unsigned int index); int parseFrames(); int parseFrame(EventPtr event); int parseControl(EventPtr event, libcamera::ControlList &controls); diff --git a/src/apps/cam/main.cpp b/src/apps/cam/main.cpp index 460dbc813060..4291b64e250d 100644 --- a/src/apps/cam/main.cpp +++ b/src/apps/cam/main.cpp @@ -175,7 +175,9 @@ int CamApp::parseOptions(int argc, char *argv[]) "metadata", ArgumentNone, nullptr, false, OptCamera); parser.addOption(OptCaptureScript, OptionString, - "Load a capture session configuration script from a file", + "Load a capture session configuration script from a file.\n" + "Configuration options specified in the capture script will be\n" + "overwritten by --stream and --orientation.", "script", ArgumentRequired, "script", false, OptCamera);