Skip to content

Commit

Permalink
feat(external-api) add commands/event for remote screen capture and f…
Browse files Browse the repository at this point in the history
…acing mode

- added command for screencapture remotely - this will trigger a response caught in an event handler
- added command for setting the camera facing mode remotely
- enhanced toggleVideo command to optionally accept the facing mode
  • Loading branch information
horymury committed Jul 7, 2023
1 parent 472c8d7 commit ef33d5a
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 14 deletions.
53 changes: 44 additions & 9 deletions modules/API/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { toggleDialog } from '../../react/features/base/dialog/actions';
import { isSupportedBrowser } from '../../react/features/base/environment/environment';
import { parseJWTFromURLParams } from '../../react/features/base/jwt/functions';
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../react/features/base/media/constants';
import { CAPTURE_SCREENSHOT_MESSAGE, MEDIA_TYPE, VIDEO_TYPE } from '../../react/features/base/media/constants';
import {
grantModerator,
kickParticipant,
Expand All @@ -50,8 +50,8 @@ import {
} from '../../react/features/base/participants/functions';
import { updateSettings } from '../../react/features/base/settings/actions';
import { getDisplayName } from '../../react/features/base/settings/functions.web';
import { toggleCamera } from '../../react/features/base/tracks/actions.any';
import { isToggleCameraEnabled } from '../../react/features/base/tracks/functions';
import { setCameraFacingMode } from '../../react/features/base/tracks/actions.web';
import { CAMERA_FACING_MODE_MESSAGE } from '../../react/features/base/tracks/constants';
import {
autoAssignToBreakoutRooms,
closeBreakoutRoom,
Expand Down Expand Up @@ -395,12 +395,8 @@ function initCommands() {
sendAnalytics(createApiEvent('film.strip.resize'));
APP.store.dispatch(resizeFilmStrip(options.width));
},
'toggle-camera': () => {
if (!isToggleCameraEnabled(APP.store.getState())) {
return;
}

APP.store.dispatch(toggleCamera());
'toggle-camera': facingMode => {
APP.store.dispatch(setCameraFacingMode(facingMode));
},
'toggle-camera-mirror': () => {
const state = APP.store.getState();
Expand Down Expand Up @@ -529,6 +525,29 @@ function initCommands() {
logger.error('Failed sending endpoint text message', err);
}
},
'send-capture-screenshot-message': to => {
if (!to || !isLocalParticipantModerator(APP.store.getState())) {
logger.warn('Participant id not set or participant not moderator');

return;
}

APP.conference.sendEndpointMessage(to, {
name: CAPTURE_SCREENSHOT_MESSAGE
});
},
'send-camera-facing-mode-message': (to, facingMode) => {
if (!to || !isLocalParticipantModerator(APP.store.getState())) {
logger.warn('Participant id not set or participant not moderator');

return;
}

APP.conference.sendEndpointMessage(to, {
name: CAMERA_FACING_MODE_MESSAGE,
facingMode
});
},
'overwrite-names': participantList => {
logger.debug('Overwrite names command received');

Expand Down Expand Up @@ -1775,6 +1794,22 @@ class API {
});
}

/**
* Notify external application (if API is enabled) that a participant's screen
* capture has been received.
*
* @param {Object} senderInfo - The sender's info.
* @param {string} dataURL - The image data.
* @returns {void}
*/
notifyLargeVideoScreenshotReceived(senderInfo, dataURL) {
this._sendEvent({
name: 'screen-capture-received',
senderInfo,
dataURL
});
}

/**
* Notify external application (if API is enabled) that the dominant speaker
* has been turned on/off.
Expand Down
5 changes: 4 additions & 1 deletion modules/API/external/external_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const commands = {
removeBreakoutRoom: 'remove-breakout-room',
resizeFilmStrip: 'resize-film-strip',
resizeLargeVideo: 'resize-large-video',
sendCameraFacingMode: 'send-camera-facing-mode-message',
sendCaptureScreenshot: 'send-capture-screenshot-message',
sendChatMessage: 'send-chat-message',
sendEndpointTextMessage: 'send-endpoint-text-message',
sendParticipantToRoom: 'send-participant-to-room',
Expand Down Expand Up @@ -111,6 +113,7 @@ const events = {
'data-channel-opened': 'dataChannelOpened',
'device-list-changed': 'deviceListChanged',
'display-name-change': 'displayNameChange',
'dominant-speaker-changed': 'dominantSpeakerChanged',
'email-change': 'emailChange',
'error-occurred': 'errorOccurred',
'endpoint-text-message-received': 'endpointTextMessageReceived',
Expand Down Expand Up @@ -151,8 +154,8 @@ const events = {
'video-availability-changed': 'videoAvailabilityChanged',
'video-mute-status-changed': 'videoMuteStatusChanged',
'video-quality-changed': 'videoQualityChanged',
'screen-capture-received': 'screenCaptureReceived',
'screen-sharing-status-changed': 'screenSharingStatusChanged',
'dominant-speaker-changed': 'dominantSpeakerChanged',
'subject-change': 'subjectChange',
'suspend-detected': 'suspendDetected',
'tile-view-changed': 'tileViewChanged',
Expand Down
9 changes: 9 additions & 0 deletions react/features/base/media/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export const CAMERA_FACING_MODE = {
USER: 'user'
};

/**
* The payload name for the capture screenshot remotely message.
*/
export const CAPTURE_SCREENSHOT_MESSAGE = 'capture-screenshot-message';

export type MediaType = 'audio' | 'video' | 'screenshare';

/**
Expand All @@ -25,6 +30,10 @@ export const MEDIA_TYPE: {
VIDEO: 'video'
};

/**
* The payload name for the received screenshot message.
*/
export const SEND_SCREENSHOT_MESSAGE = 'send-screenshot-message';

/* eslint-disable no-bitwise */

Expand Down
42 changes: 40 additions & 2 deletions react/features/base/tracks/actions.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';

import {
addLocalTrack,
replaceLocalTrack
replaceLocalTrack,
toggleCamera
} from './actions.any';
import {
createLocalTracksF,
getLocalDesktopTrack,
getLocalJitsiAudioTrack
getLocalJitsiAudioTrack,
getLocalVideoTrack,
isToggleCameraEnabled
} from './functions';
import { IShareOptions, IToggleScreenSharingOptions } from './types';

Expand Down Expand Up @@ -263,3 +266,38 @@ async function _toggleScreenSharing(
APP.API.notifyScreenSharingStatusChanged(enable, screensharingDetails);
}
}

/**
* Sets the camera facing mode(environment/user). If facing mode not provided, it will do a toggle.
*
* @param {string | undefined} facingMode - The selected facing mode.
* @returns {void}
*/
export function setCameraFacingMode(facingMode: string | undefined) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();

if (!isToggleCameraEnabled(state)) {
return;
}

if (!facingMode) {
dispatch(toggleCamera());

return;
}

const tracks = state['features/base/tracks'];
const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;

if (!tracks || !localVideoTrack) {
return;
}

const currentFacingMode = localVideoTrack.getCameraFacingMode();

if (currentFacingMode !== facingMode) {
dispatch(toggleCamera());
}
};
}
4 changes: 4 additions & 0 deletions react/features/base/tracks/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* The payload name for remotely setting the camera facing mode message.
*/
export const CAMERA_FACING_MODE_MESSAGE = 'camera-facing-mode-message';
31 changes: 31 additions & 0 deletions react/features/base/tracks/middleware.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
import { hideNotification } from '../../notifications/actions';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { CONFERENCE_JOINED } from '../conference/actionTypes';
import { IJitsiConference } from '../conference/reducer';
import { getAvailableDevices } from '../devices/actions.web';
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
import { setScreenshareMuted } from '../media/actions';
import {
MEDIA_TYPE,
Expand All @@ -20,10 +23,12 @@ import {
TRACK_UPDATED
} from './actionTypes';
import {
setCameraFacingMode,
showNoDataFromSourceVideoError,
toggleScreensharing,
trackNoDataFromSourceNotificationInfoChanged
} from './actions.web';
import { CAMERA_FACING_MODE_MESSAGE } from './constants';
import {
getTrackByJitsiTrack
} from './functions.web';
Expand Down Expand Up @@ -121,11 +126,37 @@ MiddlewareRegistry.register(store => next => action => {
return result;
}

case CONFERENCE_JOINED: {
_addsetCameraFacingModeListener(action.conference);
break;
}
}

return next(action);
});

