feat(screenshot-capture) Updated screensharing screenshot capture

Changed screen capture to non effect. Effects are used to alter the stream, this feature does not need to alter the stream, it just needs access to it

Changed image diff library. Previous library diff’ed the whole image, the new one has en early return threshold

Use ImageCaptureAPI to take the screenshot. Added polyfill for it and polyfill for createImageBitmap

Added analytics
This commit is contained in:
robertpin 2021-07-26 14:38:56 +03:00 committed by Saúl Ibarra Corretgé
parent 7c86ece9fa
commit 001ae54a7c
14 changed files with 642 additions and 237 deletions

View File

@ -134,7 +134,7 @@ import {
} from './react/features/prejoin';
import { disableReceiver, stopReceiver } from './react/features/remote-control';
import { setScreenAudioShareState, isScreenAudioShared } from './react/features/screen-share/';
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
import { toggleScreenshotCaptureSummary } from './react/features/screenshot-capture';
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
import { createPresenterEffect } from './react/features/stream-effects/presenter';
import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise';
@ -1545,8 +1545,9 @@ export default {
APP.store.dispatch(stopReceiver());
this._stopProxyConnection();
if (config.enableScreenshotCapture) {
APP.store.dispatch(toggleScreenshotCaptureEffect(false));
APP.store.dispatch(toggleScreenshotCaptureSummary(false));
}
// It can happen that presenter GUM is in progress while screensharing is being turned off. Here it needs to
@ -1924,7 +1925,7 @@ export default {
.then(() => {
this.videoSwitchInProgress = false;
if (config.enableScreenshotCapture) {
APP.store.dispatch(toggleScreenshotCaptureEffect(true));
APP.store.dispatch(toggleScreenshotCaptureSummary(true));
}
sendAnalytics(createScreenSharingEvent('started'));
logger.log('Screen sharing started');

408
package-lock.json generated
View File

@ -2774,6 +2774,152 @@
"sdp-transform": "2.3.0"
}
},
"@mapbox/node-pre-gyp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
"integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==",
"optional": true,
"requires": {
"detect-libc": "^1.0.3",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.1",
"nopt": "^5.0.0",
"npmlog": "^4.1.2",
"rimraf": "^3.0.2",
"semver": "^7.3.4",
"tar": "^6.1.0"
},
"dependencies": {
"chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"optional": true
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"optional": true,
"requires": {
"minipass": "^3.0.0"
}
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"optional": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"optional": true,
"requires": {
"yallist": "^4.0.0"
}
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"optional": true,
"requires": {
"semver": "^6.0.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"optional": true
}
}
},
"minipass": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz",
"integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==",
"optional": true,
"requires": {
"yallist": "^4.0.0"
}
},
"minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"optional": true,
"requires": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
}
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"optional": true
},
"node-fetch": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz",
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==",
"optional": true,
"requires": {
"whatwg-url": "^5.0.0"
}
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"optional": true,
"requires": {
"glob": "^7.1.3"
}
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"optional": true,
"requires": {
"lru-cache": "^6.0.0"
}
},
"tar": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
"integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
"optional": true,
"requires": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^3.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"optional": true
}
}
},
"@material-ui/core": {
"version": "4.11.3",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.3.tgz",
@ -4400,6 +4546,12 @@
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
"dev": true
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"optional": true
},
"abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@ -4447,6 +4599,32 @@
"integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==",
"dev": true
},
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"optional": true,
"requires": {
"debug": "4"
},
"dependencies": {
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"optional": true,
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"optional": true
}
}
},
"ajv": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
@ -4867,8 +5045,17 @@
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"are-we-there-yet": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
"integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
"optional": true,
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
}
},
"argparse": {
"version": "1.0.10",
@ -6135,6 +6322,17 @@
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz",
"integrity": "sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw=="
},
"canvas": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.8.0.tgz",
"integrity": "sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q==",
"optional": true,
"requires": {
"@mapbox/node-pre-gyp": "^1.0.0",
"nan": "^2.14.0",
"simple-get": "^3.0.3"
}
},
"capture-exit": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz",
@ -6670,6 +6868,12 @@
"integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==",
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
},
"constants-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
@ -7202,6 +7406,15 @@
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"decompress-response": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
"optional": true,
"requires": {
"mimic-response": "^2.0.0"
}
},
"deep-assign": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-3.0.0.tgz",
@ -7327,6 +7540,12 @@
"rimraf": "^2.2.8"
}
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"optional": true
},
"denodeify": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz",
@ -7358,6 +7577,12 @@
"integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
"dev": true
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"optional": true
},
"detect-node": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz",
@ -9451,6 +9676,44 @@
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
"dev": true
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"optional": true,
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
},
"dependencies": {
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
}
}
},
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -9644,6 +9907,12 @@
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
"integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q="
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"optional": true
},
"has-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
@ -9889,6 +10158,33 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true
},
"https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"optional": true,
"requires": {
"agent-base": "6",
"debug": "4"
},
"dependencies": {
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"optional": true,
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"optional": true
}
}
},
"hyphenate-style-name": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
@ -9984,6 +10280,11 @@
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true
},
"image-capture": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/image-capture/-/image-capture-0.4.0.tgz",
"integrity": "sha512-6RWTfqC4ij0AldG+6sQ51XSHTSbwfqMSjVl1GtwNBzbW4UrcfGZeB1Kn749BccvtLb04g5+jSTf1D7q3qHcxpA=="
},
"image-size": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz",
@ -10555,8 +10856,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isexe": {
"version": "2.0.0",
@ -12240,6 +12540,12 @@
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
},
"mimic-response": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
"optional": true
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -12606,6 +12912,15 @@
"semver": "^5.3.0"
}
},
"nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"optional": true,
"requires": {
"abbrev": "1"
}
},
"normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
@ -12643,6 +12958,18 @@
"path-key": "^2.0.0"
}
},
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"optional": true,
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
"gauge": "~2.7.3",
"set-blocking": "~2.0.0"
}
},
"nth-check": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
@ -13486,14 +13813,6 @@
"node-modules-regexp": "^1.0.0"
}
},
"pixelmatch": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.1.0.tgz",
"integrity": "sha512-HqtgvuWN12tBzKJf7jYsc38Ha28Q2NYpmBL9WostEGgDHJqbTLkjydZXL1ZHM02ZnB+Dkwlxo87HBY38kMiD6A==",
"requires": {
"pngjs": "^3.4.0"
}
},
"pkg-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
@ -13562,11 +13881,6 @@
"integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==",
"dev": true
},
"pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="
},
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
@ -15437,7 +15751,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@ -15723,6 +16036,14 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"resemblejs": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resemblejs/-/resemblejs-4.0.0.tgz",
"integrity": "sha512-vaGs/hFVx/941+RS4UJtd8DQvx5RuB61tPLOQCxPso3JpmjfDb6odH5HViT17S0d8DaZsexD01nRJI12giCz/A==",
"requires": {
"canvas": "2.8.0"
}
},
"resolve": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
@ -16197,6 +16518,23 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"optional": true
},
"simple-get": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz",
"integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==",
"optional": true,
"requires": {
"decompress-response": "^4.2.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"simple-plist": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.1.1.tgz",
@ -17133,7 +17471,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@ -17777,6 +18114,12 @@
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"dev": true
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
"optional": true
},
"traverse": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz",
@ -19661,6 +20004,24 @@
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz",
"integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"optional": true,
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
},
"dependencies": {
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
"optional": true
}
}
},
"whatwg-url-without-unicode": {
"version": "8.0.0-3",
"resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz",
@ -19722,6 +20083,15 @@
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"optional": true,
"requires": {
"string-width": "^1.0.2 || 2"
}
},
"windows-iana": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/windows-iana/-/windows-iana-3.1.0.tgz",

