Browse Source

Allow audio and video webconf (#394)

Jérémie Pardou-Piquemal 2 years ago
parent
commit
30b5a5aa10

+ 3 - 2
.env.dist

@@ -18,5 +18,6 @@ VITE_USE_PROXY=1
 # in Ricochet.js server.
 VITE_RICOCHET_SITEID=airboardgame
 
-# Set to 1 to allow openVidu video conference
-VITE_ENABLE_WEBCONFERENCE=0
+# Set to 'audio' or 'video' if you want to enable Webconference
+# /!\ Remember to also configure backend openVidu variables
+VITE_WEBCONFERENCE=

+ 1 - 1
.github/workflows/lint-deploy.yml

@@ -32,7 +32,7 @@ jobs:
           VITE_SOCKET_PATH: /wamp/socket.io
           VITE_RICOCHET_SITEID: airboardgame
           VITE_USE_PROXY: 0
-          VITE_ENABLE_WEBCONFERENCE: 1
+          VITE_WEBCONFERENCE: audio
           CYPRESS_INSTALL_BINARY: 0
       - run: |
           cat <<EOF > ./dist/_redirects

+ 11 - 5
backend/.env.dist

@@ -1,4 +1,4 @@
-# --- Ricochet configuration start here
+# --- Ricochet configuration starts here
 
 # Ricochet server port and host
 # Change this if you need external access
@@ -49,14 +49,20 @@ EMAIL_USER=
 EMAIL_PASSWORD=
 EMAIL_FROM=
 
+# To show better logs set to 1 (then use npm run ricochetjs:pino)
+USE_PINO=0
+
 # --- Ricochet configuration ends here
 
 # Wire.io port configuration
 WIREIO_PORT=4051
 
-# To show better logs set to 1 (then use npm run ricochetjs:pino)
-USE_PINO=0
-
 # Secret key generated by Ricochet.js when you've registered the site.
 # /!\ You MUST customize this value after registering the site to Ricochet.js
-RICOCHET_SITE_KEY=
+RICOCHET_SITE_KEY=
+
+# Webconference Openvidu configuration
+# You need to enable webconference on the frontend also
+# See https://docs.openvidu.io for more information
+OPENVIDU_URL=
+OPENVIDU_SECRET=

+ 5 - 2
src/utils/settings.js

@@ -35,5 +35,8 @@ export const SOCKET_OPTIONS = {
   transports: ["websocket"],
 };
 
-export const ENABLE_WEBCONFERENCE =
-  import.meta.env.VITE_ENABLE_WEBCONFERENCE === "1";
+export const WEBCONFERENCE = ["audio", "video"].includes(
+  import.meta.env.VITE_WEBCONFERENCE
+)
+  ? import.meta.env.VITE_WEBCONFERENCE
+  : false;

+ 1 - 4
src/views/BoardView/NavBar.jsx

@@ -11,7 +11,6 @@ import Touch from "../../ui/Touch";
 import WebConferenceButton from "../../webconf/WebConferenceButton";
 
 import { getBestTranslationFromConfig } from "../../utils/api";
-import { ENABLE_WEBCONFERENCE } from "../../utils/settings";
 import useLocalStorage from "../../hooks/useLocalStorage";
 
 import InfoModal from "./InfoModal";
@@ -247,9 +246,7 @@ const NavBar = ({ editMode }) => {
                 icon="add-user"
                 title={t("Invite more player")}
               />
-              {ENABLE_WEBCONFERENCE && isBeta && (
-                <WebConferenceButton room={room} />
-              )}
+              <WebConferenceButton room={room} />
             </>
           )}
           <div className="spacer" />

+ 1 - 3
src/views/RoomView/RoomNavBar.jsx

@@ -3,7 +3,6 @@ import React from "react";
 import styled from "styled-components";
 import { useTranslation } from "react-i18next";
 
-import { ENABLE_WEBCONFERENCE } from "../../utils/settings";
 import useLocalStorage from "../../hooks/useLocalStorage";
 import { useWire } from "react-sync-board";
 
