diff --git a/modules/API/API.js b/modules/API/API.js index 55d3908b1..bd70017ae 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -21,7 +21,11 @@ import { import { isEnabled as isDropboxEnabled } from '../../react/features/dropbox'; import { toggleE2EE } from '../../react/features/e2ee/actions'; import { invite } from '../../react/features/invite'; -import { resizeLargeVideo, selectParticipantInLargeVideo } from '../../react/features/large-video/actions'; +import { + captureLargeVideoScreenshot, + resizeLargeVideo, + selectParticipantInLargeVideo +} from '../../react/features/large-video/actions'; import { toggleLobbyMode } from '../../react/features/lobby/actions.web'; import { RECORDING_TYPES } from '../../react/features/recording/constants'; import { getActiveSession } from '../../react/features/recording/functions'; @@ -339,6 +343,21 @@ function initCommands() { const { name } = request; switch (name) { + case 'capture-largevideo-screenshot' : + APP.store.dispatch(captureLargeVideoScreenshot()) + .then(dataURL => { + let error; + + if (!dataURL) { + error = new Error('No large video found!'); + } + + callback({ + error, + dataURL + }); + }); + break; case 'invite': { const { invitees } = request; diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js index 895b8a3a9..cac9525ab 100644 --- a/modules/API/external/external_api.js +++ b/modules/API/external/external_api.js @@ -636,6 +636,18 @@ export default class JitsiMeetExternalAPI extends EventEmitter { } } + /** + * Captures the screenshot of the large video. + * + * @returns {dataURL} - Base64 encoded image data of the screenshot if large + * video is detected, an error otherwise. + */ + captureLargeVideoScreenshot() { + return this._transport.sendRequest({ + name: 'capture-largevideo-screenshot' + }); + } + /** * Removes the listeners and removes the Jitsi Meet frame. * diff --git a/react/features/large-video/actions.js b/react/features/large-video/actions.js index 9100403c4..f34a8e1f0 100644 --- a/react/features/large-video/actions.js +++ b/react/features/large-video/actions.js @@ -10,6 +10,7 @@ import { import { _handleParticipantError } from '../base/conference'; import { MEDIA_TYPE } from '../base/media'; import { getParticipants } from '../base/participants'; +import { getTrackByMediaTypeAndParticipant } from '../base/tracks'; import { reportError } from '../base/util'; import { shouldDisplayTileView } from '../video-layout'; @@ -20,12 +21,63 @@ import { declare var APP: Object; +/** +* Captures a screenshot of the video displayed on the large video. +* +* @returns {Function} +*/ +export function captureLargeVideoScreenshot() { + return (dispatch: Dispatch, getState: Function): Promise => { + const state = getState(); + const largeVideo = state['features/large-video']; + + if (!largeVideo) { + return Promise.resolve(); + } + const tracks = state['features/base/tracks']; + const { jitsiTrack } = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, largeVideo.participantId); + const videoStream = jitsiTrack.getOriginalStream(); + + // Create a HTML canvas and draw video from the track on to the canvas. + const [ track ] = videoStream.getVideoTracks(); + const { height, width } = track.getSettings() ?? track.getConstraints(); + const canvasElement = document.createElement('canvas'); + const ctx = canvasElement.getContext('2d'); + const videoElement = document.createElement('video'); + + videoElement.height = parseInt(height, 10); + videoElement.width = parseInt(width, 10); + videoElement.autoplay = true; + videoElement.srcObject = videoStream; + canvasElement.height = videoElement.height; + canvasElement.width = videoElement.width; + + // Wait for the video to load before drawing on to the canvas. + const promise = new Promise(resolve => { + videoElement.onloadeddata = () => resolve(); + }); + + return promise.then(() => { + ctx.drawImage(videoElement, 0, 0, videoElement.width, videoElement.height); + const dataURL = canvasElement.toDataURL('image/png', 1.0); + + // Cleanup. + ctx.clearRect(0, 0, videoElement.width, videoElement.height); + videoElement.srcObject = null; + canvasElement.remove(); + videoElement.remove(); + + return Promise.resolve(dataURL); + }); + }; +} + /** * Resizes the large video container based on the dimensions provided. * * @param {number} width - Width that needs to be applied on the large video container. * @param {number} height - Height that needs to be applied on the large video container. - * @returns {void} + * @returns {Function} */ export function resizeLargeVideo(width: number, height: number) { return (dispatch: Dispatch, getState: Function) => { @@ -72,7 +124,7 @@ export function selectParticipant() { /** * Action to select the participant to be displayed in LargeVideo based on the - * participant id provided. If a partcipant id is not provided, the LargeVideo + * participant id provided. If a participant id is not provided, the LargeVideo * participant will be selected based on a variety of factors: If there is a * dominant or pinned speaker, or if there are remote tracks, etc. *