feat(ScreenshotCaptureEffect) Implement.
This commit is contained in:
parent
22871f15d0
commit
a18ed3a779
|
@ -122,6 +122,7 @@ import { setSharedVideoStatus } from './react/features/shared-video';
|
||||||
import { createPresenterEffect } from './react/features/stream-effects/presenter';
|
import { createPresenterEffect } from './react/features/stream-effects/presenter';
|
||||||
import { endpointMessageReceived } from './react/features/subtitles';
|
import { endpointMessageReceived } from './react/features/subtitles';
|
||||||
import { createRnnoiseProcessorPromise } from './react/features/rnnoise';
|
import { createRnnoiseProcessorPromise } from './react/features/rnnoise';
|
||||||
|
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
|
||||||
|
@ -1460,6 +1461,8 @@ export default {
|
||||||
promise = promise.then(() => this.useVideoStream(null));
|
promise = promise.then(() => this.useVideoStream(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
APP.store.dispatch(toggleScreenshotCaptureEffect(false));
|
||||||
|
|
||||||
return promise.then(
|
return promise.then(
|
||||||
() => {
|
() => {
|
||||||
this.videoSwitchInProgress = false;
|
this.videoSwitchInProgress = false;
|
||||||
|
@ -1731,6 +1734,7 @@ export default {
|
||||||
.then(stream => this.useVideoStream(stream))
|
.then(stream => this.useVideoStream(stream))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.videoSwitchInProgress = false;
|
this.videoSwitchInProgress = false;
|
||||||
|
APP.store.dispatch(toggleScreenshotCaptureEffect(true));
|
||||||
sendAnalytics(createScreenSharingEvent('started'));
|
sendAnalytics(createScreenSharingEvent('started'));
|
||||||
logger.log('Screen sharing started');
|
logger.log('Screen sharing started');
|
||||||
})
|
})
|
||||||
|
|
|
@ -188,7 +188,12 @@ var interfaceConfig = {
|
||||||
*
|
*
|
||||||
* Note: this mode is experimental and subject to breakage.
|
* Note: this mode is experimental and subject to breakage.
|
||||||
*/
|
*/
|
||||||
AUTO_PIN_LATEST_SCREEN_SHARE: 'remote-only'
|
AUTO_PIN_LATEST_SCREEN_SHARE: 'remote-only',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we should capture periodic screenshots of the content sharing.
|
||||||
|
*/
|
||||||
|
ENABLE_SCREENSHOT_CAPTURE: false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How many columns the tile view can expand to. The respected range is
|
* How many columns the tile view can expand to. The respected range is
|
||||||
|
|
|
@ -13314,6 +13314,14 @@
|
||||||
"node-modules-regexp": "^1.0.0"
|
"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": {
|
"pkg-dir": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
|
||||||
|
@ -13375,6 +13383,11 @@
|
||||||
"integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==",
|
"integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"pngjs": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="
|
||||||
|
},
|
||||||
"popper.js": {
|
"popper.js": {
|
||||||
"version": "1.14.4",
|
"version": "1.14.4",
|
||||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.4.tgz",
|
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.4.tgz",
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
"lodash": "4.17.13",
|
"lodash": "4.17.13",
|
||||||
"moment": "2.19.4",
|
"moment": "2.19.4",
|
||||||
"moment-duration-format": "2.2.2",
|
"moment-duration-format": "2.2.2",
|
||||||
|
"pixelmatch": "5.1.0",
|
||||||
"react": "16.9",
|
"react": "16.9",
|
||||||
"react-dom": "16.9",
|
"react-dom": "16.9",
|
||||||
"react-emoji-render": "1.0.0",
|
"react-emoji-render": "1.0.0",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* global APP */
|
/* global APP */
|
||||||
|
|
||||||
|
import { createScreenshotCaptureEffect } from '../../stream-effects/screenshot-capture';
|
||||||
import { getBlurEffect } from '../../blur';
|
import { getBlurEffect } from '../../blur';
|
||||||
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
|
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
|
||||||
import { MEDIA_TYPE } from '../media';
|
import { MEDIA_TYPE } from '../media';
|
||||||
|
@ -101,21 +102,30 @@ export function createLocalTracksF(
|
||||||
const constraints = options.constraints
|
const constraints = options.constraints
|
||||||
?? state['features/base/config'].constraints;
|
?? state['features/base/config'].constraints;
|
||||||
|
|
||||||
// Do not load blur effect if option for ignoring effects is present.
|
const blurPromise = state['features/blur'].blurEnabled
|
||||||
// This is needed when we are creating a video track for presenter mode.
|
|
||||||
const loadEffectsPromise = state['features/blur'].blurEnabled
|
|
||||||
? getBlurEffect()
|
? getBlurEffect()
|
||||||
.then(blurEffect => [ blurEffect ])
|
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
logger.error('Failed to obtain the blur effect instance with error: ', error);
|
logger.error('Failed to obtain the blur effect instance with error: ', error);
|
||||||
|
|
||||||
return Promise.resolve([]);
|
return Promise.resolve();
|
||||||
})
|
})
|
||||||
: 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();
|
||||||
|
const loadEffectsPromise = Promise.all([ blurPromise, screenshotCapturePromise ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
loadEffectsPromise.then(effects =>
|
loadEffectsPromise.then(effectsArray => {
|
||||||
JitsiMeetJS.createLocalTracks(
|
// Filter any undefined values returned by Promise.resolve().
|
||||||
|
const effects = effectsArray.filter(effect => Boolean(effect));
|
||||||
|
|
||||||
|
return JitsiMeetJS.createLocalTracks(
|
||||||
{
|
{
|
||||||
cameraDeviceId,
|
cameraDeviceId,
|
||||||
constraints,
|
constraints,
|
||||||
|
@ -138,7 +148,8 @@ export function createLocalTracksF(
|
||||||
logger.error('Failed to create local tracks', options.devices, err);
|
logger.error('Failed to create local tracks', options.devices, err);
|
||||||
|
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
})));
|
});
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redux action type dispatched in order to toggle screenshot captures.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: SET_SCREENSHOT_CAPTURE
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const SET_SCREENSHOT_CAPTURE = 'SET_SCREENSHOT_CAPTURE';
|
|
@ -0,0 +1,52 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { createScreenshotCaptureEffect } from '../stream-effects/screenshot-capture';
|
||||||
|
import { getLocalVideoTrack } from '../../features/base/tracks';
|
||||||
|
|
||||||
|
import { SET_SCREENSHOT_CAPTURE } from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the on-off state of screenshot captures.
|
||||||
|
*
|
||||||
|
* @param {boolean} enabled - Whether to turn screen captures on or off.
|
||||||
|
* @returns {{
|
||||||
|
* type: START_SCREENSHOT_CAPTURE,
|
||||||
|
* payload: enabled
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
function setScreenshotCapture(enabled) {
|
||||||
|
return {
|
||||||
|
type: SET_SCREENSHOT_CAPTURE,
|
||||||
|
payload: enabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action that toggles the screenshot captures.
|
||||||
|
*
|
||||||
|
* @param {boolean} enabled - Bool that represents the intention to start/stop screenshot captures.
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
export function toggleScreenshotCaptureEffect(enabled: boolean) {
|
||||||
|
return function(dispatch: (Object) => Object, getState: () => any) {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
if (state['features/screenshot-capture'].capturesEnabled !== enabled) {
|
||||||
|
const { jitsiTrack } = getLocalVideoTrack(state['features/base/tracks']);
|
||||||
|
|
||||||
|
return createScreenshotCaptureEffect(state)
|
||||||
|
.then(effect =>
|
||||||
|
jitsiTrack.setEffect(enabled ? effect : undefined)
|
||||||
|
.then(() => {
|
||||||
|
dispatch(setScreenshotCapture(enabled));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(setScreenshotCapture(!enabled));
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(() => dispatch(setScreenshotCapture(false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './actions';
|
||||||
|
|
||||||
|
import './reducer';
|
|
@ -0,0 +1,23 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { ReducerRegistry } from '../base/redux';
|
||||||
|
import { PersistenceRegistry } from '../base/storage';
|
||||||
|
|
||||||
|
import { SET_SCREENSHOT_CAPTURE } from './actionTypes';
|
||||||
|
|
||||||
|
PersistenceRegistry.register('features/screnshot-capture', true, {
|
||||||
|
capturesEnabled: false
|
||||||
|
});
|
||||||
|
|
||||||
|
ReducerRegistry.register('features/screenshot-capture', (state = {}, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_SCREENSHOT_CAPTURE: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
capturesEnabled: action.payload
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
|
@ -0,0 +1,176 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import pixelmatch from 'pixelmatch';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CLEAR_INTERVAL,
|
||||||
|
INTERVAL_TIMEOUT,
|
||||||
|
PIXEL_LOWER_BOUND,
|
||||||
|
POLL_INTERVAL,
|
||||||
|
SET_INTERVAL
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
import { getCurrentConference } from '../../base/conference';
|
||||||
|
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);
|
||||||
|
this._streamWorker.onmessage = this._handleWorkerAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the local track supports this effect.
|
||||||
|
*
|
||||||
|
* @param {JitsiLocalTrack} jitsiLocalTrack - Targeted local track.
|
||||||
|
* @returns {boolean} - Returns true if this effect can run on the specified track, false otherwise.
|
||||||
|
*/
|
||||||
|
isEnabled(jitsiLocalTrack: Object) {
|
||||||
|
return (
|
||||||
|
interfaceConfig.ENABLE_SCREENSHOT_CAPTURE
|
||||||
|
&& jitsiLocalTrack.isVideoTrack()
|
||||||
|
&& jitsiLocalTrack.videoType === 'desktop'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the screenshot capture event on a loop.
|
||||||
|
*
|
||||||
|
* @param {MediaStream} stream - The desktop stream from which screenshots are to be sent.
|
||||||
|
* @returns {MediaStream} - The same stream, with the interval set.
|
||||||
|
*/
|
||||||
|
startEffect(stream: MediaStream) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of pixels that signal if two images should be considered different.
|
||||||
|
*/
|
||||||
|
export const PIXEL_LOWER_BOUND = 100000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of milliseconds that represent how often screenshots should be taken.
|
||||||
|
*/
|
||||||
|
export const POLL_INTERVAL = 30000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SET_INTERVAL constant is used to set interval and it is set in
|
||||||
|
* the id property of the request.data property. timeMs property must
|
||||||
|
* also be set. request.data example:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* id: SET_INTERVAL,
|
||||||
|
* timeMs: 33
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const SET_INTERVAL = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLEAR_INTERVAL constant is used to clear the interval and it is set in
|
||||||
|
* the id property of the request.data property.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* id: CLEAR_INTERVAL
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const CLEAR_INTERVAL = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INTERVAL_TIMEOUT constant is used as response and it is set in the id property.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* id: INTERVAL_TIMEOUT
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const INTERVAL_TIMEOUT = 3;
|
|
@ -0,0 +1,19 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import ScreenshotCaptureEffect from './ScreenshotCaptureEffect';
|
||||||
|
import { toState } from '../../base/redux';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)));
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method used to process screenshots captured by the {@code ScreenshotCaptureEffect}.
|
||||||
|
*
|
||||||
|
* @param {HTMLCanvasElement} canvas - The canvas containing a screenshot to be processed.
|
||||||
|
* @param {Object} options - Custom options required for processing.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function processScreenshot(canvas: HTMLCanvasElement, options: Object) { // eslint-disable-line no-unused-vars
|
||||||
|
return;
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
CLEAR_INTERVAL,
|
||||||
|
INTERVAL_TIMEOUT,
|
||||||
|
SET_INTERVAL
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
const code = `
|
||||||
|
var timer;
|
||||||
|
|
||||||
|
onmessage = function(request) {
|
||||||
|
switch (request.data.id) {
|
||||||
|
case ${SET_INTERVAL}: {
|
||||||
|
timer = setInterval(() => {
|
||||||
|
postMessage({ id: ${INTERVAL_TIMEOUT} });
|
||||||
|
}, request.data.timeMs);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ${CLEAR_INTERVAL}: {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const timerWorkerScript = URL.createObjectURL(new Blob([ code ], { type: 'application/javascript' }));
|
Loading…
Reference in New Issue