@@ -121,7 +120,6 @@ const RoomNavBar = () => {
   const { t } = useTranslation();
   const { room } = useWire("room");
   const [showLink, setShowLink] = React.useState(false);
-  const [isBeta] = useLocalStorage("isBeta", false);
 
   return (
     <StyledNavBar>
@@ -144,7 +142,7 @@ const RoomNavBar = () => {
             title={t("Invite more player")}
           />
         }
-        {ENABLE_WEBCONFERENCE && isBeta && <WebConferenceButton room={room} />}
+        <WebConferenceButton room={room} />
       </div>
     </StyledNavBar>
   );

+ 0 - 139
src/webconf/StreamList.jsx

@@ -1,139 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-
-import useOpenVidu from "./useOpenVidu";
-import useLocalStorage from "../hooks/useLocalStorage";
-
-import LocalStream from "./LocalStream";
-import RemoteStream from "./RemoteStream";
-
-const StyledWebConference = styled.div`
-  position: fixed;
-  top: 5em;
-  right: 1em;
-  bottom: 5.5em;
-  z-index: 1;
-  width: 150px;
-  overflow-y: auto;
-  overflow-x: hidden;
-  padding: 0.5em;
-  & video {
-    border-radius: 5px;
-  }
-  & .local-stream {
-    position: relative;
-    & .actions {
-      position: absolute;
-      bottom: 2px;
-      right: 0;
-      left: 0;
-      display: flex;
-      justify-content: center;
-    }
-
-    & button {
-      padding: 5px;
-      margin: 5px;
-      border-radius: 50%;
-      opacity: 0.5;
-      width: 30px;
-      height: 30px;
-    }
-    & button.active {
-      background-color: var(--color-primary);
-    }
-    &:hover button {
-      opacity: 1;
-    }
-  }
-
-  & .remote-stream {
-    position: relative;
-    margin-bottom: 0.5em;
-    & .name {
-      position: absolute;
-      bottom: 0;
-      left: 0;
-      right: 0;
-      padding: 2px 5px;
-      background-color: #95959540;
-      text-align: center;
-      border-radius: 0 0 5px 5px;
-    }
-  }
-`;
-
-const WebConferenceContent = ({ users }) => {
-  const { remoteStreams = [], localStream } = useOpenVidu();
-
-  const [showLocalVideo, setShowLocalVideo] = useLocalStorage(
-    "showLocalVideo",
-    true
-  );
-  const [showLocalAudio, setShowLocalAudio] = useLocalStorage(
-    "showLocalAudio",
-    true
-  );
-
-  const toggleVideo = React.useCallback(() => {
-    setShowLocalVideo((prev) => !prev);
-  }, [setShowLocalVideo]);
-
-  const toggleAudio = React.useCallback(() => {
-    setShowLocalAudio((prev) => !prev);
-  }, [setShowLocalAudio]);
-
-  const streamMap = React.useMemo(
-    () =>
-      remoteStreams.reduce((acc, stream) => {
-        acc[stream.data.uid] = stream;
-        return acc;
-      }, {}),
-    [remoteStreams]
-  );
-
-  return (
-    <StyledWebConference>
-      {localStream && (
-        <div className="local-stream">
-          <LocalStream
-            stream={localStream}
-            video={showLocalVideo}
-            audio={showLocalAudio}
-          />
-          <div className="actions">
-            <button
-              onClick={toggleVideo}
-              className={showLocalVideo ? "active" : ""}
-            >
-              <img
-                src={"https://icongr.am/entypo/camera.svg?size=16&color=f9fbfa"}
-              />
-            </button>
-            <button
-              onClick={toggleAudio}
-              className={showLocalAudio ? "active" : ""}
-            >
-              <img
-                src={"https://icongr.am/entypo/mic.svg?size=16&color=f9fbfa"}
-              />
-            </button>
-          </div>
-        </div>
-      )}
-      {users.map(
-        ({ name, uid, color }) =>
-          streamMap[uid] && (
-            <div key={uid} className="remote-stream">
-              <RemoteStream stream={streamMap[uid]} />
-              <span className="name" style={{ backgroundColor: color }}>
-                {name}
-              </span>
-            </div>
-          )
-      )}
-    </StyledWebConference>
-  );
-};
-
-export default WebConferenceContent;

+ 94 - 4
src/webconf/WebConference.jsx

@@ -1,16 +1,96 @@
 import React from "react";
+import styled from "styled-components";
 
 import { getConfToken } from "../utils/api";
+import useLocalStorage from "../hooks/useLocalStorage";
 
-import { OpenViduProvider } from "./useOpenVidu";
-import StreamList from "./StreamList";
+import { OpenViduProvider, StreamList } from "./react-useOpenVidu";
 
-const WebConference = ({ room, currentUser, users }) => {
+const StyledWebConference = styled.div`
+  position: fixed;
+  top: 5em;
+  right: 1em;
+  line-height: 1em;
+  bottom: 5.5em;
+  z-index: 1;
+  width: ${({ audioOnly }) => (audioOnly ? 80 : 150)}px;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 0.5em;
+
+  .stream-list {
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+  }
+
+  & .local-stream {
+    position: relative;
+    & .actions {
+      position: absolute;
+      bottom: 2px;
+      right: 0;
+      left: 0;
+      display: flex;
+      justify-content: center;
+    }
+
+    & button {
+      padding: 0;
+      margin: 0;
+      background-color: transparent;
+      width: 30px;
+      height: 30px;
+    }
+  }
+
+  & .remote-stream {
+    position: relative;
+  }
+
+  & .user-stream {
+    position: relative;
+    border: 1px solid #333;
+    & .name {
+      line-height: 1.6em;
+      display: block;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      width: 100%;
+      padding: 1px 2px;
+      background-color: #95959540;
+      text-align: center;
+    }
+    & .mic {
+      position: absolute;
+      top: 2px;
+      right: 2px;
+      width: 30px;
+      height: 30px;
+      padding: 7px;
+      & img {
+        max-height: 100%;
+      }
+    }
+  }
+`;
+
+const WebConference = ({ room, currentUser, users, enableVideo = false }) => {
   const getUserData = React.useCallback(
     () => JSON.stringify({ uid: currentUser.uid }),
     [currentUser.uid]
   );
 
+  const [showLocalVideo, setShowLocalVideo] = useLocalStorage(
+    "wcEnableLocalVideo",
+    false
+  );
+  const [showLocalAudio, setShowLocalAudio] = useLocalStorage(
+    "wcEnableLocalAudio",
+    true
+  );
+
   return (
     <OpenViduProvider
       room={room}
@@ -18,7 +98,17 @@ const WebConference = ({ room, currentUser, users }) => {
       getUserData={getUserData}
       getToken={getConfToken}
     >
-      <StreamList users={users} />
+      <StyledWebConference audioOnly={true}>
+        <StreamList
+          currentUser={currentUser}
+          users={users}
+          audioOnly={!enableVideo}
+          setLocalAudio={setShowLocalAudio}
+          setLocalVideo={setShowLocalVideo}
+          enableLocalVideo={showLocalVideo}
+          enableLocalAudio={showLocalAudio}
+        />
+      </StyledWebConference>
     </OpenViduProvider>
   );
 };

+ 12 - 1
src/webconf/WebConferenceButton.jsx

@@ -5,6 +5,7 @@ import { useUsers } from "react-sync-board";
 
 import Touch from "../ui/Touch";
 import useLocalStorage from "../hooks/useLocalStorage";
+import { WEBCONFERENCE } from "../utils/settings";
 
 import WebConference from "./WebConference";
 
@@ -16,13 +17,22 @@ export const WeConferenceButton = ({ room }) => {
     false
   );
 
+  if (!WEBCONFERENCE) {
+    return null;
+  }
+
+  const icon =
+    WEBCONFERENCE === "video"
+      ? "https://icongr.am/material/webcam.svg?size=24&color=f9fbfa"
+      : "https://icongr.am/material/microphone.svg?size=24&color=f9fbfa";
+
   return (
     <>
       <Touch
         onClick={() => setWebConference((prev) => !prev)}
         alt={t("Web conference")}
         title={t("Web conference")}
-        icon="https://icongr.am/material/message-video.svg?size=24&color=f9fbfa"
+        icon={icon}
         active={webConference}
       />
       {webConference && (
@@ -30,6 +40,7 @@ export const WeConferenceButton = ({ room }) => {
           room={room}
           currentUser={currentUser}
           users={localUsers}
+          enableVideo={WEBCONFERENCE === "video"}
         />
       )}
     </>

+ 12 - 2
src/webconf/LocalStream.jsx → src/webconf/react-useOpenVidu/LocalStream.jsx

@@ -1,9 +1,10 @@
 import React from "react";
 
-import Stream from "./Stream";
+import UserStream from "./UserStream";
 
 const LocalStream = ({
   stream,
+  user,
   publish = true,
   audio = true,
   video = true,
@@ -46,7 +47,16 @@ const LocalStream = ({
     return null;
   }
 
-  return <Stream stream={stream} />;
+  return (
+    <UserStream
+      stream={stream}
+      self={true}
+      name={user.name}
+      color={user.color}
+      audio={audio}
+      video={video}
+    />
+  );
 };
 
 export default LocalStream;

+ 11 - 2
src/webconf/RemoteStream.jsx → src/webconf/react-useOpenVidu/RemoteStream.jsx

@@ -1,9 +1,10 @@
 import React from "react";
 
-import Stream from "./Stream";
+import UserStream from "./UserStream";
 
 const RemoteStream = ({
   stream,
+  user,
   subscribe = true,
   audio = true,
   video = true,
@@ -44,7 +45,15 @@ const RemoteStream = ({
     return null;
   }
 
-  return <Stream stream={stream} />;
+  return (
+    <UserStream
+      stream={stream}
+      name={user.name}
+      color={user.color}
+      audio={stream.stream.audioActive}
+      video={stream.stream.videoActive}
+    />
+  );
 };
 
 export default RemoteStream;

+ 0 - 0
src/webconf/Stream.jsx → src/webconf/react-useOpenVidu/Stream.jsx


+ 80 - 0
src/webconf/react-useOpenVidu/StreamList.jsx

@@ -0,0 +1,80 @@
+import React from "react";
+
+import useOpenVidu from "./useOpenVidu";
+
+import LocalStream from "./LocalStream";
+import RemoteStream from "./RemoteStream";
+
+const StreamList = ({
+  currentUser,
+  users,
+  audioOnly = true,
+  enableLocalVideo = false,
+  enableLocalAudio = true,
+  setLocalVideo = () => {},
+  setLocalAudio = () => {},
+}) => {
+  const { remoteStreams = [], localStream } = useOpenVidu();
+
+  const streamMap = React.useMemo(
+    () =>
+      remoteStreams.reduce((acc, stream) => {
+        acc[stream.data.uid] = stream;
+        return acc;
+      }, {}),
+    [remoteStreams]
+  );
+
+  return (
+    <div className="stream-list">
+      {localStream && (
+        <div className="local-stream">
+          <LocalStream
+            stream={localStream}
+            video={enableLocalVideo && !audioOnly}
+            audio={enableLocalAudio}
+            user={currentUser}
+          />
+          <div className="actions">
+            {!audioOnly && (
+              <button
+                onClick={() => setLocalVideo(!enableLocalVideo)}
+                className={enableLocalVideo ? "active" : ""}
+              >
+                <img
+                  src={
+                    enableLocalVideo
+                      ? "https://icongr.am/feather/camera.svg?size=16&color=f9fbfa"
+                      : "https://icongr.am/feather/camera-off.svg?size=16&color=f9fbfa"
+                  }
+                />
+              </button>
+            )}
+            <button
+              onClick={() => setLocalAudio(!enableLocalAudio)}
+              className={enableLocalAudio ? "active" : ""}
+            >
+              <img
+                src={
+                  enableLocalAudio
+                    ? "https://icongr.am/feather/mic.svg?size=16&color=f9fbfa"
+                    : "https://icongr.am/feather/mic-off.svg?size=16&color=f9fbfa"
+                }
+              />
+            </button>
+          </div>
+        </div>
+      )}
+      {users.map(
+        (user) =>
+          streamMap[user.uid] && (
+            <div key={user.uid} className="remote-stream">
+              <RemoteStream stream={streamMap[user.uid]} user={user} />
+            </div>
+          )
+      )}
+    </div>
+  );
+};
+
+export default StreamList;

+ 22 - 0
src/webconf/react-useOpenVidu/UserStream.jsx

@@ -0,0 +1,22 @@
+import React from "react";
+import Stream from "./Stream";
+
+const UserStream = ({ stream, name, color, audio, video, self = false }) => {
+  return (
+    <div className="user-stream">
+      <Stream stream={stream} />
+      {!self && !audio && (
+        <div className="mic">
+          <img src="https://icongr.am/feather/mic-off.svg?size=16&color=ffffff" />
+        </div>
+      )}
+      {!self && (
+        <span className="name" style={{ backgroundColor: color }}>
+          {name}
+        </span>
+      )}
+    </div>
+  );
+};
+
+export default UserStream;

+ 2 - 0
src/webconf/react-useOpenVidu/index.js

@@ -0,0 +1,2 @@
+export { default as useOpenVidu, OpenViduProvider } from "./useOpenVidu";
+export { default as StreamList } from "./StreamList";

+ 7 - 2
src/webconf/useOpenVidu.jsx → src/webconf/react-useOpenVidu/useOpenVidu.jsx

@@ -183,7 +183,7 @@ export const OpenViduProvider = ({
 }) => {
   const OVRef = React.useRef(new OpenVidu());
   const instanceRef = React.useRef({});
-  const [connected, setconnected] = React.useState(false);
+  const [connected, setConnected] = React.useState(false);
   const [remoteStreams, setRemoteStreams] = React.useState([]);
   const [localStream, setLocalStream] = React.useState(null);
 
@@ -215,6 +215,11 @@ export const OpenViduProvider = ({
         );
       });
 
+      newSession.on("streamPropertyChanged", () => {
+        // Just update the map to trigger the render
+        setRemoteStreams((prev) => prev.slice());
+      });
+
       const token = await getToken(room);
       await newSession.connect(token, getUserData());
 
@@ -222,7 +227,7 @@ export const OpenViduProvider = ({
         new LocalStream({ session: newSession, OV: OVRef.current })
       );
 
-      setconnected(true);
+      setConnected(true);
 
       instanceRef.current.session = newSession;
     },