From d09cc6d9abcaa9f032164dfe0e768edf20bb91c9 Mon Sep 17 00:00:00 2001 From: agsantos <aline.gondimsantos@savoirfairelinux.com> Date: Fri, 6 Nov 2020 17:34:46 -0500 Subject: [PATCH] tutorial: HelloWorld Change-Id: Ie19a7749ba028ceda16a2479398645daeb08f277 --- .gitignore | 1 - HelloWorld/CMakeLists.txt | 91 +++++++++ HelloWorld/CenterCircleMediaHandler.cpp | 116 +++++++++++ HelloWorld/CenterCircleMediaHandler.h | 55 ++++++ HelloWorld/CenterCircleVideoSubscriber.cpp | 187 ++++++++++++++++++ HelloWorld/CenterCircleVideoSubscriber.h | 71 +++++++ HelloWorld/CoinCircleMediaHandler.cpp | 107 ++++++++++ HelloWorld/CoinCircleMediaHandler.h | 55 ++++++ HelloWorld/CoinCircleVideoSubscriber.cpp | 200 +++++++++++++++++++ HelloWorld/CoinCircleVideoSubscriber.h | 72 +++++++ HelloWorld/build.sh | 220 +++++++++++++++++++++ HelloWorld/data/icon.png | Bin 0 -> 21340 bytes HelloWorld/data/preferences.json | 38 ++++ HelloWorld/main.cpp | 77 ++++++++ HelloWorld/manifest.json | 5 + HelloWorld/package.json | 19 ++ 16 files changed, 1313 insertions(+), 1 deletion(-) create mode 100644 HelloWorld/CMakeLists.txt create mode 100644 HelloWorld/CenterCircleMediaHandler.cpp create mode 100644 HelloWorld/CenterCircleMediaHandler.h create mode 100644 HelloWorld/CenterCircleVideoSubscriber.cpp create mode 100644 HelloWorld/CenterCircleVideoSubscriber.h create mode 100644 HelloWorld/CoinCircleMediaHandler.cpp create mode 100644 HelloWorld/CoinCircleMediaHandler.h create mode 100644 HelloWorld/CoinCircleVideoSubscriber.cpp create mode 100644 HelloWorld/CoinCircleVideoSubscriber.h create mode 100755 HelloWorld/build.sh create mode 100644 HelloWorld/data/icon.png create mode 100644 HelloWorld/data/preferences.json create mode 100644 HelloWorld/main.cpp create mode 100644 HelloWorld/manifest.json create mode 100644 HelloWorld/package.json diff --git a/.gitignore b/.gitignore index 548a5c9..451c560 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,3 @@ config.mak *__pycache__* /foo/ /.vscode/ -/HelloWorld/ diff --git a/HelloWorld/CMakeLists.txt b/HelloWorld/CMakeLists.txt new file mode 100644 index 0000000..bbecc0f --- /dev/null +++ b/HelloWorld/CMakeLists.txt @@ -0,0 +1,91 @@ +cmake_minimum_required(VERSION 3.10) + +# set the project name +set (ProjectName HelloWorld) +set (Version 1.0.0) + +project(${ProjectName} VERSION ${Version}) + +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) +set (LIBS_DIR ${PROJECT_SOURCE_DIR}/../contrib/Libs) + +if(WIN32) + message(OS:\ WINDOWS\ ${CMAKE_SYSTEM_PROCESSOR}) + if (NOT ${CMAKE_CL_64}) + message( FATAL_ERROR "\nUse CMake only for x64 Windows" ) + endif() + set (CONTRIB_PLATFORM_CURT x64) + set (CONTRIB_PLATFORM ${CONTRIB_PLATFORM_CURT}-windows) + set (LIBRARY_FILE_NAME ${ProjectName}.dll) + set (FFMPEG ${CONTRIB_PATH}/build/ffmpeg/Build/win32/x64) +else() + message( FATAL_ERROR "\nUse CMake only for Windows! For linux or Android (linux host), use our bash scripts." ) +endif() + +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}) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED True) +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") + +set(plugin_SRC CoinCircleMediaHandler.cpp + CenterCircleVideoSubscriber.cpp + CenterCircleMediaHandler.cpp + CoinCircleVideoSubscriber.cpp + main.cpp + ../lib/accel.cpp + ) + +set(plugin_HDR CoinCircleVideoSubscriber.h + CenterCircleVideoSubscriber.h + CoinCircleMediaHandler.h + CenterCircleMediaHandler.h + ../lib/accel.h + ../lib/framescaler.h + ../lib/pluglog.h + ) + +add_library(${ProjectName} SHARED ${plugin_SRC} + ${plugin_HDR} + ) + +target_include_directories(${ProjectName} PUBLIC ${PROJECT_BINARY_DIR} + ${PROJECT_SOURCE_DIR} + ${PLUGINS_LIB} + ${DAEMON_SRC} + ${CONTRIB_PATH} + ${FFMPEG}/include + ${CONTRIB_PATH}/build/opencv/build/install/include + ) +target_link_directories(${ProjectName} PUBLIC ${CONTRIB_PATH} + ${FFMPEG}/bin + ${CONTRIB_PATH}/build/opencv/build/lib/Release + ${CONTRIB_PATH}/build/opencv/build/3rdparty/lib/Release + ) + +target_link_libraries(${ProjectName} PUBLIC swscale avutil opencv_imgproc411 opencv_core411 zlib) + +add_custom_command( + TARGET ${ProjectName} + PRE_BUILD + COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/jplManipulation.py --preassemble --plugin=${ProjectName} + COMMENT "Assembling Plugin files" +) + +add_custom_command( + TARGET ${ProjectName} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/Release/${ProjectName}.lib ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM} + COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/Release/${LIBRARY_FILE_NAME} ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM} + COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/jplManipulation.py --assemble --plugin=${ProjectName} + COMMENT "Generating JPL archive" +) diff --git a/HelloWorld/CenterCircleMediaHandler.cpp b/HelloWorld/CenterCircleMediaHandler.cpp new file mode 100644 index 0000000..903f96f --- /dev/null +++ b/HelloWorld/CenterCircleMediaHandler.cpp @@ -0,0 +1,116 @@ +/** + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * 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 "CenterCircleMediaHandler.h" + +#include "pluglog.h" +const char sep = separator(); +const std::string TAG = "CenterCircle"; + +#define NAME "CenterCircle" + +namespace jami { + +CenterCircleMediaHandler::CenterCircleMediaHandler(std::map<std::string, std::string>&& ppm, + std::string&& datapath) + : datapath_ {datapath} + , ppm_ {ppm} +{ + setId(datapath_); + mVS = std::make_shared<CenterCircleVideoSubscriber>(datapath_); + auto it = ppm_.find("color"); + if (it != ppm_.end()) { + mVS->setColor(it->second); + } else { + mVS->setColor("#0000FF"); + } +} + +void +CenterCircleMediaHandler::notifyAVFrameSubject(const StreamData& data, jami::avSubjectPtr subject) +{ + Plog::log(Plog::LogPriority::INFO, TAG, "IN AVFRAMESUBJECT"); + std::ostringstream oss; + std::string direction = data.direction ? "Receive" : "Preview"; + oss << "NEW SUBJECT: [" << data.id << "," << direction << "]" << std::endl; + + bool preferredStreamDirection = false; // false for output; true for input + auto it = ppm_.find("videostream"); + if (it != ppm_.end()) { + preferredStreamDirection = it->second == "1"; + } + oss << "preferredStreamDirection " << preferredStreamDirection << std::endl; + if (data.type == StreamType::video && !data.direction + && data.direction == preferredStreamDirection) { + subject->attach(mVS.get()); // your image + oss << "got my sent image attached" << std::endl; + } else if (data.type == StreamType::video && data.direction + && data.direction == preferredStreamDirection) { + subject->attach(mVS.get()); // the image you receive from others on the call + oss << "got received image attached" << std::endl; + } + + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); +} + +std::map<std::string, std::string> +CenterCircleMediaHandler::getCallMediaHandlerDetails() +{ + return {{"name", NAME}, {"iconPath", datapath_ + sep + "icon.png"}, {"pluginId", id()}}; +} + +void +CenterCircleMediaHandler::setPreferenceAttribute(const std::string& key, const std::string& value) +{ + auto it = ppm_.find(key); + if (it != ppm_.end() && it->second != value) { + it->second = value; + + if (key == "color") { + mVS->setColor(value); + return; + } + } +} + +bool +CenterCircleMediaHandler::preferenceMapHasKey(const std::string& key) +{ + if (key == "color") { + return true; + } + + return false; +} + +void +CenterCircleMediaHandler::detach() +{ + mVS->detach(); +} + +CenterCircleMediaHandler::~CenterCircleMediaHandler() +{ + std::ostringstream oss; + oss << " ~CenterCircleMediaHandler from HelloWorld Plugin" << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); + detach(); +} +} // namespace jami diff --git a/HelloWorld/CenterCircleMediaHandler.h b/HelloWorld/CenterCircleMediaHandler.h new file mode 100644 index 0000000..91a4b6f --- /dev/null +++ b/HelloWorld/CenterCircleMediaHandler.h @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * 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 + +// Project +#include "CenterCircleVideoSubscriber.h" + +// Plugin +#include "plugin/jamiplugin.h" +#include "plugin/mediahandler.h" + +using avSubjectPtr = std::weak_ptr<jami::Observable<AVFrame*>>; + +namespace jami { + +class CenterCircleMediaHandler : public jami::CallMediaHandler +{ +public: + CenterCircleMediaHandler(std::map<std::string, std::string>&& ppm, std::string&& dataPath); + ~CenterCircleMediaHandler(); + + virtual void notifyAVFrameSubject(const StreamData& data, avSubjectPtr subject) override; + virtual std::map<std::string, std::string> getCallMediaHandlerDetails() override; + + virtual void detach() override; + virtual void setPreferenceAttribute(const std::string& key, const std::string& value) override; + virtual bool preferenceMapHasKey(const std::string& key) override; + + std::shared_ptr<CenterCircleVideoSubscriber> mVS; + + const std::string& dataPath() const { return datapath_; } + +private: + const std::string datapath_; + std::map<std::string, std::string> ppm_; +}; +} // namespace jami diff --git a/HelloWorld/CenterCircleVideoSubscriber.cpp b/HelloWorld/CenterCircleVideoSubscriber.cpp new file mode 100644 index 0000000..b1e1377 --- /dev/null +++ b/HelloWorld/CenterCircleVideoSubscriber.cpp @@ -0,0 +1,187 @@ +/** + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * 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 "CenterCircleVideoSubscriber.h" + +extern "C" { +#include <libavutil/display.h> +} +#include <accel.h> + +// LOGGING +#include <pluglog.h> + +#include <stdio.h> +#include <opencv2/imgproc.hpp> + +const std::string TAG = "CenterCircle"; +const char sep = separator(); + +namespace jami { + +CenterCircleVideoSubscriber::CenterCircleVideoSubscriber(const std::string& dataPath) + : path_ {dataPath} +{} + +CenterCircleVideoSubscriber::~CenterCircleVideoSubscriber() +{ + std::ostringstream oss; + oss << "~CenterCircleMediaProcessor" << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); +} + +void +CenterCircleVideoSubscriber::update(jami::Observable<AVFrame*>*, AVFrame* const& iFrame) +{ + if (!iFrame) + return; + AVFrame* pluginFrame = const_cast<AVFrame*>(iFrame); + + //====================================================================================== + // GET FRAME ROTATION + AVFrameSideData* side_data = av_frame_get_side_data(iFrame, AV_FRAME_DATA_DISPLAYMATRIX); + + int angle {0}; + if (side_data) { + auto matrix_rotation = reinterpret_cast<int32_t*>(side_data->data); + angle = static_cast<int>(av_display_rotation_get(matrix_rotation)); + } + + //====================================================================================== + // GET RAW FRAME + // Use a non-const Frame + // Convert input frame to RGB + int inputHeight = pluginFrame->height; + int inputWidth = pluginFrame->width; + + FrameUniquePtr bgrFrame = scaler.convertFormat(transferToMainMemory(pluginFrame, + AV_PIX_FMT_NV12), + AV_PIX_FMT_RGB24); + resultFrame = cv::Mat {bgrFrame->height, + bgrFrame->width, + CV_8UC3, + bgrFrame->data[0], + static_cast<size_t>(bgrFrame->linesize[0])}; + + // First clone the frame as the original one is unusable because of + // linespace + processingFrame = resultFrame.clone(); + + if (firstRun) { + // we set were the circle will be draw. + circlePos.y = static_cast<int>(inputHeight / 2); + circlePos.x = static_cast<int>(inputWidth / 2); + int w = resultFrame.size().width; + int h = resultFrame.size().height; + radius = std::min(w, h) / 8; + firstRun = false; + } + + drawCenterCircle(); + copyByLine(bgrFrame->linesize[0]); + + //====================================================================================== + // REPLACE AVFRAME DATA WITH FRAME DATA + if (bgrFrame && bgrFrame->data[0]) { + uint8_t* frameData = bgrFrame->data[0]; + if (angle == 90 || angle == -90) { + std::memmove(frameData, + resultFrame.data, + static_cast<size_t>(pluginFrame->width * pluginFrame->height * 3) + * sizeof(uint8_t)); + } + } + // Copy Frame meta data + if (bgrFrame && pluginFrame) { + av_frame_copy_props(bgrFrame.get(), pluginFrame); + scaler.moveFrom(pluginFrame, bgrFrame.get()); + } + + // Remove the pointer + pluginFrame = nullptr; +} + +void +CenterCircleVideoSubscriber::setColor(const std::string& color) +{ + int r, g, b = 0; + std::sscanf(color.c_str(), "#%02x%02x%02x", &r, &g, &b); + baseColor = cv::Scalar(r, g, b); + Plog::log(Plog::LogPriority::INFO, TAG, "Color set to: " + color); +} + +void +CenterCircleVideoSubscriber::copyByLine(const int lineSize) +{ + if (3 * processingFrame.cols == lineSize) { + std::memcpy(resultFrame.data, + processingFrame.data, + processingFrame.rows * processingFrame.cols * 3); + } else { + int rows = processingFrame.rows; + int offset = 0; + int frameOffset = 0; + for (int i = 0; i < rows; i++) { + std::memcpy(resultFrame.data + offset, processingFrame.data + frameOffset, lineSize); + offset += lineSize; + frameOffset += 3 * processingFrame.cols; + } + } +} + +void +CenterCircleVideoSubscriber::drawCenterCircle() +{ + if (!processingFrame.empty()) { + cv::circle(processingFrame, circlePos, radius, baseColor, cv::FILLED); + } +} + +void +CenterCircleVideoSubscriber::attached(jami::Observable<AVFrame*>* observable) +{ + std::ostringstream oss; + oss << "::Attached ! " << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); + observable_ = observable; +} + +void +CenterCircleVideoSubscriber::detached(jami::Observable<AVFrame*>*) +{ + firstRun = true; + observable_ = nullptr; + std::ostringstream oss; + oss << "::Detached()" << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); +} + +void +CenterCircleVideoSubscriber::detach() +{ + if (observable_) { + 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/HelloWorld/CenterCircleVideoSubscriber.h b/HelloWorld/CenterCircleVideoSubscriber.h new file mode 100644 index 0000000..8fb9c02 --- /dev/null +++ b/HelloWorld/CenterCircleVideoSubscriber.h @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * 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 + +// AvFrame +extern "C" { +#include <libavutil/frame.h> +} +#include <observer.h> + +// Frame Scaler +#include <framescaler.h> + +#include <opencv2/core.hpp> + +namespace jami { + +class CenterCircleVideoSubscriber : public jami::Observer<AVFrame*> +{ +public: + CenterCircleVideoSubscriber(const std::string& dataPath); + ~CenterCircleVideoSubscriber(); + + virtual void update(jami::Observable<AVFrame*>*, AVFrame* const&) override; + virtual void attached(jami::Observable<AVFrame*>*) override; + virtual void detached(jami::Observable<AVFrame*>*) override; + + void detach(); + + void setColor(const std::string& color); + +private: + // Observer pattern + Observable<AVFrame*>* observable_ = nullptr; + + // Data + std::string path_; + FrameScaler scaler; + + // Status variables of the processing + bool firstRun {true}; + + // define custom variables + void copyByLine(const int lineSize); + void drawCenterCircle(); + + cv::Point circlePos; + cv::Mat processingFrame; + cv::Mat resultFrame; + int radius; + cv::Scalar baseColor; +}; +} // namespace jami diff --git a/HelloWorld/CoinCircleMediaHandler.cpp b/HelloWorld/CoinCircleMediaHandler.cpp new file mode 100644 index 0000000..213e836 --- /dev/null +++ b/HelloWorld/CoinCircleMediaHandler.cpp @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * 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 "CoinCircleMediaHandler.h" +// Logger +#include "pluglog.h" +const char sep = separator(); +const std::string TAG = "CoinCircle"; + +#define NAME "CoinCircle" + +namespace jami { + +CoinCircleMediaHandler::CoinCircleMediaHandler(std::map<std::string, std::string>&& ppm, + std::string&& datapath) + : datapath_ {datapath} + , ppm_ {ppm} +{ + setId(datapath_); + mVS = std::make_shared<CoinCircleVideoSubscriber>(datapath_); + auto it = ppm_.find("color"); + if (it != ppm_.end()) { + mVS->setColor(it->second); + } else { + mVS->setColor("#0000FF"); + } +} + +void +CoinCircleMediaHandler::notifyAVFrameSubject(const StreamData& data, jami::avSubjectPtr subject) +{ + Plog::log(Plog::LogPriority::INFO, TAG, "IN AVFRAMESUBJECT"); + std::ostringstream oss; + std::string direction = data.direction ? "Receive" : "Preview"; + oss << "NEW SUBJECT: [" << data.id << "," << direction << "]" << std::endl; + + bool preferredStreamDirection = false; // false for output; true for input + auto it = ppm_.find("videostream"); + if (it != ppm_.end()) { + preferredStreamDirection = it->second == "1"; + } + oss << "preferredStreamDirection " << preferredStreamDirection << std::endl; + if (data.type == StreamType::video && !data.direction + && data.direction == preferredStreamDirection) { + subject->attach(mVS.get()); // your image + oss << "got my sent image attached" << std::endl; + } else if (data.type == StreamType::video && data.direction + && data.direction == preferredStreamDirection) { + subject->attach(mVS.get()); // the image you receive from others on the call + oss << "got received image attached" << std::endl; + } + + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); +} + +std::map<std::string, std::string> +CoinCircleMediaHandler::getCallMediaHandlerDetails() +{ + return {{"name", NAME}, {"iconPath", datapath_ + sep + "icon.png"}, {"pluginId", id()}}; +} + +void +CoinCircleMediaHandler::setPreferenceAttribute(const std::string& key, const std::string& value) +{ + auto it = ppm_.find(key); + if (it != ppm_.end() && it->second != value) { + it->second = value; + } +} + +bool +CoinCircleMediaHandler::preferenceMapHasKey(const std::string& key) +{ + return false; +} + +void +CoinCircleMediaHandler::detach() +{ + mVS->detach(); +} + +CoinCircleMediaHandler::~CoinCircleMediaHandler() +{ + std::ostringstream oss; + oss << " ~CoinCircleMediaHandler from HelloWorld Plugin" << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); + detach(); +} +} // namespace jami diff --git a/HelloWorld/CoinCircleMediaHandler.h b/HelloWorld/CoinCircleMediaHandler.h new file mode 100644 index 0000000..c902f58 --- /dev/null +++ b/HelloWorld/CoinCircleMediaHandler.h @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * 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 + +// Project +#include "CoinCircleVideoSubscriber.h" + +// Plugin +#include "plugin/jamiplugin.h" +#include "plugin/mediahandler.h" + +using avSubjectPtr = std::weak_ptr<jami::Observable<AVFrame*>>; + +namespace jami { + +class CoinCircleMediaHandler : public jami::CallMediaHandler +{ +public: + CoinCircleMediaHandler(std::map<std::string, std::string>&& ppm, std::string&& dataPath); + ~CoinCircleMediaHandler(); + + virtual void notifyAVFrameSubject(const StreamData& data, avSubjectPtr subject) override; + virtual std::map<std::string, std::string> getCallMediaHandlerDetails() override; + + virtual void detach() override; + virtual void setPreferenceAttribute(const std::string& key, const std::string& value) override; + virtual bool preferenceMapHasKey(const std::string& key) override; + + std::shared_ptr<CoinCircleVideoSubscriber> mVS; + + const std::string& dataPath() const { return datapath_; } + +private: + const std::string datapath_; + std::map<std::string, std::string> ppm_; +}; +} // namespace jami diff --git a/HelloWorld/CoinCircleVideoSubscriber.cpp b/HelloWorld/CoinCircleVideoSubscriber.cpp new file mode 100644 index 0000000..6b4a94e --- /dev/null +++ b/HelloWorld/CoinCircleVideoSubscriber.cpp @@ -0,0 +1,200 @@ +/** + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * 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 "CoinCircleVideoSubscriber.h" + +extern "C" { +#include <libavutil/display.h> +} +#include <accel.h> + +// LOGGING +#include <pluglog.h> + +#include <stdio.h> +#include <opencv2/imgproc.hpp> + +const std::string TAG = "CoinCircle"; +const char sep = separator(); + +namespace jami { + +CoinCircleVideoSubscriber::CoinCircleVideoSubscriber(const std::string& dataPath) + : path_ {dataPath} +{} + +CoinCircleVideoSubscriber::~CoinCircleVideoSubscriber() +{ + std::ostringstream oss; + oss << "~CoinCircleMediaProcessor" << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); +} + +void +CoinCircleVideoSubscriber::update(jami::Observable<AVFrame*>*, AVFrame* const& iFrame) +{ + if (!iFrame) + return; + AVFrame* pluginFrame = const_cast<AVFrame*>(iFrame); + + //====================================================================================== + // GET FRAME ROTATION + AVFrameSideData* side_data = av_frame_get_side_data(iFrame, AV_FRAME_DATA_DISPLAYMATRIX); + + int angle {0}; + if (side_data) { + auto matrix_rotation = reinterpret_cast<int32_t*>(side_data->data); + angle = static_cast<int>(av_display_rotation_get(matrix_rotation)); + } + + //====================================================================================== + // GET RAW FRAME + // Use a non-const Frame + // Convert input frame to RGB + int inputHeight = pluginFrame->height; + int inputWidth = pluginFrame->width; + FrameUniquePtr bgrFrame = scaler.convertFormat(transferToMainMemory(pluginFrame, + AV_PIX_FMT_NV12), + AV_PIX_FMT_RGB24); + + resultFrame = cv::Mat {bgrFrame->height, + bgrFrame->width, + CV_8UC3, + bgrFrame->data[0], + static_cast<size_t>(bgrFrame->linesize[0])}; + + // First clone the frame as the original one is unusable because of + // linespace + processingFrame = resultFrame.clone(); + + if (firstRun) { + // we set were the circle will be draw. + int w = resultFrame.size().width; + int h = resultFrame.size().height; + radius = std::min(w, h) / 8; + circlePos.y = static_cast<int>(radius); + circlePos.x = static_cast<int>(radius); + firstRun = false; + } + + drawCoinCircle(angle); + copyByLine(bgrFrame->linesize[0]); + + //====================================================================================== + // REPLACE AVFRAME DATA WITH FRAME DATA + if (bgrFrame && bgrFrame->data[0]) { + uint8_t* frameData = bgrFrame->data[0]; + if (angle == 90 || angle == -90) { + std::memmove(frameData, + resultFrame.data, + static_cast<size_t>(pluginFrame->width * pluginFrame->height * 3) + * sizeof(uint8_t)); + } + } + // Copy Frame meta data + if (bgrFrame && pluginFrame) { + av_frame_copy_props(bgrFrame.get(), pluginFrame); + scaler.moveFrom(pluginFrame, bgrFrame.get()); + } + + // Remove the pointer + pluginFrame = nullptr; +} + +void +CoinCircleVideoSubscriber::drawCoinCircle(const int angle) +{ + if (!processingFrame.empty()) { + rotateFrame(angle, processingFrame); + cv::circle(processingFrame, circlePos, radius, baseColor, cv::FILLED); + rotateFrame(-angle, processingFrame); + } +} + +void +CoinCircleVideoSubscriber::rotateFrame(const int angle, cv::Mat& frame) +{ + if (angle == -90) + cv::rotate(frame, frame, cv::ROTATE_90_COUNTERCLOCKWISE); + else if (std::abs(angle) == 180) + cv::rotate(frame, frame, cv::ROTATE_180); + else if (angle == 90) + cv::rotate(frame, frame, cv::ROTATE_90_CLOCKWISE); +} + +void +CoinCircleVideoSubscriber::setColor(const std::string& color) +{ + int r, g, b = 0; + std::sscanf(color.c_str(), "#%02x%02x%02x", &r, &g, &b); + baseColor = cv::Scalar(r, g, b); + Plog::log(Plog::LogPriority::INFO, TAG, "Color set to: " + color); +} + +void +CoinCircleVideoSubscriber::copyByLine(const int lineSize) +{ + if (3 * processingFrame.cols == lineSize) { + std::memcpy(resultFrame.data, + processingFrame.data, + processingFrame.rows * processingFrame.cols * 3); + } else { + int rows = processingFrame.rows; + int offset = 0; + int frameOffset = 0; + for (int i = 0; i < rows; i++) { + std::memcpy(resultFrame.data + offset, processingFrame.data + frameOffset, lineSize); + offset += lineSize; + frameOffset += 3 * processingFrame.cols; + } + } +} + +void +CoinCircleVideoSubscriber::attached(jami::Observable<AVFrame*>* observable) +{ + std::ostringstream oss; + oss << "::Attached ! " << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); + observable_ = observable; +} + +void +CoinCircleVideoSubscriber::detached(jami::Observable<AVFrame*>*) +{ + firstRun = true; + observable_ = nullptr; + std::ostringstream oss; + oss << "::Detached()" << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); +} + +void +CoinCircleVideoSubscriber::detach() +{ + if (observable_) { + 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/HelloWorld/CoinCircleVideoSubscriber.h b/HelloWorld/CoinCircleVideoSubscriber.h new file mode 100644 index 0000000..bc94ee3 --- /dev/null +++ b/HelloWorld/CoinCircleVideoSubscriber.h @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * 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 + +// AvFrame +extern "C" { +#include <libavutil/frame.h> +} +#include <observer.h> + +// Frame Scaler +#include <framescaler.h> + +#include <opencv2/core.hpp> + +namespace jami { + +class CoinCircleVideoSubscriber : public jami::Observer<AVFrame*> +{ +public: + CoinCircleVideoSubscriber(const std::string& dataPath); + ~CoinCircleVideoSubscriber(); + + virtual void update(jami::Observable<AVFrame*>*, AVFrame* const&) override; + virtual void attached(jami::Observable<AVFrame*>*) override; + virtual void detached(jami::Observable<AVFrame*>*) override; + + void detach(); + + void setColor(const std::string& color); + +private: + // Observer pattern + Observable<AVFrame*>* observable_ = nullptr; + + // Data + std::string path_; + FrameScaler scaler; + + // Status variables of the processing + bool firstRun {true}; + + // define custom variables + void copyByLine(const int lineSize); + void drawCoinCircle(const int angle); + void rotateFrame(const int angle, cv::Mat& frame); + + cv::Point circlePos; + cv::Mat processingFrame; + cv::Mat resultFrame; + int radius; + cv::Scalar baseColor; +}; +} // namespace jami diff --git a/HelloWorld/build.sh b/HelloWorld/build.sh new file mode 100755 index 0000000..6c7b833 --- /dev/null +++ b/HelloWorld/build.sh @@ -0,0 +1,220 @@ +#! /bin/bash +# Build the plugin for the project +set -e +export OSTYPE +ARCH=$(arch) +EXTRAPATH='' +# Flags: + +# -p: number of processors to use +# -c: Runtime plugin cpu/gpu setting. +# -t: target platform. + + +if [ -z "${DAEMON}" ]; then + DAEMON="./../../daemon" + echo "DAEMON not provided, building with ${DAEMON}" +fi + +PLUGIN_NAME="HelloWorld" +JPL_FILE_NAME="${PLUGIN_NAME}.jpl" +SO_FILE_NAME="lib${PLUGIN_NAME}.so" +DAEMON_SRC="${DAEMON}/src" +CONTRIB_PATH="${DAEMON}/contrib" +PLUGINS_LIB="../lib" +LIBS_DIR="./../contrib/Libs" + +if [ -z "${PLATFORM}" ]; then + PLATFORM="linux-gnu" + echo "PLATFORM not provided, building with ${PLATFORM}" + echo "Other options: redhat-linux" +fi + +while getopts t:c:p OPT; do + case "$OPT" in + t) + PLATFORM="${OPTARG}" + ;; + c) + PROCESSOR="${OPTARG}" + ;; + p) + ;; + \?) + exit 1 + ;; + esac +done + +if [ "${PLATFORM}" = "linux-gnu" ] || [ "${PLATFORM}" = "redhat-linux" ] +then + python3 ./../SDK/jplManipulation.py --preassemble --plugin=${PLUGIN_NAME} + + CONTRIB_PLATFORM_CURT=${ARCH} + CONTRIB_PLATFORM=${CONTRIB_PLATFORM_CURT}-${PLATFORM} + + # Compile + clang++ -std=c++17 -shared -fPIC \ + -Wl,-Bsymbolic,-rpath,"\${ORIGIN}" \ + -Wall -Wextra \ + -Wno-unused-variable \ + -Wno-unused-function \ + -Wno-unused-parameter \ + -I"." \ + -I"${DAEMON_SRC}" \ + -I"${CONTRIB_PATH}/${CONTRIB_PLATFORM}/include" \ + -I"${CONTRIB_PATH}/${CONTRIB_PLATFORM}/include/opencv4" \ + -I"${PLUGINS_LIB}" \ + ./../lib/accel.cpp \ + CoinCircleMediaHandler.cpp \ + CenterCircleVideoSubscriber.cpp \ + CenterCircleMediaHandler.cpp \ + CoinCircleVideoSubscriber.cpp \ + main.cpp \ + -L"${CONTRIB_PATH}/${CONTRIB_PLATFORM}/lib/" \ + -l:libswscale.a \ + -l:libavutil.a \ + -lopencv_imgproc \ + -lopencv_core \ + -lva \ + -o "build-local/jpl/lib/${CONTRIB_PLATFORM_CURT}-linux-gnu/${SO_FILE_NAME}" + +elif [ "${PLATFORM}" = "android" ] +then + python3 ./../SDK/jplManipulation.py --preassemble --plugin=${PLUGIN_NAME} --distribution=${PLATFORM} + + if [ -z "$ANDROID_NDK" ]; then + ANDROID_NDK="/home/${USER}/Android/Sdk/ndk/21.1.6352462" + echo "ANDROID_NDK not provided, building with ${ANDROID_NDK}" + fi + + #========================================================= + # Check if the ANDROID_ABI was provided + # if not, set default + #========================================================= + if [ -z "$ANDROID_ABI" ]; then + ANDROID_ABI="armeabi-v7a arm64-v8a x86_64" + echo "ANDROID_ABI not provided, building for ${ANDROID_ABI}" + fi + + buildlib() { + echo "$CURRENT_ABI" + + #========================================================= + # ANDROID TOOLS + #========================================================= + export HOST_TAG=linux-x86_64 + export TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG + + if [ "$CURRENT_ABI" = armeabi-v7a ] + then + export AR=$TOOLCHAIN/bin/arm-linux-android-ar + export AS=$TOOLCHAIN/bin/arm-linux-android-as + export CC=$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang + export CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang++ + export LD=$TOOLCHAIN/bin/arm-linux-android-ld + export RANLIB=$TOOLCHAIN/bin/arm-linux-android-ranlib + export STRIP=$TOOLCHAIN/bin/arm-linux-androideabi-strip + export ANDROID_SYSROOT=${DAEMON}/../client-android/android-toolchain-21-arm/sysroot + + elif [ "$CURRENT_ABI" = arm64-v8a ] + then + export AR=$TOOLCHAIN/bin/aarch64-linux-android-ar + export AS=$TOOLCHAIN/bin/aarch64-linux-android-as + export CC=$TOOLCHAIN/bin/aarch64-linux-android21-clang + export CXX=$TOOLCHAIN/bin/aarch64-linux-android21-clang++ + export LD=$TOOLCHAIN/bin/aarch64-linux-android-ld + export RANLIB=$TOOLCHAIN/bin/aarch64-linux-android-ranlib + export STRIP=$TOOLCHAIN/bin/aarch64-linux-android-strip + export ANDROID_SYSROOT=${DAEMON}/../client-android/android-toolchain-21-arm64/sysroot + + elif [ "$CURRENT_ABI" = x86_64 ] + then + export AR=$TOOLCHAIN/bin/x86_64-linux-android-ar + export AS=$TOOLCHAIN/bin/x86_64-linux-android-as + export CC=$TOOLCHAIN/bin/x86_64-linux-android21-clang + export CXX=$TOOLCHAIN/bin/x86_64-linux-android21-clang++ + export LD=$TOOLCHAIN/bin/x86_64-linux-android-ld + export RANLIB=$TOOLCHAIN/bin/x86_64-linux-android-ranlib + export STRIP=$TOOLCHAIN/bin/x86_64-linux-android-strip + export ANDROID_SYSROOT=${DAEMON}/../client-android/android-toolchain-21-x86_64/sysroot + + else + echo "ABI NOT OK" >&2 + exit 1 + fi + + #========================================================= + # CONTRIBS + #========================================================= + if [ "$CURRENT_ABI" = armeabi-v7a ] + then + CONTRIB_PLATFORM=arm-linux-androideabi + + elif [ "$CURRENT_ABI" = arm64-v8a ] + then + CONTRIB_PLATFORM=aarch64-linux-android + + elif [ "$CURRENT_ABI" = x86_64 ] + then + CONTRIB_PLATFORM=x86_64-linux-android + fi + + #NDK SOURCES FOR cpufeatures + NDK_SOURCES=${ANDROID_NDK}/sources/android + + #========================================================= + # LD_FLAGS + #========================================================= + if [ "$CURRENT_ABI" = armeabi-v7a ] + then + export EXTRA_LDFLAGS="${EXTRA_LDFLAGS} -L${ANDROID_SYSROOT}/usr/lib/arm-linux-androideabi -L${ANDROID_SYSROOT}/usr/lib/arm-linux-androideabi/21" + elif [ "$CURRENT_ABI" = arm64-v8a ] + then + export EXTRA_LDFLAGS="${EXTRA_LDFLAGS} -L${ANDROID_SYSROOT}/usr/lib/aarch64-linux-android -L${ANDROID_SYSROOT}/usr/lib/aarch64-linux-android/21" + elif [ "$CURRENT_ABI" = x86_64 ] + then + export EXTRA_LDFLAGS="${EXTRA_LDFLAGS} -L${ANDROID_SYSROOT}/usr/lib/x86_64-linux-android -L${ANDROID_SYSROOT}/usr/lib/x86_64-linux-android/21" + fi + + #========================================================= + # Compile the plugin + #========================================================= + + # Create so destination folder + $CXX --std=c++14 -O3 -g -fPIC \ + -Wl,-Bsymbolic,-rpath,"\${ORIGIN}" \ + -shared \ + -Wall -Wextra \ + -Wno-unused-variable \ + -Wno-unused-function \ + -Wno-unused-parameter \ + -I"." \ + -I"${DAEMON_SRC}" \ + -I"${CONTRIB_PATH}/${CONTRIB_PLATFORM}/include" \ + -I"${CONTRIB_PATH}/${CONTRIB_PLATFORM}/include/opencv4" \ + -I"${PLUGINS_LIB}" \ + ./../lib/accel.cpp \ + CoinCircleMediaHandler.cpp \ + CenterCircleVideoSubscriber.cpp \ + CenterCircleMediaHandler.cpp \ + CoinCircleVideoSubscriber.cpp \ + main.cpp \ + -L"${CONTRIB_PATH}/${CONTRIB_PLATFORM}/lib/" \ + -lswscale \ + -lavutil \ + -lopencv_imgproc \ + -lopencv_core \ + -llog -lz \ + --sysroot=$ANDROID_SYSROOT \ + -o "build-local/jpl/lib/$CURRENT_ABI/${SO_FILE_NAME}" + } + + # Build the so + for i in ${ANDROID_ABI}; do + CURRENT_ABI=$i + buildlib + done +fi + +python3 ./../SDK/jplManipulation.py --assemble --plugin=${PLUGIN_NAME} --distribution=${PLATFORM} --extraPath=${EXTRAPATH} diff --git a/HelloWorld/data/icon.png b/HelloWorld/data/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2ce0aab9f71679f71f7b3d53d8448f7e9852b322 GIT binary patch literal 21340 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 literal 0 HcmV?d00001 diff --git a/HelloWorld/data/preferences.json b/HelloWorld/data/preferences.json new file mode 100644 index 0000000..a8ba793 --- /dev/null +++ b/HelloWorld/data/preferences.json @@ -0,0 +1,38 @@ +[ + { + "category": "stream", + "type": "List", + "key": "videostream", + "title": "Video stream", + "summary": "select a video direction", + "defaultValue": "0", + "scope": "plugin", + "entryValues": [ + "0", + "1" + ], + "entries": [ + "sent", + "received" + ] + }, + { + "category": "color", + "type": "List", + "key": "color", + "title": "Circle color", + "summary": "select a color", + "defaultValue": "#00FF00", + "scope": "plugin,CenterCircle", + "entryValues": [ + "#0000FF", + "#00FF00", + "#FF0000" + ], + "entries": [ + "blue", + "green", + "red" + ] + } +] \ No newline at end of file diff --git a/HelloWorld/main.cpp b/HelloWorld/main.cpp new file mode 100644 index 0000000..11591b9 --- /dev/null +++ b/HelloWorld/main.cpp @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * 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 <plugin/jamiplugin.h> + +#include "CenterCircleMediaHandler.h" +#include "CoinCircleMediaHandler.h" + +#ifdef WIN32 +#define EXPORT_PLUGIN __declspec(dllexport) +#else +#define EXPORT_PLUGIN +#endif +#define HelloWorld_VERSION_MAJOR 1 +#define HelloWorld_VERSION_MINOR 0 +#define HelloWorld_VERSION_PATCH 0 +extern "C" { +void +pluginExit(void) +{} + +EXPORT_PLUGIN JAMI_PluginExitFunc +JAMI_dynPluginInit(const JAMI_PluginAPI* api) +{ + std::cout << "**************************" << std::endl << std::endl; + std::cout << "** HelloWorld **" << std::endl; + std::cout << "**************************" << std::endl << std::endl; + std::cout << " Version " << HelloWorld_VERSION_MAJOR << "." << HelloWorld_VERSION_MINOR << "." + << HelloWorld_VERSION_PATCH << std::endl; + + // If invokeService doesn't return an error + if (api) { + std::map<std::string, std::string> ppm; + api->invokeService(api, "getPluginPreferences", &ppm); + std::string dataPath; + api->invokeService(api, "getPluginDataPath", &dataPath); + + auto fmpCenterCircleMediaHandler + = std::make_unique<jami::CenterCircleMediaHandler>(std::move(ppm), std::move(dataPath)); + if (api->manageComponent(api, + "CallMediaHandlerManager", + fmpCenterCircleMediaHandler.release())) { + return nullptr; + } + + auto fmpCoinCircleMediaHandler + = std::make_unique<jami::CoinCircleMediaHandler>(std::move(ppm), std::move(dataPath)); + if (api->manageComponent(api, + "CallMediaHandlerManager", + fmpCoinCircleMediaHandler.release())) { + return nullptr; + } + } + return pluginExit; +} +} diff --git a/HelloWorld/manifest.json b/HelloWorld/manifest.json new file mode 100644 index 0000000..b524bb8 --- /dev/null +++ b/HelloWorld/manifest.json @@ -0,0 +1,5 @@ +{ + "name": "HelloWorld", + "description": "HelloWorld draws a circle in the center of a call's video", + "version": "1.0.0" +} \ No newline at end of file diff --git a/HelloWorld/package.json b/HelloWorld/package.json new file mode 100644 index 0000000..91b5ea8 --- /dev/null +++ b/HelloWorld/package.json @@ -0,0 +1,19 @@ +{ + "name": "HelloWorld", + "version": "1.0.0", + "extractLibs": false, + "deps": [ + "ffmpeg", + "opencv" + ], + "defines": [], + "custom_scripts": { + "pre_build": [ + "mkdir msvc" + ], + "build": [ + "cmake --build ./msvc --config Release" + ], + "post_build": [] + } +} \ No newline at end of file -- GitLab