View File

@ -54,6 +54,7 @@
"i18next": "17.0.6",
"i18next-browser-languagedetector": "3.0.1",
"i18next-xhr-backend": "3.0.0",
"image-capture": "0.4.0",
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
"jquery": "3.5.1",
"jquery-i18next": "1.2.1",
@ -65,7 +66,6 @@
"moment": "2.29.1",
"moment-duration-format": "2.2.2",
"optional-require": "1.0.3",
"pixelmatch": "5.1.0",
"promise.allsettled": "1.0.4",
"punycode": "2.1.1",
"react": "16.12",
@ -102,6 +102,7 @@
"react-youtube": "7.13.1",
"redux": "4.0.4",
"redux-thunk": "2.2.0",
"resemblejs": "4.0.0",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",
"rtcstats": "github:jitsi/rtcstats#v8.1.0",
"styled-components": "3.4.9",

View File

@ -867,3 +867,15 @@ export function createWelcomePageEvent(action, actionSubject, attributes = {}) {
source: 'welcomePage'
};
}
/**
* Creates an event which indicates a screenshot of the screensharing has been taken.
*
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createScreensharingCaptureTakenEvent() {
return {
action: 'screen.sharing.capture'
};
}

View File

@ -1,6 +1,5 @@
// @flow
import { createScreenshotCaptureEffect } from '../../stream-effects/screenshot-capture';
import { createVirtualBackgroundEffect } from '../../stream-effects/virtual-background';
import logger from './logger';
@ -23,14 +22,6 @@ export default function loadEffects(store: Object): Promise<any> {
return Promise.resolve();
})
: Promise.resolve();
const screenshotCapturePromise = state['features/screenshot-capture']?.capturesEnabled
? createScreenshotCaptureEffect(state)
.catch(error => {
logger.error('Failed to obtain the screenshot capture effect effect instance with error: ', error);
return Promise.resolve();
})
: Promise.resolve();
return Promise.all([ backgroundPromise, screenshotCapturePromise ]);
return Promise.all([ backgroundPromise ]);
}

View File

@ -0,0 +1,173 @@
// @flow
import resemble from 'resemblejs';
import 'image-capture';
import './createImageBitmap';
import { createScreensharingCaptureTakenEvent, sendAnalytics } from '../analytics';
import { getCurrentConference } from '../base/conference';
import {
CLEAR_INTERVAL,
INTERVAL_TIMEOUT,
PERCENTAGE_LOWER_BOUND,
POLL_INTERVAL,
SET_INTERVAL
} from './constants';
import { processScreenshot } from './processScreenshot';
import { timerWorkerScript } from './worker';
declare var interfaceConfig: Object;
declare var ImageCapture: any;
/**
* Effect that wraps {@code MediaStream} adding periodic screenshot captures.
* Manipulates the original desktop stream and performs custom processing operations, if implemented.
*/
export default class ScreenshotCaptureSummary {
_state: Object;
_currentCanvas: HTMLCanvasElement;
_currentCanvasContext: CanvasRenderingContext2D;
_handleWorkerAction: Function;
_initScreenshotCapture: Function;
_imageCapture: any;
_streamWorker: Worker;
_streamHeight: any;
_streamWidth: any;
_storedImageData: ImageData;
/**
* Initializes a new {@code ScreenshotCaptureEffect} instance.
*
* @param {Object} state - The redux state.
*/
constructor(state: Object) {
this._state = state;
this._currentCanvas = document.createElement('canvas');
this._currentCanvasContext = this._currentCanvas.getContext('2d');
// Bind handlers such that they access the same instance.
this._handleWorkerAction = this._handleWorkerAction.bind(this);
this._initScreenshotCapture = this._initScreenshotCapture.bind(this);
this._streamWorker = new Worker(timerWorkerScript, { name: 'Screenshot capture worker' });
this._streamWorker.onmessage = this._handleWorkerAction;
}
/**
* Starts the screenshot capture event on a loop.
*
* @param {Track} track - The track that contains the stream from which screenshots are to be sent.
* @returns {Promise} - Promise that resolves once effect has started or rejects if the
* videoType parameter is not desktop.
*/
start(track: Object) {
const { videoType } = track;
const stream = track.getOriginalStream();
if (videoType !== 'desktop') {
return;
}
const desktopTrack = stream.getVideoTracks()[0];
const { height, width }
= desktopTrack.getSettings() ?? desktopTrack.getConstraints();
this._streamHeight = height;
this._streamWidth = width;
this._currentCanvas.height = parseInt(height, 10);
this._currentCanvas.width = parseInt(width, 10);
this._imageCapture = new ImageCapture(desktopTrack);
this._initScreenshotCapture();
}
/**
* Stops the ongoing {@code ScreenshotCaptureEffect} by clearing the {@code Worker} interval.
*
* @returns {void}
*/
stop() {
this._streamWorker.postMessage({ id: CLEAR_INTERVAL });
}
/**
* Method that is called as soon as the first frame of the video loads from stream.
* The method is used to store the {@code ImageData} object from the first frames
* in order to use it for future comparisons based on which we can process only certain
* screenshots.
*
* @private
* @returns {void}
*/
async _initScreenshotCapture() {
const imageBitmap = await this._imageCapture.grabFrame();
this._currentCanvasContext.drawImage(imageBitmap, 0, 0, this._streamWidth, this._streamHeight);
const imageData = this._currentCanvasContext.getImageData(0, 0, this._streamWidth, this._streamHeight);
this._storedImageData = imageData;
this._streamWorker.postMessage({
id: SET_INTERVAL,
timeMs: POLL_INTERVAL
});
}
/**
* Handler of the {@code EventHandler} message that calls the appropriate method based on the parameter's id.
*
* @private
* @param {EventHandler} message - Message received from the Worker.
* @returns {void}
*/
_handleWorkerAction(message: Object) {
return message.data.id === INTERVAL_TIMEOUT && this._handleScreenshot();
}
/**
* Method that processes the screenshot.
*
* @private
* @param {ImageData} imageData - The image data of the new screenshot.
* @returns {void}
*/
_doProcessScreenshot(imageData) {
sendAnalytics(createScreensharingCaptureTakenEvent());
const conference = getCurrentConference(this._state);
const sessionId = conference.getMeetingUniqueId();
const { connection, timeEstablished } = this._state['features/base/connection'];
const jid = connection.getJid();
const timeLapseSeconds = timeEstablished && Math.floor((Date.now() - timeEstablished) / 1000);
const { jwt } = this._state['features/base/jwt'];
this._storedImageData = imageData;
processScreenshot(this._currentCanvas, {
jid,
jwt,
sessionId,
timeLapseSeconds
});
}
/**
* Screenshot handler.
*
* @private
* @returns {void}
*/
async _handleScreenshot() {
const imageBitmap = await this._imageCapture.grabFrame();
this._currentCanvasContext.drawImage(imageBitmap, 0, 0, this._streamWidth, this._streamHeight);
const imageData = this._currentCanvasContext.getImageData(0, 0, this._streamWidth, this._streamHeight);
resemble(imageData)
.compareTo(this._storedImageData)
.setReturnEarlyThreshold(PERCENTAGE_LOWER_BOUND)
.onComplete(resultData => {
if (resultData.rawMisMatchPercentage > PERCENTAGE_LOWER_BOUND) {
this._doProcessScreenshot(imageData);
}
});
}
}

