| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 | // @flow
import {
    CLEAR_INTERVAL,
    INTERVAL_TIMEOUT,
    SET_INTERVAL,
    timerWorkerScript
} from './TimeWorker';
/**
 * Represents a modified MediaStream that adds video as pip on a desktop stream.
 * <tt>JitsiStreamPresenterEffect</tt> does the processing of the original
 * desktop stream.
 */
export default class JitsiStreamPresenterEffect {
    _canvas: HTMLCanvasElement;
    _ctx: CanvasRenderingContext2D;
    _desktopElement: HTMLVideoElement;
    _desktopStream: MediaStream;
    _frameRate: number;
    _onVideoFrameTimer: Function;
    _onVideoFrameTimerWorker: Function;
    _renderVideo: Function;
    _videoFrameTimerWorker: Worker;
    _videoElement: HTMLVideoElement;
    isEnabled: Function;
    startEffect: Function;
    stopEffect: Function;
    /**
     * Represents a modified MediaStream that adds a camera track at the
     * bottom right corner of the desktop track using a HTML canvas.
     * <tt>JitsiStreamPresenterEffect</tt> does the processing of the original
     * video stream.
     *
     * @param {MediaStream} videoStream - The video stream which is user for
     * creating the canvas.
     */
    constructor(videoStream: MediaStream) {
        const videoDiv = document.createElement('div');
        const firstVideoTrack = videoStream.getVideoTracks()[0];
        const { height, width, frameRate } = firstVideoTrack.getSettings() ?? firstVideoTrack.getConstraints();
        this._canvas = document.createElement('canvas');
        this._ctx = this._canvas.getContext('2d');
        if (document.body !== null) {
            document.body.appendChild(this._canvas);
        }
        this._desktopElement = document.createElement('video');
        this._videoElement = document.createElement('video');
        videoDiv.appendChild(this._videoElement);
        videoDiv.appendChild(this._desktopElement);
        if (document.body !== null) {
            document.body.appendChild(videoDiv);
        }
        // Set the video element properties
        this._frameRate = parseInt(frameRate, 10);
        this._videoElement.width = parseInt(width, 10);
        this._videoElement.height = parseInt(height, 10);
        this._videoElement.autoplay = true;
        this._videoElement.srcObject = videoStream;
        // set the style attribute of the div to make it invisible
        videoDiv.style.display = 'none';
        // Bind event handler so it is only bound once for every instance.
        this._onVideoFrameTimer = this._onVideoFrameTimer.bind(this);
        this._videoFrameTimerWorker = new Worker(timerWorkerScript);
        this._videoFrameTimerWorker.onmessage = this._onVideoFrameTimer;
    }
    /**
     * EventHandler onmessage for the videoFrameTimerWorker WebWorker.
     *
     * @private
     * @param {EventHandler} response - The onmessage EventHandler parameter.
     * @returns {void}
     */
    _onVideoFrameTimer(response) {
        if (response.data.id === INTERVAL_TIMEOUT) {
            this._renderVideo();
        }
    }
    /**
     * Loop function to render the video frame input and draw presenter effect.
     *
     * @private
     * @returns {void}
     */
    _renderVideo() {
        // adjust the canvas width/height on every frame incase the window has been resized.
        const [ track ] = this._desktopStream.getVideoTracks();
        const { height, width } = track.getSettings() ?? track.getConstraints();
        this._canvas.width = parseInt(width, 10);
        this._canvas.height = parseInt(height, 10);
        this._ctx.drawImage(this._desktopElement, 0, 0, this._canvas.width, this._canvas.height);
        this._ctx.drawImage(this._videoElement, this._canvas.width - this._videoElement.width, this._canvas.height
            - this._videoElement.height, this._videoElement.width, this._videoElement.height);
        // draw a border around the video element.
        this._ctx.beginPath();
        this._ctx.lineWidth = 2;
        this._ctx.strokeStyle = '#A9A9A9'; // dark grey
        this._ctx.rect(this._canvas.width - this._videoElement.width, this._canvas.height - this._videoElement.height,
            this._videoElement.width, this._videoElement.height);
        this._ctx.stroke();
    }
    /**
     * Checks if the local track supports this effect.
     *
     * @param {JitsiLocalTrack} jitsiLocalTrack - Track to apply effect.
     * @returns {boolean} - Returns true if this effect can run on the
     * specified track, false otherwise.
     */
    isEnabled(jitsiLocalTrack: Object) {
        return jitsiLocalTrack.isVideoTrack() && jitsiLocalTrack.videoType === 'desktop';
    }
    /**
     * Starts loop to capture video frame and render presenter effect.
     *
     * @param {MediaStream} desktopStream - Stream to be used for processing.
     * @returns {MediaStream} - The stream with the applied effect.
     */
    startEffect(desktopStream: MediaStream) {
        const firstVideoTrack = desktopStream.getVideoTracks()[0];
        const { height, width } = firstVideoTrack.getSettings() ?? firstVideoTrack.getConstraints();
        // set the desktop element properties.
        this._desktopStream = desktopStream;
        this._desktopElement.width = parseInt(width, 10);
        this._desktopElement.height = parseInt(height, 10);
        this._desktopElement.autoplay = true;
        this._desktopElement.srcObject = desktopStream;
        this._canvas.width = parseInt(width, 10);
        this._canvas.height = parseInt(height, 10);
        this._videoFrameTimerWorker.postMessage({
            id: SET_INTERVAL,
            timeMs: 1000 / this._frameRate
        });
        return this._canvas.captureStream(this._frameRate);
    }
    /**
     * Stops the capture and render loop.
     *
     * @returns {void}
     */
    stopEffect() {
        this._videoFrameTimerWorker.postMessage({
            id: CLEAR_INTERVAL
        });
    }
}
 |