From 03e13290e69c0af4d93127395f604757283bef9c Mon Sep 17 00:00:00 2001
From: Julien Robert <julien.robert@savoirfairelinux.com>
Date: Thu, 16 May 2024 11:23:01 -0400
Subject: [PATCH] SDK: Add a new Example plugin with documentation

Gitlab: #63

Change-Id: I347661661c188089291b9707c44d4b804899c0ab
---
 .gitignore                          |   6 +-
 AutoAnswer/CMakeLists.txt           |  94 ++++--
 Example/BotChatHandler.cpp          |  78 +++++
 Example/BotChatHandler.h            |  58 ++++
 Example/BotPeerChatSubscriber.cpp   | 115 +++++++
 Example/BotPeerChatSubscriber.h     |  55 ++++
 Example/CMakeLists.txt              | 221 +++++++++++++
 Example/ExampleAudioSubscriber.cpp  | 237 ++++++++++++++
 Example/ExampleMediaHandler.cpp     | 128 ++++++++
 Example/ExampleVideoSubscriber.cpp  | 479 ++++++++++++++++++++++++++++
 Example/data/locale/Example_en.json |   4 +
 Example/main.cpp                    | 166 ++++++++++
 Example/manifest.json               |   8 +
 SDK/Templates/CMakeLists.txt        |  35 +-
 build-plugin.py                     |   4 +-
 build.md                            |  55 ++++
 contrib/build-dependencies.sh       |  18 +-
 daemon                              |   2 +-
 docker/Dockerfile_ubuntu_20.04      |   3 +-
 extras/ci/android/Jenkinsfile       |   2 +
 notarize.sh                         |   2 +-
 21 files changed, 1703 insertions(+), 67 deletions(-)
 create mode 100644 Example/BotChatHandler.cpp
 create mode 100644 Example/BotChatHandler.h
 create mode 100644 Example/BotPeerChatSubscriber.cpp
 create mode 100644 Example/BotPeerChatSubscriber.h
 create mode 100644 Example/CMakeLists.txt
 create mode 100644 Example/ExampleAudioSubscriber.cpp
 create mode 100644 Example/ExampleMediaHandler.cpp
 create mode 100644 Example/ExampleVideoSubscriber.cpp
 create mode 100644 Example/data/locale/Example_en.json
 create mode 100644 Example/main.cpp
 create mode 100644 Example/manifest.json
 create mode 100644 build.md

diff --git a/.gitignore b/.gitignore
index 413a3e7..82f819e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
-/build/
+build/
+build-local/
 *.jpl
 *msvc*
-*build*
 *android-toolchain-*
 config.mak
 /contrib/Libs/
@@ -13,3 +13,5 @@ config.mak
 *.key
 *.sign
 /plugin-builder/
+CMakeFiles/
+*.so
\ No newline at end of file
diff --git a/AutoAnswer/CMakeLists.txt b/AutoAnswer/CMakeLists.txt
index 7cbc5d2..1eece74 100644
--- a/AutoAnswer/CMakeLists.txt
+++ b/AutoAnswer/CMakeLists.txt
@@ -1,3 +1,18 @@
+# Copyright (C) 2023-2024 Savoir-faire Linux Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+
 cmake_minimum_required(VERSION 3.10)
 
 # set the project name
@@ -6,6 +21,12 @@ set (Version 2.0.0)
 
 project(${ProjectName} VERSION ${Version})
 
+option(VIDEO_FFMPEG "If you'd like to listen to Jami's video stream or modify it, set to ON." OFF) # {FILL}
+option(AUDIO_FFMPEG "If you'd like to listen to Jami's audio stream or modify it, set to ON." OFF) # {FILL}
+option(ARCHIVE_JPL "If you'd like to end the build process before the generation of a JPL archive, set to OFF." ON) # {FILL}
+option(NVIDIA "To disable hardware acceleration and use the graphics card for ONNX computation (useful for AI plugins), set to ON." OFF) # {FILL}
+option(CERTIFICATION "If you'd like to certify the JPL archive created by ARCHIVE_JPL, set to ON. Requires ARCHIVE_JPL to be ON." ON) # {FILL}
+
 set (DAEMON ${PROJECT_SOURCE_DIR}/../daemon)
 set (JPL_FILE_NAME ${ProjectName}.jpl)
 set (DAEMON_SRC ${DAEMON}/src)
@@ -43,35 +64,40 @@ message(Build path: ${PROJECT_BINARY_DIR})
 message(JPL assembling path: ${JPL_DIRECTORY})
 message(JPL path: ${JPL_DIRECTORY}/../../../build/${ProjectName}/${JPL_FILE_NAME})
 
+# This is specifically to disable hardware acceleration and do computing on the graphics card for computationally expensive AI plugins.
+if(NVIDIA)
+add_definitions(-DNVIDIA)
+set(ONNX_DIR ${ONNX_DIR}/nvidia-gpu)
+message(Provider:\ NVIDIA)
+set(EXTRA_PATH nvidia-gpu)
+set (PREFERENCESFILENAME ${PREFERENCESFILENAME}-accel)
+else()
+set(ONNX_DIR ${ONNX_DIR}/cpu)
+message(Provider:\ NONE)
+set(EXTRA_PATH cpu)
+endif()
+
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED True)
 set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /DMSGPACK_NO_BOOST /MT")
 set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /DMSGPACK_NO_BOOST /MTd")
 
-# Android-specific flags
-if(ANDROID)
-    set(CMAKE_ANDROID_ARCH_ABI arm64-v8a) # Set the desired ABI
-    set(CMAKE_ANDROID_STL_TYPE c++_shared) # Use C++ shared library
-    set(CMAKE_ANDROID_NDK_TOOLCHAIN_FILE ${CONTRIB_PATH}/build/cmake/android.toolchain.cmake)
-    set(CMAKE_ANDROID_STL_INCLUDE_DIR ${CONTRIB_PATH}/sysroot/usr/include)
-    set(CMAKE_ANDROID_STL_LIBRARIES ${CONTRIB_PATH}/sysroot/usr/lib)
+set(SOURCES "")
+file(GLOB CPP_SOURCES "*.cpp") # All .cpp files in the directory of your plugin ({root}/PLUGIN_NAME) will be collected by cmake.
+list(APPEND SOURCES ${CPP_SOURCES})
+
+if(VIDEO_FFMPEG) # If you previously turned ON video stream capture, these will be imported.
+    list(APPEND SOURCES "./../lib/accel.cpp")
+    list(APPEND SOURCES "./../lib/frameUtils.cpp")
 endif()
 
-set(plugin_SRC BotPeerChatSubscriber.cpp
-               BotChatHandler.cpp
-               PluginPreferenceHandler.cpp
-               main.cpp
-               )
+if(AUDIO_FFMPEG) # If you previously turned ON audio stream capture, these will be imported.
+    list(APPEND SOURCES "./../lib/frameUtils.cpp")
+endif()
 
-set(plugin_HDR BotPeerChatSubscriber.h
-               PluginPreferenceHandler.h
-               BotChatHandler.h
-               ./../lib/pluglog.h
-               )
+# list(APPEND SOURCES "./../lib/EDIT_ME.cpp") # {FILL} If you'd like to import any other .cpp files, uncomment this line, and copy it for each import.
 
-add_library(${ProjectName} SHARED ${plugin_SRC}
-                                  ${plugin_HDR}
-                                  )
+add_library(${ProjectName} SHARED ${SOURCES})
 
 target_include_directories(${ProjectName} PUBLIC ${PROJECT_BINARY_DIR}
                                                  ${PROJECT_SOURCE_DIR}
@@ -81,11 +107,9 @@ target_include_directories(${ProjectName} PUBLIC ${PROJECT_BINARY_DIR}
                                                  ${CONTRIB_PATH}/build/fmt/include
                                                  ${CONTRIB_PATH}/build/opendht/include
                                                  ${CONTRIB_PATH}/build/msgpack-c/include
-                                                 
                                                  )
 target_link_directories(${ProjectName} PUBLIC ${CONTRIB_PATH}
                                               ${CONTRIB_PATH}/build/fmt/msvc/Release
-                                        
                                         )
 
 target_link_libraries(${ProjectName} PUBLIC )
@@ -139,9 +163,33 @@ else()
     )
 endif()
 
