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