/**
* Registers listener for {@link JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED} that
* will perform various chat related activities.
*
* @param {IJitsiConference} conference - The conference.
* @returns {void}
*/
function _addsetCameraFacingModeListener(conference: IJitsiConference) {
conference.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(...args: any) => {
if (args && args.length >= 2) {
const [ , eventData ] = args;

if (eventData.name === CAMERA_FACING_MODE_MESSAGE) {
APP.store.dispatch(setCameraFacingMode(eventData.facingMode));
}
}
}
);
}

/**
* Handles no data from source errors.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ import { TOGGLE_DOCUMENT_EDITING } from '../etherpad/actionTypes';
import { selectParticipantInLargeVideo } from './actions';
import logger from './logger';

import './subscriber';

/**
* Middleware that catches actions related to participants and tracks and
* dispatches an action to select a participant depicted by LargeVideo.
Expand Down Expand Up @@ -72,3 +70,4 @@ MiddlewareRegistry.register(store => next => action => {

return result;
});

2 changes: 2 additions & 0 deletions react/features/large-video/middleware.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './middleware.any';
import './subscriber.native';
116 changes: 116 additions & 0 deletions react/features/large-video/middleware.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { v4 as uuidv4 } from 'uuid';

import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { IJitsiConference } from '../base/conference/reducer';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { CAPTURE_SCREENSHOT_MESSAGE, SEND_SCREENSHOT_MESSAGE } from '../base/media/constants';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';

import { captureLargeVideoScreenshot } from './actions';

import './subscriber.web';
import './middleware.any';

/**
* A {@code Map} temporary holding in transit screen capture chunks.
*/
const receivedScreencaptureMap = new Map();

