| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 | /* @flow */
import { getLogger } from 'jitsi-meet-logger';
import * as KeyCodes from '../keycode/keycode';
import {
    EVENTS,
    PERMISSIONS_ACTIONS,
    REMOTE_CONTROL_MESSAGE_NAME
} from '../../service/remotecontrol/Constants';
import UIEvents from '../../service/UI/UIEvents';
import RemoteControlParticipant from './RemoteControlParticipant';
declare var $: Function;
declare var APP: Object;
declare var JitsiMeetJS: Object;
const ConferenceEvents = JitsiMeetJS.events.conference;
const logger = getLogger(__filename);
/**
 * Extract the keyboard key from the keyboard event.
 *
 * @param {KeyboardEvent} event - The event.
 * @returns {KEYS} The key that is pressed or undefined.
 */
function getKey(event) {
    return KeyCodes.keyboardEventToKey(event);
}
/**
 * Extract the modifiers from the keyboard event.
 *
 * @param {KeyboardEvent} event - The event.
 * @returns {Array} With possible values: "shift", "control", "alt", "command".
 */
function getModifiers(event) {
    const modifiers = [];
    if (event.shiftKey) {
        modifiers.push('shift');
    }
    if (event.ctrlKey) {
        modifiers.push('control');
    }
    if (event.altKey) {
        modifiers.push('alt');
    }
    if (event.metaKey) {
        modifiers.push('command');
    }
    return modifiers;
}
/**
 * This class represents the controller party for a remote controller session.
 * It listens for mouse and keyboard events and sends them to the receiver
 * party of the remote control session.
 */
