[{"id":2212,"web_url":"https://patchwork.libcamera.org/comment/2212/","msgid":"<20190711052451.GA8867@wyvern>","date":"2019-07-11T05:24:51","subject":"Re: [libcamera-devel] [PATCH 2/6] libcamera: thread: Add a\n\tmessaging passing API","submitter":{"id":5,"url":"https://patchwork.libcamera.org/api/people/5/","name":"Niklas Söderlund","email":"niklas.soderlund@ragnatech.se"},"content":"Hi Laurent,\n\nThanks for your patch.\n\nOn 2019-07-10 22:17:04 +0300, Laurent Pinchart wrote:\n> Create a new Message class to model a message that can be passed to an\n> object living in another thread. Only an invalid message type is\n> currently defined, more messages will be added in the future.\n> \n> The Thread class is extended with a messages queue, and the Object class\n> with thread affinity.\n> \n> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n> ---\n>  include/libcamera/object.h      |  13 +++\n>  src/libcamera/include/message.h |  37 ++++++++\n>  src/libcamera/include/thread.h  |   9 ++\n>  src/libcamera/meson.build       |   2 +\n>  src/libcamera/message.cpp       |  71 +++++++++++++++\n>  src/libcamera/object.cpp        |  77 ++++++++++++++++-\n>  src/libcamera/thread.cpp        | 147 +++++++++++++++++++++++++++++++-\n>  7 files changed, 354 insertions(+), 2 deletions(-)\n>  create mode 100644 src/libcamera/include/message.h\n>  create mode 100644 src/libcamera/message.cpp\n> \n> diff --git a/include/libcamera/object.h b/include/libcamera/object.h\n> index eadd41f9a41f..d61dfb1ebaef 100644\n> --- a/include/libcamera/object.h\n> +++ b/include/libcamera/object.h\n> @@ -8,26 +8,39 @@\n>  #define __LIBCAMERA_OBJECT_H__\n>  \n>  #include <list>\n> +#include <memory>\n>  \n>  namespace libcamera {\n>  \n> +class Message;\n>  class SignalBase;\n>  template<typename... Args>\n>  class Signal;\n> +class Thread;\n>  \n>  class Object\n>  {\n>  public:\n> +\tObject();\n>  \tvirtual ~Object();\n>  \n> +\tvoid postMessage(std::unique_ptr<Message> msg);\n> +\tvirtual void message(Message *msg);\n> +\n> +\tThread *thread() const { return thread_; }\n> +\tvoid moveToThread(Thread *thread);\n> +\n>  private:\n>  \ttemplate<typename... Args>\n>  \tfriend class Signal;\n> +\tfriend class Thread;\n>  \n>  \tvoid connect(SignalBase *signal);\n>  \tvoid disconnect(SignalBase *signal);\n>  \n> +\tThread *thread_;\n>  \tstd::list<SignalBase *> signals_;\n> +\tunsigned int pendingMessages_;\n>  };\n>  \n>  }; /* namespace libcamera */\n> diff --git a/src/libcamera/include/message.h b/src/libcamera/include/message.h\n> new file mode 100644\n> index 000000000000..97c9b80ec0e0\n> --- /dev/null\n> +++ b/src/libcamera/include/message.h\n> @@ -0,0 +1,37 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2019, Google Inc.\n> + *\n> + * message.h - Message queue support\n> + */\n> +#ifndef __LIBCAMERA_MESSAGE_H__\n> +#define __LIBCAMERA_MESSAGE_H__\n> +\n> +namespace libcamera {\n> +\n> +class Object;\n> +class Thread;\n> +\n> +class Message\n> +{\n> +public:\n> +\tenum Type {\n> +\t\tNone = 0,\n> +\t};\n> +\n> +\tMessage(Type type);\n> +\tvirtual ~Message();\n> +\n> +\tType type() const { return type_; }\n> +\tObject *receiver() const { return receiver_; }\n> +\n> +private:\n> +\tfriend class Thread;\n> +\n> +\tType type_;\n> +\tObject *receiver_;\n> +};\n> +\n> +} /* namespace libcamera */\n> +\n> +#endif /* __LIBCAMERA_MESSAGE_H__ */\n> diff --git a/src/libcamera/include/thread.h b/src/libcamera/include/thread.h\n> index e881d90e9367..acae91cb6457 100644\n> --- a/src/libcamera/include/thread.h\n> +++ b/src/libcamera/include/thread.h\n> @@ -16,6 +16,8 @@\n>  namespace libcamera {\n>  \n>  class EventDispatcher;\n> +class Message;\n> +class Object;\n>  class ThreadData;\n>  class ThreadMain;\n>  \n> @@ -49,9 +51,16 @@ private:\n>  \tvoid startThread();\n>  \tvoid finishThread();\n>  \n> +\tvoid postMessage(std::unique_ptr<Message> msg, Object *receiver);\n> +\tvoid removeMessages(Object *receiver);\n> +\tvoid dispatchMessages();\n> +\n> +\tfriend class Object;\n>  \tfriend class ThreadData;\n>  \tfriend class ThreadMain;\n>  \n> +\tvoid moveObject(Object *object);\n> +\n>  \tstd::thread thread_;\n>  \tThreadData *data_;\n>  };\n> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build\n> index bf71524f768c..3e5097a4cdc7 100644\n> --- a/src/libcamera/meson.build\n> +++ b/src/libcamera/meson.build\n> @@ -18,6 +18,7 @@ libcamera_sources = files([\n>      'log.cpp',\n>      'media_device.cpp',\n>      'media_object.cpp',\n> +    'message.cpp',\n>      'object.cpp',\n>      'pipeline_handler.cpp',\n>      'request.cpp',\n> @@ -45,6 +46,7 @@ libcamera_headers = files([\n>      'include/log.h',\n>      'include/media_device.h',\n>      'include/media_object.h',\n> +    'include/message.h',\n>      'include/pipeline_handler.h',\n>      'include/thread.h',\n>      'include/utils.h',\n> diff --git a/src/libcamera/message.cpp b/src/libcamera/message.cpp\n> new file mode 100644\n> index 000000000000..47caf44dc82d\n> --- /dev/null\n> +++ b/src/libcamera/message.cpp\n> @@ -0,0 +1,71 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2019, Google Inc.\n> + *\n> + * message.cpp - Message queue support\n> + */\n> +\n> +#include \"message.h\"\n> +\n> +#include \"log.h\"\n> +\n> +/**\n> + * \\file message.h\n> + * \\brief Message queue support\n> + *\n> + * The messaging API enables inter-thread communication through message\n> + * posting. Messages can be sent from any thread to any recipient deriving from\n> + * the Object class.\n> + *\n> + * The post a message, the sender allocates it dynamically as instance of a\n\ns/The/To/\n\nReviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>\n\n> + * class derived from Message. It then posts the message to an Object recipient\n> + * through Object::postMessage(). Message ownership is passed to the object,\n> + * the message shall thus not store any temporary data.\n> + *\n> + * The message is delivered in the context of the object's thread, through the\n> + * Object::message() virtual method. After delivery the message is\n> + * automatically deleted.\n> + */\n> +\n> +namespace libcamera {\n> +\n> +LOG_DEFINE_CATEGORY(Message)\n> +\n> +/**\n> + * \\class Message\n> + * \\brief A message that can be posted to a Thread\n> + */\n> +\n> +/**\n> + * \\enum Message::Type\n> + * \\brief The message type\n> + * \\var Message::None\n> + * \\brief Invalid message type\n> + */\n> +\n> +/**\n> + * \\brief Construct a message object of type \\a type\n> + * \\param[in] type The message type\n> + */\n> +Message::Message(Message::Type type)\n> +\t: type_(type)\n> +{\n> +}\n> +\n> +Message::~Message()\n> +{\n> +}\n> +\n> +/**\n> + * \\fn Message::type()\n> + * \\brief Retrieve the message type\n> + * \\return The message type\n> + */\n> +\n> +/**\n> + * \\fn Message::receiver()\n> + * \\brief Retrieve the message receiver\n> + * \\return The message receiver\n> + */\n> +\n> +}; /* namespace libcamera */\n> diff --git a/src/libcamera/object.cpp b/src/libcamera/object.cpp\n> index a504ca2c9daf..695e6c11b3a4 100644\n> --- a/src/libcamera/object.cpp\n> +++ b/src/libcamera/object.cpp\n> @@ -9,6 +9,9 @@\n>  \n>  #include <libcamera/signal.h>\n>  \n> +#include \"log.h\"\n> +#include \"thread.h\"\n> +\n>  /**\n>   * \\file object.h\n>   * \\brief Base object to support automatic signal disconnection\n> @@ -24,13 +27,85 @@ namespace libcamera {\n>   * slots. By inheriting from Object, an object is automatically disconnected\n>   * from all connected signals when it gets destroyed.\n>   *\n> - * \\sa Signal\n> + * Object instance are bound to the thread in which they're created. When a\n> + * message is posted to an object, its handler will run in the object's thread.\n> + * This allows implementing easy message passing between threads by inheriting\n> + * from the Object class.\n> + *\n> + * \\sa Message, Signal, Thread\n>   */\n>  \n> +Object::Object()\n> +\t: pendingMessages_(0)\n> +{\n> +\tthread_ = Thread::current();\n> +}\n> +\n>  Object::~Object()\n>  {\n>  \tfor (SignalBase *signal : signals_)\n>  \t\tsignal->disconnect(this);\n> +\n> +\tif (pendingMessages_)\n> +\t\tthread()->removeMessages(this);\n> +}\n> +\n> +/**\n> + * \\brief Post a message to the object's thread\n> + * \\param[in] msg The message\n> + *\n> + * This method posts the message \\a msg to the message queue of the object's\n> + * thread, to be delivered to the object through the message() method in the\n> + * context of its thread. Message ownership is passed to the thread, and the\n> + * message will be deleted after being delivered.\n> + *\n> + * Messages are delivered through the thread's event loop. If the thread is not\n> + * running its event loop the message will not be delivered until the event\n> + * loop gets started.\n> + */\n> +void Object::postMessage(std::unique_ptr<Message> msg)\n> +{\n> +\tthread()->postMessage(std::move(msg), this);\n> +}\n> +\n> +/**\n> + * \\brief Message handler for the object\n> + * \\param[in] msg The message\n> + *\n> + * This virtual method receives messages for the object. It is called in the\n> + * context of the object's thread, and can be overridden to process custom\n> + * messages. The parent QObject::message() method shall be called for any\n> + * message not handled by the override method.\n> + *\n> + * The message \\a msg is valid only for the duration of the call, no reference\n> + * to it shall be kept after this method returns.\n> + */\n> +void Object::message(Message *msg)\n> +{\n> +}\n> +\n> +/**\n> + * \\fn Object::thread()\n> + * \\brief Retrieve the thread the object is bound to\n> + * \\return The thread the object is bound to\n> + */\n> +\n> +/**\n> + * \\brief Move the object to a different thread\n> + * \\param[in] thread The target thread\n> + *\n> + * This method moves the object from the current thread to the new \\a thread.\n> + * It shall be called from the thread in which the object currently lives,\n> + * otherwise the behaviour is undefined.\n> + */\n> +void Object::moveToThread(Thread *thread)\n> +{\n> +\tASSERT(Thread::current() == thread_);\n> +\n> +\tif (thread_ == thread)\n> +\t\treturn;\n> +\n> +\tthread->moveObject(this);\n>  }\n>  \n>  void Object::connect(SignalBase *signal)\n> diff --git a/src/libcamera/thread.cpp b/src/libcamera/thread.cpp\n> index 95636ecaab53..5d46eeb8d3a5 100644\n> --- a/src/libcamera/thread.cpp\n> +++ b/src/libcamera/thread.cpp\n> @@ -8,11 +8,13 @@\n>  #include \"thread.h\"\n>  \n>  #include <atomic>\n> +#include <list>\n>  \n>  #include <libcamera/event_dispatcher.h>\n>  \n>  #include \"event_dispatcher_poll.h\"\n>  #include \"log.h\"\n> +#include \"message.h\"\n>  \n>  /**\n>   * \\file thread.h\n> @@ -25,6 +27,22 @@ LOG_DEFINE_CATEGORY(Thread)\n>  \n>  class ThreadMain;\n>  \n> +/**\n> + * \\brief A queue of posted messages\n> + */\n> +class MessageQueue\n> +{\n> +public:\n> +\t/**\n> +\t * \\brief List of queued Message instances\n> +\t */\n> +\tstd::list<std::unique_ptr<Message>> list_;\n> +\t/**\n> +\t * \\brief Protects the \\ref list_\n> +\t */\n> +\tMutex mutex_;\n> +};\n> +\n>  /**\n>   * \\brief Thread-local internal data\n>   */\n> @@ -51,6 +69,8 @@ private:\n>  \n>  \tstd::atomic<bool> exit_;\n>  \tint exitCode_;\n> +\n> +\tMessageQueue messages_;\n>  };\n>  \n>  /**\n> @@ -192,8 +212,10 @@ int Thread::exec()\n>  \n>  \tlocker.unlock();\n>  \n> -\twhile (!data_->exit_.load(std::memory_order_acquire))\n> +\twhile (!data_->exit_.load(std::memory_order_acquire)) {\n> +\t\tdispatchMessages();\n>  \t\tdispatcher->processEvents();\n> +\t}\n>  \n>  \tlocker.lock();\n>  \n> @@ -332,4 +354,127 @@ EventDispatcher *Thread::eventDispatcher()\n>  \treturn data_->dispatcher_.load(std::memory_order_relaxed);\n>  }\n>  \n> +/**\n> + * \\brief Post a message to the thread for the \\a receiver\n> + * \\param[in] msg The message\n> + * \\param[in] receiver The receiver\n> + *\n> + * This method stores the message \\a msg in the message queue of the thread for\n> + * the \\a receiver and wake up the thread's event loop. Message ownership is\n> + * passed to the thread, and the message will be deleted after being delivered.\n> + *\n> + * Messages are delivered through the thread's event loop. If the thread is not\n> + * running its event loop the message will not be delivered until the event\n> + * loop gets started.\n> + *\n> + * If the \\a receiver is not bound to this thread the behaviour is undefined.\n> + *\n> + * \\sa exec()\n> + */\n> +void Thread::postMessage(std::unique_ptr<Message> msg, Object *receiver)\n> +{\n> +\tmsg->receiver_ = receiver;\n> +\n> +\tASSERT(data_ == receiver->thread()->data_);\n> +\n> +\tMutexLocker locker(data_->messages_.mutex_);\n> +\tdata_->messages_.list_.push_back(std::move(msg));\n> +\treceiver->pendingMessages_++;\n> +\tlocker.unlock();\n> +\n> +\tEventDispatcher *dispatcher =\n> +\t\tdata_->dispatcher_.load(std::memory_order_acquire);\n> +\tif (dispatcher)\n> +\t\tdispatcher->interrupt();\n> +}\n> +\n> +/**\n> + * \\brief Remove all posted messages for the \\a receiver\n> + * \\param[in] receiver The receiver\n> + *\n> + * If the \\a receiver is not bound to this thread the behaviour is undefined.\n> + */\n> +void Thread::removeMessages(Object *receiver)\n> +{\n> +\tASSERT(data_ == receiver->thread()->data_);\n> +\n> +\tMutexLocker locker(data_->messages_.mutex_);\n> +\tif (!receiver->pendingMessages_)\n> +\t\treturn;\n> +\n> +\tstd::vector<std::unique_ptr<Message>> toDelete;\n> +\tfor (std::unique_ptr<Message> &msg : data_->messages_.list_) {\n> +\t\tif (!msg)\n> +\t\t\tcontinue;\n> +\t\tif (msg->receiver_ != receiver)\n> +\t\t\tcontinue;\n> +\n> +\t\t/*\n> +\t\t * Move the message to the pending deletion list to delete it\n> +\t\t * after releasing the lock. The messages list element will\n> +\t\t * contain a null pointer, and will be removed when dispatching\n> +\t\t * messages.\n> +\t\t */\n> +\t\ttoDelete.push_back(std::move(msg));\n> +\t\treceiver->pendingMessages_--;\n> +\t}\n> +\n> +\tASSERT(!receiver->pendingMessages_);\n> +\tlocker.unlock();\n> +\n> +\ttoDelete.clear();\n> +}\n> +\n> +/**\n> + * \\brief Dispatch all posted messages for this thread\n> + */\n> +void Thread::dispatchMessages()\n> +{\n> +\tMutexLocker locker(data_->messages_.mutex_);\n> +\n> +\twhile (!data_->messages_.list_.empty()) {\n> +\t\tstd::unique_ptr<Message> msg = std::move(data_->messages_.list_.front());\n> +\t\tdata_->messages_.list_.pop_front();\n> +\t\tif (!msg)\n> +\t\t\tcontinue;\n> +\n> +\t\tObject *receiver = msg->receiver_;\n> +\t\tASSERT(data_ == receiver->thread()->data_);\n> +\n> +\t\tlocker.unlock();\n> +\t\treceiver->message(msg.get());\n> +\t\tlocker.lock();\n> +\n> +\t\treceiver->pendingMessages_--;\n> +\t}\n> +}\n> +\n> +/**\n> + * \\brief Move an \\a object to the thread\n> + * \\param[in] object The object\n> + */\n> +void Thread::moveObject(Object *object)\n> +{\n> +\tThreadData *currentData = object->thread_->data_;\n> +\tThreadData *targetData = data_;\n> +\n> +\tMutexLocker lockerFrom(currentData->mutex_, std::defer_lock);\n> +\tMutexLocker lockerTo(targetData->mutex_, std::defer_lock);\n> +\tstd::lock(lockerFrom, lockerTo);\n> +\n> +\t/* Move pending messages to the message queue of the new thread. */\n> +\tif (object->pendingMessages_) {\n> +\t\tfor (std::unique_ptr<Message> &msg : currentData->messages_.list_) {\n> +\t\t\tif (!msg)\n> +\t\t\t\tcontinue;\n> +\t\t\tif (msg->receiver_ != object)\n> +\t\t\t\tcontinue;\n> +\n> +\t\t\ttargetData->messages_.list_.push_back(std::move(msg));\n> +\t\t}\n> +\t}\n> +\n> +\tobject->thread_ = this;\n> +}\n> +\n>  }; /* namespace libcamera */\n> -- \n> Regards,\n> \n> Laurent Pinchart\n> \n> _______________________________________________\n> libcamera-devel mailing list\n> libcamera-devel@lists.libcamera.org\n> https://lists.libcamera.org/listinfo/libcamera-devel","headers":{"Return-Path":"<niklas.soderlund@ragnatech.se>","Received":["from mail-pg1-x543.google.com (mail-pg1-x543.google.com\n\t[IPv6:2607:f8b0:4864:20::543])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id D1FD160C23\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 11 Jul 2019 07:24:56 +0200 (CEST)","by mail-pg1-x543.google.com with SMTP id p10so2344042pgn.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 10 Jul 2019 22:24:56 -0700 (PDT)","from localhost (softbank126163157105.bbtec.net. [126.163.157.105])\n\tby smtp.gmail.com with ESMTPSA id\n\tq126sm3945353pfq.123.2019.07.10.22.24.53\n\t(version=TLS1_3 cipher=AEAD-AES256-GCM-SHA384 bits=256/256);\n\tWed, 10 Jul 2019 22:24:54 -0700 (PDT)"],"DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=ragnatech-se.20150623.gappssmtp.com; s=20150623;\n\th=date:from:to:cc:subject:message-id:references:mime-version\n\t:content-disposition:content-transfer-encoding:in-reply-to\n\t:user-agent; bh=hbJDYKK+T76nvcXLlTxZbnLUY21B3469Gx2G4FUdWM8=;\n\tb=tMc067UCCpvP9xo6hRsBAa96IJcdLY+vovVCfjrLnd4IGlqALINg+xnMC9GE0vsFHX\n\tELCTGSkK9SDw+AskhjyukwanYA01rqgdaKQUwRDvIC20Ur3suOSQN7+G9kiFf+NA6Nhu\n\t4w/Twt8/LOue03f+tBdPaxfpnhbI9XwttGsWdoSvTnf2w0zzIJnu0HJ3NgC46YAtlaeh\n\t9tVVSJe7xmjyOMTHFHXQYdX6VY+1ZaCIFfLEUgN2Z0qu1ZzyDeP+LBBapSBd6a9xabhZ\n\tz945gQ7JA01eqslg9qlDxh9QRhyUJ2+//JTseEkE8jRx6XIz6IAPTVQzw7BSsNKMtp54\n\tiVZw==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20161025;\n\th=x-gm-message-state:date:from:to:cc:subject:message-id:references\n\t:mime-version:content-disposition:content-transfer-encoding\n\t:in-reply-to:user-agent;\n\tbh=hbJDYKK+T76nvcXLlTxZbnLUY21B3469Gx2G4FUdWM8=;\n\tb=EPr4aH26fBz4+vYQqPZFa7as5TIdQgnLokKTjcOzrHb0ehM0MsfSMs/EoaiVOahIpE\n\tu0uf4OpJHVydw04Un3bkHli8eh9cLCQ2yhm+31Fm3Thwg4v8RrVLxzd0R7STdi4uNApF\n\t1LA+D1u7BUR7KhU5aIl5UNAGbJ51v8248hEp09QoWI7cJFCxQ24YHiAzX9Eq8cWbenVa\n\twBTlmld4E3cUbbyhppOp8fA3OKaq8uHIiAVtcYdj7iUKsuLqQhD1+y8tN69V23K1D8uL\n\tsAjg6PunmATkF6ANyolkqW0tmrWQTrP/dSD4OpwZRMwdoE24tBkDzBGijKP7T5xcb1of\n\tqnMQ==","X-Gm-Message-State":"APjAAAXsWenymKoImO6E21YfstJL99kIctWJ6Q3wuLfXV5x7BQg1luMp\n\taWMwS2xK9CSW9tvAA9RZG4agdCt9IIg=","X-Google-Smtp-Source":"APXvYqyzRygZ6UfDXEVWHSIowEBEnaYsMSSwq1gw7iTM7/VIein/8LZHnoq30U/u8STxHUpVY7qvKQ==","X-Received":"by 2002:a63:c342:: with SMTP id e2mr2336032pgd.79.1562822695323; \n\tWed, 10 Jul 2019 22:24:55 -0700 (PDT)","Date":"Thu, 11 Jul 2019 14:24:51 +0900","From":"Niklas =?iso-8859-1?q?S=F6derlund?= <niklas.soderlund@ragnatech.se>","To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Message-ID":"<20190711052451.GA8867@wyvern>","References":"<20190710191708.13049-1-laurent.pinchart@ideasonboard.com>\n\t<20190710191708.13049-2-laurent.pinchart@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=iso-8859-1","Content-Disposition":"inline","Content-Transfer-Encoding":"8bit","In-Reply-To":"<20190710191708.13049-2-laurent.pinchart@ideasonboard.com>","User-Agent":"Mutt/1.12.1 (2019-06-15)","Subject":"Re: [libcamera-devel] [PATCH 2/6] libcamera: thread: Add a\n\tmessaging passing API","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.23","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","X-List-Received-Date":"Thu, 11 Jul 2019 05:24:57 -0000"}}]