+if (NOT ARCHIVE_JPL)
+    exit()
+endif()
+
 add_custom_command(
     TARGET ${ProjectName}
     POST_BUILD
     COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/jplManipulation.py --assemble --plugin=${ProjectName}
     COMMENT "Generating JPL archive"
-)
\ No newline at end of file
+)
+
+if(NOT EXISTS "${PROJECT_SOURCE_DIR}/../.cert")
+    file(MAKE_DIRECTORY "${PROJECT_SOURCE_DIR}/../.cert")
+endif()
+
+if (CERTIFICATION)
+    add_custom_command(
+        TARGET ${ProjectName}
+        POST_BUILD
+        COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/certKey.py create --subject Dev ../.cert/Dev
+        COMMENT "Generating developer certificate"
+    )
+
+    add_custom_command(
+        TARGET ${ProjectName}
+        POST_BUILD
+        COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/certKey.py create --issuer ../.cert/Dev --subject ${ProjectName} ../.cert/${ProjectName}/${ProjectName}
+        COMMENT "Generating plugin certificate"
+    )
+endif()
diff --git a/Example/BotChatHandler.cpp b/Example/BotChatHandler.cpp
new file mode 100644
index 0000000..5802a62
--- /dev/null
+++ b/Example/BotChatHandler.cpp
@@ -0,0 +1,78 @@
+/**
+ *  Copyright (C) 2023-2024 Savoir-faire Linux Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include "BotChatHandler.h"
+
+#include "pluglog.h"
+
+const char sep = separator();
+const std::string TAG = "Bot";
+
+#define NAME "Bot"
+
+namespace jami {
+
+BotChatHandler::BotChatHandler(const JAMI_PluginAPI* api, // Construct the handler.
+                               std::string&& dataPath,
+                               PluginPreferenceHandler* prefHandler)
+    : api_ {api}
+    , datapath_ {dataPath}
+{
+    setId(datapath_);
+    aph_ = prefHandler;
+    peerChatSubscriber_ = std::make_shared<BotPeerChatSubscriber>(api_, aph_);
+};
+
+void
+BotChatHandler::notifyChatSubject(std::pair<std::string, std::string>& subjectConnection, // When a new chat needs to be handled, this is the function responsible for it.
+                                  chatSubjectPtr subject)
+{
+    if (peerChatSubscriber_ && subjects.find(subject) == subjects.end()) {
+        std::ostringstream oss;
+        oss << "NEW SUBJECT: account = " << subjectConnection.first
+            << " peer = " << subjectConnection.second << std::endl;
+        Plog::log(Plog::LogPriority::INFO, TAG, oss.str());
+        subject->attach(peerChatSubscriber_.get()); // Attach the bot to the chat session.
+        subjects.insert(subject);
+    }
+}
+
+std::map<std::string, std::string>
+BotChatHandler::getChatHandlerDetails()
+{
+    return {{"name", NAME}, {"iconPath", datapath_ + sep + "icon.svg"}, {"pluginId", id()}}; // Fetch the bot's info, to represent it in the extensions UI.
+}
+
+void
+BotChatHandler::detach(chatSubjectPtr subject) // Detach the bot from a chat stream.
+{
+    auto it = subjects.find(subject);
+    if (it != subjects.end()) {
+        subject->detach(peerChatSubscriber_.get());
+        subjects.erase(it);
+    }
+}
+
+BotChatHandler::~BotChatHandler() // Deconstruct the handler.
+{
+    const auto copy = subjects;
+    for (const auto& subject : copy) {
+        detach(subject);
+    }
+}
+} // namespace jami
diff --git a/Example/BotChatHandler.h b/Example/BotChatHandler.h
new file mode 100644
index 0000000..11c63ad
--- /dev/null
+++ b/Example/BotChatHandler.h
@@ -0,0 +1,58 @@
+/**
+ *  Copyright (C) 2023-2024 Savoir-faire Linux Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+#include "BotPeerChatSubscriber.h"
+
+#include "plugin/jamiplugin.h"
+#include "plugin/chathandler.h"
+
+#include <string>
+#include <map>
+#include <memory>
+#include <set>
+
+using chatSubjectPtr = std::shared_ptr<jami::PublishObservable<jami::pluginMessagePtr>>;
+
+namespace jami {
+
+class BotChatHandler : public jami::ChatHandler
+{
+public:
+    BotChatHandler(const JAMI_PluginAPI* api,
+                   std::string&& dataPath,
+                   PluginPreferenceHandler* prefHandler);
+    ~BotChatHandler();
+
+    void notifyChatSubject(std::pair<std::string, std::string>& subjectConnection,
+                           chatSubjectPtr subject) override;
+    std::map<std::string, std::string> getChatHandlerDetails() override;
+    void detach(chatSubjectPtr subject) override;
+    void setPreferenceAttribute(const std::string& key, const std::string& value) override {}
+    bool preferenceMapHasKey(const std::string& key) override { return false; }
+
+    std::shared_ptr<BotPeerChatSubscriber> peerChatSubscriber_ {};
+
+private:
+    const JAMI_PluginAPI* api_;
+    const std::string datapath_;
+    PluginPreferenceHandler* aph_;
+    std::set<chatSubjectPtr> subjects;
+};
+} // namespace jami
diff --git a/Example/BotPeerChatSubscriber.cpp b/Example/BotPeerChatSubscriber.cpp
new file mode 100644
index 0000000..557b208
--- /dev/null
+++ b/Example/BotPeerChatSubscriber.cpp
@@ -0,0 +1,115 @@
+/**
+ *  Copyright (c) 2023-2024 savoir-faire linux inc.
+ *
+ *  this program is free software; you can redistribute it and/or modify
+ *  it under the terms of the gnu general public license as published by
+ *  the free software foundation; either version 3 of the license, or
+ *  (at your option) any later version.
+ *
+ *  this program is distributed in the hope that it will be useful,
+ *  but without any warranty; without even the implied warranty of
+ *  merchantability or fitness for a particular purpose.  see the
+ *  gnu general public license for more details.
+ *
+ *  you should have received a copy of the gnu general public license
+ *  along with this program; if not, write to the free software
+ *  foundation, inc., 51 franklin street, fifth floor, boston, ma  02110-1301 usa.
+ */
+
+#include "botpeerchatsubscriber.h"
+#include "pluglog.h"
+
+const std::string tag = "bot";
+
+namespace jami {
+
+botpeerchatsubscriber::botpeerchatsubscriber(const jami_pluginapi* api, // Construct the subscriber.
+                                             pluginpreferencehandler* prefhandler)
+    : api_ {api}
+{
+    aph_ = prefhandler;
+}
+
+botpeerchatsubscriber::~botpeerchatsubscriber() // Deconstruct the subscriber.
+{
+    std::ostringstream oss;
+    oss << "~botchatprocessor" << std::endl;
+    plog::log(plog::logpriority::info, tag, oss.str());
+}
+
+void
+botpeerchatsubscriber::update(observable<pluginmessageptr>*, const pluginmessageptr& message)
+{
+    if (!aph_) // If the preference handler is not initialized, return.
+        return;
+    std::string input = aph_->getpreferences(message->accountid, "intext"); // Fetch the input text from preferences.
+    std::string answer = aph_->getpreferences(message->accountid, "answer"); // Fetch how to answer the input text from preferences.
+    if (isattached) { // If the stream is attached...
+        if (message->direction) {
+            std::map<std::string, std::string> sendmsg;
+            if (message->fromhistory) // A message coming from the chat history should not be answered.
+                return;
+            if (!message->isswarm) // In a swarm chat...
+                for (auto& pair : message->data) {
+                    if (pair.first == "text/plain" && pair.second == input) { // If the input matches the preference trigger, answer it.
+                        sendmsg[pair.first] = answer;
+                    }
+                }
+            else if (message->data.at("type") == "text/plain" && message->data.at("body") == input) { // In a normal chat, also appropriately answer input text.
+                sendmsg["type"] = "text/plain";
+                sendmsg["body"] = answer;
+#ifdef __debug__
+                plog::log(plog::logpriority::info, tag, "input " + message->data.at("body"));
+                plog::log(plog::logpriority::info, tag, "ouput " + answer);
+#endif
+            }
+            if (!sendmsg.empty()) {
+                sendtext(message->accountid, message->peerid, sendmsg, message->isswarm);
+            }
+        }
+    }
+}
+
+void
+botpeerchatsubscriber::attached(observable<pluginmessageptr>* observable)
+{
+    if (observables_.find(observable) == observables_.end()) {
+        std::ostringstream oss;
+        oss << "::attached ! " << std::endl;
+        plog::log(plog::logpriority::info, tag, oss.str());
+        observables_.insert(observable);
+        isattached = true;
+    }
+}
+
+void
+botpeerchatsubscriber::detached(observable<pluginmessageptr>* observable)
+{
+    auto it = observables_.find(observable);
+    if (it != observables_.end()) {
+        observables_.erase(it);
+        std::ostringstream oss;
+        oss << "::detached()" << std::endl;
+        plog::log(plog::logpriority::info, tag, oss.str());
+        if (observables_.empty())
+            isattached = false;
+    }
+}
+
+void
+botpeerchatsubscriber::sendtext(std::string& accountid,
+                                std::string& peerid,
+                                std::map<std::string, std::string>& sendmsg,
+                                bool swarm)
+{
+    pluginmessageptr botanswer = std::make_shared<jamimessage>(accountid, // Send the message from this account.
+                                                               peerid,
+                                                               false,
+                                                               sendmsg,
+                                                               true);
+    botanswer->isswarm = swarm;
+#ifndef __debug__
+    api_->invokeservice(api_, "sendtextmessage", botanswer.get());
+#endif
+}
+} // namespace jami
diff --git a/Example/BotPeerChatSubscriber.h b/Example/BotPeerChatSubscriber.h
new file mode 100644
index 0000000..6619a98
--- /dev/null
+++ b/Example/BotPeerChatSubscriber.h
@@ -0,0 +1,55 @@
+/**
+ *  Copyright (C) 2023-2024 Savoir-faire Linux Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+#include "observer.h"
+
+#include "plugin/streamdata.h"
+#include "plugin/jamiplugin.h"
+#include "plugin/chathandler.h"
+#include "PluginPreferenceHandler.h"
+
+#include <map>
+#include <set>
+
+namespace jami {
+
+class BotPeerChatSubscriber : public Observer<pluginMessagePtr>
+{
+public:
+    BotPeerChatSubscriber(const JAMI_PluginAPI* api,
+                          PluginPreferenceHandler* prefHandler);
+    ~BotPeerChatSubscriber();
+    virtual void update(Observable<pluginMessagePtr>*, pluginMessagePtr const&) override;
+    virtual void attached(Observable<pluginMessagePtr>*) override;
+    virtual void detached(Observable<pluginMessagePtr>*) override;
+
+    void sendText(std::string& accountId,
+                  std::string& peerId,
+                  std::map<std::string, std::string>& sendMsg,
+                  bool swarm);
+
+protected:
+    // Observer pattern
+    std::set<Observable<pluginMessagePtr>*> observables_;
+    bool isAttached {false};
+    const JAMI_PluginAPI* api_;
+    PluginPreferenceHandler* aph_;
+};
+} // namespace jami
diff --git a/Example/CMakeLists.txt b/Example/CMakeLists.txt
new file mode 100644
index 0000000..071d4bb
--- /dev/null
+++ b/Example/CMakeLists.txt
@@ -0,0 +1,221 @@
+# Copyright (C) 2023-2024 Savoir-faire Linux Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+
+cmake_minimum_required(VERSION 3.10)
+
+set (PROJECT_NAME Example) # {FILL} Replace Example with the name of your plugin.
+set (VERSION 0.0.0) # {FILL} Replace 0.0.0 with the version of your plugin.
+
+project(${PROJECT_NAME} VERSION ${VERSION})
+
+option(VIDEO_FFMPEG "If you'd like to listen to Jami's video stream or modify it, set to ON." OFF) # {FILL}
+option(AUDIO_FFMPEG "If you'd like to listen to Jami's audio stream or modify it, set to ON." OFF) # {FILL}
+option(ARCHIVE_JPL "If you'd like to end the build process before the generation of a JPL archive, set to OFF." ON) # {FILL}
+option(NVIDIA "If you'd like to disable hardware acceleration and use the graphics card for ONNX computation (useful for AI plugins), set to ON." OFF) # {FILL}
+option(DEBUG_SIGN "If you'd like to sign the JPL archive created by ARCHIVE_JPL to test it in your Jami client, set to ON. Requires ARCHIVE_JPL to be ON." ON) # {FILL}
+
+set (DAEMON ${PROJECT_SOURCE_DIR}/../daemon)
+set (JPL_FILE_NAME ${ProjectName}.jpl)
+set (DAEMON_SRC ${DAEMON}/src)
+set (CONTRIB_PATH ${DAEMON}/contrib)
+set (PLUGINS_LIB ${PROJECT_SOURCE_DIR}/../lib)/
+set (JPL_DIRECTORY ${PROJECT_BINARY_DIR}/jpl)
+
+# Detect the operating system
+if(WIN32)
+    set(OS_NAME "WINDOWS")
+    set(OUTPUT_JPL "x64-windows")
+elseif(ANDROID)
+    set(OS_NAME "ANDROID")
+    set(OUTPUT_JPL "x86_64-linux-gnu")
+elseif(DARWIN)
+    set(OS_NAME "UNIX")
+    set(OUTPUT_JPL "x86_64-apple-Darwin")
+else()
+    set(OS_NAME "UNIX")
+    set(OUTPUT_JPL "x86_64-linux-gnu")
+endif()
+
+# Detect the architecture
+if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+    set(ARCH "x64")
+else()
+    message(FATAL_ERROR "Unsupported architecture. Only x64 is supported.")
+endif()
+
+# Set platform-specific variables
+set(CONTRIB_PLATFORM_CURT ${ARCH})
+set(CONTRIB_PLATFORM ${CONTRIB_PLATFORM_CURT}-${OS_NAME})
+
+message(OS: ${OS_NAME} ${ARCH})
+message(Building: ${ProjectName} ${Version})
+message(Build path: ${PROJECT_BINARY_DIR})
+message(JPL assembling path: ${JPL_DIRECTORY})
+message(JPL path: ${JPL_DIRECTORY}/../../../build/${ProjectName}/${JPL_FILE_NAME})
+
+# This is specifically to disable hardware acceleration and do computing on the graphics card for computationally expensive AI plugins.
+if(NVIDIA)
+add_definitions(-DNVIDIA)
+set(ONNX_DIR ${ONNX_DIR}/nvidia-gpu)
+message(Provider:\ NVIDIA)
+set(EXTRA_PATH nvidia-gpu)
+set (PREFERENCESFILENAME ${PREFERENCESFILENAME}-accel)
+else()
+set(ONNX_DIR ${ONNX_DIR}/cpu)
+message(Provider:\ NONE)
+set(EXTRA_PATH cpu)
+endif()
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED True)
+set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /DMSGPACK_NO_BOOST /MT")
+set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /DMSGPACK_NO_BOOST /MTd")
+
+set(SOURCES "")
+
+
+list(APPEND SOURCES "FILL.cpp"
+                    "FILL.cpp" # {FILL} Import manually all .cpp files here in your plugin directory.
+    )
+
+# Alternatively, you may comment the list command above, and uncomment the lines below, to automatically include all files ending with .cpp in your project.
+
+# file(GLOB CPP_SOURCES "*.cpp") # All .cpp files in the directory of your plugin ({root}/PLUGIN_NAME) will be collected by cmake.
+# list(APPEND SOURCES ${CPP_SOURCES})
+
+if(VIDEO_FFMPEG) # If you previously turned ON video stream capture, these will be imported.
+    list(APPEND SOURCES "./../lib/accel.cpp")
+    list(APPEND SOURCES "./../lib/frameUtils.cpp")
+endif()
+
+if(AUDIO_FFMPEG) # If you previously turned ON audio stream capture, these will be imported.
+    list(APPEND SOURCES "./../lib/frameUtils.cpp")
+endif()
+
+# list(APPEND SOURCES "./../lib/EDIT_ME.cpp") # {FILL} If you'd like to import any other .cpp files, uncomment this line, and copy it for each import.
+
+add_library(${ProjectName} SHARED ${SOURCES})
+
+target_include_directories(${ProjectName} PUBLIC ${PROJECT_BINARY_DIR}
+                                                 ${PROJECT_SOURCE_DIR}
+                                                 ${PLUGINS_LIB}
+                                                 ${DAEMON_SRC}
+                                                 ${CONTRIB_PATH}
+                                                 ${CONTRIB_PATH}/build/fmt/include
+                                                 ${CONTRIB_PATH}/build/opendht/include
+                                                 ${CONTRIB_PATH}/build/msgpack-c/include
+                                                 # {FILL} Plugins with computer vision or AI-related functions may be interested in importing the following.
+                                                 # ${FFMPEG}/include
+                                                 # ${CONTRIB_PATH}/build/opencv/build/install/include
+                                                 # ${ONNX_DIR}/../include/session
+                                                 # ${ONNX_DIR}/../include/providers/cuda
+                                                 )
+target_link_directories(${ProjectName} PUBLIC ${CONTRIB_PATH}
+                                              ${CONTRIB_PATH}/build/fmt/msvc/Release
+                                              # ${FFMPEG}/bin # {FILL} If using FFMPEG.
+                                        )
+
+target_link_libraries(${ProjectName} PUBLIC ---FFMPEGLIBS---)
+
+if(CMAKE_CXX_FLAGS_DEBUG)
+  set(OUTPUT "${ProjectName}")
+  set(CLANG_OPTS "-g -fsanitize=address")
+  set(EXTRA_DEBUG_LIBRARIES "-lyaml-cpp")
+  set(EXTRA_DEFINES "-D__DEBUG__")
+else()
+  add_custom_command(
+      TARGET ${ProjectName}
+      PRE_BUILD
+      COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/jplManipulation.py --preassemble --plugin=${ProjectName}
+      COMMENT "Assembling Plugin files"
+  )
+endif()
+
+add_custom_command(
+    TARGET ${ProjectName}
+    PRE_BUILD
+    COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/jplManipulation.py --preassemble --plugin=${ProjectName}
+    COMMENT "Assembling Plugin files"
+)
+
+
+if(WIN32)
+# Windows-specific file copying
+add_custom_command(
+    TARGET ${ProjectName}
+    POST_BUILD
+    COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/${ProjectName}.lib ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM}
+    COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/${ProjectName}.dll ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM}
+    COMMENT "Copying files to jpl directory for Windows"
+)
+elseif(APPLE)
+    # macOS-specific file copying
+    add_custom_command(
+        TARGET ${ProjectName}
+        POST_BUILD
+        COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/lib${ProjectName}.dylib ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM}
+        COMMENT "Copying files to jpl directory for macOS"
+    )
+else()
+    # Unix-like systems (Linux, etc.)
+    add_custom_command(
+        TARGET ${ProjectName}
+        POST_BUILD
+        COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/lib${ProjectName}.so ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM}
+        COMMENT "Copying files to jpl directory for Unix-like systems or Android"
+    )
+endif()
+
+if (NOT ARCHIVE_JPL)
+    exit()
+endif()
+
+add_custom_command(
+    TARGET ${ProjectName}
+    POST_BUILD
+    COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/jplManipulation.py --assemble --plugin=${ProjectName}
+    COMMENT "Generating JPL archive"
+)
+
+if(NOT EXISTS "${PROJECT_SOURCE_DIR}/../.cert")
+    file(MAKE_DIRECTORY "${PROJECT_SOURCE_DIR}/.cert")
+endif()
+
+if (DEBUG_SIGN)
+    if(NOT EXISTS "${PROJECT_SOURCE_DIR}/../.cert and sign/deb")
+        add_custom_command(
+            TARGET ${ to test it in your Jami clientProjectName}
+            POST_BUILD
+            COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/certKey.py create --subject debug ../.cert/debug
+            COMMENT "Generating developer certificate"
+        )
+    endif()
+    if(NOT EXISTS "${PROJECT_SOURCE_DIR}/../.cert/${ProjectName}")
+        add_custom_command(
+            TARGET ${ProjectName}
+            POST_BUILD
+            COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/certKey.py create --issuer ../.cert/debug --subject ${ProjectName} ../.cert/${ProjectName}/${ProjectName}
+            COMMENT "Generating plugin certificate"
+        )
+    endif()
+    add_custom_command(
+        TARGET ${ProjectName}
+        POST_BUILD
+        COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/certKey.py sign --path ../build/${OUTPUT_JPL}/${ProjectName} --issuer ../.cert/debug ../build/${OUTPUT_JPL}/${ProjectName}
+        COMMENT "Generating plugin certificate"
+    )
+
+python3 ./SDK/certKey.py --plugin sign --path /tmp/plugins/foo --issuer /tmp/foo /tmp/plugins/foo
+endif()
diff --git a/Example/ExampleAudioSubscriber.cpp b/Example/ExampleAudioSubscriber.cpp
new file mode 100644
index 0000000..94f55f7
--- /dev/null
+++ b/Example/ExampleAudioSubscriber.cpp
@@ -0,0 +1,237 @@
+/**
+ *  Copyright (C) 2023-2024 Savoir-faire Linux Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include "ExampleAudioSubscriber.h"
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+}
+#include <frameUtils.h>
+
+#include <pluglog.h>
+
+const std::string TAG = "Example";
+const char sep = separator();
+
+namespace jami {
+
+ExampleAudioSubscriber::ExampleAudioSubscriber(const std::string& dataPath, const std::string& irFile) // Construct the audio stream subscriber.
+    : path_ {dataPath}
+{
+    setIRFile(irFile);
+}
+
+ExampleAudioSubscriber::~ExampleAudioSubscriber() // Deconstruct the audio stream subscriber.
+{
+    if(pFormatCtx_) {
+        avformat_close_input(&pFormatCtx_);
+        avformat_free_context(pFormatCtx_);
+    }
+    std::ostringstream oss;
+    oss << "~ExampleMediaProcessor" << std::endl;
+    Plog::log(Plog::LogPriority::INFO, TAG, oss.str());
+}
+
+void
+ExampleAudioSubscriber::setIRFile(const std::string& irFile) // Initialize a file to use for the reverb effect.
+{
+    irFile_ = path_ + "/" + irFile;
+    firstRun = true;
+    reverbExample_.clean();
+}
+
+void
+ExampleAudioSubscriber::setExampleDescription(const int pSampleRate, const int pSamples, const int pFormat)
+// Constructs a filter description string based on the sample rate, number of samples, and format of the incoming audio frames.
+// This string describes how the audio should be processed (resampled and applied with the IR).
+{
+    int rSamples = 1024; // due to afir internal fifo
+    int midSampleRate = pSampleRate * rSamples / pSamples;
+    std::string outFormat = av_get_sample_fmt_name((AVSampleFormat)pFormat);
+    filterDescription_
+        = "[ input ] aformat=sample_fmts=s16:sample_rates=" + std::to_string(midSampleRate)
+          + ":channel_layouts=stereo [ resample1 ] , "
+          + "[ resample1 ] [ ir0 ] afir=maxir=1:wet=10:dry=10:irgain=1:irfmt=mono:maxp="
+          + std::to_string(rSamples) + ":minp=" + std::to_string(rSamples) + " [ reverb ] , "
+          + "[ reverb ] aformat=sample_fmts=" + outFormat + ":sample_rates="
+          + std::to_string(pSampleRate) + ":channel_layouts=stereo ";
+}
+
+AudioFormat
+ExampleAudioSubscriber::getIRAVFrameInfos() // Find the audio stream.
+{
+    AudioFormat rAudioFormat = AudioFormat(0, 0);
+    int i;
+
+    pFormatCtx_ = avformat_alloc_context();
+    // Open
+    if (avformat_open_input(&pFormatCtx_, irFile_.c_str(), NULL, NULL) != 0) {
+        Plog::log(Plog::LogPriority::INFO, TAG, "Couldn't open input stream.");
+        return rAudioFormat;
+    }
+    // Retrieve stream information
+    if (avformat_find_stream_info(pFormatCtx_, NULL) < 0) {
+        Plog::log(Plog::LogPriority::INFO, TAG, "Couldn't find stream information.");
+        return rAudioFormat;
+    }
+
+    // Dump valid information onto standard error
+    av_dump_format(pFormatCtx_, 0, irFile_.c_str(), false);
+
+    // Find the first audio stream
+    audioStream_ = -1;
+    for (i = 0; i < static_cast<int>(pFormatCtx_->nb_streams); i++)
+        if (pFormatCtx_->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
+            audioStream_ = i;
+            break;
+        }
+
+    if (audioStream_ == -1) {
+        Plog::log(Plog::LogPriority::INFO, TAG, "Didn't find a audio stream.");
+        return rAudioFormat;
+    }
+    rAudioFormat = AudioFormat(pFormatCtx_->streams[audioStream_]->codecpar->sample_rate,
+                               pFormatCtx_->streams[audioStream_]->codecpar->ch_layout.nb_channels,
+                               static_cast<AVSampleFormat>(
+                                   pFormatCtx_->streams[audioStream_]->codecpar->format));
+
+    return rAudioFormat;
+}
+
+void
+ExampleAudioSubscriber::setIRAVFrame() // Feed data into the reverb filter.
+{
+    AVCodecContext* pCodecCtx;
+
+    const AVCodec* pCodec = avcodec_find_decoder(pFormatCtx_->streams[audioStream_]->codecpar->codec_id);
+    if (pCodec == NULL) {
+        Plog::log(Plog::LogPriority::INFO, TAG, "Codec not found.");
+        return;
+    }
+
+    pCodecCtx = avcodec_alloc_context3(pCodec);
+    if (avcodec_parameters_to_context(pCodecCtx, pFormatCtx_->streams[audioStream_]->codecpar) < 0) {
+        Plog::log(Plog::LogPriority::INFO, __FILE__, "Failed to copy decoder parameters to decoder context.");
+        return;
+    }
+    // Open codec
+    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
+        Plog::log(Plog::LogPriority::INFO, TAG, "Could not open codec.");
+        return;
+    }
+
+    AVPacket* packet = av_packet_alloc();
+    AVFrame* pFrame = av_frame_alloc();
+
+    int idx = 0;
+    while (av_read_frame(pFormatCtx_, packet) == 0 && idx < 40) { // Limit for filter coefficients
+        idx++;
+        av_frame_unref(pFrame);
+        av_frame_free(&pFrame);
+        pFrame = av_frame_alloc();
+
+        if (avcodec_send_packet(pCodecCtx, packet) < 0) {
+            Plog::log(Plog::LogPriority::INFO, __FILE__, "Error submitting the packet to the decoder");
+            break;
+        }
+
+        // Read frames from decoder
+        while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {
+            reverbExample_.feedInput(pFrame, "ir0");
+        }
+        av_packet_unref(packet);
+    }
+
+    reverbExample_.feedEOF("ir0");
+
+    av_frame_unref(pFrame);
+    av_frame_free(&pFrame);
+    av_packet_unref(packet);
+    av_packet_free(&packet);
+    avcodec_close(pCodecCtx);
+    avcodec_free_context(&pCodecCtx);
+    avformat_close_input(&pFormatCtx_);
+    avformat_free_context(pFormatCtx_);
+}
+
+void
+ExampleAudioSubscriber::update(Observable<AVFrame*>*, AVFrame* const& pluginFrame) // Edit each audio frame with the application of the filter.
+{
+    if (!pluginFrame)
+        return;
+
+    if (firstRun) { // If this is the first run, initialize.
+        setExampleDescription(pluginFrame->sample_rate, pluginFrame->nb_samples, pluginFrame->format);
+        AudioFormat afmt_ = AudioFormat(pluginFrame->sample_rate,
+                                        pluginFrame->ch_layout.nb_channels,
+                                        static_cast<AVSampleFormat>(pluginFrame->format));
+        AudioFormat irfmt_ = getIRAVFrameInfos();
+        MediaStream ms_ = MediaStream("input", afmt_);
+        MediaStream irms_ = MediaStream("ir0", irfmt_);
+        reverbExample_.initialize(filterDescription_, {ms_, irms_});
+        setIRAVFrame();
+        firstRun = false;
+    }
+
+    if (!reverbExample_.initialized_)
+        return;
+
+    if (reverbExample_.feedInput(pluginFrame, "input") == 0) {
+        AVFrame* filteredFrame = reverbExample_.readOutput();
+        if (filteredFrame && filteredFrame->nb_samples == pluginFrame->nb_samples) {
+            moveFrom(pluginFrame, filteredFrame); // Edit the frame, applying reverb.
+            av_frame_unref(filteredFrame);
+            av_frame_free(&filteredFrame);
+        }
+    }
+}
+
+void
+ExampleAudioSubscriber::attached(Observable<AVFrame*>* observable)
+{
+    std::ostringstream oss;
+    oss << "::Attached ! " << std::endl;
+    Plog::log(Plog::LogPriority::INFO, TAG, oss.str());
+    observable_ = observable;
+}
+
+void
+ExampleAudioSubscriber::detached(Observable<AVFrame*>*)
+{
+    reverbExample_.clean();
+    firstRun = true;
+    observable_ = nullptr;
+    std::ostringstream oss;
+    oss << "::Detached()" << std::endl;
+    Plog::log(Plog::LogPriority::INFO, TAG, oss.str());
+}
+
+void
+ExampleAudioSubscriber::detach() // Detach from the audio stream.
+{
+    if (observable_) {
+        reverbExample_.clean();
+        firstRun = true;
+        std::ostringstream oss;
+        oss << "::Calling detach()" << std::endl;
+        Plog::log(Plog::LogPriority::INFO, TAG, oss.str());
+        observable_->detach(this);
+    }
+}
+} // namespace jami
diff --git a/Example/ExampleMediaHandler.cpp b/Example/ExampleMediaHandler.cpp
new file mode 100644
index 0000000..83ff791
--- /dev/null
+++ b/Example/ExampleMediaHandler.cpp
@@ -0,0 +1,128 @@
+/**
+ *  Copyright (C) 2023-2024 Savoir-faire Linux Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include "ExampleMediaHandler.h"
+
+#include "PluginPreferenceHandler.h"
+#include "pluglog.h"
+#include <string_view>
+
+const char sep = separator();
+const std::string TAG = "Example";
+
+#define NAME "Example"
+
+namespace jami {
+
+ExampleMediaHandler::ExampleMediaHandler(std::string&& dataPath, // Construct the handler.
+                                             PluginPreferenceHandler* prefHandler)
+    : datapath_ {dataPath}
+{
+    aph_ = prefHandler;
+    setId(datapath_);
+    mediaSubscriber_ = std::make_shared<ExampleVideoSubscriber>(datapath_);
+    setParameters("default");
+}
+
+void
+ExampleMediaHandler::notifyAVFrameSubject(const StreamData& data, jami::avSubjectPtr subject) // When a new video stream is detected, check the direction, and attach a subscriber to it if conditions match.
+{
+    std::ostringstream oss;
+    std::string_view direction = data.direction ? "Receive" : "Preview";
+    oss << "NEW SUBJECT: [" << data.id << "," << direction << "]" << std::endl;
+
+    accountId_ = data.source;
+    auto preferences = aph_->getPreferences(accountId_);
+
+    bool preferredStreamDirection {false}; // false for output; true for input
+    auto it = preferences.find("videostream");
+    if (it != preferences.end()) {
+        preferredStreamDirection = it->second == "1";
+    }
+    oss << "preferredStreamDirection " << preferredStreamDirection << std::endl;
+    if (data.type == StreamType::video && !data.direction
+        && data.direction == preferredStreamDirection) {
+        if (attached_ == "1")
+            detach();
+
+        setParameters(data.source);
+        subject->attach(mediaSubscriber_.get()); // your image
+        oss << "got my sent image attached" << std::endl;
+        attached_ = "1";
+    } else if (data.type == StreamType::video && data.direction
+               && data.direction == preferredStreamDirection) {
+        if (attached_ == "1")
+            detach();
+        setParameters(data.source);
+        subject->attach(mediaSubscriber_.get()); // the image you receive from others on the call
+        oss << "got received image attached" << std::endl;
+        attached_ = "1";
+    }
+
+    Plog::log(Plog::LogPriority::INFO, TAG, oss.str());
+}
+
+void
+ExampleMediaHandler::setParameters(const std::string& accountId) // Set the plugin preferences.
+{
+    if (!accountId.empty() && accountId != accountId_)
+        return;
+    auto preferences = aph_->getPreferences(accountId_);
+    try {
+        mediaSubscriber_->setParameter(preferences["fontsize"], Parameter::FONTSIZE);
+        mediaSubscriber_->setParameter(preferences["logosize"], Parameter::LOGOSIZE);
+        mediaSubscriber_->setParameter(preferences["markbackground"], Parameter::LOGOBACKGROUND);
+        mediaSubscriber_->setParameter(preferences["showinfos"], Parameter::SHOWINFOS);
+        mediaSubscriber_->setParameter(preferences["showlogo"], Parameter::SHOWLOGO);
+        mediaSubscriber_->setParameter(preferences["mark"], Parameter::LOGOPATH);
+        mediaSubscriber_->setParameter(preferences["date"], Parameter::DATE);
+        mediaSubscriber_->setParameter(preferences["dateformat"], Parameter::DATEFORMAT);
+        mediaSubscriber_->setParameter(preferences["time"], Parameter::TIME);
+        mediaSubscriber_->setParameter(preferences["timezone"], Parameter::TIMEZONE);
+        mediaSubscriber_->setParameter(preferences["timeformat"], Parameter::TIMEFORMAT);
+        mediaSubscriber_->setParameter(preferences["location"], Parameter::LOCATION);
+        mediaSubscriber_->setParameter(preferences["infosposition"], Parameter::INFOSPOSITION);
+        mediaSubscriber_->setParameter(preferences["logoposition"], Parameter::LOGOPOSITION);
+    } catch (std::exception& e) {
+        Plog::log(Plog::LogPriority::ERR, TAG, e.what());
+    }
+}
+
+std::map<std::string, std::string>
+ExampleMediaHandler::getCallMediaHandlerDetails()
+{
+    return {{"name", NAME},
+            {"iconPath", datapath_ + sep + "icon.svg"},
+            {"pluginId", id()},
+            {"attached", attached_},
+            {"dataType", "1"}};
+}
+
+void
+ExampleMediaHandler::detach() // Detach a subscriber.
+{
+    attached_ = "0";
+    mediaSubscriber_->detach();
+}
+
+ExampleMediaHandler::~ExampleMediaHandler() // Deconstruct the handler.
+{
+    Plog::log(Plog::LogPriority::INFO, TAG, "~ExampleMediaHandler from WaterMark Plugin");
+    detach();
+}
+} // namespace jami
diff --git a/Example/ExampleVideoSubscriber.cpp b/Example/ExampleVideoSubscriber.cpp
new file mode 100644
index 0000000..6deb2cd
--- /dev/null
+++ b/Example/ExampleVideoSubscriber.cpp
@@ -0,0 +1,479 @@
+/**
+ *  Copyright (C) 2023-2024 Savoir-faire Linux Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include "ExampleVideoSubscriber.h"
+
+extern "C" {
+#include <libavutil/display.h>
+}
+#include <accel.h>
+#include <frameScaler.h>
+#include <common.h>
+
+#include <pluglog.h>
+#include <algorithm>
+#include <ctime>
+#include <clocale>
+#include <iostream>
+
+const std::string TAG = "Example";
+const char sep = separator();
+
+namespace jami {
+
+ExampleVideoSubscriber::ExampleVideoSubscriber(const std::string& dataPath) // Construct the video subscriber.
+{
+    if (std::setlocale(LC_TIME, std::locale("").name().c_str()) == NULL) {
+        Plog::log(Plog::LogPriority::INFO, TAG, "error while setting locale");
+    }
+
+    std::setlocale(LC_NUMERIC, "C");
+    fontFile_ = string_utils::ffmpegFormatString(dataPath + sep + "Muli-Light.ttf");
+}
+
+ExampleVideoSubscriber::~ExampleVideoSubscriber() // Deconstruct the video subscriber.
+{
+    validLogo_ = false;
+    logoFilter_.clean();
+    detach();
+    std::lock_guard<std::mutex> lk(mtx_);
+    Plog::log(Plog::LogPriority::INFO, TAG, "~ExampleMediaProcessor");
+}
+
+MediaStream
+ExampleVideoSubscriber::getLogoAVFrameInfos() // Open an image logo and a video stream, so that the logo can be applied to the stream later on.
+{
+    AVFormatContext* ctx = avformat_alloc_context();
+
+    // Open
+    if (avformat_open_input(&ctx, logoPath_.c_str(), NULL, NULL) != 0) {
+        avformat_free_context(ctx);
+        Plog::log(Plog::LogPriority::INFO, TAG, "Couldn't open input stream.");
+        validLogo_ = false;
+        return {};
+    }
+    pFormatCtx_.reset(ctx);
+    // Retrieve stream information
+    if (avformat_find_stream_info(pFormatCtx_.get(), NULL) < 0) {
+        Plog::log(Plog::LogPriority::INFO, TAG, "Couldn't find stream information.");
+        validLogo_ = false;
+        return {};
+    }
+
+    // Dump valid information onto standard error
+    av_dump_format(pFormatCtx_.get(), 0, logoPath_.c_str(), false);
+
+    // Find the video stream
+    for (int i = 0; i < static_cast<int>(pFormatCtx_->nb_streams); i++)
+        if (pFormatCtx_->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
+            videoStream_ = i;
+            break;
+        }
+
+    if (videoStream_ == -1) {
+        Plog::log(Plog::LogPriority::INFO, TAG, "Didn't find a video stream.");
+        validLogo_ = false;
+        return {};
+    }
+
+    rational<int> fr = pFormatCtx_->streams[videoStream_]->r_frame_rate;
+    return MediaStream("logo",
+                       pFormatCtx_->streams[videoStream_]->codecpar->format,
+                       1 / fr,
+                       pFormatCtx_->streams[videoStream_]->codecpar->width,
+                       pFormatCtx_->streams[videoStream_]->codecpar->height,
+                       0,
+                       fr);
+}
+
+void
+ExampleVideoSubscriber::loadMarkLogo() // Load a logo, altering the image data so it can be safely placed onto the video stream.
+{
+    if (logoPath_.empty())
+        return;
+
+    logoFilter_.clean();
+    logoDescription_ = "[logo]scale=" + logoSize_ + "*" + std::to_string(pluginFrameSize_.first)
+                       + ":" + logoSize_ + "*" + std::to_string(pluginFrameSize_.second)
+                       + ":force_original_aspect_ratio='decrease',format=yuva444p,"
+                         "split=2[bg][fg],[bg]drawbox=c='"
+                       + backgroundColor_
+                       + "':replace=1:t=fill[bg],"
+                         "[bg][fg]overlay=format=auto";
+    Plog::log(Plog::LogPriority::INFO, TAG, logoDescription_);
+    logoStream_ = getLogoAVFrameInfos();
+    logoFilter_.initialize(logoDescription_, {logoStream_});
+
+    AVCodecContext* pCodecCtx;
+
+    const AVCodec* pCodec = avcodec_find_decoder(
+        pFormatCtx_->streams[videoStream_]->codecpar->codec_id);
+    if (pCodec == nullptr) {
+        pFormatCtx_.reset();
+        Plog::log(Plog::LogPriority::INFO, TAG, "Codec not found.");
+        validLogo_ = false;
+        return;
+    }
+
+    pCodecCtx = avcodec_alloc_context3(pCodec);
+    // Open codec
+    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
+        pFormatCtx_.reset();
+        Plog::log(Plog::LogPriority::INFO, TAG, "Could not open codec.");
+        validLogo_ = false;
+        return;
+    }
+
+    AVPacket* packet = av_packet_alloc();
+
+    if (av_read_frame(pFormatCtx_.get(), packet) < 0) {
+        avcodec_close(pCodecCtx);
+        avcodec_free_context(&pCodecCtx);
+        av_packet_unref(packet);
+        av_packet_free(&packet);
+        pFormatCtx_.reset();
+        Plog::log(Plog::LogPriority::INFO, TAG, "Could not read packet from context.");
+        validLogo_ = false;
+        return;
+    }
+
+    if (avcodec_send_packet(pCodecCtx, packet) < 0) {
+        avcodec_close(pCodecCtx);
+        avcodec_free_context(&pCodecCtx);
+        av_packet_unref(packet);
+        av_packet_free(&packet);
+        pFormatCtx_.reset();
+        Plog::log(Plog::LogPriority::INFO, TAG, "Could not send packet no codec.");
+        validLogo_ = false;
+        return;
+    }
+
+    uniqueFramePtr logoImage = {av_frame_alloc(), frameFree};
+    if (avcodec_receive_frame(pCodecCtx, logoImage.get()) < 0) {
+        avcodec_close(pCodecCtx);
+        avcodec_free_context(&pCodecCtx);
+        av_packet_unref(packet);
+        av_packet_free(&packet);
+        pFormatCtx_.reset();
+        Plog::log(Plog::LogPriority::INFO, TAG, "Could not read packet from codec.");
+        validLogo_ = false;
+        return;
+    }
+
+    logoFilter_.feedInput(logoImage.get(), "logo");
+    logoFilter_.feedEOF("logo");
+
+    avcodec_close(pCodecCtx);
+    avcodec_free_context(&pCodecCtx);
+    av_packet_unref(packet);
+    av_packet_free(&packet);
+    pFormatCtx_.reset();
+    mark_.reset(logoFilter_.readOutput());
+    mark_->pts = 0;
+    mark_->best_effort_timestamp = 0;
+    validLogo_ = mark_->width && mark_->height;
+}
+
+void
+ExampleVideoSubscriber::setParameter(std::string& parameter, Parameter type) // Apply the parameters set by the user.
+{
+    switch (type) {
+    case (Parameter::LOGOSIZE):
+        logoSize_ = parameter;
+        break;
+    case (Parameter::TIMEZONE):
+        timeZone_ = parameter == "1";
+        return;
+    case (Parameter::FONTSIZE):
+        fontSize_ = std::stoi(parameter);
+        break;
+    case (Parameter::LOGOBACKGROUND):
+        backgroundColor_ = parameter;
+        if (backgroundColor_.find("black") == std::string::npos) {
+            fontColor_ = "black";
+            fontBackground_ = "white@0.5";
+        } else {
+            fontColor_ = "white";
+            fontBackground_ = "black@0.5";
+        }
+        break;
+    case (Parameter::SHOWLOGO):
+        showLogo_ = parameter == "1";
+        break;
+    case (Parameter::SHOWINFOS):
+        showInfos_ = parameter == "1";
+        break;
+    case (Parameter::LOGOPATH):
+        logoPath_ = parameter;
+        break;
+    case (Parameter::TIME):
+        time_ = parameter == "1";
+        break;
+    case (Parameter::DATE):
+        date_ = parameter == "1";
+        break;
+    case (Parameter::TIMEFORMAT):
+        timeFormat_ = "%{localtime\\:'" + parameter + "'}";
+        if (timeZone_)
+            timeFormat_ = "%{localtime\\:'" + parameter + " %Z'}";
+        break;
+    case (Parameter::DATEFORMAT):
+        dateFormat_ = "%{localtime\\:'" + parameter + "'}";
+        break;
+    case (Parameter::LOCATION):
+        location_ = parameter;
+        break;
+    case (Parameter::INFOSPOSITION):
+        infosposition_ = parameter;
+        break;
+    case (Parameter::LOGOPOSITION):
+        logoposition_ = parameter;
+        break;
+    default:
+        return;
+    }
+
+    firstRun = true;
+}
+
+void
+ExampleVideoSubscriber::setFilterDescription() // Prepare the logo to be processed by ffmpeg.
+{
+    loadMarkLogo();
+
+    std::string infoSep = ", ";
+    if (pluginFrameSize_.first < pluginFrameSize_.second)
+        infoSep = ",\n";
+
+    std::vector<std::string> infos;
+    infosSize_ = 0;
+    if (!location_.empty()) {
+        infosSize_++;
+        infos.emplace_back(location_);
+    }
+    if (date_) {
+        infosSize_++;
+        infos.emplace_back(dateFormat_);
+    }
+    if (time_) {
+        infosSize_++;
+        infos.emplace_back(timeFormat_);
+    }
+    infosString.clear();
+    for (int i = 0; i < infosSize_ - 1; i++)
+        infosString += infos[i] + infoSep;
+    if (infosSize_ > 0)
+        infosString += infos.back();
+
+    setMarkPosition();
+
+    std::string rotateSides = "";
+    if (std::abs(angle_) == 90)
+        rotateSides = ":out_w=ih:out_h=iw";
+    std::string formatedPath = string_utils::ffmpegFormatString(logoPath_);
+
+    auto gifDescription = "movie='" + formatedPath + "':loop=0,setpts=N/(FR*TB)[logo],"
+                          + logoDescription_ + "[loop],";
+
+    if (angle_ != 0)
+        pluginFilterDescription_ = gifDescription
+                                   + "[input]rotate=" + rotation[angle_] + rotateSides
+                                   + "[rot],[rot][loop]overlay=" + std::to_string(points_[0].first)
+                                   + ":" + std::to_string(points_[0].second)
+                                   + ",rotate=" + rotation[-angle_] + rotateSides;
+    else
+        pluginFilterDescription_ = gifDescription
+                                   + "[input][loop]overlay="
+                                   + std::to_string(points_[0].first) + ":"
+                                   + std::to_string(points_[0].second);
+
+    std::string baseInfosDescription = "[input]rotate=" + rotation[angle_] + rotateSides
+                                       + ",drawtext=fontfile='" + fontFile_ + "':text='"
+                                       + infosString + "':fontcolor=" + fontColor_
+                                       + ":fontsize=" + std::to_string(fontSize_)
+                                       + ":line_spacing=" + std::to_string(lineSpacing_)
+                                       + ":box=1:boxcolor=" + fontBackground_ + ":boxborderw=5:x=";
+
+    if (infosposition_ == "1")
+        infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
+                            + "-text_w:y=" + std::to_string(points_[1].second);
+    else if (infosposition_ == "2")
+        infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
+                            + ":y=" + std::to_string(points_[1].second);
+    else if (infosposition_ == "3")
+        infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
+                            + ":y=" + std::to_string(points_[1].second) + "-text_h";
+    else if (infosposition_ == "4")
+        infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
+                            + "-text_w:y=" + std::to_string(points_[1].second) + "-text_h";
+    infosDescription_ += ",rotate=" + rotation[-angle_] + rotateSides + ",format=yuv420p";
+
+    Plog::log(Plog::LogPriority::INFO, TAG, infosDescription_);
+    Plog::log(Plog::LogPriority::INFO, TAG, pluginFilterDescription_);
+}
+
+void
+ExampleVideoSubscriber::setMarkPosition() // Set the position where the logo will be applied on the video stream.
+{
+    if (!validLogo_)
+        return;
+    // 1, 2, 3, and 4 are cartesian positions
+    int margin = 10;
+    int markWidth = showLogo_ ? mark_->width : 0;
+    int markHeight = showLogo_ ? mark_->height : 0;
+    int infoHeight = (std::abs(angle_) == 90) ? (fontSize_ + lineSpacing_) * infosSize_
+                                              : lineSpacing_ * 2 + fontSize_;
+    if (pluginFrameSize_.first == 0 || pluginFrameSize_.second == 0)
+        return;
+
+    if (infosposition_ == "1") {
+        points_[1] = {pluginFrameSize_.first - margin, margin};
+    } else if (infosposition_ == "2") {
+        points_[1] = {margin, margin};
+    } else if (infosposition_ == "3") {
+        points_[1] = {margin, pluginFrameSize_.second - margin};
+    } else if (infosposition_ == "4") {
+        points_[1] = {pluginFrameSize_.first - margin, pluginFrameSize_.second - margin};
+    }
+    if (logoposition_ == "1") {
+        points_[0] = {pluginFrameSize_.first - mark_->width - margin / 2, margin};
+    } else if (logoposition_ == "2") {
+        points_[0] = {margin / 2, margin};
+    } else if (logoposition_ == "3") {
+        points_[0] = {margin / 2, pluginFrameSize_.second - markHeight - margin};
+    } else if (logoposition_ == "4") {
+        points_[0] = {pluginFrameSize_.first - markWidth - margin / 2,
+                      pluginFrameSize_.second - markHeight - margin};
+    }
+
+    if (infosposition_ == logoposition_ && showInfos_ && showLogo_) {
+        if (logoposition_ == "1" || logoposition_ == "2") {
+            points_[0].second += infoHeight;
+        } else if (logoposition_ == "3" || logoposition_ == "4") {
+            points_[0].second -= infoHeight;
+        }
+    }
+}
+
+void
+ExampleVideoSubscriber::update(jami::Observable<AVFrame*>*, AVFrame* const& pluginFrame) // Get a video frame, and apply the logo onto the video frame.
+{
+    if (!observable_ || !pluginFrame || (showLogo_ && !validLogo_))
+        return;
+
+    AVFrameSideData* side_data = av_frame_get_side_data(pluginFrame, AV_FRAME_DATA_DISPLAYMATRIX);
+    int newAngle {0};
+    if (side_data) {
+        auto matrix_rotation = reinterpret_cast<int32_t*>(side_data->data);
+        newAngle = static_cast<int>(av_display_rotation_get(matrix_rotation));
+    }
+    if (newAngle != angle_) {
+        angle_ = newAngle;
+        firstRun = true;
+    }
+
+    //======================================================================================
+    // GET RAW FRAME
+    uniqueFramePtr rgbFrame = {transferToMainMemory(pluginFrame, AV_PIX_FMT_NV12), frameFree};
+    rgbFrame.reset(FrameScaler::convertFormat(rgbFrame.get(), AV_PIX_FMT_YUV420P));
+    if (!rgbFrame.get())
+        return;
+
+    if (sourceTimeBase_.num != pluginFrame->time_base.num || sourceTimeBase_.den != pluginFrame->time_base.den)
+        firstRun = true;
+
+    rgbFrame->pts = pluginFrame->pts;
+    rgbFrame->time_base = pluginFrame->time_base;
+    sourceTimeBase_ = pluginFrame->time_base;
+
+    if (firstRun) {
+        pluginFilter_.clean();
+        infosFilter_.clean();
+        pluginFrameSize_ = {rgbFrame->width, rgbFrame->height};
+        if (std::abs(angle_) == 90)
+            pluginFrameSize_ = {rgbFrame->height, rgbFrame->width};
+
+        setFilterDescription();
+
+        rational<int> fr(sourceTimeBase_.den, sourceTimeBase_.num);
+        pluginstream_ = MediaStream("input",
+                                    rgbFrame->format,
+                                    1 / fr,
+                                    rgbFrame->width,
+                                    rgbFrame->height,
+                                    0,
+                                    fr);
+
+        if (showLogo_ && validLogo_) {
+            pluginFilter_.initialize(pluginFilterDescription_, {pluginstream_});
+        }
+
+        infosFilter_.initialize(infosDescription_, {pluginstream_});
+        firstRun = false;
+    }
+
+    if (!infosFilter_.initialized_ && !pluginFilter_.initialized_)
+        return;
+
+    if (showLogo_ && validLogo_) {
+        if (pluginFilter_.feedInput(rgbFrame.get(), "input") == 0) {
+            uniqueFramePtr filteredFrame = {pluginFilter_.readOutput(), frameFree};
+            if (filteredFrame.get())
+                moveFrom(rgbFrame.get(), filteredFrame.get());
+        }
+    }
+    if (showInfos_) {
+        if (infosFilter_.feedInput(rgbFrame.get(), "input") == 0) {
+            uniqueFramePtr filteredFrame = {infosFilter_.readOutput(), frameFree};
+            if (filteredFrame.get())
+                moveFrom(rgbFrame.get(), filteredFrame.get());
+        }
+    }
+    if (showInfos_ || showLogo_) {
+        moveFrom(pluginFrame, rgbFrame.get());
+    }
+}
+
+void
+ExampleVideoSubscriber::attached(jami::Observable<AVFrame*>* observable)
+{
+    Plog::log(Plog::LogPriority::INFO, TAG, "Attached!");
+    observable_ = observable;
+}
+
+void
+ExampleVideoSubscriber::detached(jami::Observable<AVFrame*>*)
+{
+    pluginFilter_.clean();
+    infosFilter_.clean();
+    firstRun = true;
+    observable_ = nullptr;
+    Plog::log(Plog::LogPriority::INFO, TAG, "Detached!");
+    mtx_.unlock();
+}
+
+void
+ExampleVideoSubscriber::detach() // Detach from the video stream.
+{
+    if (observable_) {
+        mtx_.lock();
+        firstRun = true;
+        observable_->detach(this);
+    }
+}
+} // namespace jami
diff --git a/Example/data/locale/Example_en.json b/Example/data/locale/Example_en.json
new file mode 100644
index 0000000..40438f9
--- /dev/null
+++ b/Example/data/locale/Example_en.json
@@ -0,0 +1,4 @@
+{
+    "name": "Example",
+    "description_summary": "An example plugin.",
+}
diff --git a/Example/main.cpp b/Example/main.cpp
new file mode 100644
index 0000000..6b65e39
--- /dev/null
+++ b/Example/main.cpp
@@ -0,0 +1,166 @@
+/**
+ *  Copyright (C) 2023-2024 Savoir-faire Linux Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include <iostream>
+#include <string.h>
+#include <thread>
+#include <memory>
+#include <map>
+#include "plugin/jamiplugin.h"
+#include "BotChatHandler.h"
+
+#ifdef __DEBUG__
+#include <common.h>
+#include <assert.h>
+#include <yaml-cpp/yaml.h>
+#include <fstream>
+#endif
+
+#ifdef WIN32
+#define EXPORT_PLUGIN __declspec(dllexport)
+#else
+#define EXPORT_PLUGIN
+#endif
+
+#define Example_VERSION_MAJOR 2
+#define Example_VERSION_MINOR 0
+#define Example_VERSION_PATCH 0
+
+extern "C" {
+
+void
+pluginExit(void)
+{}
+
+EXPORT_PLUGIN JAMI_PluginExitFunc
+JAMI_dynPluginInit(const JAMI_PluginAPI* api)
+{
+    // Print the plugin name and version.
+    std::cout << "*****************" << std::endl;
+    std::cout << "**   Example   **" << std::endl;
+    std::cout << "*****************" << std::endl << std::endl;
+    std::cout << "Version " << Example_VERSION_MAJOR << "." << Example_VERSION_MINOR << "."
+              << Example_VERSION_PATCH << std::endl;
+
+    // If invokeService doesn't return an error
+    if (api) { // Ensure the API version is up to date.
+        if (api->version.api < JAMI_PLUGIN_API_VERSION)
+            return nullptr;
+
+        std::map<std::string, std::map<std::string, std::string>> preferences;
+        api->invokeService(api, "getPluginAccPreferences", &preferences); // Retrieve plugin preferences...
+        std::string dataPath;
+        api->invokeService(api, "getPluginDataPath", &dataPath); // And the assets, like the icon and background.
+
+        auto fmpPluginPreferenceHandler
+            = std::make_unique<jami::PluginPreferenceHandler>(api, std::move(preferences), dataPath); // Create a preference handler.
+        auto fmpBotChatHandler
+            = std::make_unique<jami::BotChatHandler>(api, // Create a chat handler, which will dispatch bots to subscribe to chat streams.
+                                                     std::move(dataPath),
+                                                     fmpPluginPreferenceHandler.get());
+        if (api->manageComponent(api, "ChatHandlerManager", fmpBotChatHandler.release())) {
+            return nullptr;
+        }
+        if (api->manageComponent(api,
+                                 "PreferenceHandlerManager",
+                                 fmpPluginPreferenceHandler.release())) {
+            return nullptr;
+        }
+    }
+
+    return pluginExit;
+}
+}
+
+#ifdef __DEBUG__
+
+int
+main ()
+{
+    std::cout << "*****************************" << std::endl; // A debug feature for plugins is also available.
+    std::cout << "**  Example Debug Version  **" << std::endl;
+    std::cout << "*****************************" << std::endl;
+    std::cout << "Version " << Example_VERSION_MAJOR << "." << Example_VERSION_MINOR << "."
+              << Example_VERSION_PATCH << std::endl << std::endl;
+
+    std::ifstream file;
+    file_utils::openStream(file, "testPreferences.yml");
+
+    assert(file.is_open());
+    YAML::Node node = YAML::Load(file);
+
+    assert(node.IsMap());
+    std::map<std::string, std::map<std::string, std::string>> preferences;
+    preferences["default"] = {};
+    for (const auto& kv : node) {
+        preferences["default"][kv.first.as<std::string>()] = kv.second.as<std::string>();
+        std::cout << "Key: " << kv.first.as<std::string>() << "; Value: " << kv.second.as<std::string>() << std::endl;
+    }
+
+    std::string dataPath = "tester";
+    // Simulate the function of the plugin by creating bots...
+    auto fmpPluginPreferenceHandler
+        = std::make_unique<jami::PluginPreferenceHandler>(nullptr, std::move(preferences), dataPath);
+
+    auto fmpBotChatHandler
+        = std::make_unique<jami::BotChatHandler>(nullptr,
+                                                    std::move(dataPath),
+                                                    fmpPluginPreferenceHandler.get());
+    // ...and having these bots subscribe to streams.
+    auto subject = std::make_shared<jami::PublishObservable<jami::pluginMessagePtr>>();
+    std::pair<std::string, std::string> subjectConnection("origin", "destiny");
+    fmpBotChatHandler->notifyChatSubject(subjectConnection, subject);
+
+    // Only test for swarm
+
+    // Valid Sender, Receiver, direction and message
+    std::cout << "Test 1" << std::endl << "Should print input/output" << std::endl;
+    std::map<std::string, std::string> sendMsg = {{"type", "text/plain"}, {"body", preferences["default"]["inText"]}};
+    jami::pluginMessagePtr jamiMsg = std::make_shared<JamiMessage>("origin",
+                                                                   "destiny",
+                                                                   true,
+                                                                   sendMsg,
+                                                                   false);
+    jamiMsg->isSwarm = true;
+    subject->publish(jamiMsg);
+
+
+    // Valid Sender, Receiver and message but not direction
+    std::cout << "Test 2" << std::endl << "Should NOT print input/output" << std::endl;
+    jamiMsg.reset(new JamiMessage("origin",
+                                  "destiny",
+                                  false,
+                                  sendMsg,
+                                  false));
+    jamiMsg->isSwarm = true;
+    subject->publish(jamiMsg);
+
+    // Invalid Sender, Receiver, direction and message
+    std::cout << "Test 3" << std::endl << "Should NOT print input/output" << std::endl;
+    sendMsg["body"] = preferences["default"]["invalid"];
+    jamiMsg.reset(new JamiMessage("destiny",
+                                  "origin",
+                                  true,
+                                  sendMsg,
+                                  false));
+    jamiMsg->isSwarm = true;
+    subject->publish(jamiMsg);
+
+    return 0;
+}
+#endif
diff --git a/Example/manifest.json b/Example/manifest.json
new file mode 100644
index 0000000..2b19105
--- /dev/null
+++ b/Example/manifest.json
@@ -0,0 +1,8 @@
+{
+    "id": "Example",
+    "name": "{{name}}",
+    "description": "{{description_summary}}",
+    "version": "1.0.0",
+    "iconPath": "icon.svg",
+    "backgroundPath": "background.jpg"
+}
\ No newline at end of file
diff --git a/SDK/Templates/CMakeLists.txt b/SDK/Templates/CMakeLists.txt
index 628d404..d7daa53 100644
--- a/SDK/Templates/CMakeLists.txt
+++ b/SDK/Templates/CMakeLists.txt
@@ -6,6 +6,9 @@ set (Version MANIFESTVERSION)
 
 project(${ProjectName} VERSION ${Version})
 
+option(VIDEO_FFMPEG "Include Video FFMPEG Dependency" OFF)
+option(AUDIO_FFMPEG "Include Audio FFMPEG Dependency" OFF)
+
 set (DAEMON ${PROJECT_SOURCE_DIR}/../daemon)
 set (JPL_FILE_NAME ${ProjectName}.jpl)
 set (DAEMON_SRC ${DAEMON}/src)
@@ -17,13 +20,10 @@ set (LIBS_DIR ${PROJECT_SOURCE_DIR}/../contrib/Libs)
 # Detect the operating system
 if(WIN32)
     set(OS_NAME "WINDOWS")
-    ---set(FFMPEG_PATH "${CONTRIB_PATH}/build/ffmpeg/Build/windows/x64")---
 elseif(ANDROID)
     set(OS_NAME "ANDROID")
-    ---set(FFMPEG_PATH "${CONTRIB_PATH}/build/ffmpeg/Build/android")---
 else()
     set(OS_NAME "UNIX")
-    ---set(FFMPEG_PATH "${CONTRIB_PATH}/build/ffmpeg/Build/unix")---
 endif()
 
 # Detect the architecture
@@ -48,27 +48,20 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
 set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /DMSGPACK_NO_BOOST /MT")
 set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /DMSGPACK_NO_BOOST /MTd")
 
-# Android-specific flags
-if(ANDROID)
-    set(CMAKE_ANDROID_ARCH_ABI arm64-v8a) # Set the desired ABI
-    set(CMAKE_ANDROID_STL_TYPE c++_shared) # Use C++ shared library
-    set(CMAKE_ANDROID_NDK_TOOLCHAIN_FILE ${CONTRIB_PATH}/build/cmake/android.toolchain.cmake)
-    set(CMAKE_ANDROID_STL_INCLUDE_DIR ${CONTRIB_PATH}/sysroot/usr/include)
-    set(CMAKE_ANDROID_STL_LIBRARIES ${CONTRIB_PATH}/sysroot/usr/lib)
-endif()
+set(SOURCES "")
+file(GLOB CPP_SOURCES "*.cpp")
+list(APPEND SOURCES ${CPP_SOURCES})
 
-set(plugin_SRC ---CPPFILENAME
-               ---FFMPEGCPP
-               ---)
+if(VIDEO_FFMPEG)
+    list(APPEND SOURCES "./../lib/accel.cpp")
+    list(APPEND SOURCES "./../lib/frameUtils.cpp")
+endif()
 
-set(plugin_HDR ---HFILENAME
-               ---FFMPEGH
-               ---./../lib/pluglog.h
-               )
+if(AUDIO_FFMPEG)
+    list(APPEND SOURCES "./../lib/frameUtils.cpp")
+endif()
 
-add_library(${ProjectName} SHARED ${plugin_SRC}
-                                  ${plugin_HDR}
-                                  )
+add_library(${ProjectName} SHARED ${SOURCES})
 
 target_include_directories(${ProjectName} PUBLIC ${PROJECT_BINARY_DIR}
                                                  ${PROJECT_SOURCE_DIR}
diff --git a/build-plugin.py b/build-plugin.py
index 83b690d..f9d8275 100755
--- a/build-plugin.py
+++ b/build-plugin.py
@@ -41,12 +41,12 @@ UBUNTU_DISTRIBUTION_NAME = "ubuntu"
 def parse():
     parser = argparse.ArgumentParser(description='Builds Plugins projects')
     parser.add_argument('--projects', type=str,
-                        help='Select plugins to be build.')
+                        help='Select plugins to be built.')
     parser.add_argument('--distribution')
     parser.add_argument('--processor', type=str, default="GPU",
                         help='Runtime plugin CPU/GPU setting.')
     parser.add_argument('--buildOptions', default='', type=str,
-                        help="Type all build optionsto pass to package.json 'defines' property.\nThis argument consider that you're using cmake.")
+                        help="Type all build options to pass to package.json 'defines' property.\nThis argument consider that you're using cmake.")
     parser.add_argument('--arch', type=str, default=None,
                         help='Specify the architecture (arm64 or x86_64).')
 
diff --git a/build.md b/build.md
new file mode 100644
index 0000000..85fab08
--- /dev/null
+++ b/build.md
@@ -0,0 +1,55 @@
+1. Copy and rename the Example directory.
+
+```
+cp -R Example YourPluginName
+sed -i 's/Example/YourPluginName/g' manifest.json
+```
+
+2. Change the version to fit the appropriate version of your plugin. You must change `manifest.json`, `main.cpp` and `CMakeLists.txt`. For example, for version 1.0.1:
+
+```cpp
+#define Example_VERSION_MAJOR 1
+#define Example_VERSION_MINOR 0
+#define Example_VERSION_PATCH 1
+```
+
+```json
+    "version": "1.0.1",
+```
+
+```
+set (VERSION 1.0.1) # {FILL} Replace 0.0.0 with the version of your plugin.
+```
+
+3. Edit the CMakeLists.txt of your project directory. Each comment containing {FILL} should be adjusted depending on the associated instructions.
+
+In order, that means:
+- Line 2: Set PROJECT_NAME as your plugin name.
+- Line 3: Set VERSION as your plugin version.
+- Line 8: Turn VIDEO_FFMPEG ON if you'd like to subscribe to or modify Jami's video stream.
+- Line 9: Turn AUDIO_FFMPEG ON if you'd like to subscribe to or modify Jami's audio stream.
+- Line 62: Add all further `.cpp` imports to SOURCES
+
+**NOTE: All files ending with `.cpp` in your plugin directory will be automatically picked up by `CMake`.**
+Header files ending with `.h` which are included in your C++ files will be automatically added as dependencies.
+
+4. Program the functionality of your plugin, taking the `Example` plugin's Video, Audio and Chat features as inspiration.
+
+5. To add a new language, rename `Example_en.json`, replacing `Example` with your plugin name, and edit the contents to reflect your translations.
+
+**NOTE: The language code is ISO 639.**
+
+6. Build your plugin:
+```
+cd YourPluginName
+mkdir -p build-local
+cd build-local
+cmake ..
+cmake --build .
+```
+
+By default, you may find the resulting `.jpl` file inside the `build` directory. You may set the `ARCHIVE_JPL` variable to `OFF` in `CMakeLists.txt` to disable the creation of the `.jpl` archive.
+
+7. By default, the `cmake` script will generate a developer certificate/key, and a plugin-specific certificate/key, in `plugins/.cert`. Once you have built your plugin on all platforms of interest and merged them together with `python3 SDK/pluginMainSDK.py`, then by typing `merge` in the SDK, you will be able to sign your final `.jpl` file. Call:
+
+`python3 ./SDK/certKey.py --plugin sign --issuer <path-to-your-developer-certificate> --path <path-to-your-.jpl-file-to-sign> <path-to-save-the-final-.jpl-signed-output>`
diff --git a/contrib/build-dependencies.sh b/contrib/build-dependencies.sh
index b66d75f..74a7f0b 100755
--- a/contrib/build-dependencies.sh
+++ b/contrib/build-dependencies.sh
@@ -109,25 +109,11 @@ CONTRIB_SYSROOT=${DAEMON_DIR}/contrib/${TARGET}
 mkdir -p ${CONTRIB_DIR}
 mkdir -p ${CONTRIB_SYSROOT}/lib/pkgconfig
 
-echo "copying files"
-cp -r ${CURRENTDIR}/freetype ${DAEMON_DIR}/contrib/src
-
 cd ${CONTRIB_DIR}
 
-../bootstrap --host=${TARGET} --disable-x264 --enable-ffmpeg \
-             --disable-webrtc-audio-processing --disable-argon2 \
-             --disable-asio --enable-fmt --disable-gcrypt --disable-gmp \
-             --disable-gnutls --disable-gpg-error --disable-gsm \
-             --disable-http_parser --disable-jack --disable-jsoncpp \
-             --disable-libarchive --disable-libressl --enable-msgpack \
-             --disable-natpmp --disable-nettle --enable-opencv --enable-opendht \
-             --disable-pjproject --disable-portaudio --disable-restinio \
-             --disable-secp256k1 --disable-speex --disable-speexdsp --disable-upnp \
-             --disable-uuid --disable-yaml-cpp --enable-onnx --disable-dhtnet --enable-opus \
-             --enable-freetype
+../bootstrap --host=${TARGET}
 
 make list
 make fetch
 export PATH="$PATH:$CONTRIB_SYSROOT/bin"
-make $MAKEFLAGS
-
+make $MAKEFLAGS .ffmpeg .fmt .opencv .onnx .freetype
diff --git a/daemon b/daemon
index 072fef2..90766f9 160000
--- a/daemon
+++ b/daemon
@@ -1 +1 @@
-Subproject commit 072fef2e67cf551c062ec738f1715e2aa7e1e07e
+Subproject commit 90766f95a36e030e36f89c05e7781cb3d6b09fc6
diff --git a/docker/Dockerfile_ubuntu_20.04 b/docker/Dockerfile_ubuntu_20.04
index 576c19d..16cf4ed 100644
--- a/docker/Dockerfile_ubuntu_20.04
+++ b/docker/Dockerfile_ubuntu_20.04
@@ -30,7 +30,7 @@ RUN apt-get clean && \
         libcanberra-gtk3-dev \
         libclutter-gtk-1.0-dev \
         libclutter-1.0-dev \
-        libfreetype6-dev \
+        libfreetype-dev \
         libglib2.0-dev \
         libgtk-3-dev \
         libnotify-dev \
@@ -80,6 +80,7 @@ RUN apt-get clean && \
         ninja-build \
         libsystemd-dev
 
+ENV DISABLE_PIPEWIRE=true
 RUN apt-get install -y python3 python3-pip python3-setuptools \
                        python3-wheel
 
diff --git a/extras/ci/android/Jenkinsfile b/extras/ci/android/Jenkinsfile
index 8087f5b..bc455db 100644
--- a/extras/ci/android/Jenkinsfile
+++ b/extras/ci/android/Jenkinsfile
@@ -125,6 +125,7 @@ pipeline {
                         "arm-linux-android",
                         "native-arm-linux-android"
                     ]
+
                     sh(script: "mkdir -p ${hostBaseDir}", returnStatus: true).with {
                         if (it!= 0) {
                             error("Failed to create base directory: ${hostBaseDir}")
@@ -138,6 +139,7 @@ pipeline {
                         } else {
                             println "Successfully created directory: ${hostBaseDir}/${subdir}"
                         }
+
                         println "Inside container, ensure this directory exists: ${containerBaseDir}/${subdir}"
                     }
                     docker.image('plugins-android').withRun('-t \
diff --git a/notarize.sh b/notarize.sh
index e46564a..a8e791b 100755
--- a/notarize.sh
+++ b/notarize.sh
@@ -11,4 +11,4 @@ else
 echo "notarization failed"
 break
 exit 1
-fi
\ No newline at end of file
+fi
-- 
GitLab