├── node_addons └── sourceId2Coordinates │ ├── src │ ├── sourceId2Coordinates.h │ ├── index.cc │ └── sourceId2Coordinates.cc │ └── index.js ├── pip ├── constants.js ├── index.js ├── render.js └── main.js ├── remotecontrol ├── index.js ├── constants.js ├── main.js └── render.js ├── .eslintignore ├── screensharing ├── index.js ├── preload.js ├── screenSharingTracker.js ├── constants.js ├── utils.js ├── screenSharingTracker.html ├── render.js └── main.js ├── .husky └── pre-push ├── powermonitor ├── index.js ├── constants.js ├── render.js └── main.js ├── install.js ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── popupsconfig ├── render.js ├── index.js ├── constants.js ├── PopupsConfigRegistry.js ├── main.js └── functions.js ├── test └── sourceId2Coordinates.test.js ├── binding.gyp ├── index.js ├── .gitignore ├── package.json ├── .eslintrc.js ├── helpers └── functions.js ├── CLAUDE.md ├── README.md └── LICENSE /node_addons/sourceId2Coordinates/src/sourceId2Coordinates.h: -------------------------------------------------------------------------------- 1 | struct Point { 2 | int x; 3 | int y; 4 | Point(): x(0), y(0) {}; 5 | }; 6 | 7 | bool sourceId2Coordinates(int sourceId, Point* res); 8 | -------------------------------------------------------------------------------- /pip/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * IPC channel for picture-in-picture communication between renderer and main process. 3 | */ 4 | const PIP_CHANNEL = 'jitsi-pip-channel'; 5 | 6 | module.exports = { PIP_CHANNEL }; 7 | -------------------------------------------------------------------------------- /remotecontrol/index.js: -------------------------------------------------------------------------------- 1 | const setupRemoteControlMain = require('./main'); 2 | const setupRemoteControlRender = require('./render'); 3 | 4 | module.exports = { 5 | setupRemoteControlMain, 6 | setupRemoteControlRender 7 | }; -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # ESLint will by default ignore its own configuration file. However, there does 2 | # not seem to be a reason why we will want to risk being inconsistent with our 3 | # remaining JavaScript source code. 4 | !.eslintrc.js 5 | -------------------------------------------------------------------------------- /pip/index.js: -------------------------------------------------------------------------------- 1 | const setupPictureInPictureMain = require('./main'); 2 | const setupPictureInPictureRender = require('./render'); 3 | 4 | module.exports = { 5 | setupPictureInPictureMain, 6 | setupPictureInPictureRender 7 | }; 8 | -------------------------------------------------------------------------------- /screensharing/index.js: -------------------------------------------------------------------------------- 1 | const setupScreenSharingMain = require('./main'); 2 | const setupScreenSharingRender = require('./render'); 3 | 4 | module.exports = { 5 | setupScreenSharingMain, 6 | setupScreenSharingRender 7 | }; 8 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | branch="$(git rev-parse --abbrev-ref HEAD)" 5 | 6 | if [ "$branch" = "master" ]; then 7 | echo "You can't commit directly to master branch" 8 | exit 1 9 | fi 10 | 11 | -------------------------------------------------------------------------------- /powermonitor/index.js: -------------------------------------------------------------------------------- 1 | const { cleanupPowerMonitorMain, setupPowerMonitorMain } = require('./main'); 2 | const setupPowerMonitorRender = require('./render'); 3 | 4 | module.exports = { 5 | cleanupPowerMonitorMain, 6 | setupPowerMonitorRender, 7 | setupPowerMonitorMain 8 | }; 9 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | const { spawnSync } = require('child_process'); 2 | const process = require('process'); 3 | 4 | if (process.platform === 'win32') { 5 | spawnSync('npm.cmd', ['run', 'node-gyp-build'], { 6 | input: 'win32 detected. Ensure native code prebuild or rebuild it', 7 | shell: true, 8 | stdio: 'inherit' 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "semiannually" 11 | -------------------------------------------------------------------------------- /popupsconfig/render.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Initializes the popup configuration module. 4 | * 5 | * @param {JitsiMeetExternalAPI} api - The iframe api instance. 6 | */ 7 | // eslint-disable-next-line no-unused-vars 8 | function initPopupsConfiguration(api) { 9 | // The empty function is defined only for backward compatability. 10 | } 11 | 12 | module.exports = initPopupsConfiguration; 13 | -------------------------------------------------------------------------------- /popupsconfig/index.js: -------------------------------------------------------------------------------- 1 | const popupsConfigRegistry = require('./PopupsConfigRegistry'); 2 | const initPopupsConfigurationMain = require('./main'); 3 | const initPopupsConfigurationRender = require('./render'); 4 | const { getPopupTarget } = require('./functions'); 5 | 6 | module.exports = { 7 | popupsConfigRegistry, 8 | initPopupsConfigurationMain, 9 | initPopupsConfigurationRender, 10 | getPopupTarget 11 | }; 12 | -------------------------------------------------------------------------------- /popupsconfig/constants.js: -------------------------------------------------------------------------------- 1 | const popupConfigs = { 2 | 'google-auth': { 3 | matchPatterns: { 4 | url: '^https:\\/\\/(www\\.)?accounts\\.google\\.com\\/' 5 | }, 6 | target: 'electron' 7 | }, 8 | 'dropbox-auth': { 9 | matchPatterns: { 10 | url: '^https:\\/\\/(www\\.)?dropbox\\.com\\/oauth2\\/authorize' 11 | }, 12 | target: 'electron' 13 | } 14 | }; 15 | 16 | module.exports = { 17 | popupConfigs 18 | }; 19 | -------------------------------------------------------------------------------- /screensharing/preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron'); 2 | 3 | const { SCREEN_SHARE_EVENTS_CHANNEL, SCREEN_SHARE_EVENTS } = require('./constants'); 4 | 5 | contextBridge.exposeInMainWorld('JitsiScreenSharingTracker', { 6 | EVENTS: SCREEN_SHARE_EVENTS, 7 | sendEvent: ev => { 8 | if (Object.values(SCREEN_SHARE_EVENTS).includes(ev)) { 9 | ipcRenderer.send(SCREEN_SHARE_EVENTS_CHANNEL, { 10 | data: { 11 | name: ev 12 | } 13 | }); 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /test/sourceId2Coordinates.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const process = require('process'); 3 | 4 | 5 | describe('sourceId2Coordinates', () => { 6 | describe('native_addon', () => { 7 | it('returns undefined for fake value', () => { 8 | if (process.platform === 'win32') { 9 | const sourceId2Coordinates = require('../node_addons/sourceId2Coordinates'); 10 | const result = sourceId2Coordinates("foo"); 11 | 12 | assert.equal(undefined, result); 13 | } 14 | }); 15 | }); 16 | }); -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [{ 3 | 'target_name': 'sourceId2Coordinates', 4 | 'cflags!': [ '-fno-exceptions' ], 5 | 'cflags_cc!': [ '-fno-exceptions' ], 6 | 'msvs_settings': { 7 | 'VCCLCompilerTool': { 'ExceptionHandling': 1 }, 8 | }, 9 | 'include_dirs': [ 10 | ' 2 | #include "sourceId2Coordinates.h" 3 | 4 | Napi::Value sourceId2CoordinatesWrapper(const Napi::CallbackInfo& info) 5 | { 6 | Napi::Env env = info.Env(); 7 | const int sourceID = info[0].As().Int32Value(); 8 | Napi::Object obj = Napi::Object::New(env); 9 | Point coordinates; 10 | if(!sourceId2Coordinates(sourceID, &coordinates)) 11 | { // return undefined if sourceId2Coordinates function fail. 12 | return env.Undefined(); 13 | } 14 | else 15 | { // return the coordinates if sourceId2Coordinates function succeed. 16 | obj.Set(Napi::String::New(env, "x"), Napi::Number::New(env, coordinates.x)); 17 | obj.Set(Napi::String::New(env, "y"), Napi::Number::New(env, coordinates.y)); 18 | return obj; 19 | } 20 | } 21 | 22 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 23 | exports.Set(Napi::String::New(env, "sourceId2Coordinates"), Napi::Function::New(env, sourceId2CoordinatesWrapper)); 24 | 25 | return exports; 26 | } 27 | 28 | NODE_API_MODULE(sourceId2CoordinatesModule, Init) 29 | -------------------------------------------------------------------------------- /node_addons/sourceId2Coordinates/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | const sourceId2Coordinates = require('node-gyp-build')(__dirname + '/../../').sourceId2Coordinates; 3 | 4 | /** 5 | * Returns the coordinates of a desktop using the passed desktop sharing source 6 | * id. 7 | * 8 | * @param {string} sourceId - The desktop sharing source id. 9 | * @returns {Object.|undefined} - The x and y coordinates of the 10 | * top left corner of the desktop. Currently works only for windows. Returns 11 | * undefined for Mac OS, Linux. 12 | */ 13 | module.exports = function(sourceId) { 14 | if(typeof sourceId !== "string" || sourceId === '') { 15 | return undefined; 16 | } 17 | // On windows the source id will have the following format "desktop_id:0". 18 | // we need the "desktop_id" only to get the coordinates. 19 | const idArr = sourceId.split(":"); 20 | const id = Number(idArr.length > 1 ? idArr[0] : sourceId); 21 | if(!isNaN(id)) { 22 | return sourceId2Coordinates(id); 23 | } 24 | return undefined; 25 | }; 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { setupRemoteControlMain, setupRemoteControlRender } = require('./remotecontrol'); 2 | const { setupScreenSharingRender, setupScreenSharingMain } = require('./screensharing'); 3 | const { 4 | cleanupPowerMonitorMain, 5 | setupPowerMonitorRender, 6 | setupPowerMonitorMain 7 | } = require('./powermonitor'); 8 | const { 9 | popupsConfigRegistry, 10 | initPopupsConfigurationMain, 11 | initPopupsConfigurationRender, 12 | getPopupTarget 13 | } = require('./popupsconfig'); 14 | const { 15 | setupPictureInPictureRender, 16 | setupPictureInPictureMain 17 | } = require('./pip'); 18 | 19 | module.exports = { 20 | cleanupPowerMonitorMain, 21 | setupScreenSharingRender, 22 | setupScreenSharingMain, 23 | setupPowerMonitorRender, 24 | setupPowerMonitorMain, 25 | setupRemoteControlMain, 26 | setupRemoteControlRender, 27 | popupsConfigRegistry, 28 | initPopupsConfigurationMain, 29 | initPopupsConfigurationRender, 30 | getPopupTarget, 31 | setupPictureInPictureRender, 32 | setupPictureInPictureMain 33 | }; 34 | -------------------------------------------------------------------------------- /screensharing/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The name of the channel that exchange events between render and main process. 3 | * @type {string} 4 | */ 5 | const SCREEN_SHARE_EVENTS_CHANNEL = 'jitsi-screen-sharing-marker'; 6 | 7 | /** 8 | * The name of the channel that returns desktopCapturer.getSources 9 | * @type {string} 10 | */ 11 | const SCREEN_SHARE_GET_SOURCES = 'jitsi-screen-sharing-get-sources'; 12 | 13 | /** 14 | * Size of the screen sharing tracker window. 15 | */ 16 | const TRACKER_SIZE = { 17 | height: 40, 18 | width: 530 19 | }; 20 | 21 | /** 22 | * Possible events passed on the SCREEN_SHARE_EVENTS_CHANNEL. 23 | */ 24 | const SCREEN_SHARE_EVENTS = { 25 | OPEN_TRACKER: 'open-tracker-window' , 26 | CLOSE_TRACKER: 'close-tracker-window', 27 | HIDE_TRACKER: 'hide-tracker-window', 28 | STOP_SCREEN_SHARE: 'stop-screen-share', 29 | OPEN_PICKER: 'open-picker', 30 | DO_GDM: 'do-gdm' 31 | }; 32 | 33 | module.exports = { 34 | SCREEN_SHARE_EVENTS_CHANNEL, 35 | SCREEN_SHARE_EVENTS, 36 | SCREEN_SHARE_GET_SOURCES, 37 | TRACKER_SIZE 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | build/ 61 | .jshint* 62 | 63 | # vscode settings files 64 | .vscode/ 65 | -------------------------------------------------------------------------------- /node_addons/sourceId2Coordinates/src/sourceId2Coordinates.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "sourceId2Coordinates.h" 3 | 4 | /** 5 | * Tries to get the coordinates of a desktop from passed sourceId 6 | * (which identifies a desktop sharing source). Used to match the source id to a 7 | * screen in Electron. 8 | * 9 | * Returns true on success and false on failure. 10 | * 11 | * NOTE: Works on windows only because on the other platforms there is an easier 12 | * way to match the source id and the screen. 13 | */ 14 | bool sourceId2Coordinates(int sourceId, Point* res) 15 | { 16 | DISPLAY_DEVICE device; 17 | device.cb = sizeof(device); 18 | 19 | if (!EnumDisplayDevices(NULL, sourceId, &device, 0) // device not found 20 | || !(device.StateFlags & DISPLAY_DEVICE_ACTIVE))// device is not active 21 | { 22 | return false; 23 | } 24 | 25 | DEVMODE deviceSettings; 26 | deviceSettings.dmSize = sizeof(deviceSettings); 27 | deviceSettings.dmDriverExtra = 0; 28 | if(!EnumDisplaySettingsEx(device.DeviceName, ENUM_CURRENT_SETTINGS, 29 | &deviceSettings, 0)) 30 | { 31 | return false; 32 | } 33 | 34 | res->x = deviceSettings.dmPosition.x; 35 | res->y = deviceSettings.dmPosition.y; 36 | 37 | return true; 38 | } 39 | -------------------------------------------------------------------------------- /screensharing/utils.js: -------------------------------------------------------------------------------- 1 | /* global process */ 2 | const log = require('@jitsi/logger'); 3 | 4 | let logger; 5 | 6 | const setLogger = loggerTransports => { 7 | logger = log.getLogger('ScreenSharing', loggerTransports || []); 8 | }; 9 | 10 | /** 11 | * Wrapper over the logger's info 12 | * 13 | * @param {string} info - The info text 14 | */ 15 | const logInfo = info => { 16 | if (!logger) { 17 | return; 18 | } 19 | 20 | logger.info(`[RENDERER] ${info}`); 21 | }; 22 | 23 | /** 24 | * Wrapper over the logger's error 25 | * 26 | * @param {Object} err - the error object 27 | */ 28 | const logError = err => { 29 | if (!logger) { 30 | return; 31 | } 32 | 33 | logger.error({ err }, '[RENDERER ERROR]'); 34 | }; 35 | 36 | /** 37 | * Wrapper over the logger's warning 38 | * 39 | * @param {Object} warn - the warn object 40 | */ 41 | const logWarning = warn => { 42 | if (!logger) { 43 | return; 44 | } 45 | 46 | logger.error({ warn }, '[RENDERER WARNING]'); 47 | }; 48 | 49 | const isMac = () => process.platform === 'darwin'; 50 | 51 | const isWayland = () => process.platform === 'linux' && process.env.XDG_SESSION_TYPE === 'wayland'; 52 | 53 | module.exports = { 54 | isMac, 55 | isWayland, 56 | logError, 57 | logInfo, 58 | logWarning, 59 | setLogger 60 | }; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jitsi/electron-sdk", 3 | "version": "9.0.7", 4 | "description": "Utilities for jitsi-meet-electron project", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "test": "mocha", 9 | "validate": "npm ls", 10 | "install": "node install.js", 11 | "node-gyp-build": "node-gyp-build", 12 | "prebuild": "prebuildify -t 20.0.0 --strip" 13 | }, 14 | "pre-commit": [ 15 | "lint" 16 | ], 17 | "repository": "https://github.com/jitsi/jitsi-meet-electron-sdk", 18 | "keywords": [ 19 | "jingle", 20 | "webrtc", 21 | "xmpp", 22 | "electron", 23 | "jitsi-meet", 24 | "utils" 25 | ], 26 | "author": "", 27 | "readmeFilename": "README.md", 28 | "license": "Apache-2.0", 29 | "gypfile": true, 30 | "binary": { 31 | "napi_versions": [ 32 | 3 33 | ] 34 | }, 35 | "dependencies": { 36 | "@jitsi/logger": "^2.0.2", 37 | "@jitsi/robotjs": "^0.6.15", 38 | "electron-store": "^8.0.1", 39 | "node-addon-api": "^8.0.0", 40 | "node-gyp-build": "4.8.4", 41 | "postis": "^2.2.0" 42 | }, 43 | "devDependencies": { 44 | "eslint": ">=3", 45 | "eslint-plugin-jsdoc": "*", 46 | "husky": "^9.0.10", 47 | "mocha": "^11.7.3", 48 | "prebuildify": "^6.0.0", 49 | "precommit-hook": "3.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'commonjs': true, 5 | 'es6': true, 6 | "mocha": true 7 | }, 8 | 'extends': 'eslint:recommended', 9 | 'globals': { 10 | // The globals that (1) are accessed but not defined within many of our 11 | // files, (2) are certainly defined, and (3) we would like to use 12 | // without explicitly specifying them (using a comment) inside of our 13 | // files. 14 | '__filename': false 15 | }, 16 | 'parserOptions': { 17 | 'ecmaVersion': 9, 18 | 'sourceType': 'module' 19 | }, 20 | 'rules': { 21 | 'new-cap': [ 22 | 'error', 23 | { 24 | 'capIsNew': false // Behave like JSHint's newcap. 25 | } 26 | ], 27 | // While it is considered a best practice to avoid using methods on 28 | // console in JavaScript that is designed to be executed in the browser 29 | // and ESLint includes the rule among its set of recommended rules, (1) 30 | // the general practice is to strip such calls before pushing to 31 | // production and (2) we prefer to utilize console in lib-jitsi-meet 32 | // (and jitsi-meet). 33 | 'no-console': 'off', 34 | 'semi': 'error' 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /popupsconfig/PopupsConfigRegistry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class that stores popup configurations. 3 | */ 4 | class PopupsConfigRegistry { 5 | /** 6 | * Creates new PopupsConfigRegistry instance. 7 | */ 8 | constructor() { 9 | this._registry = {}; 10 | } 11 | 12 | /** 13 | * Registers new popup config. 14 | * 15 | * @param {string} name - The name of the popup. 16 | * @param {Object} config - The config object. 17 | */ 18 | registerPopupConfig(name, config) { 19 | this._registry[name] = config; 20 | } 21 | 22 | /** 23 | * Registers multiple popup config. 24 | * 25 | * @param {Object} configs - The config objects. 26 | */ 27 | registerPopupConfigs(configs) { 28 | this._registry = Object.assign(this._registry, configs); 29 | } 30 | 31 | /** 32 | * Returns a config object for the popup with the passed name. 33 | * 34 | * @param {string} name - The name of the popup. 35 | * @returns {Object} 36 | */ 37 | getConfigByName(name) { 38 | return this._registry[name]; 39 | } 40 | 41 | /** 42 | * Returns all config objects in the registry. 43 | * 44 | * @param {string} name - The name of the popup. 45 | * @returns {Object} 46 | */ 47 | getAllConfigs() { 48 | return Object.values(this._registry); 49 | } 50 | } 51 | 52 | module.exports = new PopupsConfigRegistry(); 53 | -------------------------------------------------------------------------------- /powermonitor/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Types of METHODS. 3 | */ 4 | const METHODS = { 5 | queryIdleState: 'query-system-idle-state', 6 | queryIdleTime: 'query-system-idle-time' 7 | }; 8 | 9 | /** 10 | * The power monitor events. 11 | * @type {{ 12 | * SUSPEND: string, 13 | * LOCK_SCREEN: string, 14 | * SHUTDOWN: string, 15 | * ON_AC: string, 16 | * ON_BATTERY: string, 17 | * RESUME: string, 18 | * UNLOCK_SCREEN: string}} 19 | */ 20 | const POWER_MONITOR_EVENTS = { 21 | LOCK_SCREEN: 'lock-screen' , 22 | ON_AC: 'on-ac', 23 | ON_BATTERY: 'on-battery', 24 | RESUME: 'resume', 25 | SHUTDOWN: 'shutdown', 26 | SUSPEND: 'suspend', 27 | UNLOCK_SCREEN: 'unlock-screen' 28 | }; 29 | 30 | /** 31 | * The name of power monitor messages exchanged with Jitsi Meet window. 32 | */ 33 | const POWER_MONITOR_MESSAGE_NAME = 'power-monitor'; 34 | 35 | /** 36 | * The name of the channel that exchange events between render and main process. 37 | * @type {string} 38 | */ 39 | const POWER_MONITOR_EVENTS_CHANNEL = 'power-monitor-events'; 40 | 41 | /** 42 | * The name of the channel that is used to query power monitor from render to main process. 43 | * @type {string} 44 | */ 45 | const POWER_MONITOR_QUERIES_CHANNEL = 'power-monitor-queries'; 46 | 47 | module.exports = { 48 | POWER_MONITOR_EVENTS, 49 | POWER_MONITOR_EVENTS_CHANNEL, 50 | POWER_MONITOR_MESSAGE_NAME, 51 | POWER_MONITOR_QUERIES_CHANNEL, 52 | METHODS 53 | }; 54 | -------------------------------------------------------------------------------- /remotecontrol/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * electron.screen event 4 | */ 5 | DISPLAY_METRICS_CHANGED: 'display-metrics-changed', 6 | 7 | /** 8 | * Types of remote-control events. 9 | */ 10 | EVENTS: { 11 | mousemove: "mousemove", 12 | mousedown: "mousedown", 13 | mouseup: "mouseup", 14 | mousedblclick: "mousedblclick", 15 | mousescroll: "mousescroll", 16 | keydown: "keydown", 17 | keyup: "keyup", 18 | stop: "stop", 19 | supported: "supported" 20 | }, 21 | 22 | /** 23 | * Event for retrieving display metrics 24 | */ 25 | GET_DISPLAY_EVENT: 'jitsi-remotecontrol-get-display', 26 | 27 | /** 28 | * Key actions mapping between the values in remote control key event and 29 | * robotjs methods. 30 | */ 31 | KEY_ACTIONS_FROM_EVENT_TYPE: { 32 | keydown: "down", 33 | keyup: "up" 34 | }, 35 | 36 | /** 37 | * Mouse actions mapping between the values in remote control mouse event and 38 | * robotjs methods. 39 | */ 40 | MOUSE_ACTIONS_FROM_EVENT_TYPE: { 41 | mousedown: "down", 42 | mouseup: "up" 43 | }, 44 | 45 | /** 46 | * Mouse button mapping between the values in remote control mouse event and 47 | * robotjs methods. 48 | */ 49 | MOUSE_BUTTONS: { 50 | 1: "left", 51 | 2: "middle", 52 | 3: "right" 53 | }, 54 | 55 | /** 56 | * The name of remote control messages. 57 | */ 58 | REMOTE_CONTROL_MESSAGE_NAME: "remote-control", 59 | 60 | /** 61 | * Types of remote-control requests. 62 | */ 63 | REQUESTS: { 64 | start: "start" 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /helpers/functions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compares two versions. 3 | * 4 | * @param {*} oldVer - the previous version. 5 | * @param {*} newVer - the newer version. 6 | * @returns {boolean} - whether the new version is higher or equal to old version. 7 | */ 8 | const isVersionNewerOrEqual = (oldVer, newVer) => { 9 | try { 10 | const oldParts = oldVer.split('.'); 11 | const newParts = newVer.split('.'); 12 | for (let i = 0; i < newParts.length; i++) { 13 | const a = parseInt(newParts[i]); 14 | const b = parseInt(oldParts[i]); 15 | if (a > b) return true; 16 | if (a < b) return false; 17 | } 18 | return true; 19 | } catch (e) { 20 | return false; 21 | } 22 | }; 23 | 24 | /** 25 | * This is the Windows release which introduced WDA_EXCLUDEFROMCAPTURE (Windows 10 Version 2004), 26 | * which is used by Electron to hide the window on screen captures. For older Windows OS's WDA_MONITOR flag is used, 27 | * which shows a black screen on screen captures. 28 | * https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowdisplayaffinity 29 | */ 30 | const HIDE_SCREEN_CAPTURE_WINDOWS_RELEASE = '10.0.19041'; 31 | 32 | /** 33 | * Enable screen capture protection only for Windows versions newer or equal to Windows 10 Version 2004. 34 | * 35 | * @param {string} currentVer - current OS version. 36 | * @returns {boolean} - whether the given version is equal or newer than the Windows 10 Version 2004 release. 37 | */ 38 | const windowsEnableScreenProtection = currentVer => isVersionNewerOrEqual(HIDE_SCREEN_CAPTURE_WINDOWS_RELEASE, currentVer); 39 | 40 | module.exports = { 41 | windowsEnableScreenProtection 42 | }; 43 | -------------------------------------------------------------------------------- /screensharing/screenSharingTracker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Screen Sharing Tracker 8 | 59 | 60 | 61 | 62 | 63 |
64 |  is sharing your screen. 65 |
66 |
67 | 68 | Hide 69 |
70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | permissions: 10 | contents: write 11 | id-token: write 12 | 13 | jobs: 14 | build: 15 | name: Build ${{ matrix.build-group }} 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | include: 21 | - os: windows-latest 22 | arch: x86 23 | build-group: win32-x86 24 | - os: windows-latest 25 | arch: x64 26 | build-group: win32-x64 27 | - os: windows-11-arm 28 | arch: arm64 29 | build-group: win32-arm64 30 | steps: 31 | - uses: actions/checkout@v5 32 | - uses: actions/setup-node@v5 33 | with: 34 | node-version: '20' 35 | architecture: ${{ matrix.arch }} 36 | - run: npm ci 37 | - run: npm run lint 38 | - run: npm test 39 | - name: Prebuildify 40 | run: npm run prebuild 41 | shell: bash 42 | - uses: actions/upload-artifact@v4 43 | with: 44 | name: prebuilds-${{ matrix.build-group }} 45 | path: prebuilds/ 46 | publish: 47 | name: Publish to npm 48 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 49 | runs-on: ubuntu-latest 50 | needs: build 51 | concurrency: 52 | group: ${{ github.ref }} 53 | steps: 54 | - uses: actions/checkout@v5 55 | - uses: actions/download-artifact@v5 56 | with: 57 | path: prebuilds 58 | pattern: prebuilds-* 59 | merge-multiple: true 60 | - uses: phips28/gh-action-bump-version@ba04cec2b3fb07806ab4448c8825f6ff98fac594 61 | with: 62 | tag-prefix: 'v' 63 | version-type: 'patch' 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | - uses: actions/setup-node@v5 67 | with: 68 | node-version: '20' 69 | registry-url: 'https://registry.npmjs.org' 70 | # Ensure latest npm version for provenance support 71 | - name: "Update npm" 72 | run: npm install -g npm@latest 73 | - run: npm publish --provenance --access public 74 | -------------------------------------------------------------------------------- /popupsconfig/main.js: -------------------------------------------------------------------------------- 1 | const popupsConfigRegistry = require('./PopupsConfigRegistry'); 2 | const { testMatchPatterns } = require('./functions'); 3 | const { popupConfigs } = require('./constants'); 4 | 5 | /** 6 | * Initializes the popup configuration module. 7 | * 8 | * @param {BrowserWindow} jitsiMeetWindow - The window where jitsi meet is rendered. 9 | * @param {Function} existingWindowOpenHandler - A window open handler that will be called after the local 10 | * window open handler is executed. This parameter is useful when the consumer of the api wants to avoid overwriting 11 | * the host app window open handler. 12 | * @returns {void} 13 | */ 14 | function initPopupsConfiguration(jitsiMeetWindow, externalWindowOpenHandler) { 15 | popupsConfigRegistry.registerPopupConfigs(popupConfigs); 16 | 17 | // Configuration for the google auth popup. 18 | jitsiMeetWindow.webContents.setWindowOpenHandler(details => { 19 | const {url, frameName} = details; 20 | const configGoogle 21 | = popupsConfigRegistry.getConfigByName('google-auth') || {}; 22 | if (testMatchPatterns(url, frameName, configGoogle.matchPatterns)) { 23 | return { 24 | action: 'allow', 25 | overrideBrowserWindowOptions: { 26 | webPreferences: { nodeIntegration: false } 27 | } 28 | }; 29 | } 30 | 31 | const configDropbox 32 | = popupsConfigRegistry.getConfigByName('dropbox-auth') || {}; 33 | 34 | if (testMatchPatterns(url, frameName, configDropbox.matchPatterns)) { 35 | return { 36 | action: 'allow', 37 | overrideBrowserWindowOptions: { 38 | webPreferences: { nodeIntegration: false } 39 | } 40 | }; 41 | } 42 | 43 | if (externalWindowOpenHandler) { 44 | try { 45 | return externalWindowOpenHandler(details); 46 | } catch (e) { 47 | console.error(`Error while executing external window open handler: ${e}`); 48 | } 49 | } 50 | 51 | return { action: 'deny' }; 52 | }); 53 | } 54 | 55 | module.exports = initPopupsConfiguration; 56 | -------------------------------------------------------------------------------- /pip/render.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | 3 | const { PIP_CHANNEL } = require('./constants'); 4 | 5 | /** 6 | * Renderer process hook that sets up Electron-specific picture-in-picture functionality. 7 | * Listens for postMessage requests from the jitsi-meet iframe and forwards them to the main process 8 | * which can execute requestPictureInPicture with userGesture: true. 9 | */ 10 | class PictureInPictureRenderHook { 11 | /** 12 | * Creates a PictureInPictureRenderHook that listens for PiP requests from the jitsi-meet iframe. 13 | * 14 | * @param {JitsiIFrameApi} api - The Jitsi Meet iframe api object. 15 | */ 16 | constructor(api) { 17 | this._api = api; 18 | this._handlePipRequested = this._handlePipRequested.bind(this); 19 | this._onApiDispose = this._onApiDispose.bind(this); 20 | 21 | // Listen for PiP requests. 22 | this._api.on('_pipRequested', this._handlePipRequested); 23 | 24 | // Clean up on API disposal. 25 | this._api.on('_willDispose', this._onApiDispose); 26 | } 27 | 28 | /** 29 | * Handles external API events from the jitsi-meet iframe. 30 | * Forwards picture-in-picture requests to the main process via IPC. 31 | * 32 | * @returns {void} 33 | */ 34 | _handlePipRequested() { 35 | const iframe = this._api.getIFrame(); 36 | const frameName = iframe ? iframe.name : undefined; 37 | 38 | // Forward to main process via IPC. 39 | ipcRenderer.send(PIP_CHANNEL, frameName); 40 | } 41 | 42 | /** 43 | * Cleans up event listeners when the API is disposed. 44 | * 45 | * @returns {void} 46 | */ 47 | _onApiDispose() { 48 | this._api.removeListener('_pipRequested', this._handlePipRequested); 49 | this._api.removeListener('_willDispose', this._onApiDispose); 50 | } 51 | } 52 | 53 | /** 54 | * Initializes the picture-in-picture electron specific functionality in the renderer process. 55 | * 56 | * @param {JitsiIFrameApi} api - The Jitsi Meet iframe api object. 57 | * @returns {PictureInPictureRenderHook} The PiP render hook instance. 58 | */ 59 | module.exports = function setupPictureInPictureRender(api) { 60 | return new PictureInPictureRenderHook(api); 61 | }; 62 | -------------------------------------------------------------------------------- /popupsconfig/functions.js: -------------------------------------------------------------------------------- 1 | const popupsConfigRegistry = require('./PopupsConfigRegistry'); 2 | 3 | /** 4 | * Finds a config object from the popup config registry that will match the 5 | * passed URL or frame name. 6 | * 7 | * @param {string} url - The URL. 8 | * @param {string} frameName - The frame name. 9 | * @returns {Object|undefined} - A config object from the popup config registry 10 | * or undefined if no config object has been found. 11 | */ 12 | function _findConfig(url, frameName) { 13 | return popupsConfigRegistry.getAllConfigs().find(({ matchPatterns }) => 14 | testMatchPatterns(url, frameName, matchPatterns)); 15 | } 16 | 17 | /** 18 | * Tests passed regular expressions agains the url and frameName parameters. 19 | * 20 | * @param {string} url - The URL. 21 | * @param {string} frameName - The frame name. 22 | * @param {Object} matchPatterns - An object with regular expresions for url 23 | * and/or frame name 24 | * @param {string} matchPatterns.url 25 | * @param {string} matchPatterns.frameName 26 | * @returns {boolean} - Returns true if url or frameName match the passed 27 | * regular expresions. 28 | */ 29 | function testMatchPatterns(url, frameName, matchPatterns = {}) { 30 | let urlMatched = false; 31 | let frameNameMatched = false; 32 | 33 | if (typeof matchPatterns.url !== 'undefined' 34 | && typeof url !== 'undefined') { 35 | urlMatched = RegExp(matchPatterns.url).test(url); 36 | } 37 | 38 | if (typeof matchPatterns.frameName !== 'undefined' 39 | && typeof frameName !== 'undefined') { 40 | frameNameMatched 41 | = RegExp(matchPatterns.frameName).test(frameName); 42 | } 43 | 44 | return urlMatched || frameNameMatched; 45 | } 46 | 47 | /** 48 | * Returns the target property from the config object that corresponds to the 49 | * passed url and frameName. 50 | * 51 | * @param {string} url - The URL. 52 | * @param {string} frameName - The frame name. 53 | * @returns {string|undefined} - Returns the target property from the found 54 | * config object or undefined if a matching config object hasn't been found. 55 | */ 56 | function getPopupTarget(url, frameName) { 57 | let config = _findConfig(url, frameName) || {}; 58 | 59 | return config.target; 60 | } 61 | 62 | module.exports = { 63 | getPopupTarget, 64 | testMatchPatterns 65 | }; 66 | -------------------------------------------------------------------------------- /powermonitor/render.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const postis = require('postis'); 3 | const { ipcRenderer } = electron; 4 | 5 | const { 6 | POWER_MONITOR_EVENTS_CHANNEL, 7 | POWER_MONITOR_MESSAGE_NAME, 8 | POWER_MONITOR_QUERIES_CHANNEL 9 | } = require('./constants'); 10 | 11 | /** 12 | * The channel we use to communicate with Jitsi Meet window. 13 | */ 14 | let _channel; 15 | 16 | /** 17 | * The listener for query responses. 18 | * @param _ - Not used. 19 | * @param response - The response to send. 20 | */ 21 | function queriesChannelListener(_, response) { 22 | _sendMessage(response); 23 | } 24 | 25 | /** 26 | * The listener we use to listen for power monitor events. 27 | * @param _ - Not used. 28 | * @param event - The event. 29 | */ 30 | function eventsChannelListener(_, event) { 31 | _sendEvent(event); 32 | } 33 | 34 | /** 35 | * Sends event to Jitsi Meet. 36 | * 37 | * @param {Object} event the remote control event. 38 | */ 39 | function _sendEvent(event) { 40 | _sendMessage({ 41 | data: Object.assign({ name: POWER_MONITOR_MESSAGE_NAME }, event) 42 | }); 43 | } 44 | 45 | /** 46 | * Sends a message to Jitsi Meet. 47 | * 48 | * @param {Object} message the message to be sent. 49 | */ 50 | function _sendMessage(message) { 51 | _channel.send({ 52 | method: 'message', 53 | params: message 54 | }); 55 | } 56 | 57 | /** 58 | * Disposes the power monitor functionality. 59 | */ 60 | function dispose() { 61 | ipcRenderer.removeListener( 62 | POWER_MONITOR_QUERIES_CHANNEL, queriesChannelListener); 63 | ipcRenderer.removeListener( 64 | POWER_MONITOR_EVENTS_CHANNEL, eventsChannelListener); 65 | 66 | if(_channel) { 67 | _channel.destroy(); 68 | _channel = null; 69 | } 70 | } 71 | 72 | /** 73 | * Initializes the power monitor in the render process of the 74 | * window which displays Jitsi Meet. 75 | * 76 | * @param {JitsiIFrameApi} api - the Jitsi Meet iframe api object. 77 | */ 78 | module.exports = function setupPowerMonitorRender(api) { 79 | const iframe = api.getIFrame(); 80 | 81 | api.on('_willDispose', dispose); 82 | api.on('readyToClose', dispose); 83 | 84 | iframe.addEventListener('load', () => { 85 | _channel = postis({ 86 | window: iframe.contentWindow, 87 | windowForEventListening: window, 88 | scope: 'jitsi-power-monitor' 89 | }); 90 | _channel.ready(() => { 91 | ipcRenderer.on(POWER_MONITOR_QUERIES_CHANNEL, queriesChannelListener); 92 | ipcRenderer.on(POWER_MONITOR_EVENTS_CHANNEL, eventsChannelListener); 93 | _channel.listen('message', message => { 94 | const { name } = message.data; 95 | if(name === POWER_MONITOR_MESSAGE_NAME) { 96 | ipcRenderer.send(POWER_MONITOR_QUERIES_CHANNEL, message); 97 | } 98 | }); 99 | }); 100 | }); 101 | }; 102 | -------------------------------------------------------------------------------- /powermonitor/main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const { app, ipcMain } = electron; 3 | const { 4 | METHODS, 5 | POWER_MONITOR_EVENTS, 6 | POWER_MONITOR_EVENTS_CHANNEL, 7 | POWER_MONITOR_QUERIES_CHANNEL 8 | } = require('./constants'); 9 | 10 | let browserWindow; 11 | 12 | /** 13 | * Attaches listening to all events from POWER_MONITOR_EVENTS on powerMonitor. 14 | * @param {BrowserWindow} jitsiMeetWindow - the BrowserWindow object which 15 | * displays Jitsi Meet. 16 | * @private 17 | */ 18 | function _attachEvents(jitsiMeetWindow) { 19 | browserWindow = jitsiMeetWindow; 20 | Object.values(POWER_MONITOR_EVENTS).forEach(event => { 21 | electron.powerMonitor.on(event, () => { 22 | if (browserWindow && !browserWindow.isDestroyed()) { 23 | browserWindow.webContents.send(POWER_MONITOR_EVENTS_CHANNEL, { event }); 24 | } 25 | }); 26 | }); 27 | } 28 | 29 | /** 30 | * The result from the querySystemIdleState or querySystemIdleTime to pass back 31 | * to Jitsi Meet. 32 | * @param id - Id of the request. 33 | * @param idleState - The result state retrieved. 34 | */ 35 | function systemIdleResult(id, idleState) { 36 | browserWindow.webContents.send(POWER_MONITOR_QUERIES_CHANNEL, { 37 | id, 38 | result: idleState, 39 | type: 'response' 40 | }); 41 | } 42 | 43 | /** 44 | * The error result to pass back to Jitsi Meet. 45 | * @param id - Id of the request. 46 | * @param error - The error to send. 47 | */ 48 | function systemIdleErrorResult(id, error) { 49 | browserWindow.webContents.send(POWER_MONITOR_QUERIES_CHANNEL, { 50 | id, 51 | error, 52 | type: 'response' 53 | }); 54 | } 55 | 56 | /** 57 | * 58 | * @param {IPCMainEvent} event - electron.ipcMain event 59 | * @param {Object} powerMonitor event data 60 | */ 61 | function handlePowerMonitorQuery(event, { id, data }) { 62 | const { powerMonitor } = electron; 63 | 64 | switch(data.type) { 65 | case METHODS.queryIdleState: 66 | systemIdleResult(id, powerMonitor.getSystemIdleState(data.idleThreshold)); 67 | break; 68 | case METHODS.queryIdleTime: 69 | systemIdleResult(id, powerMonitor.getSystemIdleTime()); 70 | break; 71 | default: { 72 | const error = 'Unknown event type!'; 73 | 74 | console.error(error); 75 | systemIdleErrorResult(id, error); 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * Cleanup any handlers 82 | */ 83 | function cleanup() { 84 | ipcMain.removeListener(POWER_MONITOR_QUERIES_CHANNEL, handlePowerMonitorQuery); 85 | } 86 | 87 | /** 88 | * Initializes the power monitor functionality in the main electron process. 89 | * 90 | * @param {BrowserWindow} jitsiMeetWindow - the BrowserWindow object which 91 | * displays Jitsi Meet 92 | */ 93 | function setupPowerMonitorMain(jitsiMeetWindow) { 94 | app.whenReady().then(() => { 95 | _attachEvents(jitsiMeetWindow); 96 | }); 97 | 98 | ipcMain.on(POWER_MONITOR_QUERIES_CHANNEL, handlePowerMonitorQuery); 99 | 100 | jitsiMeetWindow.on('close', cleanup); 101 | } 102 | 103 | module.exports = { 104 | cleanupPowerMonitorMain: cleanup, 105 | setupPowerMonitorMain 106 | }; 107 | -------------------------------------------------------------------------------- /pip/main.js: -------------------------------------------------------------------------------- 1 | const { ipcMain } = require('electron'); 2 | const { PIP_CHANNEL } = require('./constants'); 3 | const log = require('@jitsi/logger'); 4 | 5 | let logger; 6 | 7 | 8 | /** 9 | * Main process hook that handles picture-in-picture requests from the renderer. 10 | * Executes requestPictureInPicture with userGesture: true to bypass transient activation requirements. 11 | */ 12 | class PictureInPictureMainHook { 13 | /** 14 | * Creates a PictureInPictureMainHook linked to the jitsi meet window. 15 | * 16 | * @param {BrowserWindow} jitsiMeetWindow - BrowserWindow where jitsi-meet is loaded. 17 | */ 18 | constructor(jitsiMeetWindow) { 19 | this._jitsiMeetWindow = jitsiMeetWindow; 20 | this._handlePipRequest = this._handlePipRequest.bind(this); 21 | this.cleanup = this.cleanup.bind(this); 22 | 23 | // Listen for PiP requests from the renderer process. 24 | ipcMain.on(PIP_CHANNEL, this._handlePipRequest); 25 | 26 | // Automatically cleanup when the window is closed. 27 | this._jitsiMeetWindow.on('closed', this.cleanup); 28 | } 29 | 30 | /** 31 | * Handles picture-in-picture requests from the renderer. 32 | * Finds the jitsi-meet iframe and executes requestPictureInPicture with userGesture: true. 33 | * 34 | * @param {IpcMainEvent} event - The IPC event object. 35 | * @param {number} frameName - The name of the frame where the PiP request originated. 36 | * @returns {void} 37 | */ 38 | _handlePipRequest(event, frameName) { 39 | if (!this._jitsiMeetWindow || !frameName) { 40 | logger.error('[PiP Main] Cannot handle PiP request: window not available'); 41 | 42 | return; 43 | } 44 | 45 | // Find the jitsi-meet iframe (non-file:// URL). 46 | const frames = this._jitsiMeetWindow.webContents.mainFrame.frames; 47 | const jitsiFrame = frames.find(frame => frame.name === frameName); 48 | 49 | if (!jitsiFrame) { 50 | logger.error(`[PiP Main]: Cannot find jitsi-meet iframe with name ${frameName}`); 51 | 52 | return; 53 | } 54 | 55 | // Execute requestPictureInPicture with userGesture: true. 56 | const pipScript = ` 57 | if (window.JitsiMeetJS && window.JitsiMeetJS.app && window.JitsiMeetJS.app.electron 58 | && typeof window.JitsiMeetJS.app.electron.requestPictureInPicture === 'function') { 59 | window.JitsiMeetJS.app.electron.requestPictureInPicture(); 60 | }`; 61 | 62 | jitsiFrame.executeJavaScript(pipScript, true) 63 | .catch(error => { 64 | logger.error('[PiP Main] Failed to execute PiP script:', error); 65 | }); 66 | } 67 | 68 | /** 69 | * Cleans up event listeners. Called automatically when the window is closed, 70 | * but can also be called manually if needed. 71 | * 72 | * @returns {void} 73 | */ 74 | cleanup() { 75 | ipcMain.removeListener(PIP_CHANNEL, this._handlePipRequest); 76 | 77 | // Remove the window close listener if the window still exists. 78 | if (this._jitsiMeetWindow && !this._jitsiMeetWindow.isDestroyed()) { 79 | this._jitsiMeetWindow.removeListener('closed', this.cleanup); 80 | } 81 | 82 | // Prevent double cleanup. 83 | this._jitsiMeetWindow = null; 84 | } 85 | } 86 | 87 | /** 88 | * Initializes the picture-in-picture electron specific functionality in the main process. 89 | * 90 | * @param {BrowserWindow} jitsiMeetWindow - BrowserWindow where jitsi-meet is loaded. 91 | * @param {Array} loggerTransports - Optional array of logger transports for configuring the logger. 92 | * @returns {PictureInPictureMainHook} The PiP main hook instance. 93 | */ 94 | module.exports = function setupPictureInPictureMain(jitsiMeetWindow, loggerTransports) { 95 | logger = log.getLogger('PIP', loggerTransports || []); 96 | 97 | return new PictureInPictureMainHook(jitsiMeetWindow); 98 | }; 99 | -------------------------------------------------------------------------------- /remotecontrol/main.js: -------------------------------------------------------------------------------- 1 | const { 2 | app, 3 | ipcMain, 4 | screen, 5 | } = require('electron'); 6 | const process = require('process'); 7 | 8 | const { DISPLAY_METRICS_CHANGED, GET_DISPLAY_EVENT } = require('./constants'); 9 | 10 | /** 11 | * Module to run on main process to get display dimensions for remote control. 12 | */ 13 | class RemoteControlMain { 14 | constructor(jitsiMeetWindow) { 15 | this._jitsiMeetWindow = jitsiMeetWindow; 16 | 17 | this.cleanup = this.cleanup.bind(this); 18 | this._handleDisplayMetricsChanged = this._handleDisplayMetricsChanged.bind(this); 19 | this._handleGetDisplayEvent = this._handleGetDisplayEvent.bind(this); 20 | 21 | ipcMain.on(GET_DISPLAY_EVENT, this._handleGetDisplayEvent); 22 | 23 | app.whenReady().then(() => { 24 | screen.on(DISPLAY_METRICS_CHANGED, this._handleDisplayMetricsChanged); 25 | }); 26 | 27 | // Clean up ipcMain handlers to avoid leaks. 28 | this._jitsiMeetWindow.on('closed', this.cleanup); 29 | } 30 | 31 | /** 32 | * Cleanup any handlers 33 | */ 34 | cleanup() { 35 | ipcMain.removeListener(GET_DISPLAY_EVENT, this._handleGetDisplayEvent); 36 | screen.removeListener(DISPLAY_METRICS_CHANGED, this._handleDisplayMetricsChanged); 37 | } 38 | 39 | /** 40 | * Handles GET_DISPLAY_EVENT event 41 | * @param {IPCMainEvent} event - The electron event 42 | * @param {string} sourceId - The source id of the desktop sharing stream. 43 | */ 44 | _handleGetDisplayEvent(event, sourceId) { 45 | event.returnValue = this._getDisplay(sourceId); 46 | } 47 | 48 | /** 49 | * Handles DISPLAY_METRICS_CHANGED event 50 | */ 51 | _handleDisplayMetricsChanged() { 52 | if (!this._jitsiMeetWindow.isDestroyed()) { 53 | this._jitsiMeetWindow.webContents.send('jitsi-remotecontrol-displays-changed'); 54 | } 55 | } 56 | 57 | /** 58 | * Returns the display metrics(x, y, width, height, scaleFactor, etc...) of the display that will be used for the 59 | * remote control. 60 | * 61 | * @param {string} sourceId - The source id of the desktop sharing stream. 62 | * @returns {Object} bounds and scaleFactor of display matching sourceId. 63 | */ 64 | _getDisplay(sourceId) { 65 | const { screen } = require('electron'); 66 | 67 | const displays = screen.getAllDisplays(); 68 | 69 | switch(displays.length) { 70 | case 0: 71 | return undefined; 72 | case 1: 73 | // On Linux probably we'll end up here even if there are 74 | // multiple monitors. 75 | return displays[0]; 76 | // eslint-disable-next-line no-case-declarations 77 | default: { // > 1 display 78 | // Remove the type part from the sourceId 79 | const parsedSourceId = sourceId.replace('screen:', ''); 80 | 81 | // Currently native code sourceId2Coordinates is only necessary for windows. 82 | if (process.platform === 'win32') { 83 | const sourceId2Coordinates = require("../node_addons/sourceId2Coordinates"); 84 | const coordinates = sourceId2Coordinates(parsedSourceId); 85 | if(coordinates) { 86 | const { x, y } = coordinates; 87 | const display 88 | = screen.getDisplayNearestPoint({ 89 | x: x + 1, 90 | y: y + 1 91 | }); 92 | 93 | if (typeof display !== 'undefined') { 94 | // We need to use x and y returned from sourceId2Coordinates because the ones returned from 95 | // Electron don't seem to respect the scale factors of the other displays. 96 | const { width, height } = display.bounds; 97 | 98 | return { 99 | bounds: { 100 | x, 101 | y, 102 | width, 103 | height 104 | }, 105 | scaleFactor: display.scaleFactor 106 | }; 107 | } else { 108 | return undefined; 109 | } 110 | } 111 | } else if (process.platform === 'darwin') { 112 | // On Mac OS the sourceId = 'screen' + displayId. 113 | // Try to match displayId with sourceId. 114 | let displayId = Number(parsedSourceId); 115 | 116 | if (isNaN(displayId)) { 117 | // The source id may have the following format "desktop_id:0". 118 | 119 | const idArr = parsedSourceId.split(":"); 120 | 121 | if (idArr.length <= 1) { 122 | return; 123 | } 124 | 125 | displayId = Number(idArr[0]); 126 | } 127 | return displays.find(display => display.id === displayId); 128 | } else { 129 | return undefined; 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * Initializes the remote control functionality in the main electron process. 138 | * 139 | * @param {BrowserWindow} jitsiMeetWindow - the BrowserWindow object which displays the meeting. 140 | * @returns {RemoteControlMain} - the remote control object. 141 | */ 142 | module.exports = function setupRemoteControlMain(jitsiMeetWindow) { 143 | return new RemoteControlMain(jitsiMeetWindow); 144 | }; -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | Jitsi Meet Electron SDK provides utilities for integrating Jitsi Meet into Electron applications. The SDK supports Electron >= 16 and includes native code (C++) on Windows for remote control functionality, distributed as prebuilt binaries via prebuildify. 8 | 9 | ## Development Commands 10 | 11 | ### Setup 12 | ```bash 13 | npx husky install # Enable git hooks to prevent accidental pushes to main 14 | npm ci # Install dependencies 15 | ``` 16 | 17 | ### Building and Testing 18 | ```bash 19 | npm run lint # Run ESLint checks 20 | npm test # Run mocha tests 21 | npm run validate # Validate dependency tree 22 | npx node-gyp rebuild # Rebuild native code (Windows only) 23 | npm run prebuild # Prebuildify native addons for distribution 24 | ``` 25 | 26 | ### CI Workflow 27 | The `.github/workflows/ci.yml` runs on every PR and push to master: 28 | - Builds native code for Windows (x86, x64, arm64) 29 | - Runs lint and tests on all platforms 30 | - Auto-publishes to npm on master branch pushes with automatic version bumping (patch by default, use keywords in commit message for major/minor) 31 | 32 | ## Architecture 33 | 34 | ### Module Structure 35 | The SDK is organized into feature-based modules that expose both **main process** and **renderer process** APIs: 36 | 37 | ``` 38 | index.js (main export) 39 | ├── remotecontrol/ # Remote desktop control via robotjs 40 | ├── screensharing/ # Screen sharing with desktop picker and tracker 41 | ├── pip/ # Picture-in-picture for active speaker video 42 | ├── powermonitor/ # System idle and power events 43 | ├── popupsconfig/ # Popup window configuration registry 44 | ├── node_addons/ # Native C++ addons (Windows only) 45 | └── helpers/ # Shared utility functions 46 | ``` 47 | 48 | ### Main/Renderer Process Pattern 49 | Each feature module follows a consistent pattern: 50 | - **`main.js`**: Code running in Electron's main process (privileged access to OS APIs) 51 | - **`render.js`**: Code running in Electron's renderer process (iframe/web context) 52 | - **`index.js`**: Exports both main and render components 53 | - **`constants.js`**: Shared IPC event names and constants 54 | 55 | Communication between processes uses Electron's IPC (ipcMain/ipcRenderer) and postis for iframe messaging. 56 | 57 | ### Core Features 58 | 59 | #### Remote Control (`remotecontrol/`) 60 | Enables remote desktop control during Jitsi Meet sessions: 61 | - `RemoteControlMain`: Runs in main process, handles display metrics and IPC 62 | - `RemoteControl`: Runs in renderer, uses robotjs to execute mouse/keyboard events from remote participants 63 | - Windows native addon `sourceId2Coordinates` converts screen source IDs to coordinates 64 | 65 | #### Screen Sharing (`screensharing/`) 66 | Custom screen/window picker and sharing tracker: 67 | - `setupScreenSharingMain`: Sets up `desktopCapturer`, handles getDisplayMedia requests, manages screen sharing tracker window 68 | - `setupScreenSharingRender`: Interfaces with Jitsi Meet iframe API 69 | - Special handling for Wayland (uses native picker) and macOS (permission checks) 70 | - `screenSharingTracker.js`: Small always-visible window showing "X is sharing your screen" 71 | 72 | #### Picture in Picture (`pip/`) 73 | Browser native picture-in-picture functionality for active speaker video: 74 | - `setupPictureInPictureMain`: Runs in main process, handles IPC requests and executes PiP with userGesture privileges 75 | - `setupPictureInPictureRender`: Runs in renderer, listens for `_pipRequested` events from Jitsi Meet iframe API 76 | - Uses IPC channel `jitsi-pip-channel` for communication between processes 77 | - Bypasses transient activation requirements by executing PiP from main process 78 | 79 | #### Power Monitor (`powermonitor/`) 80 | Queries system idle state and power events: 81 | - Bridges Electron's powerMonitor API to Jitsi Meet iframe 82 | - Used for presence detection and power-saving features 83 | 84 | #### Popups Config (`popupsconfig/`) 85 | Registry pattern for managing popup window configurations: 86 | - `PopupsConfigRegistry`: Singleton storing popup configs by name 87 | - Decouples popup configuration from popup handling logic 88 | 89 | ### Native Code 90 | - **Windows only**: `node_addons/sourceId2Coordinates/` contains C++ code (N-API) for remote control 91 | - Built with node-gyp, configured in `binding.gyp` 92 | - Prebuilt binaries generated via prebuildify and included in npm package 93 | 94 | ### Dependencies 95 | - `@jitsi/robotjs`: Fork of robotjs for remote control functionality 96 | - `electron-store`: Persistent storage 97 | - `postis`: Cross-origin iframe messaging 98 | - `@jitsi/logger`: Logging utility 99 | 100 | ## Code Style 101 | 102 | ### ESLint Configuration 103 | Follow the rules in `.eslintrc.js`: 104 | - ECMAScript 9 syntax 105 | - Semicolons required 106 | - `new-cap` enforced (except capIsNew) 107 | - Console statements allowed (stripped before production) 108 | - JSDoc plugin enabled 109 | 110 | ### JSDoc Requirements 111 | All exports and class members require JSDoc comments explaining purpose and parameters. 112 | 113 | ## Testing 114 | 115 | Tests are located in `test/` and run with mocha. Currently covers: 116 | - `sourceId2Coordinates.test.js`: Native addon functionality (Windows) 117 | 118 | ## Integration 119 | 120 | Apps using this SDK must: 121 | 1. Initialize Jitsi Meet via iframe API 122 | 2. Set up main process components before renderer components 123 | 3. Add `'disable-site-isolation-trials'` switch (see [Electron issue #18214](https://github.com/electron/electron/issues/18214)) 124 | 4. Clean up handlers on window close to prevent leaks 125 | 126 | Example integrations: [jitsi-meet-electron](https://github.com/jitsi/jitsi-meet-electron) 127 | 128 | ## Publishing 129 | 130 | Automated via GitHub Actions: 131 | - Every push to `master` triggers version bump and npm publish 132 | - Prebuilt native addons from all Windows platforms are downloaded and bundled 133 | - Published with npm provenance for supply chain security 134 | -------------------------------------------------------------------------------- /screensharing/render.js: -------------------------------------------------------------------------------- 1 | 2 | const { ipcRenderer } = require('electron'); 3 | 4 | const { SCREEN_SHARE_EVENTS_CHANNEL, SCREEN_SHARE_EVENTS, SCREEN_SHARE_GET_SOURCES } = require('./constants'); 5 | const { logWarning, setLogger } = require('./utils'); 6 | 7 | /** 8 | * Renderer process component that sets up electron specific screen sharing functionality, like screen sharing 9 | * marker and window selection. 10 | * {@link ScreenShareMainHook} needs to be initialized in the main process for the always on top tracker window 11 | * to work. 12 | */ 13 | class ScreenShareRenderHook { 14 | /** 15 | * Creates a ScreenShareRenderHook hooked to jitsi meet iframe events. 16 | * 17 | * @param {JitsiIFrameApi} api - The Jitsi Meet iframe api object. 18 | */ 19 | constructor(api) { 20 | this._api = api; 21 | 22 | this._onScreenSharingStatusChanged = this._onScreenSharingStatusChanged.bind(this); 23 | this._sendCloseTrackerEvent = this._sendCloseTrackerEvent.bind(this); 24 | this._onScreenSharingEvent = this._onScreenSharingEvent.bind(this); 25 | this._onRequestDesktopSources = this._onRequestDesktopSources.bind(this); 26 | this._onApiDispose = this._onApiDispose.bind(this); 27 | 28 | ipcRenderer.on(SCREEN_SHARE_EVENTS_CHANNEL, this._onScreenSharingEvent); 29 | 30 | this._api.on('screenSharingStatusChanged', this._onScreenSharingStatusChanged); 31 | this._api.on('videoConferenceLeft', this._sendCloseTrackerEvent); 32 | this._api.on('_requestDesktopSources', this._onRequestDesktopSources); 33 | this._api.on('_willDispose', this._onApiDispose); 34 | } 35 | 36 | /** 37 | * Handle requests for desktop sources. 38 | * @param {Object} request - Request object from Electron. 39 | * @param {Function} callback - Callback to be invoked with the sources or error. 40 | * 41 | * @returns {void} 42 | */ 43 | _onRequestDesktopSources(request, callback) { 44 | const { options } = request; 45 | 46 | ipcRenderer.invoke(SCREEN_SHARE_GET_SOURCES, options) 47 | .then(sources => { 48 | sources.forEach(item => { 49 | item.thumbnail.dataUrl = item.thumbnail.toDataURL(); 50 | }); 51 | callback({ sources }); 52 | }) 53 | .catch((error) => callback({ error })); 54 | } 55 | 56 | /** 57 | * Listen for events coming on the screen sharing event channel. 58 | * 59 | * @param {Object} event - Electron event data. 60 | * @param {Object} data - Channel specific data. 61 | * 62 | * @returns {void} 63 | */ 64 | _onScreenSharingEvent(event, { data }) { 65 | switch (data.name) { 66 | // Event send by the screen sharing tracker window when a user stops screen sharing from it. 67 | // Send appropriate command to jitsi meet api. 68 | case SCREEN_SHARE_EVENTS.STOP_SCREEN_SHARE: 69 | if (this._isScreenSharing) { 70 | this._api.executeCommand('toggleShareScreen'); 71 | } 72 | break; 73 | case SCREEN_SHARE_EVENTS.OPEN_PICKER: { 74 | // Store requestId to match response with request 75 | const { requestId } = data; 76 | 77 | this._api._openDesktopPicker().then(r => { 78 | ipcRenderer.send(SCREEN_SHARE_EVENTS_CHANNEL, { 79 | data: { 80 | name: SCREEN_SHARE_EVENTS.DO_GDM, 81 | requestId, 82 | ...r 83 | } 84 | }); 85 | }).catch(error => { 86 | // If picker fails, still send DO_GDM with requestId to complete the flow 87 | logWarning(`Desktop picker error: ${error}`); 88 | ipcRenderer.send(SCREEN_SHARE_EVENTS_CHANNEL, { 89 | data: { 90 | name: SCREEN_SHARE_EVENTS.DO_GDM, 91 | requestId, 92 | source: null 93 | } 94 | }); 95 | }); 96 | break; 97 | } 98 | default: 99 | logWarning(`Unhandled ${SCREEN_SHARE_EVENTS_CHANNEL}: ${data}`); 100 | 101 | } 102 | } 103 | 104 | /** 105 | * React to screen sharing events coming from the jitsi meet api. There should be 106 | * a {@link ScreenShareMainHook} listening on the main process for the forwarded events. 107 | * 108 | * @param {Object} event 109 | * 110 | * @returns {void} 111 | */ 112 | _onScreenSharingStatusChanged(event) { 113 | if (event.on) { 114 | this._isScreenSharing = true; 115 | // Send event which should open an always on top tracker window from the main process. 116 | ipcRenderer.send(SCREEN_SHARE_EVENTS_CHANNEL, { 117 | data: { 118 | name: SCREEN_SHARE_EVENTS.OPEN_TRACKER 119 | } 120 | }); 121 | } else { 122 | this._isScreenSharing = false; 123 | this._sendCloseTrackerEvent(); 124 | } 125 | } 126 | 127 | /** 128 | * Send event which should close the always on top tracker window. 129 | * 130 | * @return {void} 131 | */ 132 | _sendCloseTrackerEvent() { 133 | ipcRenderer.send(SCREEN_SHARE_EVENTS_CHANNEL, { 134 | data: { 135 | name: SCREEN_SHARE_EVENTS.CLOSE_TRACKER 136 | } 137 | }); 138 | } 139 | 140 | /** 141 | * Clear all event handlers in order to avoid any potential leaks. 142 | * 143 | * @returns {void} 144 | */ 145 | _onApiDispose() { 146 | ipcRenderer.removeListener(SCREEN_SHARE_EVENTS_CHANNEL, this._onScreenSharingEvent); 147 | this._sendCloseTrackerEvent(); 148 | 149 | this._api.removeListener('screenSharingStatusChanged', this._onScreenSharingStatusChanged); 150 | this._api.removeListener('videoConferenceLeft', this._sendCloseTrackerEvent); 151 | this._api.removeListener('_requestDesktopSources', this._onRequestDesktopSources); 152 | this._api.removeListener('_willDispose', this._onApiDispose); 153 | } 154 | } 155 | 156 | /** 157 | * Initializes the screen sharing electron specific functionality in the renderer process containing the 158 | * jitsi meet iframe. 159 | * 160 | * @param {JitsiIFrameApi} api - The Jitsi Meet iframe api object. 161 | */ 162 | module.exports = function setupScreenSharingRender(api, loggerTransports = null) { 163 | setLogger(loggerTransports); 164 | 165 | return new ScreenShareRenderHook(api); 166 | }; 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jitsi Meet Electron SDK 2 | 3 | SDK for integrating Jitsi Meet into Electron applications. 4 | 5 | ## Installation 6 | 7 | Install from npm: 8 | 9 | npm install @jitsi/electron-sdk 10 | 11 | Note: This package contains native code on Windows for the remote control module. Binary prebuilds are packaged with prebuildify as part of the npm package. 12 | 13 | ## Usage 14 | #### Remote Control 15 | 16 | **Requirements**: 17 | 1. Jitsi Meet should be initialized through our [iframe API](https://github.com/jitsi/jitsi-meet/blob/master/doc/api.md) 18 | 2. The remote control utility requires the Jitsi Meet iframe API object. 19 | 20 | **Enable the remote control:** 21 | 22 | In the **render** electron process of the window where Jitsi Meet is displayed: 23 | 24 | ```Javascript 25 | const { 26 | setupRemoteControlRender 27 | } = require("@jitsi/electron-sdk"); 28 | 29 | // api - The Jitsi Meet iframe api object. 30 | const remoteControl = setupRemoteControlRender(api); 31 | ``` 32 | 33 | To disable the remote control: 34 | ```Javascript 35 | remoteControl.dispose(); 36 | ``` 37 | 38 | NOTE: `dispose` method will be called automatically when the Jitsi Meet API `readyToClose` event or when the `dispose` method of the Jitsi Meet iframe API object. 39 | 40 | In the **main** electron process: 41 | 42 | ```Javascript 43 | const { 44 | setupRemoteControlMain 45 | } = require("@jitsi/electron-sdk"); 46 | 47 | // jitsiMeetWindow - The BrowserWindow instance of the window where Jitsi Meet is loaded. 48 | setupRemoteControlMain(mainWindow); 49 | ``` 50 | 51 | #### Screen Sharing 52 | 53 | **Requirements**: 54 | The screen sharing utility requires iframe HTML Element that will load Jitsi Meet. 55 | 56 | **Enable the screen sharing:** 57 | 58 | In the **render** electron process of the window where Jitsi Meet is displayed: 59 | 60 | ```Javascript 61 | const { 62 | setupScreenSharingRender 63 | } = require("@jitsi/electron-sdk"); 64 | 65 | // api - The Jitsi Meet iframe api object. 66 | setupScreenSharingRender(api); 67 | ``` 68 | In the **main** electron process: 69 | 70 | ```Javascript 71 | const { 72 | setupScreenSharingMain 73 | } = require("@jitsi/electron-sdk"); 74 | 75 | // jitsiMeetWindow - The BrowserWindow instance of the window where Jitsi Meet is loaded. 76 | // appName - Application name which will be displayed inside the content sharing tracking window 77 | // i.e. [appName] is sharing your screen. 78 | // osxBundleId - Mac Application bundleId for which screen capturer permissions will be reset if user denied them. 79 | setupScreenSharingMain(mainWindow, appName, osxBundleId); 80 | ``` 81 | 82 | #### Picture in Picture 83 | 84 | Enables the browser's native picture-in-picture functionality for the active speaker video. This allows users to keep the active speaker video visible in a floating window while using other applications. 85 | 86 | **Requirements**: 87 | 1. Jitsi Meet should be initialized through the [iframe API](https://github.com/jitsi/jitsi-meet/blob/master/doc/api.md) 88 | 2. The feature requires Electron's main process to execute the PiP request with userGesture privileges to bypass browser security restrictions 89 | 90 | **Enable picture in picture:** 91 | 92 | In the **main** electron process: 93 | 94 | ```Javascript 95 | const { 96 | setupPictureInPictureMain 97 | } = require("@jitsi/electron-sdk"); 98 | 99 | // jitsiMeetWindow - The BrowserWindow instance where Jitsi Meet is loaded. 100 | // loggerTransports - Optional array of logger transports for configuring the logger. 101 | const pipMain = setupPictureInPictureMain(jitsiMeetWindow, loggerTransports); 102 | ``` 103 | 104 | In the **render** electron process of the window where Jitsi Meet is displayed: 105 | 106 | ```Javascript 107 | const { 108 | setupPictureInPictureRender 109 | } = require("@jitsi/electron-sdk"); 110 | 111 | const api = new JitsiMeetExternalAPI(...); 112 | const pipRender = setupPictureInPictureRender(api); 113 | ``` 114 | 115 | #### Popups Configuration 116 | 117 | Configures handling of popup windows for OAuth authentication flows (Google, Dropbox). This module sets up a `setWindowOpenHandler` on the Jitsi Meet window to allow OAuth popups while delegating other window.open requests to a custom handler provided by the host application. 118 | 119 | **Enable popup configuration:** 120 | 121 | In the **main** electron process: 122 | 123 | ```Javascript 124 | const { 125 | initPopupsConfigurationMain 126 | } = require("@jitsi/electron-sdk"); 127 | 128 | // jitsiMeetWindow - The BrowserWindow instance where Jitsi Meet is loaded. 129 | // OAuth popups (Google, Dropbox) will be allowed, all other window.open requests will be denied. 130 | initPopupsConfigurationMain(jitsiMeetWindow); 131 | ``` 132 | 133 | **With a custom window open handler:** 134 | 135 | If your application needs to handle other window.open requests (e.g., opening external links in the default browser), pass your handler as the second parameter: 136 | 137 | ```Javascript 138 | const { shell } = require('electron'); 139 | const { 140 | initPopupsConfigurationMain 141 | } = require("@jitsi/electron-sdk"); 142 | 143 | // Define how to handle non-OAuth window.open requests 144 | const windowOpenHandler = ({ url }) => { 145 | // Open external links in the default browser 146 | shell.openExternal(url); 147 | return { action: 'deny' }; 148 | }; 149 | 150 | // jitsiMeetWindow - The BrowserWindow instance where Jitsi Meet is loaded. 151 | // windowOpenHandler - Called for window.open requests that are not OAuth popups. 152 | initPopupsConfigurationMain(jitsiMeetWindow, windowOpenHandler); 153 | ``` 154 | 155 | The SDK handles OAuth popups (Google, Dropbox) internally, allowing them to open in Electron windows with secure settings. All other window.open requests are passed to your handler, which typically opens them in the system's default browser. 156 | 157 | #### Power Monitor 158 | 159 | Provides a way to query electron for system idle and receive power monitor events. 160 | 161 | **enable power monitor:** 162 | In the **main** electron process: 163 | ```Javascript 164 | const { 165 | setupPowerMonitorMain 166 | } = require("@jitsi/electron-sdk"); 167 | 168 | // jitsiMeetWindow - The BrowserWindow instance 169 | // of the window where Jitsi Meet is loaded. 170 | setupPowerMonitorMain(jitsiMeetWindow); 171 | ``` 172 | 173 | In the **render** electron process of the window where Jitsi Meet is displayed: 174 | ```Javascript 175 | const { 176 | setupPowerMonitorRender 177 | } = require("@jitsi/electron-sdk"); 178 | 179 | const api = new JitsiMeetExternalAPI(...); 180 | setupPowerMonitorRender(api); 181 | ``` 182 | 183 | ## Example 184 | 185 | For examples of installation and usage checkout the [Jitsi Meet Electron](https://github.com/jitsi/jitsi-meet-electron) project. 186 | 187 | ## Development 188 | 189 | Enable husky to avoid accidental pushes to the main branch: 190 | 191 | npx husky install 192 | 193 | To rebuild the native code, use: 194 | 195 | npx node-gyp rebuild 196 | 197 | ## Publishing 198 | 199 | On every push to main branch, the .github/workflows/ci.yml will create a new version and publish to npm. 200 | 201 | If a major or minor release is required, use respective key words in the commit message, see https://github.com/phips28/gh-action-bump-version#workflow 202 | -------------------------------------------------------------------------------- /remotecontrol/render.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | const os = require('os'); 3 | const postis = require("postis"); 4 | const constants = require("./constants"); 5 | const robot = require("@jitsi/robotjs"); 6 | 7 | const { 8 | EVENTS, 9 | KEY_ACTIONS_FROM_EVENT_TYPE, 10 | MOUSE_ACTIONS_FROM_EVENT_TYPE, 11 | MOUSE_BUTTONS, 12 | REMOTE_CONTROL_MESSAGE_NAME, 13 | REQUESTS 14 | } = constants; 15 | 16 | /** 17 | * Parses the remote control events and executes them via robotjs. 18 | * {@link RemoteControlMain} needs to be initialized in the main process. 19 | * to work. 20 | */ 21 | class RemoteControl { 22 | /** 23 | * Constructs new instance and initializes the remote control functionality. 24 | * 25 | * @param {JitsiIFrameApi} api - The Jitsi Meet iframe api object. 26 | */ 27 | constructor(api) { 28 | this._api = api; 29 | this._iframe = this._api.getIFrame(); 30 | this._iframe.addEventListener('load', () => this._onIFrameLoad()); 31 | /** 32 | * The status ("up"/"down") of the mouse button. 33 | * FIXME: Assuming that one button at a time can be pressed. Haven't 34 | * noticed any issues but maybe we should store the status for every 35 | * mouse button that we are processing. 36 | */ 37 | this._mouseButtonStatus = "up"; 38 | } 39 | 40 | /** 41 | * Disposes the remote control functionality. 42 | */ 43 | dispose() { 44 | if(this._channel) { 45 | this._channel.destroy(); 46 | this._channel = null; 47 | } 48 | this._stop(); 49 | } 50 | 51 | /** 52 | * Returns the scale factor for the current display used to calculate the resolution of the display. 53 | * 54 | * NOTE: On Mac OS this._display.scaleFactor will always be 2 for some reason. But the values returned from 55 | * this._display.bounds will already take into account the scale factor. That's why we are returning 1 for Mac OS. 56 | * 57 | * @returns {number} The scale factor. 58 | */ 59 | _getDisplayScaleFactor() { 60 | return os.type() === 'Darwin' ? 1 : this._display.scaleFactor || 1; 61 | } 62 | 63 | /** 64 | * Sets the display metrics(x, y, width, height, scaleFactor, etc...) of the display that will be used for the 65 | * remote control. 66 | * 67 | * @param {string} sourceId - The source id of the desktop sharing stream. 68 | * @returns {void} 69 | */ 70 | _setDisplayMetrics(sourceId) { 71 | this._display = ipcRenderer.sendSync('jitsi-remotecontrol-get-display', sourceId); 72 | } 73 | 74 | /** 75 | * Handles remote control start messages. 76 | * 77 | * @param {number} id - the id of the request that will be used for the 78 | * response. 79 | * @param {string} sourceId - The source id of the desktop sharing stream. 80 | */ 81 | _start(id, sourceId) { 82 | this._displayMetricsChangeListener = () => { 83 | this._setDisplayMetrics(sourceId); 84 | }; 85 | ipcRenderer.on('jitsi-remotecontrol-displays-changed', this._displayMetricsChangeListener); 86 | this._setDisplayMetrics(sourceId); 87 | 88 | const response = { 89 | id, 90 | type: 'response' 91 | }; 92 | 93 | if(this._display) { 94 | response.result = true; 95 | } else { 96 | response.error 97 | = 'Error: Can\'t detect the display that is currently shared'; 98 | } 99 | 100 | this._sendMessage(response); 101 | } 102 | 103 | /** 104 | * Stops processing the events. 105 | */ 106 | _stop() { 107 | this._display = undefined; 108 | if (this._displayMetricsChangeListener) { 109 | ipcRenderer.removeListener('jitsi-remotecontrol-displays-changed', this._displayMetricsChangeListener); 110 | this._displayMetricsChangeListener = undefined; 111 | } 112 | } 113 | 114 | /** 115 | * Handles iframe load events. 116 | */ 117 | _onIFrameLoad() { 118 | this._api.on('_willDispose', () => { 119 | this.dispose(); 120 | }); 121 | this._api.on('readyToClose', () => { 122 | this.dispose(); 123 | }); 124 | this._channel = postis({ 125 | window: this._iframe.contentWindow, 126 | windowForEventListening: window, 127 | scope: 'jitsi-remote-control' 128 | }); 129 | this._channel.ready(() => { 130 | this._channel.listen('message', message => { 131 | const { name } = message.data; 132 | if(name === REMOTE_CONTROL_MESSAGE_NAME) { 133 | this._onRemoteControlMessage(message); 134 | } 135 | }); 136 | this._sendEvent({ type: EVENTS.supported }); 137 | }); 138 | } 139 | 140 | /** 141 | * Executes the passed message. 142 | * @param {Object} message the remote control message. 143 | */ 144 | _onRemoteControlMessage(message) { 145 | const { id, data } = message; 146 | 147 | // If we haven't set the display prop. We haven't received the remote 148 | // control start message or there was an error associating a display. 149 | if(!this._display 150 | && data.type != REQUESTS.start) { 151 | return; 152 | } 153 | switch(data.type) { 154 | case EVENTS.mousemove: { 155 | const { width, height, x, y } = this._display.bounds; 156 | const scaleFactor = this._getDisplayScaleFactor(); 157 | const destX = data.x * width * scaleFactor + x; 158 | const destY = data.y * height * scaleFactor + y; 159 | if(this._mouseButtonStatus === "down") { 160 | robot.dragMouse(destX, destY); 161 | } else { 162 | robot.moveMouse(destX, destY); 163 | } 164 | break; 165 | } 166 | case EVENTS.mousedown: 167 | case EVENTS.mouseup: { 168 | this._mouseButtonStatus 169 | = MOUSE_ACTIONS_FROM_EVENT_TYPE[data.type]; 170 | robot.mouseToggle( 171 | this._mouseButtonStatus, 172 | (data.button 173 | ? MOUSE_BUTTONS[data.button] : undefined)); 174 | break; 175 | } 176 | case EVENTS.mousedblclick: { 177 | robot.mouseClick( 178 | (data.button 179 | ? MOUSE_BUTTONS[data.button] : undefined), 180 | true); 181 | break; 182 | } 183 | case EVENTS.mousescroll:{ 184 | const { x, y } = data; 185 | if(x !== 0 || y !== 0) { 186 | robot.scrollMouse(x, y); 187 | } 188 | break; 189 | } 190 | case EVENTS.keydown: 191 | case EVENTS.keyup: { 192 | if (data.key) { 193 | robot.keyToggle( 194 | data.key === 'caps_lock' ? 'capslock' : data.key, 195 | KEY_ACTIONS_FROM_EVENT_TYPE[data.type], 196 | data.modifiers); 197 | } 198 | break; 199 | } 200 | case REQUESTS.start: { 201 | this._start(id, data.sourceId); 202 | break; 203 | } 204 | case EVENTS.stop: { 205 | this._stop(); 206 | break; 207 | } 208 | default: 209 | console.error("Unknown event type!"); 210 | } 211 | } 212 | 213 | /** 214 | * Sends remote control event to the controlled participant. 215 | * 216 | * @param {Object} event the remote control event. 217 | */ 218 | _sendEvent(event) { 219 | const remoteControlEvent = Object.assign( 220 | { name: REMOTE_CONTROL_MESSAGE_NAME }, 221 | event 222 | ); 223 | this._sendMessage({ data: remoteControlEvent }); 224 | } 225 | 226 | /** 227 | * Sends a message to Jitsi Meet. 228 | * 229 | * @param {Object} message the message to be sent. 230 | */ 231 | _sendMessage(message) { 232 | this._channel.send({ 233 | method: 'message', 234 | params: message 235 | }); 236 | } 237 | } 238 | 239 | /** 240 | * Initializes the remote control functionality in the render process of the 241 | * window which displays Jitsi Meet. 242 | * 243 | * @param {JitsiIFrameApi} api - the Jitsi Meet iframe api object. 244 | * @returns {RemoteControl} - the remote control object. 245 | */ 246 | module.exports = function setupRemoteControlRender(api) { 247 | return new RemoteControl(api); 248 | }; 249 | 250 | -------------------------------------------------------------------------------- /screensharing/main.js: -------------------------------------------------------------------------------- 1 | /* global __dirname */ 2 | const { exec } = require('child_process'); 3 | const { 4 | BrowserWindow, 5 | desktopCapturer, 6 | ipcMain, 7 | screen, 8 | systemPreferences 9 | } = require('electron'); 10 | const os = require('os'); 11 | const path = require('path'); 12 | 13 | const { SCREEN_SHARE_EVENTS_CHANNEL, SCREEN_SHARE_EVENTS, SCREEN_SHARE_GET_SOURCES, TRACKER_SIZE } = require('./constants'); 14 | const { isMac, isWayland } = require('./utils'); 15 | const { windowsEnableScreenProtection } = require('../helpers/functions'); 16 | 17 | /** 18 | * Main process component that sets up electron specific screen sharing functionality, like screen sharing 19 | * tracker and window selection. 20 | * The class will process events from {@link ScreenShareRenderHook} initialized in the renderer, and the 21 | * always on top screen sharing tracker window. 22 | */ 23 | class ScreenShareMainHook { 24 | /** 25 | * Create ScreenShareMainHook linked to jitsiMeetWindow. 26 | * 27 | * @param {BrowserWindow} jitsiMeetWindow - BrowserWindow where jitsi-meet api is loaded. 28 | * @param {string} identity - Name of the application doing screen sharing, will be displayed in the 29 | * screen sharing tracker window text i.e. {identity} is sharing your screen. 30 | */ 31 | constructor(jitsiMeetWindow, identity, osxBundleId) { 32 | this._jitsiMeetWindow = jitsiMeetWindow; 33 | this._identity = identity; 34 | this._onScreenSharingEvent = this._onScreenSharingEvent.bind(this); 35 | this._gdmRequestId = 0; 36 | this._pendingGdmRequests = new Map(); 37 | 38 | this.cleanup = this.cleanup.bind(this); 39 | 40 | if (osxBundleId && isMac()) { 41 | this._verifyScreenCapturePermissions(osxBundleId); 42 | } 43 | 44 | // Handle getDisplayMedia requests. 45 | jitsiMeetWindow.webContents.session.setDisplayMediaRequestHandler((request, callback) => { 46 | // On Wayland the native picker will show up and will resolve to what the user selected, so there 47 | // is no need to use the Jitsi picker. 48 | if (isWayland()) { 49 | const options = { 50 | types: ['screen', 'window'] 51 | }; 52 | 53 | desktopCapturer.getSources(options).then(sources => { 54 | const source = sources[0]; 55 | 56 | if (source) { 57 | callback({ video: source }); 58 | } else { 59 | callback(null); 60 | } 61 | }); 62 | } else { 63 | // Generate unique ID for this request to handle multiple simultaneous requests 64 | const requestId = ++this._gdmRequestId; 65 | 66 | this._pendingGdmRequests.set(requestId, { 67 | request, 68 | callback 69 | }); 70 | 71 | const ev = { 72 | data: { 73 | name: SCREEN_SHARE_EVENTS.OPEN_PICKER, 74 | requestId 75 | } 76 | }; 77 | 78 | this._jitsiMeetWindow.webContents.send(SCREEN_SHARE_EVENTS_CHANNEL, ev); 79 | } 80 | }, { useSystemPicker: false /* TODO: enable this when not experimental. It's macOS >= 15 only for now. */ } 81 | ); 82 | 83 | // Listen for events coming in from the main render window and the screen share tracker window. 84 | ipcMain.on(SCREEN_SHARE_EVENTS_CHANNEL, this._onScreenSharingEvent); 85 | ipcMain.handle(SCREEN_SHARE_GET_SOURCES, (_event, opts) => desktopCapturer.getSources(opts)); 86 | 87 | // Clean up ipcMain handlers to avoid leaks. 88 | this._jitsiMeetWindow.on('closed', this.cleanup); 89 | } 90 | 91 | /** 92 | * Cleanup any handlers 93 | */ 94 | cleanup() { 95 | // Reject all pending getDisplayMedia requests 96 | this._pendingGdmRequests.forEach((gdmData, requestId) => { 97 | console.warn(`[screensharing] Cleaning up pending request ${requestId}`); 98 | gdmData.callback(null); 99 | }); 100 | this._pendingGdmRequests.clear(); 101 | 102 | ipcMain.removeListener(SCREEN_SHARE_EVENTS_CHANNEL, this._onScreenSharingEvent); 103 | ipcMain.removeHandler(SCREEN_SHARE_GET_SOURCES); 104 | } 105 | 106 | /** 107 | * Listen for events coming on the screen sharing event channel. 108 | * 109 | * @param {Object} event - Electron event data. 110 | * @param {Object} data - Channel specific data. 111 | */ 112 | _onScreenSharingEvent(event, { data }) { 113 | switch (data.name) { 114 | case SCREEN_SHARE_EVENTS.OPEN_TRACKER: 115 | this._createScreenShareTracker(); 116 | break; 117 | case SCREEN_SHARE_EVENTS.CLOSE_TRACKER: 118 | if (this._screenShareTracker) { 119 | this._screenShareTracker.close(); 120 | this._screenShareTracker = undefined; 121 | } 122 | break; 123 | case SCREEN_SHARE_EVENTS.HIDE_TRACKER: 124 | if (this._screenShareTracker) { 125 | this._screenShareTracker.minimize(); 126 | } 127 | break; 128 | case SCREEN_SHARE_EVENTS.STOP_SCREEN_SHARE: 129 | this._jitsiMeetWindow.webContents.send(SCREEN_SHARE_EVENTS_CHANNEL, { data }); 130 | break; 131 | case SCREEN_SHARE_EVENTS.DO_GDM: { 132 | const { requestId } = data; 133 | 134 | if (!requestId || !this._pendingGdmRequests.has(requestId)) { 135 | console.warn(`[screensharing] DO_GDM received for unknown/expired requestId: ${requestId}`); 136 | break; 137 | } 138 | 139 | const { callback } = this._pendingGdmRequests.get(requestId); 140 | this._pendingGdmRequests.delete(requestId); 141 | 142 | if (!data.source) { 143 | callback(null); 144 | break; 145 | } 146 | 147 | const constraints = { 148 | video: data.source 149 | }; 150 | 151 | // Setting `audio` to `undefined` throws an exception. 152 | if (data.screenShareAudio) { 153 | // TODO: maybe make this configurable somehow in case 154 | // someone wants to use `loopbackWithMute`? 155 | constraints.audio = 'loopback'; 156 | } 157 | 158 | callback(constraints); 159 | 160 | break; 161 | } 162 | default: 163 | console.warn(`Unhandled ${SCREEN_SHARE_EVENTS_CHANNEL}: ${data}`); 164 | } 165 | } 166 | 167 | /** 168 | * Opens an always on top window, in the bottom center of the screen, that lets a user know 169 | * a content sharing session is currently active. 170 | * 171 | * @return {void} 172 | */ 173 | _createScreenShareTracker() { 174 | if (this._screenShareTracker) { 175 | return; 176 | } 177 | 178 | // Display always on top screen sharing tracker window in the center bottom of the screen. 179 | const display = screen.getPrimaryDisplay(); 180 | 181 | this._screenShareTracker = new BrowserWindow({ 182 | height: TRACKER_SIZE.height, 183 | width: TRACKER_SIZE.width, 184 | x: (display.workArea.width - TRACKER_SIZE.width) / 2, 185 | y: display.workArea.height - TRACKER_SIZE.height - 5, 186 | transparent: true, 187 | minimizable: true, 188 | maximizable: false, 189 | resizable: false, 190 | alwaysOnTop: true, 191 | fullscreen: false, 192 | fullscreenable: false, 193 | skipTaskbar: false, 194 | frame: false, 195 | show: false, 196 | webPreferences: { 197 | contextIsolation: true, 198 | nodeIntegration: false, 199 | preload: path.resolve(__dirname, './preload.js'), 200 | sandbox: false 201 | } 202 | }); 203 | 204 | // for Windows OS, only enable protection for builds higher or equal to Windows 10 Version 2004 205 | // which have the flag WDA_EXCLUDEFROMCAPTURE(which makes the window completely invisible on capture) 206 | // For older Windows versions, we leave the window completely visible, including content, on capture, 207 | // otherwise we'll have a black content window on share. 208 | if (os.platform() !== 'win32' || windowsEnableScreenProtection(os.release())) { 209 | // Avoid this window from being captured. 210 | this._screenShareTracker.setContentProtection(true); 211 | } 212 | 213 | this._screenShareTracker.on('closed', () => { 214 | this._screenShareTracker = undefined; 215 | }); 216 | 217 | // Prevent newly created window to take focus from main application. 218 | this._screenShareTracker.once('ready-to-show', () => { 219 | if (this._screenShareTracker && !this._screenShareTracker.isDestroyed()) { 220 | this._screenShareTracker.showInactive(); 221 | } 222 | }); 223 | 224 | this._screenShareTracker 225 | .loadURL(`file://${__dirname}/screenSharingTracker.html?sharingIdentity=${this._identity}`); 226 | } 227 | 228 | /** 229 | * Verifies whether app has already asked for capture permissions. 230 | * If it did but the user denied, resets permissions for the app 231 | * 232 | * @param {string} bundleId- OSX Application BundleId 233 | */ 234 | _verifyScreenCapturePermissions(bundleId) { 235 | const hasPermission = systemPreferences.getMediaAccessStatus('screen') === 'granted'; 236 | if (!hasPermission) { 237 | exec('tccutil reset ScreenCapture ' + bundleId); 238 | } 239 | } 240 | } 241 | 242 | /** 243 | * Initializes the screen sharing electron specific functionality in the main electron process. 244 | * 245 | * @param {BrowserWindow} jitsiMeetWindow - the BrowserWindow object which displays Jitsi Meet 246 | * @param {string} identity - Name of the application doing screen sharing, will be displayed in the 247 | * screen sharing tracker window text i.e. {identity} is sharing your screen. 248 | * @param {string} bundleId- OSX Application BundleId 249 | */ 250 | module.exports = function setupScreenSharingMain(jitsiMeetWindow, identity, osxBundleId) { 251 | return new ScreenShareMainHook(jitsiMeetWindow, identity, osxBundleId); 252 | }; 253 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | 205 | 206 | Note: 207 | 208 | This project was originally contributed to the community under the MIT license and with the following notice: 209 | 210 | The MIT License (MIT) 211 | 212 | Copyright (c) 2013 ESTOS GmbH 213 | Copyright (c) 2013 BlueJimp SARL 214 | 215 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 216 | 217 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 218 | 219 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 220 | --------------------------------------------------------------------------------