From 74efe8735b05b43492022d3784efb19e2d34fd2c Mon Sep 17 00:00:00 2001
From: Lars Kiesow <lkiesow@uos.de>
Date: Sun, 29 Nov 2020 02:13:19 +0100
Subject: [PATCH] Allow mirroring individual webcams
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

BigBlueButton already allows mirroring the users own webcam as a global
setting set by administrators. Users have no way of choosing this on
their own.

This patch turns this functionality into a user setting for all webcams.
Every camera menu now gets a “mirror” entry.

The global setting is still used as a default value, keeping the current
behavior as it is to not confuse users.
---
 .../video-provider/video-list/component.jsx   | 37 +++++++++++++++++--
 .../video-list/video-list-item/component.jsx  |  3 +-
 bigbluebutton-html5/private/locales/en.json   |  2 +
 3 files changed, 38 insertions(+), 4 deletions(-)

diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx
index f9bdf6eea6..61031db226 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx
@@ -35,6 +35,12 @@ const intlMessages = defineMessages({
   unfocusDesc: {
     id: 'app.videoDock.webcamUnfocusDesc',
   },
+  mirrorLabel: {
+    id: 'app.videoDock.webcamMirrorLabel',
+  },
+  mirrorDesc: {
+    id: 'app.videoDock.webcamMirrorDesc',
+  },
   autoplayBlockedDesc: {
     id: 'app.videoDock.autoplayBlockedDesc',
   },
@@ -84,6 +90,7 @@ class VideoList extends Component {
         filledArea: 0,
       },
       autoplayBlocked: false,
+      mirroredCameras: [],
     };
 
     this.ticking = false;
@@ -201,6 +208,24 @@ class VideoList extends Component {
     window.dispatchEvent(new Event('videoFocusChange'));
   }
 
+  mirrorCamera(cameraId) {
+    const { mirroredCameras } = this.state;
+    if (this.cameraIsMirrored(cameraId)) {
+      this.setState({
+        mirroredCameras: mirroredCameras.filter(x => x != cameraId),
+      });
+    } else {
+      this.setState({
+        mirroredCameras: mirroredCameras.concat([cameraId]),
+      });
+    }
+  }
+
+  cameraIsMirrored(cameraId) {
+    const { mirroredCameras } = this.state;
+    return mirroredCameras.indexOf(cameraId) >= 0;
+  }
+
   handleCanvasResize() {
     if (!this.ticking) {
       window.requestAnimationFrame(() => {
@@ -273,14 +298,19 @@ class VideoList extends Component {
       const { cameraId, userId, name } = stream;
       const isFocused = focusedId === cameraId;
       const isFocusedIntlKey = !isFocused ? 'focus' : 'unfocus';
-      let actions = [];
+      const isMirrored = this.cameraIsMirrored(cameraId);
+      let actions = [{
+        label: intl.formatMessage(intlMessages['mirrorLabel']),
+        description: intl.formatMessage(intlMessages['mirrorDesc']),
+        onClick: () => this.mirrorCamera(cameraId),
+      }];
 
       if (numOfStreams > 2) {
-        actions = [{
+        actions.push({
           label: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Label`]),
           description: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Desc`]),
           onClick: () => this.handleVideoFocus(cameraId),
-        }];
+        });
       }
 
       return (
@@ -296,6 +326,7 @@ class VideoList extends Component {
             cameraId={cameraId}
             userId={userId}
             name={name}
+            mirrored={isMirrored}
             actions={actions}
             onMount={(videoRef) => {
               this.handleCanvasResize();
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx
index 792ffe7bcf..53e17e4935 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx
@@ -142,6 +142,7 @@ class VideoListItem extends Component {
       numOfStreams,
       webcamDraggableState,
       swapLayout,
+      mirrored
     } = this.props;
     const availableActions = this.getAvailableActions();
     const enableVideoMenu = Meteor.settings.public.kurento.enableVideoMenu || false;
@@ -173,7 +174,7 @@ class VideoListItem extends Component {
                 && !isFullscreen && !swapLayout,
               [styles.cursorGrabbing]: webcamDraggableState.dragging
                 && !isFullscreen && !swapLayout,
-              [styles.mirroredVideo]: this.mirrorOwnWebcam,
+              [styles.mirroredVideo]: (this.mirrorOwnWebcam && !mirrored) || (!this.mirrorOwnWebcam && mirrored),
             })}
             ref={(ref) => { this.videoTag = ref; }}
             autoPlay
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index 42a11674b9..7bac8d4e73 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -643,6 +643,8 @@
     "app.videoDock.webcamFocusDesc": "Focus the selected webcam",
     "app.videoDock.webcamUnfocusLabel": "Unfocus",
     "app.videoDock.webcamUnfocusDesc": "Unfocus the selected webcam",
+    "app.videoDock.webcamMirrorLabel": "Mirror",
+    "app.videoDock.webcamMirrorDesc": "Mirror the selected webcam",
     "app.videoDock.autoplayBlockedDesc": "We need your permission to show you other users' webcams.",
     "app.videoDock.autoplayAllowLabel": "View webcams",
     "app.invitation.title": "Breakout room invitation",
-- 
GitLab