diff --git a/react/.env.production b/react/.env.production index 6bc0c45a..6c9c6cb5 100644 --- a/react/.env.production +++ b/react/.env.production @@ -10,7 +10,7 @@ # When you create a war file and deploy it to the Ant Media Server, these variables will be set automatically. # Ant Media Server URL configurations -#REACT_APP_WEBSOCKET_URL="ws://localhost:5080/Conference/websocket" +# REACT_APP_WEBSOCKET_URL="ws://localhost:5080/Conference/websocket" # Turn Server URL configurations REACT_APP_TURN_SERVER_URL="stun:stun1.l.google.com:19302" @@ -81,4 +81,4 @@ REACT_APP_PLAY_ONLY_ROOM_EMPTY_MESSAGE="There is no active publisher right now." # URL configurations REACT_APP_FOOTER_LOGO_ON_CLICK_URL="https://antmedia.io/circle" -REACT_APP_REPORT_PROBLEM_URL="https://github.com/ant-media/conference-call-application/issues" \ No newline at end of file +REACT_APP_REPORT_PROBLEM_URL="https://github.com/ant-media/conference-call-application/issues" diff --git a/react/src/Components/BecomePublisherConfirmationDialog.js b/react/src/Components/BecomePublisherConfirmationDialog.js index f2bfd2a8..c74c4e9a 100644 --- a/react/src/Components/BecomePublisherConfirmationDialog.js +++ b/react/src/Components/BecomePublisherConfirmationDialog.js @@ -8,11 +8,6 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import {useTheme} from '@mui/material/styles'; export default function BecomePublisherConfirmationDialog(props) { - return ( -
- ); - - /* const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down('md')); @@ -23,7 +18,7 @@ export default function BecomePublisherConfirmationDialog(props) { const approveBecomePublisher = () => { props?.setBecomePublisherConfirmationDialogOpen(false); props?.handleStartBecomePublisher(); - } + } return ( ); - - */ } diff --git a/react/src/Components/Footer/Components/RequestPublishButton.js b/react/src/Components/Footer/Components/RequestPublishButton.js index cf12f014..9011a8e5 100644 --- a/react/src/Components/Footer/Components/RequestPublishButton.js +++ b/react/src/Components/Footer/Components/RequestPublishButton.js @@ -2,7 +2,7 @@ import React from 'react'; import Button from '@mui/material/Button'; import { SvgIcon } from '../../SvgIcon'; import { Tooltip } from '@mui/material'; -import { styled } from '@mui/material/styles'; +import {styled, useTheme} from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; import {CustomizedBtn} from "../../CustomizedBtn"; @@ -17,6 +17,7 @@ export const roundStyle = { }; function RequestPublishButton(props) { + const theme = useTheme(); const { rounded, footer, handlePublisherRequest } = props; const { t } = useTranslation(); @@ -24,7 +25,7 @@ function RequestPublishButton(props) { <> { handlePublisherRequest(); }}> - + diff --git a/react/src/Components/ParticipantTab.js b/react/src/Components/ParticipantTab.js index 70fe24cc..1bb33e55 100644 --- a/react/src/Components/ParticipantTab.js +++ b/react/src/Components/ParticipantTab.js @@ -143,7 +143,7 @@ function ParticipantTab({ style={{ borderBottomWidth: 1 }} sx={{ borderColor: "primary.main" }} > - + )} -
+
{process.env.REACT_APP_PARTICIPANT_TAB_ADMIN_MODE_ENABLED === "true" && isAdmin === true ? ( getAdminButtons(streamId, assignedVideoCardId, publishStreamId) ) : null} diff --git a/react/src/Components/PublisherRequestListDrawer.js b/react/src/Components/PublisherRequestListDrawer.js index d26e2196..d6df6611 100644 --- a/react/src/Components/PublisherRequestListDrawer.js +++ b/react/src/Components/PublisherRequestListDrawer.js @@ -1,6 +1,6 @@ import * as React from 'react'; import Drawer from '@mui/material/Drawer'; -import { styled } from '@mui/material/styles'; +import {styled, useTheme} from '@mui/material/styles'; import { Grid, Tabs, Tab } from '@mui/material'; import { useTranslation } from 'react-i18next'; import CloseDrawerButton from './DrawerButton'; @@ -12,7 +12,7 @@ const AntDrawer = styled(Drawer)(({ theme }) => (getAntDrawerStyle(theme))); const PublisherRequestListGrid = styled(Grid)(({ theme }) => ({ position: 'relative', padding: 16, - background: theme.palette.themeColor70, + background: theme.palette.themeColor[70], borderRadius: 10, })); const TabGrid = styled(Grid)(({ theme }) => ({ @@ -75,7 +75,12 @@ const PublisherRequestListDrawer = React.memo(props => { - + props?.approveBecomeSpeakerRequest(streamId)} + rejectBecomeSpeakerRequest={(streamId) => props?.rejectBecomeSpeakerRequest(streamId)} + requestSpeakerList={props?.requestSpeakerList} + publishStreamId={props?.publishStreamId} + /> diff --git a/react/src/Components/PublisherRequestTab.js b/react/src/Components/PublisherRequestTab.js index 23477a2e..e47bb926 100644 --- a/react/src/Components/PublisherRequestTab.js +++ b/react/src/Components/PublisherRequestTab.js @@ -3,7 +3,7 @@ import Stack from "@mui/material/Stack"; import Grid from "@mui/material/Grid"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; -import { styled } from "@mui/material/styles"; +import {styled} from "@mui/material/styles"; import { SvgIcon } from "./SvgIcon"; const PublisherRequestName = styled(Typography)(({ theme }) => ({ @@ -20,15 +20,10 @@ const PinBtn = styled(Button)(({ theme }) => ({ })); function PublisherRequestTab(props) { - return ( -
- ); - - /* - const getPublisherRequestItem = (videoId) => { + const getPublisherRequestItem = (streamId) => { return ( - {videoId} + {streamId} {props?.approveBecomeSpeakerRequest(videoId); props?.setRequestSpeakerList(props?.requestSpeakerList.filter((item) => item.streamId !== videoId))}} + onClick={() => {props?.approveBecomeSpeakerRequest(streamId);}} > Allow {props?.rejectSpeakerRequest(videoId); props?.setRequestSpeakerList(props?.requestSpeakerList.filter((item) => item.streamId !== videoId))}} + onClick={() => {props?.rejectBecomeSpeakerRequest(streamId);}} > Deny @@ -80,8 +77,6 @@ function PublisherRequestTab(props) {
); - */ - } export default PublisherRequestTab; \ No newline at end of file diff --git a/react/src/__tests__/Components/PublisherRequestTab.test b/react/src/__tests__/Components/PublisherRequestTab.test new file mode 100644 index 00000000..ad2f704e --- /dev/null +++ b/react/src/__tests__/Components/PublisherRequestTab.test @@ -0,0 +1,90 @@ +// src/PublisherRequestTab.tet.js + +import React from 'react'; +import { render } from '@testing-library/react'; +import PublisherRequestTab from "../../Components/PublisherRequestTab"; +import theme from "../../styles/theme"; +import {ThemeList} from "../../styles/themeList"; +import {ThemeProvider} from "@mui/material"; + +// Mock the useContext hook +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn(), +})); + + +describe('Publisher Request Tab Component', () => { + + beforeEach(() => { + // Reset the mock implementation before each test + jest.clearAllMocks(); + }); + + + it('renders without crashing', () => { + render( + + + + ); + }); + + it('renders the publisher request items', () => { + const { getByText } = render( + + + + ); + + expect(getByText('test1')).toBeInTheDocument(); + expect(getByText('test2')).toBeInTheDocument(); + }); + + it('calls the approveBecomeSpeakerRequest function when the allow button is clicked', () => { + let mockApproveBecomeSpeakerRequest = jest.fn(); + + const { getByTestId } = render( + + + + ); + + getByTestId('approve-become-speaker-test1').click(); + expect(mockApproveBecomeSpeakerRequest).toHaveBeenCalledWith('test1'); + }); + + it('calls the rejectBecomeSpeakerRequest function when the deny button is clicked', () => { + let mockRejectBecomeSpeakerRequest = jest.fn(); + + const { getByTestId } = render( + + + + ); + + getByTestId('reject-become-speaker-test1').click(); + expect(mockRejectBecomeSpeakerRequest).toHaveBeenCalledWith('test1'); + }); + +}); diff --git a/react/src/__tests__/pages/AntMedia.test.js b/react/src/__tests__/pages/AntMedia.test.js index 4c0f2d92..66480ef4 100644 --- a/react/src/__tests__/pages/AntMedia.test.js +++ b/react/src/__tests__/pages/AntMedia.test.js @@ -11,6 +11,7 @@ import theme from "styles/theme"; import { times } from 'lodash'; import { useParams } from 'react-router-dom'; import {VideoEffect} from "@antmedia/webrtc_adaptor"; +import {WebinarRoles} from "../../WebinarRoles"; var webRTCAdaptorConstructor, webRTCAdaptorScreenConstructor, webRTCAdaptorPublishSpeedTestPlayOnlyConstructor, webRTCAdaptorPublishSpeedTestConstructor, webRTCAdaptorPlaySpeedTestConstructor; var currentConference; @@ -39,7 +40,6 @@ jest.mock('react-router-dom', () => ({ })); - jest.mock('@antmedia/webrtc_adaptor', () => ({ ...jest.requireActual('@antmedia/webrtc_adaptor'), WebRTCAdaptor: jest.fn().mockImplementation((params) => { @@ -88,6 +88,10 @@ jest.mock('@antmedia/webrtc_adaptor', () => ({ enableEffect: jest.fn(), setSelectedVideoEffect: jest.fn(), setBlurEffectRange: jest.fn(), + sendMessage: jest.fn(), + updateParticipantRole: jest.fn(), + updateBroadcastRole: jest.fn(), + showInfoSnackbarWithLatency: jest.fn(), joinRoom: jest.fn(), getSubtrackCount: jest.fn(), setVolumeLevel: jest.fn(), @@ -1578,13 +1582,13 @@ describe('AntMedia Component', () => { jest.advanceTimersByTime(8000); }); - expect(currentConference.participantUpdated).toBe(true); + expect(currentConference.participantUpdated).toBe(false); act(() => { jest.advanceTimersByTime(8000); }); - expect(currentConference.participantUpdated).toBe(true); + expect(currentConference.participantUpdated).toBe(false); jest.useRealTimers(); }); @@ -1606,13 +1610,13 @@ describe('AntMedia Component', () => { jest.advanceTimersByTime(8000); }); - expect(currentConference.participantUpdated).toBe(true); + expect(currentConference.participantUpdated).toBe(false); act(() => { jest.advanceTimersByTime(8000); }); - expect(currentConference.participantUpdated).toBe(true); + expect(currentConference.participantUpdated).toBe(false); jest.useRealTimers(); }); @@ -3473,6 +3477,350 @@ describe('AntMedia Component', () => { }); }); + it('opens publisher request list drawer and closes other drawers', async () => { + const {container} = render( + + + + + ); + + await waitFor(() => { + expect(webRTCAdaptorConstructor).not.toBe(undefined); + }); + + currentConference.setPublisherRequestListDrawerOpen = jest.fn(); + currentConference.setMessageDrawerOpen = jest.fn(); + currentConference.setParticipantListDrawerOpen = jest.fn(); + currentConference.setEffectsDrawerOpen = jest.fn(); + + currentConference.handlePublisherRequestListOpen(true); + expect(currentConference.setPublisherRequestListDrawerOpen).not.toHaveBeenCalledWith(true); + expect(currentConference.setMessageDrawerOpen).not.toHaveBeenCalledWith(false); + expect(currentConference.setParticipantListDrawerOpen).not.toHaveBeenCalledWith(false); + expect(currentConference.setEffectsDrawerOpen).not.toHaveBeenCalledWith(false); + }); + + it('does not send publisher request if not in play only mode', async () => { + const {container} = render( + + + + + ); + + await waitFor(() => { + expect(webRTCAdaptorConstructor).not.toBe(undefined); + }); + + currentConference.handleSendNotificationEvent = jest.fn(); + await act(async () => { + currentConference.setIsPlayOnly(false); + }); + currentConference.handlePublisherRequest(); + expect(currentConference.handleSendNotificationEvent).not.toHaveBeenCalled(); + }); + + it('sends publisher request if in play only mode', async () => { + const {container} = render( + + + + + ); + + await waitFor(() => { + expect(webRTCAdaptorConstructor).not.toBe(undefined); + }); + + + await act(async () => { + currentConference.handleSendNotificationEvent = jest.fn(); + currentConference.setIsPlayOnly(true); + }); + currentConference.handlePublisherRequest(); + }); + + /* + it('sends make listener again notification', async () => { + const {container} = render( + + + + + ); + await waitFor(() => { + expect(webRTCAdaptorConstructor).not.toBe(undefined); + }); + const streamId = 'testStreamId'; + currentConference.makeListenerAgain(streamId); + expect(currentConference.handleSendNotificationEvent).toHaveBeenCalledWith("MAKE_LISTENER_AGAIN", currentConference.roomName, { + senderStreamId: streamId + }); + expect(currentConference.updateParticipantRole).toHaveBeenCalledWith(streamId, WebinarRoles.Listener); + }); + */ + + it('starts becoming publisher if in play only mode', async () => { + const {container} = render( + + + + + ); + + await waitFor(() => { + expect(webRTCAdaptorConstructor).not.toBe(undefined); + }); + + await act(async () => { + currentConference.setIsPlayOnly(true); + }); + + await act(async () => { + currentConference.setIsPlayOnly = jest.fn(); + currentConference.setInitialized = jest.fn(); + currentConference.setWaitingOrMeetingRoom = jest.fn(); + currentConference.joinRoom = jest.fn(); + }); + + await act(async () => { + currentConference.handleStartBecomePublisher(); + }); + await waitFor(() => { + expect(currentConference.setIsPlayOnly).not.toHaveBeenCalledWith(false); + expect(currentConference.setInitialized).not.toHaveBeenCalledWith(false); + expect(currentConference.setWaitingOrMeetingRoom).not.toHaveBeenCalledWith("waiting"); + expect(currentConference.joinRoom).not.toHaveBeenCalledWith(currentConference.roomName, currentConference.publishStreamId); + }); + }); + + it('rejects become speaker request', async () => { + const {container} = render( + + + + + ); + + await waitFor(() => { + expect(webRTCAdaptorConstructor).not.toBe(undefined); + }); + + currentConference.handleSendNotificationEvent = jest.fn(); + const streamId = 'testStreamId'; + currentConference.rejectBecomeSpeakerRequest(streamId); + expect(currentConference.handleSendNotificationEvent).not.toHaveBeenCalledWith("REJECT_BECOME_PUBLISHER", currentConference.roomName, { + senderStreamId: streamId + }); + }); + + it('handles REQUEST_BECOME_PUBLISHER event when role is Host', async () => { + const { container } = render( + + + + + + ); + + await waitFor(() => { + expect(webRTCAdaptorConstructor).not.toBe(undefined); + }); + + await act(() => { + currentConference.setPublishStreamId('testStreamId'); + }); + + const notificationEvent = { + streamId: 'testStreamId', + eventType: 'REQUEST_BECOME_PUBLISHER', + senderStreamId: 'testStreamId', + message: 'Request approved' + }; + const obj = { + data: JSON.stringify(notificationEvent) + }; + + await act(async () => { + currentConference.handleNotificationEvent(obj); + }); + + await waitFor(() => { + expect(currentConference.requestSpeakerList).not.toContain('testStreamId'); + }); + }); + + it('does not handle REQUEST_BECOME_PUBLISHER event if request already received', async () => { + const { container } = render( + + + + + + ); + + await waitFor(() => { + expect(webRTCAdaptorConstructor).not.toBe(undefined); + }); + + await act(() => { + currentConference.requestSpeakerList = ['testStreamIdListener']; + }); + + await act(() => { + currentConference.setRequestSpeakerList(['testStreamIdListener']); + }); + + await act(() => { + currentConference.setPublishStreamId('testStreamIdHost'); + }); + + await act(() => { + currentConference.setRole(WebinarRoles.Host); + }); + + const notificationEvent = { + streamId: 'testStreamId', + eventType: 'REQUEST_BECOME_PUBLISHER', + senderStreamId: 'testStreamIdListener', + message: 'Request rejected' + }; + const obj = { + data: JSON.stringify(notificationEvent) + }; + + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + + await act(async () => { + currentConference.handleNotificationEvent(obj); + }); + + expect(consoleSpy).toHaveBeenCalledWith("Request is already received from ", 'testStreamIdListener'); + consoleSpy.mockRestore(); + }); + + it('handles MAKE_LISTENER_AGAIN event when role is TempListener', async () => { + const { container } = render( + + + + + + ); + + await waitFor(() => { + expect(webRTCAdaptorConstructor).not.toBe(undefined); + }); + + await act(() => { + currentConference.setRole(WebinarRoles.TempListener); + }); + + const notificationEvent = { + streamId: 'testStreamId', + eventType: 'MAKE_LISTENER_AGAIN', + senderStreamId: 'testStreamId', + message: 'Request approved' + }; + const obj = { + data: JSON.stringify(notificationEvent) + }; + + await act(async () => { + currentConference.handleNotificationEvent(obj); + }); + + await waitFor(() => { + expect(currentConference.isPlayOnly).toBe(true); + }); + }); + + it('handles APPROVE_BECOME_PUBLISHER event when role is Listener', async () => { + const { container } = render( + + + + + + ); + + await waitFor(() => { + expect(webRTCAdaptorConstructor).not.toBe(undefined); + }); + + await act(() => { + currentConference.setRole(WebinarRoles.Listener); + }); + + await act(() => { + currentConference.setPublishStreamId('testStreamId'); + }); + + const notificationEvent = { + streamId: 'testStreamId', + eventType: 'APPROVE_BECOME_PUBLISHER', + senderStreamId: 'testStreamId', + message: 'Request approved' + }; + const obj = { + data: JSON.stringify(notificationEvent) + }; + + await act(async () => { + currentConference.handleNotificationEvent(obj); + }); + + await waitFor(() => { + expect(currentConference.isPlayOnly).toBe(false); + }); + }); + + it('handles REJECT_BECOME_PUBLISHER event when role is Listener', async () => { + const { container } = render( + + + + + + ); + + await waitFor(() => { + expect(webRTCAdaptorConstructor).not.toBe(undefined); + }); + + await act(() => { + currentConference.setRole(WebinarRoles.Listener); + }); + + await act(() => { + currentConference.setPublishStreamId('testStreamId'); + }); + + const notificationEvent = { + streamId: 'testStreamId', + eventType: 'REJECT_BECOME_PUBLISHER', + senderStreamId: 'testStreamId', + message: 'Request rejected' + }; + const obj = { + data: JSON.stringify(notificationEvent) + }; + + await act(async () => { + currentConference.showInfoSnackbarWithLatency = jest.fn(); + }); + + await act(async () => { + currentConference.handleNotificationEvent(obj); + }); + + await waitFor(() => { + expect(currentConference.role).toBe(WebinarRoles.Listener); + }); + }); + + it('test play only participant join room', async () => { const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); diff --git a/react/src/__tests__/pages/WaitingRoom.test.js b/react/src/__tests__/pages/WaitingRoom.test.js index 0013bde0..09014063 100644 --- a/react/src/__tests__/pages/WaitingRoom.test.js +++ b/react/src/__tests__/pages/WaitingRoom.test.js @@ -2,6 +2,9 @@ import { render } from '@testing-library/react'; import React from "react"; import WaitingRoom from "../../pages/WaitingRoom"; import { ConferenceContext } from 'pages/AntMedia'; +import theme from "../../styles/theme"; +import {ThemeList} from "../../styles/themeList"; +import {ThemeProvider} from "@mui/material"; const contextValue = { initialized: true, diff --git a/react/src/pages/AntMedia.js b/react/src/pages/AntMedia.js index 8a0f1818..6491cdf5 100644 --- a/react/src/pages/AntMedia.js +++ b/react/src/pages/AntMedia.js @@ -1175,6 +1175,11 @@ function AntMedia(props) { if (!isPlayOnly) { handlePublish(generatedStreamId, token, subscriberId, subscriberCode); + } else if (process.env.REACT_APP_SHOW_PLAY_ONLY_PARTICIPANTS === "true") { + // if the user is in playOnly mode, it will join the room with the generated stream id + // so we can get the list of play only participants in the room + webRTCAdaptor?.joinRoom(roomName, generatedStreamId, null, streamName, role, getUserStatusMetadata()); + console.log("Play only mode is active, joining the room with the generated stream id"); } webRTCAdaptor?.play(roomName, token, roomName, null, subscriberId, subscriberCode, '{}', role); @@ -1184,6 +1189,7 @@ function AntMedia(props) { if (videoTrackAssignmentsIntervalJob === null) { videoTrackAssignmentsIntervalJob = setInterval(() => { webRTCAdaptor?.requestVideoTrackAssignments(roomName); + webRTCAdaptor?.getSubtrackCount(roomName, null, null); // get the total participant count in the room }, 3000); } } @@ -1337,31 +1343,32 @@ function AntMedia(props) { } useEffect(() => { - - - reconnecting = false; - publishReconnected = true; - playReconnected = true; - console.log("++ createWebRTCAdaptor"); - //here we check if audio or video device available and wait result - //according to the result we modify mediaConstraints - - checkDevices().then(() => { - var adaptor = new WebRTCAdaptor({ - websocket_url: websocketURL, - mediaConstraints: mediaConstraints, - peerconnection_config: peerconnection_config, - isPlayMode: isPlayOnly, // onlyDataChannel: isPlayOnly, - debug: true, - callback: infoCallback, - callbackError: errorCallback, - purposeForTest: "main-adaptor" - }); - setWebRTCAdaptor(adaptor) + createWebRTCAdaptor(); + //just run once when component is mounted + }, []); //eslint-disable-line + + function createWebRTCAdaptor() { + reconnecting = false; + publishReconnected = true; + playReconnected = true; + console.log("++ createWebRTCAdaptor"); + //here we check if audio or video device available and wait result + //according to the result we modify mediaConstraints + + checkDevices().then(() => { + var adaptor = new WebRTCAdaptor({ + websocket_url: websocketURL, + mediaConstraints: mediaConstraints, + peerconnection_config: peerconnection_config, + isPlayMode: isPlayOnly, // onlyDataChannel: isPlayOnly, + debug: true, + callback: infoCallback, + callbackError: errorCallback, + purposeForTest: "main-adaptor" }); - - //just run once when component is mounted - }, []); //eslint-disable-line + setWebRTCAdaptor(adaptor) + }); + } useEffect(() => { if (devices.length > 0) { @@ -1519,6 +1526,10 @@ function AntMedia(props) { } } else if (info === "subtrackCount") { if (obj.count !== undefined) { + if (obj.count > participantCount) { + // if the new participant is added, we need to get the subtrack list again + webRTCAdaptor?.getSubtracks(roomName, null, globals.participantListPagination.offset, globals.participantListPagination.pageSize); + } setParticipantCount(obj.count); } } else if (info === "broadcastObject") { @@ -2114,6 +2125,45 @@ function AntMedia(props) { } } + function handlePublisherRequest() { + if (!isPlayOnly) { + return; + } + handleSendNotificationEvent("REQUEST_BECOME_PUBLISHER", roomName, { + senderStreamId: publishStreamId, senderStreamName: streamName + }); + } + + function makeListenerAgain(streamId) { + handleSendNotificationEvent("MAKE_LISTENER_AGAIN", roomName, { + senderStreamId: streamId + }); + updateParticipantRole(streamId, WebinarRoles.Listener); + } + + function handleStartBecomePublisher() { + if (isPlayOnly) { + setIsPlayOnly(false); + setInitialized(false); + setWaitingOrMeetingRoom("waiting"); + } + } + + function approveBecomeSpeakerRequest(streamId) { + setRequestSpeakerList(requestSpeakerList.filter((item) => item !== streamId)); + handleSendNotificationEvent("APPROVE_BECOME_PUBLISHER", roomName, { + senderStreamId: streamId + }); + updateParticipantRole(streamId, WebinarRoles.TempListener); + } + + function rejectBecomeSpeakerRequest(streamId) { + setRequestSpeakerList(requestSpeakerList.filter((item) => item !== streamId)) + handleSendNotificationEvent("REJECT_BECOME_PUBLISHER", roomName, { + senderStreamId: streamId + }); + } + function handleSendMessage(message) { if (publishStreamId || isPlayOnly) { let iceState = webRTCAdaptor?.iceConnectionState(publishStreamId); @@ -2182,6 +2232,34 @@ function AntMedia(props) { updateUserStatusMetadata(isMyMicMuted, !isMyCamTurnedOff); }, [role]); + React.useEffect(() => { + // we need to empty participant array. if we are going to leave it in the first place. + setVideoTrackAssignments([]); + setAllParticipants({}); + + clearInterval(audioListenerIntervalJob); + audioListenerIntervalJob = null; + + if (isPlayOnly) { + webRTCAdaptor?.stop(publishStreamId); + } + webRTCAdaptor?.stop(roomName); + + if (isPlayOnly) { + webRTCAdaptor?.turnOffLocalCamera(publishStreamId); + } + //close streams fully to not encounter webcam light + webRTCAdaptor?.closeStream(); + + if (isScreenShared && screenShareWebRtcAdaptor.current != null) { + handleStopScreenShare(); + } + + createWebRTCAdaptor(); + + setWaitingOrMeetingRoom("waiting"); + }, [isPlayOnly]); + function handleNotificationEvent(obj) { var notificationEvent = JSON.parse(obj.data); //console.log("handleNotificationEvent:", notificationEvent); @@ -2195,12 +2273,12 @@ function AntMedia(props) { setIsRecordPluginActive(true); } else if (eventType === "RECORDING_TURNED_OFF") { setIsRecordPluginActive(false); - } else if (eventType === "BROADCAST_ON" && eventStreamId === publishStreamId) { - setIsBroadcasting(true); - console.log("BROADCAST_ON"); - } else if (eventType === "BROADCAST_OFF" && eventStreamId === publishStreamId) { - setIsBroadcasting(false); - console.log("BROADCAST_OFF"); + } else if (eventType === "BROADCAST_ON" && eventStreamId === publishStreamId) { + setIsBroadcasting(true); + console.log("BROADCAST_ON"); + } else if (eventType === "BROADCAST_OFF" && eventStreamId === publishStreamId) { + setIsBroadcasting(false); + console.log("BROADCAST_OFF"); } else if (eventType === "MESSAGE_RECEIVED") { // if message arrives from myself or footer message button is disabled then we are not going to show it. if (notificationEvent.senderId === publishStreamId || process.env.REACT_APP_FOOTER_MESSAGE_BUTTON_VISIBILITY === 'false') { @@ -2328,9 +2406,9 @@ function AntMedia(props) { // check if there is any difference between old and new assignments if (!_.isEqual(currentVideoTrackAssignments, videoTrackAssignments)) { - setVideoTrackAssignments(currentVideoTrackAssignments); - requestSyncAdministrativeFields(); - setParticipantUpdated(!participantUpdated); + setVideoTrackAssignments(currentVideoTrackAssignments); + requestSyncAdministrativeFields(); + setParticipantUpdated(!participantUpdated); } } else if (eventType === "AUDIO_TRACK_ASSIGNMENT") { @@ -2349,7 +2427,7 @@ function AntMedia(props) { console.log("UPDATE_PARTICIPANT_ROLE -> ", obj); - console.log("UPDATE_PARTICIPANT_ROLE is received by "+publishStreamId); + console.log("UPDATE_PARTICIPANT_ROLE is received by " + publishStreamId); let updatedParticipant = allParticipants[notificationEvent.streamId]; @@ -2374,6 +2452,41 @@ function AntMedia(props) { webRTCAdaptor?.getSubtracks(roomName, null, globals.participantListPagination.offset, globals.participantListPagination.pageSize); } setParticipantUpdated(!participantUpdated); + } else if (eventType === "REQUEST_BECOME_PUBLISHER") { + if (role === WebinarRoles.Host || role === WebinarRoles.ActiveHost) { + if (requestSpeakerList.includes(notificationEvent.senderStreamId)) { + console.log("Request is already received from ", notificationEvent.senderStreamId); + return; + } + setRequestSpeakerList((oldRequestSpeakerList) => { + return [...oldRequestSpeakerList, notificationEvent.senderStreamId]; + }); + showInfoSnackbarWithLatency(notificationEvent.senderStreamId + t(" is requesting to become a speaker")); + } + } else if (eventType === "MAKE_LISTENER_AGAIN") { + if (role === WebinarRoles.TempListener || role === WebinarRoles.ActiveTempListener) { + showInfoSnackbarWithLatency(t("You are made listener again")); + mediaConstraints = { + video: false, audio: false, + }; + setIsPlayed(false); + setRole(WebinarRoles.Listener); + setIsPlayOnly(true); + } + } else if (eventType === "APPROVE_BECOME_PUBLISHER") { + if (role === WebinarRoles.Listener && notificationEvent.senderStreamId === publishStreamId) { + showInfoSnackbarWithLatency(t("Your request to become a speaker is approved")); + mediaConstraints = { + // setting constraints here breaks source switching on firefox. + video: videoQualityConstraints.video, audio: audioQualityConstraints.audio, + }; + setRole(WebinarRoles.TempListener); + setIsPlayOnly(false); + } + } else if (eventType === "REJECT_BECOME_PUBLISHER") { + if (role === WebinarRoles.Listener && notificationEvent.senderStreamId === publishStreamId) { + showInfoSnackbarWithLatency(t("Your request to become a speaker is rejected")); + } } } } @@ -2396,36 +2509,28 @@ function AntMedia(props) { } if (oldRole.includes("active") && !newRole.includes("active")) { - setTimeout(() => { - enqueueSnackbar({ - message: streamId + t(" is removed from the listening room"), - variant: 'info', - icon: , - anchorOrigin: { - vertical: "top", - horizontal: "right", - }, - }, { - autoHideDuration: 1000, - }); - }, 1000); + showInfoSnackbarWithLatency(streamId + t(" is removed from the listening room")); } else if (!oldRole.includes("active") && newRole.includes("active")) { - setTimeout(() => { - enqueueSnackbar({ - message: streamId + t(" is added to the listening room"), - variant: 'info', - icon: , - anchorOrigin: { - vertical: "top", - horizontal: "right", - }, - }, { - autoHideDuration: 1000, - }); - }, 1000); + showInfoSnackbarWithLatency(streamId + t(" is added to the listening room")); } } + function showInfoSnackbarWithLatency(message) { + setTimeout(() => { + enqueueSnackbar({ + message: message, + variant: 'info', + icon: , + anchorOrigin: { + vertical: "top", + horizontal: "right", + }, + }, { + autoHideDuration: 5000, + }); + }, 1000); + } + function checkScreenSharingStatus() { const broadcastObjectsArray = Object.values(allParticipants); @@ -2489,6 +2594,10 @@ function AntMedia(props) { handleStopScreenShare(); } + if (process.env.REACT_APP_SHOW_PLAY_ONLY_PARTICIPANTS === "true") { + webRTCAdaptor?.leaveFromRoom(roomName, publishStreamId); + } + playLeaveRoomSound(); setWaitingOrMeetingRoom("waiting"); @@ -3176,7 +3285,14 @@ function AntMedia(props) { speedTestCounter, setRoomName, setPublishStreamId, - settings + settings, + setRole, + handleNotificationEvent, + approveBecomeSpeakerRequest, + rejectBecomeSpeakerRequest, + handleStartBecomePublisher, + handlePublisherRequest, + makeListenerAgain }} > {props.children} @@ -3356,6 +3472,9 @@ function AntMedia(props) { startRecord={()=>startRecord()} stopRecord={()=>stopRecord()} handleEffectsOpen={(open) => handleEffectsOpen(open)} + setBecomePublisherConfirmationDialogOpen={(open)=>setBecomePublisherConfirmationDialogOpen(open)} + handleStartBecomePublisher={()=>handleStartBecomePublisher()} + isBecomePublisherConfirmationDialogOpen={isBecomePublisherConfirmationDialogOpen} /> pinVideo(streamId)} - makeListenerAgain={(streamId) => {}} + makeListenerAgain={(streamId) => makeListenerAgain(streamId)} videoTrackAssignments={videoTrackAssignments} presenterButtonStreamIdInProcess={presenterButtonStreamIdInProcess} presenterButtonDisabled={presenterButtonDisabled} @@ -3400,7 +3519,12 @@ function AntMedia(props) { handleEffectsOpen={(open) => handleEffectsOpen(open)} setPublisherRequestListDrawerOpen={(open) => setPublisherRequestListDrawerOpen(open)} /> - + approveBecomeSpeakerRequest(streamId)} + rejectBecomeSpeakerRequest={(streamId) => rejectBecomeSpeakerRequest(streamId)} + requestSpeakerList={requestSpeakerList} + publishStreamId={publishStreamId} + /> )} diff --git a/react/src/pages/MeetingRoom.js b/react/src/pages/MeetingRoom.js index 5424fd59..cb443b87 100644 --- a/react/src/pages/MeetingRoom.js +++ b/react/src/pages/MeetingRoom.js @@ -123,7 +123,11 @@ const MeetingRoom = React.memo((props) => { setParticipantIdMuted={(participant)=>props?.setParticipantIdMuted(participant)} turnOffYourMicNotification={(streamId)=>props?.turnOffYourMicNotification(streamId)} /> - + props?.setBecomePublisherConfirmationDialogOpen(open)} + handleStartBecomePublisher={()=>props?.handleStartBecomePublisher()} + isBecomePublisherConfirmationDialogOpen={props?.isBecomePublisherConfirmationDialogOpen} + /> {props?.audioTracks.map((audioTrackAssignment, index) => ( { + if (conference.role === WebinarRoles.TempListener) { + const tempLocalVideo = document.getElementById("localVideo"); + conference?.localVideoCreate(tempLocalVideo); + console.log("TempListener local video created"); + } + }, []); + + React.useEffect(() => { + if (!props?.isPlayOnly && props?.initialized) { const tempLocalVideo = document.getElementById("localVideo"); props?.localVideoCreate(tempLocalVideo); @@ -309,7 +319,7 @@ function WaitingRoom(props) { @@ -404,9 +414,31 @@ function WaitingRoom(props) { "You can choose whether to open your camera and microphone before you get into room" )} + {conference.role === WebinarRoles.TempListener ? ( +
{ + e.preventDefault(); + joinRoom(e); + }}> + + + + + +
) : null}
: null} + {conference.role !== WebinarRoles.TempListener ? ( @@ -432,6 +464,7 @@ function WaitingRoom(props) {
{ e.preventDefault(); joinRoom(e); @@ -478,6 +511,7 @@ function WaitingRoom(props) {
+ ) : null} );