/**
* The image chunk size in Bytes <=> 60 KB.
*/
const IMAGE_CHUNK_SIZE = 1024 * 60;

/**
* Middleware that catches actions related to participants and tracks and
* dispatches an action to select a participant depicted by LargeVideo.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(_store => next => action => {
switch (action.type) {
case CONFERENCE_JOINED: {
_addScreenCaptureListeners(action.conference);
break;
}
}
const result = next(action);

return result;
});


/**
* Registers listener for {@link JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED} that
* will perform various chat related activities.
*
* @param {IJitsiConference} conference - The conference.
* @returns {void}
*/
function _addScreenCaptureListeners(conference: IJitsiConference) {
conference.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(...args: any) => {
if (args && args.length >= 2) {
const [ sender, eventData ] = args;

if (eventData.name === CAPTURE_SCREENSHOT_MESSAGE) {
APP.store.dispatch(captureLargeVideoScreenshot()).then(dataURL => {
if (!dataURL) {
return;
}

const uuId = uuidv4();
const size = Math.ceil(dataURL.length / IMAGE_CHUNK_SIZE);
let currentIndex = 0;
let idx = 0;

while (currentIndex < dataURL.length) {
const newIndex = currentIndex + IMAGE_CHUNK_SIZE;

conference.sendEndpointMessage(sender._id, {
name: SEND_SCREENSHOT_MESSAGE,
id: uuId,
size,
idx,
chunk: dataURL.slice(currentIndex, newIndex)
});
currentIndex = newIndex;
idx++;
}
});
}

if (eventData.name === SEND_SCREENSHOT_MESSAGE) {
if (eventData.id) {
if (!receivedScreencaptureMap.has(eventData.id)) {
receivedScreencaptureMap.set(eventData.id, new Array(eventData.size));
}

const arr = receivedScreencaptureMap.get(eventData.id);
const { id, idx, chunk, size } = eventData;

arr[idx] = chunk;
if (idx === size - 1) {
const dataURL = arr.join('');

APP.API.notifyLargeVideoScreenshotReceived({
jid: sender._jid,
id: sender._id
},
dataURL);
receivedScreencaptureMap.delete(id);
}
} else {
APP.API.notifyLargeVideoScreenshotReceived({
jid: sender._jid,
id: sender._id
},
undefined);
}
}
}
}
);
}

0 comments on commit ef33d5a

Please sign in to comment.