View File

@ -1,12 +1,13 @@
// @flow
import { getLocalVideoTrack } from '../../features/base/tracks';
import { createScreenshotCaptureEffect } from '../stream-effects/screenshot-capture';
import { SET_SCREENSHOT_CAPTURE } from './actionTypes';
import { createScreenshotCaptureSummary } from './functions';
import logger from './logger';
let ongoingEffect;
let screenshotSummary;
/**
* Marks the on-off state of screenshot captures.
@ -30,33 +31,32 @@ function setScreenshotCapture(enabled) {
* @param {boolean} enabled - Bool that represents the intention to start/stop screenshot captures.
* @returns {Promise}
*/
export function toggleScreenshotCaptureEffect(enabled: boolean) {
export function toggleScreenshotCaptureSummary(enabled: boolean) {
return async function(dispatch: (Object) => Object, getState: () => any) {
const state = getState();
if (state['features/screenshot-capture'].capturesEnabled !== enabled) {
const { jitsiTrack } = getLocalVideoTrack(state['features/base/tracks']);
if (!ongoingEffect) {
ongoingEffect = await createScreenshotCaptureEffect(state);
if (!screenshotSummary) {
try {
screenshotSummary = await createScreenshotCaptureSummary(state);
} catch (err) {
logger.error('Cannot create screenshotCaptureSummary', err);
}
}
// Screenshot capture effect doesn't return a modified stream. Therefore, we don't have to
// switch the stream at the conference level, starting/stopping the effect will suffice here.
if (enabled) {
try {
await ongoingEffect.startEffect(
jitsiTrack.getOriginalStream(),
jitsiTrack.videoType
);
await screenshotSummary.start(jitsiTrack);
dispatch(setScreenshotCapture(enabled));
} catch {
// Handle promise rejection from {@code startEffect} due to stream type not being desktop.
// Handle promise rejection from {@code start} due to stream type not being desktop.
logger.error('Unsupported stream type.');
}
} else {
ongoingEffect.stopEffect();
screenshotSummary.stop();
dispatch(setScreenshotCapture(enabled));
}
}

View File

@ -1,14 +1,14 @@
// @flow
/**
* Number of pixels that signal if two images should be considered different.
* Percent of pixels that signal if two images should be considered different.
*/
export const PIXEL_LOWER_BOUND = 100000;
export const PERCENTAGE_LOWER_BOUND = 5;
/**
* Number of milliseconds that represent how often screenshots should be taken.
*/
export const POLL_INTERVAL = 30000;
export const POLL_INTERVAL = 2000;
/**
* SET_INTERVAL constant is used to set interval and it is set in

View File

@ -0,0 +1,25 @@
/*
* Safari polyfill for createImageBitmap
* https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/createImageBitmap
*
* Support source image types: Canvas.
*/
if (!('createImageBitmap' in window)) {
window.createImageBitmap = async function(data) {
return new Promise((resolve, reject) => {
let dataURL;
if (data instanceof HTMLCanvasElement) {
dataURL = data.toDataURL();
} else {
reject(new Error('createImageBitmap does not handle the provided image source type'));
}
const img = document.createElement('img');
img.addEventListener('load', () => {
resolve(img);
});
img.src = dataURL;
});
};
}

View File

@ -0,0 +1,20 @@
// @flow
import { toState } from '../base/redux';
import ScreenshotCaptureSummary from './ScreenshotCaptureSummary';
/**
* Creates a new instance of ScreenshotCapture.
*
* @param {Object | Function} stateful - The redux store, state, or
* {@code getState} function.
* @returns {Promise<ScreenshotCapture>}
*/
export function createScreenshotCaptureSummary(stateful: Object | Function) {
if (!MediaStreamTrack.prototype.getSettings && !MediaStreamTrack.prototype.getConstraints) {
return Promise.reject(new Error('ScreenshotCaptureSummary not supported!'));
}
return new ScreenshotCaptureSummary(toState(stateful));
}

View File

@ -1,168 +0,0 @@
// @flow
import pixelmatch from 'pixelmatch';
import { getCurrentConference } from '../../base/conference';
import {
CLEAR_INTERVAL,
INTERVAL_TIMEOUT,
PIXEL_LOWER_BOUND,
POLL_INTERVAL,
SET_INTERVAL
} from './constants';
import { processScreenshot } from './processScreenshot';
import { timerWorkerScript } from './worker';
declare var interfaceConfig: Object;
/**
* Effect that wraps {@code MediaStream} adding periodic screenshot captures.
* Manipulates the original desktop stream and performs custom processing operations, if implemented.
*/
export default class ScreenshotCaptureEffect {
_state: Object;
_currentCanvas: HTMLCanvasElement;
_currentCanvasContext: CanvasRenderingContext2D;
_videoElement: HTMLVideoElement;
_handleWorkerAction: Function;
_initScreenshotCapture: Function;
_streamWorker: Worker;
_streamHeight: any;
_streamWidth: any;
_storedImageData: Uint8ClampedArray;
/**
* Initializes a new {@code ScreenshotCaptureEffect} instance.
*
* @param {Object} state - The redux state.
*/
constructor(state: Object) {
this._state = state;
this._currentCanvas = document.createElement('canvas');
this._currentCanvasContext = this._currentCanvas.getContext('2d');
this._videoElement = document.createElement('video');
// Bind handlers such that they access the same instance.
this._handleWorkerAction = this._handleWorkerAction.bind(this);
this._initScreenshotCapture = this._initScreenshotCapture.bind(this);
this._streamWorker = new Worker(timerWorkerScript, { name: 'Screenshot capture worker' });
this._streamWorker.onmessage = this._handleWorkerAction;
}
/**
* Starts the screenshot capture event on a loop.
*
* @param {MediaStream} stream - The desktop stream from which screenshots are to be sent.
* @param {string} videoType - The type of the media stream.
* @returns {Promise} - Promise that resolves once effect has started or rejects if the
* videoType parameter is not desktop.
*/
startEffect(stream: MediaStream, videoType: string) {
return new Promise<void>((resolve, reject) => {
if (videoType !== 'desktop') {
reject();
}
const desktopTrack = stream.getVideoTracks()[0];
const { height, width }
= desktopTrack.getSettings() ?? desktopTrack.getConstraints();
this._streamHeight = height;
this._streamWidth = width;
this._currentCanvas.height = parseInt(height, 10);
this._currentCanvas.width = parseInt(width, 10);
this._videoElement.height = parseInt(height, 10);
this._videoElement.width = parseInt(width, 10);
this._videoElement.srcObject = stream;
this._videoElement.play();
// Store first capture for comparisons in {@code this._handleScreenshot}.
this._videoElement.addEventListener('loadeddata', this._initScreenshotCapture);
resolve();
});
}
/**
* Stops the ongoing {@code ScreenshotCaptureEffect} by clearing the {@code Worker} interval.
*
* @returns {void}
*/
stopEffect() {
this._streamWorker.postMessage({ id: CLEAR_INTERVAL });
this._videoElement.removeEventListener('loadeddata', this._initScreenshotCapture);
}
/**
* Method that is called as soon as the first frame of the video loads from stream.
* The method is used to store the {@code ImageData} object from the first frames
* in order to use it for future comparisons based on which we can process only certain
* screenshots.
*
* @private
* @returns {void}
*/
_initScreenshotCapture() {
const storedCanvas = document.createElement('canvas');
const storedCanvasContext = storedCanvas.getContext('2d');
storedCanvasContext.drawImage(this._videoElement, 0, 0, this._streamWidth, this._streamHeight);
const { data } = storedCanvasContext.getImageData(0, 0, this._streamWidth, this._streamHeight);
this._storedImageData = data;
this._streamWorker.postMessage({
id: SET_INTERVAL,
timeMs: POLL_INTERVAL
});
}
/**
* Handler of the {@code EventHandler} message that calls the appropriate method based on the parameter's id.
*
* @private
* @param {EventHandler} message - Message received from the Worker.
* @returns {void}
*/
_handleWorkerAction(message: Object) {
return message.data.id === INTERVAL_TIMEOUT && this._handleScreenshot();
}
/**
* Method that decides whether an image should be processed based on a preset pixel lower bound.
*
* @private
* @param {integer} nbPixels - The number of pixels of the candidate image.
* @returns {boolean} - Whether the image should be processed or not.
*/
_shouldProcessScreenshot(nbPixels: number) {
return nbPixels >= PIXEL_LOWER_BOUND;
}
/**
* Screenshot handler.
*
* @private
* @returns {void}
*/
_handleScreenshot() {
this._currentCanvasContext.drawImage(this._videoElement, 0, 0, this._streamWidth, this._streamHeight);
const { data } = this._currentCanvasContext.getImageData(0, 0, this._streamWidth, this._streamHeight);
const diffPixels = pixelmatch(data, this._storedImageData, null, this._streamWidth, this._streamHeight);
if (this._shouldProcessScreenshot(diffPixels)) {
const conference = getCurrentConference(this._state);
const sessionId = conference.getMeetingUniqueId();
const { connection, timeEstablished } = this._state['features/base/connection'];
const jid = connection.getJid();
const timeLapseSeconds = timeEstablished && Math.floor((Date.now() - timeEstablished) / 1000);
const { jwt } = this._state['features/base/jwt'];
this._storedImageData = data;
processScreenshot(this._currentCanvas, {
jid,
jwt,
sessionId,
timeLapseSeconds
});
}
}
}

View File

@ -1,20 +0,0 @@
// @flow
import { toState } from '../../base/redux';
import ScreenshotCaptureEffect from './ScreenshotCaptureEffect';
/**
* Creates a new instance of ScreenshotCaptureEffect.
*
* @param {Object | Function} stateful - The redux store, state, or
* {@code getState} function.
* @returns {Promise<ScreenshotCaptureEffect>}
*/
export function createScreenshotCaptureEffect(stateful: Object | Function) {
if (!MediaStreamTrack.prototype.getSettings && !MediaStreamTrack.prototype.getConstraints) {
return Promise.reject(new Error('ScreenshotCaptureEffect not supported!'));
}
return Promise.resolve(new ScreenshotCaptureEffect(toState(stateful)));
}