export default class Controller extends RemoteControlParticipant {
    _area: ?Object;
    _controlledParticipant: string | null;
    _isCollectingEvents: boolean;
    _largeVideoChangedListener: Function;
    _requestedParticipant: string | null;
    _stopListener: Function;
    _userLeftListener: Function;
    /**
     * Creates new instance.
     */
    constructor() {
        super();
        this._isCollectingEvents = false;
        this._controlledParticipant = null;
        this._requestedParticipant = null;
        this._stopListener = this._handleRemoteControlStoppedEvent.bind(this);
        this._userLeftListener = this._onUserLeft.bind(this);
        this._largeVideoChangedListener
            = this._onLargeVideoIdChanged.bind(this);
    }
    /**
     * Requests permissions from the remote control receiver side.
     *
     * @param {string} userId - The user id of the participant that will be
     * requested.
     * @param {JQuerySelector} eventCaptureArea - The area that is going to be
     * used mouse and keyboard event capture.
     * @returns {Promise<boolean>} Resolve values - true(accept), false(deny),
     * null(the participant has left).
     */
    requestPermissions(userId: string, eventCaptureArea: Object) {
        if (!this._enabled) {
            return Promise.reject(new Error('Remote control is disabled!'));
        }
        this._area = eventCaptureArea;// $("#largeVideoWrapper")
        logger.log(`Requsting remote control permissions from: ${userId}`);
        return new Promise((resolve, reject) => {
            // eslint-disable-next-line prefer-const
            let onUserLeft, permissionsReplyListener;
            const clearRequest = () => {
                this._requestedParticipant = null;
                APP.conference.removeConferenceListener(
                    ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
                    permissionsReplyListener);
                APP.conference.removeConferenceListener(
                    ConferenceEvents.USER_LEFT,
                    onUserLeft);
            };
            permissionsReplyListener = (participant, event) => {
                let result = null;
                try {
                    result = this._handleReply(participant, event);
                } catch (e) {
                    clearRequest();
                    reject(e);
                }
                if (result !== null) {
                    clearRequest();
                    resolve(result);
                }
            };
            onUserLeft = id => {
                if (id === this._requestedParticipant) {
                    clearRequest();
                    resolve(null);
                }
            };
            APP.conference.addConferenceListener(
                ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
                permissionsReplyListener);
            APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
                onUserLeft);
            this._requestedParticipant = userId;
            this.sendRemoteControlEndpointMessage(userId, {
                type: EVENTS.permissions,
                action: PERMISSIONS_ACTIONS.request
            }, e => {
                clearRequest();
                reject(e);
            });
        });
    }
    /**
     * Handles the reply of the permissions request.
     *
     * @param {JitsiParticipant} participant - The participant that has sent the
     * reply.
     * @param {RemoteControlEvent} event - The remote control event.
     * @returns {void}
     */
    _handleReply(participant: Object, event: Object) {
        const userId = participant.getId();
        if (this._enabled
                && event.name === REMOTE_CONTROL_MESSAGE_NAME
                && event.type === EVENTS.permissions
                && userId === this._requestedParticipant) {
            if (event.action !== PERMISSIONS_ACTIONS.grant) {
                this._area = undefined;
            }
            switch (event.action) {
            case PERMISSIONS_ACTIONS.grant: {
                this._controlledParticipant = userId;
                logger.log('Remote control permissions granted to:', userId);
                this._start();
                return true;
            }
            case PERMISSIONS_ACTIONS.deny:
                return false;
            case PERMISSIONS_ACTIONS.error:
                throw new Error('Error occurred on receiver side');
            default:
                throw new Error('Unknown reply received!');
            }
        } else {
            // different message type or another user -> ignoring the message
            return null;
        }
    }
    /**
     * Handles remote control stopped.
     *
     * @param {JitsiParticipant} participant - The participant that has sent the
     * event.
     * @param {Object} event - EndpointMessage event from the data channels.
     * @property {string} type - The function process only events with
     * name REMOTE_CONTROL_MESSAGE_NAME.
     * @returns {void}
     */
    _handleRemoteControlStoppedEvent(participant: Object, event: Object) {
        if (this._enabled
                && event.name === REMOTE_CONTROL_MESSAGE_NAME
                && event.type === EVENTS.stop
                && participant.getId() === this._controlledParticipant) {
            this._stop();
        }
    }
    /**
     * Starts processing the mouse and keyboard events. Sets conference
     * listeners. Disables keyboard events.
     *
     * @returns {void}
     */
    _start() {
        logger.log('Starting remote control controller.');
        APP.UI.addListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
            this._largeVideoChangedListener);
        APP.conference.addConferenceListener(
            ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
            this._stopListener);
        APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
            this._userLeftListener);
        this.resume();
    }
    /**
     * Disables the keyboatd shortcuts. Starts collecting remote control
     * events. It can be used to resume an active remote control session wchich
     * was paused with this.pause().
     *
     * @returns {void}
     */
    resume() {
        if (!this._enabled || this._isCollectingEvents || !this._area) {
            return;
        }
        logger.log('Resuming remote control controller.');
        this._isCollectingEvents = true;
        APP.keyboardshortcut.enable(false);
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area.mousemove(event => {
            // $FlowDisableNextLine: we are sure that this._area is not null.
            const position = this._area.position();
            this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
                type: EVENTS.mousemove,
                // $FlowDisableNextLine: we are sure that this._area is not null
                x: (event.pageX - position.left) / this._area.width(),
                // $FlowDisableNextLine: we are sure that this._area is not null
                y: (event.pageY - position.top) / this._area.height()
            });
        });
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area.mousedown(this._onMouseClickHandler.bind(this,
            EVENTS.mousedown));
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area.mouseup(this._onMouseClickHandler.bind(this,
            EVENTS.mouseup));
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area.dblclick(
            this._onMouseClickHandler.bind(this, EVENTS.mousedblclick));
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area.contextmenu(() => false);
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area[0].onmousewheel = event => {
            this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
                type: EVENTS.mousescroll,
                x: event.deltaX,
                y: event.deltaY
            });
        };
        $(window).keydown(this._onKeyPessHandler.bind(this,
            EVENTS.keydown));
        $(window).keyup(this._onKeyPessHandler.bind(this, EVENTS.keyup));
    }
    /**
     * Stops processing the mouse and keyboard events. Removes added listeners.
     * Enables the keyboard shortcuts. Displays dialog to notify the user that
     * remote control session has ended.
     *
     * @returns {void}
     */
    _stop() {
        if (!this._controlledParticipant) {
            return;
        }
        logger.log('Stopping remote control controller.');
        APP.UI.removeListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
            this._largeVideoChangedListener);
        APP.conference.removeConferenceListener(
            ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
            this._stopListener);
        APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT,
            this._userLeftListener);
        this.pause();
        this._controlledParticipant = null;
        this._area = undefined;
        APP.UI.messageHandler.notify(
            'dialog.remoteControlTitle',
            'dialog.remoteControlStopMessage'
        );
    }
    /**
     * Executes this._stop() mehtod which stops processing the mouse and
     * keyboard events, removes added listeners, enables the keyboard shortcuts,
     * displays dialog to notify the user that remote control session has ended.
     * In addition sends stop message to the controlled participant.
     *
     * @returns {void}
     */
    stop() {
        if (!this._controlledParticipant) {
            return;
        }
        this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
            type: EVENTS.stop
        });
        this._stop();
    }
    /**
     * Pauses the collecting of events and enables the keyboard shortcus. But
     * it doesn't removes any other listeners. Basically the remote control
     * session will be still active after this.pause(), but no events from the
     * controller side will be captured and sent. You can resume the collecting
     * of the events with this.resume().
     *
     * @returns {void}
     */
    pause() {
        if (!this._controlledParticipant) {
            return;
        }
        logger.log('Pausing remote control controller.');
        this._isCollectingEvents = false;
        APP.keyboardshortcut.enable(true);
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area.off('mousemove');
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area.off('mousedown');
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area.off('mouseup');
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area.off('contextmenu');
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area.off('dblclick');
        $(window).off('keydown');
        $(window).off('keyup');
        // $FlowDisableNextLine: we are sure that this._area is not null.
        this._area[0].onmousewheel = undefined;
    }
    /**
     * Handler for mouse click events.
     *
     * @param {string} type - The type of event ("mousedown"/"mouseup").
     * @param {Event} event - The mouse event.
     * @returns {void}
     */
    _onMouseClickHandler(type: string, event: Object) {
        this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
            type,
            button: event.which
        });
    }
    /**
     * Returns true if the remote control session is started.
     *
     * @returns {boolean}
     */
    isStarted() {
        return this._controlledParticipant !== null;
    }
    /**
     * Returns the id of the requested participant.
     *
     * @returns {string} The id of the requested participant.
     * NOTE: This id should be the result of JitsiParticipant.getId() call.
     */
    getRequestedParticipant() {
        return this._requestedParticipant;
    }
    /**
     * Handler for key press events.
     *
     * @param {string} type - The type of event ("keydown"/"keyup").
     * @param {Event} event - The key event.
     * @returns {void}
     */
    _onKeyPessHandler(type: string, event: Object) {
        this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
            type,
            key: getKey(event),
            modifiers: getModifiers(event)
        });
    }
    /**
     * Calls the stop method if the other side have left.
     *
     * @param {string} id - The user id for the participant that have left.
     * @returns {void}
     */
    _onUserLeft(id: string) {
        if (this._controlledParticipant === id) {
            this._stop();
        }
    }
    /**
     * Handles changes of the participant displayed on the large video.
     *
     * @param {string} id - The user id for the participant that is displayed.
     * @returns {void}
     */
    _onLargeVideoIdChanged(id: string) {
        if (!this._controlledParticipant) {
            return;
        }
        if (this._controlledParticipant === id) {
            this.resume();
        } else {
            this.pause();
        }
    }
}
 |