Merge pull request #20134 from JoVictorNunes/video-streams-typings-fix
fix: tweak video-streams typings
This commit is contained in:
commit
f742a6d25d
@ -297,6 +297,7 @@ export interface Kurento {
|
||||
cameraQualityThresholds: CameraQualityThresholds
|
||||
pagination: Pagination
|
||||
paginationThresholds: PaginationThresholds
|
||||
videoMediaServer?: string
|
||||
}
|
||||
|
||||
export interface CameraWsOptions {
|
||||
|
@ -15,7 +15,7 @@ import NotificationsBarContainer from '../notifications-bar/container';
|
||||
import AudioContainer from '../audio/container';
|
||||
import BannerBarContainer from '/imports/ui/components/banner-bar/container';
|
||||
import RaiseHandNotifier from '/imports/ui/components/raisehand-notifier/container';
|
||||
import ManyWebcamsNotifier from '/imports/ui/components/video-provider/many-users-notify/container';
|
||||
import ManyWebcamsNotifier from '/imports/ui/components/video-provider/video-provider-graphql/many-users-notify/container';
|
||||
import AudioCaptionsSpeechContainer from '/imports/ui/components/audio/audio-graphql/audio-captions/speech/component';
|
||||
import UploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
|
||||
import ScreenReaderAlertContainer from '../screenreader-alert/container';
|
||||
|
@ -37,7 +37,7 @@ const STATUS = {
|
||||
|
||||
/**
|
||||
* HOC for injecting a file reader utility.
|
||||
* @param {React.Component} Component
|
||||
* @param {(props: any) => JSX.Element} Component
|
||||
* @param {string[]} mimeTypesAllowed String array containing MIME types allowed.
|
||||
* @param {number} maxFileSize Max file size allowed in Mbytes.
|
||||
* @returns A new component which accepts the same props as the wrapped component plus
|
||||
|
@ -40,12 +40,16 @@ class ConnectionStatusButton extends PureComponent {
|
||||
setModalIsOpen = (isOpen) => this.setState({ isModalOpen: isOpen });
|
||||
|
||||
renderModal(isModalOpen) {
|
||||
const { isGridLayout, paginationEnabled, viewParticipantsWebcams } = this.props;
|
||||
return (
|
||||
isModalOpen ?
|
||||
<ConnectionStatusModalComponent
|
||||
{...{
|
||||
isModalOpen,
|
||||
setModalIsOpen: this.setModalIsOpen,
|
||||
isGridLayout,
|
||||
paginationEnabled,
|
||||
viewParticipantsWebcams,
|
||||
}}
|
||||
/> : null
|
||||
)
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import ConnectionStatusButtonComponent from './component';
|
||||
import { USER_CURRENT_STATUS_SUBSCRIPTION } from '../queries';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
@ -19,8 +20,14 @@ const connectionStatusButtonContainer = (props) => {
|
||||
|
||||
export default withTracker(() => {
|
||||
const { connected } = Meteor.status();
|
||||
const isGridLayout = Session.get('isGridEnabled');
|
||||
const { paginationEnabled } = Settings.application;
|
||||
const { viewParticipantsWebcams } = Settings.dataSaving;
|
||||
|
||||
return {
|
||||
connected,
|
||||
isGridLayout,
|
||||
paginationEnabled,
|
||||
viewParticipantsWebcams,
|
||||
};
|
||||
})(connectionStatusButtonContainer);
|
||||
|
@ -216,10 +216,10 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
* @return {Promise} A Promise that resolves when process started.
|
||||
*/
|
||||
async startMonitoringNetwork() {
|
||||
const { streams } = this.props;
|
||||
let previousData = await Service.getNetworkData(streams);
|
||||
const { getVideoStreamsStats } = this.props;
|
||||
let previousData = await Service.getNetworkData(getVideoStreamsStats);
|
||||
this.rateInterval = Meteor.setInterval(async () => {
|
||||
const data = await Service.getNetworkData(streams);
|
||||
const data = await Service.getNetworkData(getVideoStreamsStats);
|
||||
|
||||
const {
|
||||
outbound: audioCurrentUploadRate,
|
||||
|
@ -4,19 +4,24 @@ import { CONNECTION_STATUS_REPORT_SUBSCRIPTION } from '../queries';
|
||||
import Service from '../service';
|
||||
import Component from './component';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { useStreams } from '../../video-provider/video-provider-graphql/hooks';
|
||||
import { useGetStats } from '../../video-provider/video-provider-graphql/hooks';
|
||||
|
||||
const ConnectionStatusContainer = (props) => {
|
||||
const { data } = useSubscription(CONNECTION_STATUS_REPORT_SUBSCRIPTION);
|
||||
const connectionData = data ? Service.sortConnectionData(data.user_connectionStatusReport) : [];
|
||||
const { data: currentUser } = useCurrentUser((u) => ({ isModerator: u.isModerator }));
|
||||
const amIModerator = !!currentUser?.isModerator;
|
||||
const { streams } = useStreams();
|
||||
const { isGridLayout, paginationsEnabled, viewParticipantsWebcams } = props;
|
||||
const getVideoStreamsStats = useGetStats(
|
||||
isGridLayout,
|
||||
paginationsEnabled,
|
||||
viewParticipantsWebcams,
|
||||
);
|
||||
return (
|
||||
<Component
|
||||
connectionData={connectionData}
|
||||
amIModerator={amIModerator}
|
||||
streams={streams}
|
||||
getVideoStreamsStats={getVideoStreamsStats}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
@ -204,8 +204,8 @@ const getAudioData = async () => {
|
||||
* @returns An Object containing video data for all video peers and screenshare
|
||||
* peer
|
||||
*/
|
||||
const getVideoData = async (streams) => {
|
||||
const camerasData = await VideoService.getStats(streams) || {};
|
||||
const getVideoData = async (getVideoStreamsStats) => {
|
||||
const camerasData = await getVideoStreamsStats() || {};
|
||||
|
||||
const screenshareData = await ScreenshareService.getStats() || {};
|
||||
|
||||
@ -220,10 +220,10 @@ const getVideoData = async (streams) => {
|
||||
* For audio, this will get information about the mic/listen-only stream.
|
||||
* @returns An Object containing all this data.
|
||||
*/
|
||||
const getNetworkData = async (streams) => {
|
||||
const getNetworkData = async (getVideoStreamsStats) => {
|
||||
const audio = await getAudioData();
|
||||
|
||||
const video = await getVideoData(streams);
|
||||
const video = await getVideoData(getVideoStreamsStats);
|
||||
|
||||
const user = {
|
||||
time: new Date(),
|
||||
|
@ -57,6 +57,15 @@ interface CameraDock {
|
||||
isResizable?: boolean;
|
||||
resizableEdge?: ResizableEdge;
|
||||
tabOrder?: number;
|
||||
presenterMaxWidth: number;
|
||||
maxWidth: number;
|
||||
maxHeight: number;
|
||||
minHeight: number;
|
||||
minWidth: number;
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
export interface ExternalVideo {
|
||||
|
@ -8,10 +8,10 @@ import ScreenShareService from '/imports/ui/components/screenshare/service';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors';
|
||||
import { EXTERNAL_VIDEO_STOP } from '../external-video-player/mutations';
|
||||
import { CAMERA_BROADCAST_STOP } from '../video-provider/mutations';
|
||||
import {
|
||||
useSharedDevices, useHasVideoStream, useHasCapReached, useIsUserLocked, useStreams,
|
||||
useExitVideo,
|
||||
useStopVideo,
|
||||
} from '/imports/ui/components/video-provider/video-provider-graphql/hooks';
|
||||
|
||||
const VideoPreviewContainer = (props) => {
|
||||
@ -22,21 +22,16 @@ const VideoPreviewContainer = (props) => {
|
||||
...rest
|
||||
} = props;
|
||||
const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP);
|
||||
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
|
||||
|
||||
const sendUserUnshareWebcam = (cameraId) => {
|
||||
cameraBroadcastStop({ variables: { cameraId } });
|
||||
};
|
||||
|
||||
const { streams } = useStreams();
|
||||
const exitVideo = useExitVideo()
|
||||
const exitVideo = useExitVideo();
|
||||
const stopVideo = useStopVideo();
|
||||
const startSharingCameraAsContent = buildStartSharingCameraAsContent(stopExternalVideoShare);
|
||||
const stopSharing = buildStopSharing(sendUserUnshareWebcam, streams, exitVideo);
|
||||
const stopSharing = buildStopSharing(streams, exitVideo, stopVideo);
|
||||
const sharedDevices = useSharedDevices();
|
||||
const hasVideoStream = useHasVideoStream();
|
||||
const camCapReached = useHasCapReached();
|
||||
const isCamLocked = useIsUserLocked();
|
||||
|
||||
|
||||
return (
|
||||
<VideoPreview
|
||||
@ -81,14 +76,14 @@ export default withTracker(({ setIsOpen, callbackToClose }) => ({
|
||||
);
|
||||
ScreenShareService.setCameraAsContentDeviceId(deviceId);
|
||||
},
|
||||
buildStopSharing: (sendUserUnshareWebcam, streams, exitVideo) => (deviceId) => {
|
||||
buildStopSharing: (streams, exitVideo, stopVideo) => (deviceId) => {
|
||||
callbackToClose();
|
||||
setIsOpen(false);
|
||||
if (deviceId) {
|
||||
const streamId = VideoService.getMyStreamId(deviceId, streams);
|
||||
if (streamId) VideoService.stopVideo(streamId, sendUserUnshareWebcam, streams);
|
||||
if (streamId) stopVideo(streamId);
|
||||
} else {
|
||||
exitVideo(sendUserUnshareWebcam, streams);
|
||||
exitVideo();
|
||||
}
|
||||
},
|
||||
stopSharingCameraAsContent: () => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import {
|
||||
VIDEO_STREAMS_SUBSCRIPTION,
|
||||
VideoStreamsResponse,
|
||||
@ -17,15 +16,18 @@ const VideoStreamAdapter: React.FC<AdapterProps> = ({
|
||||
const { data, loading, error } = useSubscription<VideoStreamsResponse>(VIDEO_STREAMS_SUBSCRIPTION);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || error) return;
|
||||
if (loading) return;
|
||||
|
||||
if (error) {
|
||||
logger.error(`Video streams subscription failed. ${error.name}: ${error.message}`, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
setStreams([]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const streams = data.user_camera.map(({ streamId, user, voice }) => ({
|
||||
stream: streamId,
|
||||
deviceId: streamId.split('_')[2],
|
||||
@ -35,18 +37,19 @@ const VideoStreamAdapter: React.FC<AdapterProps> = ({
|
||||
pin: user.pinned,
|
||||
floor: voice?.floor || false,
|
||||
lastFloorTime: voice?.lastFloorTime || '0',
|
||||
isUserModerator: user.isModerator,
|
||||
isModerator: user.isModerator,
|
||||
type: 'stream' as const,
|
||||
}));
|
||||
|
||||
setStreams(streams);
|
||||
}, [data]);
|
||||
|
||||
useEffect(()=>{
|
||||
useEffect(() => {
|
||||
if (!ready.current && !loading) {
|
||||
ready.current = true;
|
||||
onReady('VideoStreamAdapter');
|
||||
}
|
||||
}, [loading])
|
||||
}, [loading]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
@ -1,9 +1,8 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { IntlShape, defineMessages, injectIntl } from 'react-intl';
|
||||
import { debounce } from '/imports/utils/debounce';
|
||||
import VideoService from './service';
|
||||
import VideoListContainer from './video-list/container';
|
||||
@ -23,6 +22,8 @@ import {
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import { shouldForceRelay } from '/imports/ui/services/bbb-webrtc-sfu/utils';
|
||||
import WebRtcPeer from '/imports/ui/services/webrtc-base/peer';
|
||||
import { StreamItem, StreamUser, VideoItem } from './types';
|
||||
import { Output } from '../../layout/layoutTypes';
|
||||
|
||||
// Default values and default empty object to be backwards compat with 2.2.
|
||||
// FIXME Remove hardcoded defaults 2.3.
|
||||
@ -116,25 +117,46 @@ const intlSFUErrors = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
streams: PropTypes.arrayOf(Array).isRequired,
|
||||
intl: PropTypes.objectOf(Object).isRequired,
|
||||
isUserLocked: PropTypes.bool.isRequired,
|
||||
swapLayout: PropTypes.bool.isRequired,
|
||||
currentVideoPageIndex: PropTypes.number.isRequired,
|
||||
totalNumberOfStreams: PropTypes.number.isRequired,
|
||||
isMeteorConnected: PropTypes.bool.isRequired,
|
||||
playStart: PropTypes.func.isRequired,
|
||||
sendUserUnshareWebcam: PropTypes.func.isRequired,
|
||||
};
|
||||
interface VideoProviderGraphqlState {
|
||||
socketOpen: boolean;
|
||||
}
|
||||
|
||||
class VideoProviderGraphql extends Component {
|
||||
interface VideoProviderGraphqlProps {
|
||||
cameraDock: Output['cameraDock'];
|
||||
focusedId: string;
|
||||
handleVideoFocus: (id: string) => void;
|
||||
isGridEnabled: boolean;
|
||||
isMeteorConnected: boolean;
|
||||
swapLayout: boolean;
|
||||
currentUserId: string;
|
||||
paginationEnabled: boolean;
|
||||
viewParticipantsWebcams: boolean;
|
||||
totalNumberOfStreams: number;
|
||||
isUserLocked: boolean;
|
||||
currentVideoPageIndex: number;
|
||||
streams: VideoItem[];
|
||||
users: StreamUser[];
|
||||
info: {
|
||||
userId: string | null | undefined;
|
||||
userName: string | null | undefined;
|
||||
meetingId: string | null | undefined;
|
||||
sessionToken: string | null;
|
||||
voiceBridge: string | null;
|
||||
};
|
||||
playStart: (cameraId: string) => void;
|
||||
exitVideo: () => void;
|
||||
lockUser: () => void;
|
||||
stopVideo: (cameraId?: string) => void;
|
||||
intl: IntlShape;
|
||||
}
|
||||
|
||||
class VideoProviderGraphql extends Component<VideoProviderGraphqlProps, VideoProviderGraphqlState> {
|
||||
onBeforeUnload() {
|
||||
const { sendUserUnshareWebcam } = this.props;
|
||||
VideoService.onBeforeUnload(sendUserUnshareWebcam);
|
||||
const { exitVideo } = this.props;
|
||||
exitVideo();
|
||||
}
|
||||
|
||||
static shouldAttachVideoStream(peer, videoElement) {
|
||||
static shouldAttachVideoStream(peer: WebRtcPeer, videoElement: HTMLVideoElement) {
|
||||
// Conditions to safely attach a stream to a video element in all browsers:
|
||||
// 1 - Peer exists, video element exists
|
||||
// 2 - Target stream differs from videoElement's (diff)
|
||||
@ -156,15 +178,47 @@ class VideoProviderGraphql extends Component {
|
||||
&& diff;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
private info: {
|
||||
userId: string | null | undefined;
|
||||
userName: string | null | undefined;
|
||||
meetingId: string | null | undefined;
|
||||
sessionToken: string | null;
|
||||
voiceBridge: string | null;
|
||||
};
|
||||
|
||||
private mounted: boolean = false;
|
||||
|
||||
private webRtcPeers: Record<string, WebRtcPeer>;
|
||||
|
||||
private debouncedConnectStreams: Function;
|
||||
|
||||
private ws: ReconnectingWebSocket | null = null;
|
||||
|
||||
private wsQueues: Record<string, {
|
||||
id: string;
|
||||
cameraId?: string;
|
||||
type?: string;
|
||||
role?: string;
|
||||
}[] | null>;
|
||||
|
||||
private outboundIceQueues: Record<string, RTCIceCandidate[]>;
|
||||
|
||||
private restartTimeout: Record<string, NodeJS.Timeout>;
|
||||
|
||||
private restartTimer: Record<string, number>;
|
||||
|
||||
private videoTags: Record<string, HTMLVideoElement>;
|
||||
|
||||
constructor(props: VideoProviderGraphqlProps) {
|
||||
super(props);
|
||||
const { info } = this.props;
|
||||
|
||||
// socketOpen state is there to force update when the signaling socket opens or closes
|
||||
this.state = {
|
||||
socketOpen: false,
|
||||
};
|
||||
this._isMounted = false;
|
||||
this.info = VideoService.getInfo();
|
||||
this.mounted = false;
|
||||
this.info = info;
|
||||
// Signaling message queue arrays indexed by stream (== cameraId)
|
||||
this.wsQueues = {};
|
||||
this.restartTimeout = {};
|
||||
@ -190,19 +244,19 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.mounted = true;
|
||||
VideoService.updatePeerDictionaryReference(this.webRtcPeers);
|
||||
this.ws = this.openWs();
|
||||
window.addEventListener('beforeunload', this.onBeforeUnload);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: VideoProviderGraphqlProps) {
|
||||
const {
|
||||
isUserLocked,
|
||||
streams,
|
||||
currentVideoPageIndex,
|
||||
isMeteorConnected,
|
||||
sendUserUnshareWebcam,
|
||||
lockUser,
|
||||
} = this.props;
|
||||
const { socketOpen } = this.state;
|
||||
|
||||
@ -212,7 +266,7 @@ class VideoProviderGraphql extends Component {
|
||||
|
||||
if (isMeteorConnected && socketOpen) this.updateStreams(streams, shouldDebounce);
|
||||
if (!prevProps.isUserLocked && isUserLocked) {
|
||||
VideoService.lockUser(sendUserUnshareWebcam, streams);
|
||||
lockUser();
|
||||
}
|
||||
|
||||
// Signaling socket expired its retries and meteor is connected - create
|
||||
@ -225,8 +279,8 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { sendUserUnshareWebcam, streams, exitVideo } = this.props;
|
||||
this._isMounted = false;
|
||||
const { exitVideo } = this.props;
|
||||
this.mounted = false;
|
||||
VideoService.updatePeerDictionaryReference({});
|
||||
|
||||
if (this.ws) {
|
||||
@ -250,7 +304,7 @@ class VideoProviderGraphql extends Component {
|
||||
debug: WS_DEBUG,
|
||||
maxRetries: WS_MAX_RETRIES,
|
||||
maxEnqueuedMessages: 0,
|
||||
}
|
||||
},
|
||||
);
|
||||
ws.onopen = this.onWsOpen;
|
||||
ws.onclose = this.onWsClose;
|
||||
@ -267,12 +321,14 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_updateLastMsgTime() {
|
||||
this.ws.isAlive = true;
|
||||
this.ws.lastMsgTime = Date.now();
|
||||
private updateLastMsgTime() {
|
||||
if (this.ws) {
|
||||
this.ws.isAlive = true;
|
||||
this.ws.lastMsgTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
_getTimeSinceLastMsg() {
|
||||
private getTimeSinceLastMsg() {
|
||||
return Date.now() - this.ws.lastMsgTime;
|
||||
}
|
||||
|
||||
@ -290,7 +346,7 @@ class VideoProviderGraphql extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._getTimeSinceLastMsg() < (
|
||||
if (this.getTimeSinceLastMsg() < (
|
||||
WS_HEARTBEAT_OPTS.interval - WS_HEARTBEAT_OPTS.delay
|
||||
)) {
|
||||
return;
|
||||
@ -310,8 +366,8 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onWsMessage(message) {
|
||||
this._updateLastMsgTime();
|
||||
onWsMessage(message: { data: string }) {
|
||||
this.updateLastMsgTime();
|
||||
const parsedMessage = JSON.parse(message.data);
|
||||
|
||||
if (parsedMessage.id === 'pong') return;
|
||||
@ -344,7 +400,7 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
|
||||
onWsClose() {
|
||||
const { sendUserUnshareWebcam, streams, exitVideo } = this.props;
|
||||
const { exitVideo } = this.props;
|
||||
logger.info({
|
||||
logCode: 'video_provider_onwsclose',
|
||||
}, 'Multiple video provider websocket connection closed.');
|
||||
@ -372,15 +428,15 @@ class VideoProviderGraphql extends Component {
|
||||
logCode: 'video_provider_onwsopen',
|
||||
}, 'Multiple video provider websocket connection opened.');
|
||||
|
||||
this._updateLastMsgTime();
|
||||
this.updateLastMsgTime();
|
||||
this.setupWSHeartbeat();
|
||||
this.setState({ socketOpen: true });
|
||||
// Resend queued messages that happened when socket was not connected
|
||||
Object.entries(this.wsQueues).forEach(([stream, queue]) => {
|
||||
if (this.webRtcPeers[stream]) {
|
||||
if (this.webRtcPeers[stream] && queue !== null) {
|
||||
// Peer - send enqueued
|
||||
while (queue.length > 0) {
|
||||
this.sendMessage(queue.pop());
|
||||
this.sendMessage(queue.pop()!);
|
||||
}
|
||||
} else {
|
||||
// No peer - delete queue
|
||||
@ -389,18 +445,18 @@ class VideoProviderGraphql extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
findAllPrivilegedStreams () {
|
||||
findAllPrivilegedStreams() {
|
||||
const { streams } = this.props;
|
||||
// Privileged streams are: floor holders, pinned users
|
||||
return streams.filter(stream => stream.floor || stream.pin);
|
||||
return streams.filter((stream) => stream.type === 'stream' && (stream.floor || stream.pin));
|
||||
}
|
||||
|
||||
updateQualityThresholds(numberOfPublishers) {
|
||||
updateQualityThresholds(numberOfPublishers: number) {
|
||||
const { threshold, profile } = VideoService.getThreshold(numberOfPublishers);
|
||||
if (profile) {
|
||||
const privilegedStreams = this.findAllPrivilegedStreams();
|
||||
Object.values(this.webRtcPeers)
|
||||
.filter(peer => peer.isPublisher)
|
||||
.filter((peer) => peer.isPublisher)
|
||||
.forEach((peer) => {
|
||||
// Conditions which make camera revert their original profile
|
||||
// 1) Threshold 0 means original profile/inactive constraint
|
||||
@ -413,33 +469,33 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getStreamsToConnectAndDisconnect(streams) {
|
||||
const streamsCameraIds = streams.filter(s => !s?.isGridItem).map(s => s.stream);
|
||||
getStreamsToConnectAndDisconnect(streams: VideoItem[]) {
|
||||
const streamsCameraIds = streams.filter((s) => s?.type !== 'grid').map((s) => (s as StreamItem).stream);
|
||||
const streamsConnected = Object.keys(this.webRtcPeers);
|
||||
|
||||
const streamsToConnect = streamsCameraIds.filter(stream => {
|
||||
const streamsToConnect = streamsCameraIds.filter((stream) => {
|
||||
return !streamsConnected.includes(stream);
|
||||
});
|
||||
|
||||
const streamsToDisconnect = streamsConnected.filter(stream => {
|
||||
const streamsToDisconnect = streamsConnected.filter((stream) => {
|
||||
return !streamsCameraIds.includes(stream);
|
||||
});
|
||||
|
||||
return [streamsToConnect, streamsToDisconnect];
|
||||
}
|
||||
|
||||
connectStreams(streamsToConnect) {
|
||||
connectStreams(streamsToConnect: string[]) {
|
||||
streamsToConnect.forEach((stream) => {
|
||||
const isLocal = VideoService.isLocalStream(stream);
|
||||
this.createWebRTCPeer(stream, isLocal);
|
||||
});
|
||||
}
|
||||
|
||||
disconnectStreams(streamsToDisconnect) {
|
||||
streamsToDisconnect.forEach(stream => this.stopWebRTCPeer(stream, false));
|
||||
disconnectStreams(streamsToDisconnect: string[]) {
|
||||
streamsToDisconnect.forEach((stream) => this.stopWebRTCPeer(stream, false));
|
||||
}
|
||||
|
||||
updateStreams(streams, shouldDebounce = false) {
|
||||
updateStreams(streams: VideoItem[], shouldDebounce = false) {
|
||||
const [streamsToConnect, streamsToDisconnect] = this.getStreamsToConnectAndDisconnect(streams);
|
||||
|
||||
if (shouldDebounce) {
|
||||
@ -451,7 +507,8 @@ class VideoProviderGraphql extends Component {
|
||||
this.disconnectStreams(streamsToDisconnect);
|
||||
|
||||
if (CAMERA_QUALITY_THRESHOLDS_ENABLED) {
|
||||
this.updateQualityThresholds(this.props.totalNumberOfStreams);
|
||||
const { totalNumberOfStreams } = this.props;
|
||||
this.updateQualityThresholds(totalNumberOfStreams);
|
||||
}
|
||||
}
|
||||
|
||||
@ -460,14 +517,14 @@ class VideoProviderGraphql extends Component {
|
||||
this.sendMessage(message);
|
||||
}
|
||||
|
||||
sendMessage(message) {
|
||||
sendMessage(message: { id: string, cameraId?: string; type?: string; role?: string }) {
|
||||
const { ws } = this;
|
||||
|
||||
if (this.connectedToMediaServer()) {
|
||||
const jsonMessage = JSON.stringify(message);
|
||||
try {
|
||||
ws.send(jsonMessage);
|
||||
} catch (error) {
|
||||
ws?.send(jsonMessage);
|
||||
} catch (error: Error) {
|
||||
logger.error({
|
||||
logCode: 'video_provider_ws_send_error',
|
||||
extraInfo: {
|
||||
@ -481,7 +538,7 @@ class VideoProviderGraphql extends Component {
|
||||
const { cameraId } = message;
|
||||
if (cameraId) {
|
||||
if (this.wsQueues[cameraId] == null) this.wsQueues[cameraId] = [];
|
||||
this.wsQueues[cameraId].push(message);
|
||||
this.wsQueues[cameraId]?.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -490,7 +547,7 @@ class VideoProviderGraphql extends Component {
|
||||
return this.ws && this.ws.readyState === ReconnectingWebSocket.OPEN;
|
||||
}
|
||||
|
||||
processOutboundIceQueue(peer, role, stream) {
|
||||
processOutboundIceQueue(peer: WebRtcPeer, role: string, stream: string) {
|
||||
const queue = this.outboundIceQueues[stream];
|
||||
while (queue && queue.length) {
|
||||
const candidate = queue.shift();
|
||||
@ -498,7 +555,7 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
sendLocalAnswer (peer, stream, answer) {
|
||||
sendLocalAnswer (peer: WebRtcPeer, stream: string, answer) {
|
||||
const message = {
|
||||
id: 'subscriberAnswer',
|
||||
type: 'video',
|
||||
@ -510,7 +567,7 @@ class VideoProviderGraphql extends Component {
|
||||
this.sendMessage(message);
|
||||
}
|
||||
|
||||
startResponse(message) {
|
||||
startResponse(message: { cameraId: string; stream: string; role: string }) {
|
||||
const { cameraId: stream, role } = message;
|
||||
const peer = this.webRtcPeers[stream];
|
||||
|
||||
@ -530,7 +587,7 @@ class VideoProviderGraphql extends Component {
|
||||
peer.didSDPAnswered = true;
|
||||
this.processOutboundIceQueue(peer, role, stream);
|
||||
VideoService.processInboundIceQueue(peer, stream);
|
||||
}).catch((error) => {
|
||||
}).catch((error: Error & { code: string }) => {
|
||||
logger.error({
|
||||
logCode: 'video_provider_peerconnection_process_error',
|
||||
extraInfo: {
|
||||
@ -549,7 +606,7 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleIceCandidate(message) {
|
||||
handleIceCandidate(message: { cameraId: string; candidate: Record<string, unknown> }) {
|
||||
const { cameraId: stream, candidate } = message;
|
||||
const peer = this.webRtcPeers[stream];
|
||||
|
||||
@ -575,7 +632,7 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
clearRestartTimers(stream) {
|
||||
clearRestartTimers(stream: string) {
|
||||
if (this.restartTimeout[stream]) {
|
||||
clearTimeout(this.restartTimeout[stream]);
|
||||
delete this.restartTimeout[stream];
|
||||
@ -586,9 +643,9 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
stopWebRTCPeer(stream, restarting = false) {
|
||||
stopWebRTCPeer(stream: string, restarting = false) {
|
||||
const isLocal = VideoService.isLocalStream(stream);
|
||||
const { sendUserUnshareWebcam, streams } = this.props;
|
||||
const { stopVideo } = this.props;
|
||||
|
||||
// in this case, 'closed' state is not caused by an error;
|
||||
// we stop listening to prevent this from being treated as an error
|
||||
@ -599,7 +656,7 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
|
||||
if (isLocal) {
|
||||
VideoService.stopVideo(stream, sendUserUnshareWebcam, streams);
|
||||
stopVideo(stream);
|
||||
}
|
||||
|
||||
const role = VideoService.getRole(isLocal);
|
||||
@ -625,7 +682,7 @@ class VideoProviderGraphql extends Component {
|
||||
return this.destroyWebRTCPeer(stream);
|
||||
}
|
||||
|
||||
destroyWebRTCPeer(stream) {
|
||||
destroyWebRTCPeer(stream: string) {
|
||||
let stopped = false;
|
||||
const peer = this.webRtcPeers[stream];
|
||||
const isLocal = VideoService.isLocalStream(stream);
|
||||
@ -658,7 +715,7 @@ class VideoProviderGraphql extends Component {
|
||||
return stopped;
|
||||
}
|
||||
|
||||
_createPublisher(stream, peerOptions) {
|
||||
private createPublisher(stream: string, peerOptions: Record<string, unknown>) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const { id: profileId } = VideoService.getCameraProfile();
|
||||
@ -699,7 +756,7 @@ class VideoProviderGraphql extends Component {
|
||||
this.replacePCVideoTracks(stream, newStream);
|
||||
}
|
||||
});
|
||||
peer.inactivationHandler = () => this._handleLocalStreamInactive(stream);
|
||||
peer.inactivationHandler = () => this.handleLocalStreamInactive(stream);
|
||||
bbbVideoStream.once('inactive', peer.inactivationHandler);
|
||||
resolve(offer);
|
||||
}).catch(reject);
|
||||
@ -709,7 +766,7 @@ class VideoProviderGraphql extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
_createSubscriber(stream, peerOptions) {
|
||||
private createSubscriber(stream: string, peerOptions: Record<string, unknown>) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const peer = new WebRtcPeer('recvonly', peerOptions);
|
||||
@ -720,19 +777,19 @@ class VideoProviderGraphql extends Component {
|
||||
peer.inboundIceQueue = [];
|
||||
peer.isPublisher = false;
|
||||
peer.start();
|
||||
resolve();
|
||||
resolve(null);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createWebRTCPeer(stream, isLocal) {
|
||||
async createWebRTCPeer(stream: string, isLocal: boolean) {
|
||||
let iceServers = [];
|
||||
const role = VideoService.getRole(isLocal);
|
||||
const peerBuilderFunc = isLocal
|
||||
? this._createPublisher.bind(this)
|
||||
: this._createSubscriber.bind(this);
|
||||
? this.createPublisher.bind(this)
|
||||
: this.createSubscriber.bind(this);
|
||||
|
||||
// Check if the peer is already being processed
|
||||
if (this.webRtcPeers[stream]) {
|
||||
@ -747,7 +804,7 @@ class VideoProviderGraphql extends Component {
|
||||
audio: false,
|
||||
video: constraints,
|
||||
},
|
||||
onicecandidate: this._getOnIceCandidateCallback(stream, isLocal),
|
||||
onicecandidate: this.getOnIceCandidateCallback(stream, isLocal),
|
||||
configuration: {
|
||||
},
|
||||
trace: TRACE_LOGS,
|
||||
@ -778,7 +835,7 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
|
||||
peerBuilderFunc(stream, peerOptions).then((offer) => {
|
||||
if (!this._isMounted) {
|
||||
if (!this.mounted) {
|
||||
return this.stopWebRTCPeer(stream, false);
|
||||
}
|
||||
const peer = this.webRtcPeers[stream];
|
||||
@ -786,7 +843,7 @@ class VideoProviderGraphql extends Component {
|
||||
if (peer && peer.peerConnection) {
|
||||
const conn = peer.peerConnection;
|
||||
conn.onconnectionstatechange = () => {
|
||||
this._handleIceConnectionStateChange(stream, isLocal);
|
||||
this.handleIceConnectionStateChange(stream, isLocal);
|
||||
};
|
||||
}
|
||||
|
||||
@ -812,14 +869,14 @@ class VideoProviderGraphql extends Component {
|
||||
this.setReconnectionTimeout(stream, isLocal, false);
|
||||
this.sendMessage(message);
|
||||
|
||||
return;
|
||||
}).catch(error => {
|
||||
return this._onWebRTCError(error, stream, isLocal);
|
||||
return null;
|
||||
}).catch((error) => {
|
||||
return this.onWebRTCError(error, stream, isLocal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getWebRTCStartTimeout(stream, isLocal) {
|
||||
private getWebRTCStartTimeout(stream: string, isLocal: boolean) {
|
||||
const { intl } = this.props;
|
||||
|
||||
return () => {
|
||||
@ -868,12 +925,12 @@ class VideoProviderGraphql extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
_onWebRTCError(error, stream, isLocal) {
|
||||
private onWebRTCError(error: Error, stream: string, isLocal: boolean) {
|
||||
const { intl, streams } = this.props;
|
||||
const { name: errorName, message: errorMessage } = error;
|
||||
const errorLocale = intlClientErrors[errorName]
|
||||
|| intlClientErrors[errorMessage]
|
||||
|| intlSFUErrors[error];
|
||||
const errorLocale = intlClientErrors[errorName as keyof typeof intlClientErrors]
|
||||
|| intlClientErrors[errorMessage as keyof typeof intlClientErrors]
|
||||
|| intlSFUErrors[error as unknown as keyof typeof intlSFUErrors];
|
||||
|
||||
logger.error({
|
||||
logCode: 'video_provider_webrtc_peer_error',
|
||||
@ -894,7 +951,7 @@ class VideoProviderGraphql extends Component {
|
||||
// If it's a viewer, set the reconnection timeout. There's a good chance
|
||||
// no local candidate was generated and it wasn't set.
|
||||
const peer = this.webRtcPeers[stream];
|
||||
const stillExists = streams.some(({ stream: streamId }) => streamId === stream);
|
||||
const stillExists = streams.some((item) => item.type === 'stream' && item.stream === stream);
|
||||
|
||||
if (stillExists) {
|
||||
const isEstablishedConnection = peer && peer.started;
|
||||
@ -907,12 +964,12 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
reconnect(stream, isLocal) {
|
||||
reconnect(stream: string, isLocal: boolean) {
|
||||
this.stopWebRTCPeer(stream, true);
|
||||
this.createWebRTCPeer(stream, isLocal);
|
||||
}
|
||||
|
||||
setReconnectionTimeout(stream, isLocal, isEstablishedConnection) {
|
||||
setReconnectionTimeout(stream: string, isLocal: boolean, isEstablishedConnection: boolean) {
|
||||
const peer = this.webRtcPeers[stream];
|
||||
const shouldSetReconnectionTimeout = !this.restartTimeout[stream] && !isEstablishedConnection;
|
||||
|
||||
@ -932,15 +989,16 @@ class VideoProviderGraphql extends Component {
|
||||
this.restartTimer[stream] = newReconnectTimer;
|
||||
|
||||
this.restartTimeout[stream] = setTimeout(
|
||||
this._getWebRTCStartTimeout(stream, isLocal),
|
||||
this.getWebRTCStartTimeout(stream, isLocal),
|
||||
this.restartTimer[stream]
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_getOnIceCandidateCallback(stream, isLocal) {
|
||||
private getOnIceCandidateCallback(stream: string, isLocal: boolean) {
|
||||
if (SIGNAL_CANDIDATES) {
|
||||
return (candidate) => {
|
||||
return (candidate: RTCIceCandidate) => {
|
||||
const peer = this.webRtcPeers[stream];
|
||||
const role = VideoService.getRole(isLocal);
|
||||
|
||||
@ -956,7 +1014,7 @@ class VideoProviderGraphql extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
sendIceCandidateToSFU(peer, role, candidate, stream) {
|
||||
sendIceCandidateToSFU(peer: WebRtcPeer, role: string, candidate: RTCIceCandidate | undefined, stream: string) {
|
||||
const message = {
|
||||
type: 'video',
|
||||
role,
|
||||
@ -967,7 +1025,7 @@ class VideoProviderGraphql extends Component {
|
||||
this.sendMessage(message);
|
||||
}
|
||||
|
||||
_handleLocalStreamInactive(stream) {
|
||||
private handleLocalStreamInactive(stream: string) {
|
||||
const peer = this.webRtcPeers[stream];
|
||||
const isLocal = VideoService.isLocalStream(stream);
|
||||
const role = VideoService.getRole(isLocal);
|
||||
@ -985,17 +1043,16 @@ class VideoProviderGraphql extends Component {
|
||||
}, 'Local camera stream stopped unexpectedly');
|
||||
|
||||
const error = new Error('inactiveError');
|
||||
this._onWebRTCError(error, stream, isLocal);
|
||||
this.onWebRTCError(error, stream, isLocal);
|
||||
}
|
||||
|
||||
_handleIceConnectionStateChange(stream, isLocal) {
|
||||
const { intl } = this.props;
|
||||
private handleIceConnectionStateChange(stream: string, isLocal: boolean) {
|
||||
const peer = this.webRtcPeers[stream];
|
||||
const role = VideoService.getRole(isLocal);
|
||||
|
||||
if (peer && peer.peerConnection) {
|
||||
const pc = peer.peerConnection;
|
||||
const connectionState = pc.connectionState;
|
||||
const { connectionState } = pc;
|
||||
notifyStreamStateChange(stream, connectionState);
|
||||
|
||||
if (connectionState === 'failed' || connectionState === 'closed') {
|
||||
@ -1012,7 +1069,7 @@ class VideoProviderGraphql extends Component {
|
||||
},
|
||||
}, `Camera ICE connection state changed: ${connectionState}. Role: ${role}.`);
|
||||
|
||||
this._onWebRTCError(error, stream, isLocal);
|
||||
this.onWebRTCError(error, stream, isLocal);
|
||||
}
|
||||
} else {
|
||||
logger.error({
|
||||
@ -1022,7 +1079,7 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
attach (peer, videoElement) {
|
||||
static attach(peer: WebRtcPeer, videoElement: HTMLVideoElement) {
|
||||
if (peer && videoElement) {
|
||||
const stream = peer.isPublisher ? peer.getLocalStream() : peer.getRemoteStream();
|
||||
videoElement.pause();
|
||||
@ -1031,11 +1088,11 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getVideoElement(streamId) {
|
||||
getVideoElement(streamId: string) {
|
||||
return this.videoTags[streamId];
|
||||
}
|
||||
|
||||
attachVideoStream(stream) {
|
||||
attachVideoStream(stream: string) {
|
||||
const videoElement = this.getVideoElement(stream);
|
||||
const isLocal = VideoService.isLocalStream(stream);
|
||||
const peer = this.webRtcPeers[stream];
|
||||
@ -1048,7 +1105,7 @@ class VideoProviderGraphql extends Component {
|
||||
// This is necessary to ensure that the video element is properly
|
||||
// hidden/shown when the stream is attached.
|
||||
notifyStreamStateChange(stream, pc.connectionState);
|
||||
this.attach(peer, videoElement);
|
||||
VideoProviderGraphql.attach(peer, videoElement);
|
||||
|
||||
if (isLocal) {
|
||||
if (peer.bbbVideoStream == null) {
|
||||
@ -1062,14 +1119,14 @@ class VideoProviderGraphql extends Component {
|
||||
);
|
||||
const { type, name } = getSessionVirtualBackgroundInfo(deviceId);
|
||||
|
||||
this.restoreVirtualBackground(peer.bbbVideoStream, type, name).catch((error) => {
|
||||
VideoProviderGraphql.restoreVirtualBackground(peer.bbbVideoStream, type, name).catch((error) => {
|
||||
this.handleVirtualBgError(error, type, name);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startVirtualBackgroundByDrop(stream, type, name, data) {
|
||||
startVirtualBackgroundByDrop(stream: string, type: string, name: string, data: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const peer = this.webRtcPeers[stream];
|
||||
const { bbbVideoStream } = peer;
|
||||
@ -1081,13 +1138,13 @@ class VideoProviderGraphql extends Component {
|
||||
.catch(reject);
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.handleVirtualBgErrorByDropping(error, type, name);
|
||||
VideoProviderGraphql.handleVirtualBgErrorByDropping(error, type, name);
|
||||
});
|
||||
}
|
||||
|
||||
handleVirtualBgErrorByDropping(error, type, name) {
|
||||
static handleVirtualBgErrorByDropping(error: Error, type: string, name: string) {
|
||||
logger.error({
|
||||
logCode: `video_provider_virtualbg_error`,
|
||||
logCode: 'video_provider_virtualbg_error',
|
||||
extraInfo: {
|
||||
errorName: error.name,
|
||||
errorMessage: error.message,
|
||||
@ -1097,23 +1154,23 @@ class VideoProviderGraphql extends Component {
|
||||
}, `Failed to start virtual background by dropping image: ${error.message}`);
|
||||
}
|
||||
|
||||
restoreVirtualBackground(stream, type, name) {
|
||||
static restoreVirtualBackground(stream: BBBVideoStream, type: string, name: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (type !== EFFECT_TYPES.NONE_TYPE) {
|
||||
stream.startVirtualBackground(type, name).then(() => {
|
||||
resolve();
|
||||
}).catch((error) => {
|
||||
resolve(null);
|
||||
}).catch((error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
resolve();
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
|
||||
handleVirtualBgError(error, type, name) {
|
||||
handleVirtualBgError(error: Error, type?: string, name?: string) {
|
||||
const { intl } = this.props;
|
||||
logger.error({
|
||||
logCode: `video_provider_virtualbg_error`,
|
||||
logCode: 'video_provider_virtualbg_error',
|
||||
extraInfo: {
|
||||
errorName: error.name,
|
||||
errorMessage: error.message,
|
||||
@ -1125,7 +1182,7 @@ class VideoProviderGraphql extends Component {
|
||||
notify(intl.formatMessage(intlClientErrors.virtualBgGenericError), 'error', 'video');
|
||||
}
|
||||
|
||||
createVideoTag(stream, video) {
|
||||
createVideoTag(stream: string, video: HTMLVideoElement) {
|
||||
const peer = this.webRtcPeers[stream];
|
||||
this.videoTags[stream] = video;
|
||||
|
||||
@ -1134,7 +1191,7 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
destroyVideoTag(stream) {
|
||||
destroyVideoTag(stream: string) {
|
||||
const videoElement = this.videoTags[stream];
|
||||
|
||||
if (videoElement == null) return;
|
||||
@ -1147,7 +1204,7 @@ class VideoProviderGraphql extends Component {
|
||||
delete this.videoTags[stream];
|
||||
}
|
||||
|
||||
handlePlayStop(message) {
|
||||
handlePlayStop(message: { cameraId: string; role: string }) {
|
||||
const { intl } = this.props;
|
||||
const { cameraId: stream, role } = message;
|
||||
|
||||
@ -1163,7 +1220,7 @@ class VideoProviderGraphql extends Component {
|
||||
this.stopWebRTCPeer(stream, false);
|
||||
}
|
||||
|
||||
handlePlayStart(message) {
|
||||
handlePlayStart(message: { cameraId: string; role: string }) {
|
||||
const { cameraId: stream, role } = message;
|
||||
const peer = this.webRtcPeers[stream];
|
||||
const { playStart } = this.props;
|
||||
@ -1192,8 +1249,8 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleSFUError(message) {
|
||||
const { intl, streams, sendUserUnshareWebcam } = this.props;
|
||||
handleSFUError(message: { code: string; reason: string; streamId: string }) {
|
||||
const { intl, streams, stopVideo } = this.props;
|
||||
const { code, reason, streamId } = message;
|
||||
const isLocal = VideoService.isLocalStream(streamId);
|
||||
const role = VideoService.getRole(isLocal);
|
||||
@ -1211,11 +1268,14 @@ class VideoProviderGraphql extends Component {
|
||||
if (isLocal) {
|
||||
// The publisher instance received an error from the server. There's no reconnect,
|
||||
// stop it.
|
||||
VideoService.notify(intl.formatMessage(intlSFUErrors[code] || intlSFUErrors[2200]));
|
||||
VideoService.stopVideo(streamId, sendUserUnshareWebcam, streams);
|
||||
VideoService.notify(
|
||||
intl.formatMessage(intlSFUErrors[code as unknown as keyof typeof intlSFUErrors]
|
||||
|| intlSFUErrors[2200]),
|
||||
);
|
||||
stopVideo(streamId);
|
||||
} else {
|
||||
const peer = this.webRtcPeers[streamId];
|
||||
const stillExists = streams.some(({ stream }) => streamId === stream);
|
||||
const stillExists = streams.some((item) => item.type === 'stream' && streamId === item.stream);
|
||||
|
||||
if (stillExists) {
|
||||
const isEstablishedConnection = peer && peer.started;
|
||||
@ -1226,7 +1286,7 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
replacePCVideoTracks(streamId, mediaStream) {
|
||||
replacePCVideoTracks(streamId: string, mediaStream: MediaStream) {
|
||||
const peer = this.webRtcPeers[streamId];
|
||||
const videoElement = this.getVideoElement(streamId);
|
||||
|
||||
@ -1252,7 +1312,7 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
});
|
||||
Promise.all(trackReplacers).then(() => {
|
||||
this.attach(peer, videoElement);
|
||||
VideoProviderGraphql.attach(peer, videoElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1262,7 +1322,7 @@ class VideoProviderGraphql extends Component {
|
||||
swapLayout,
|
||||
currentVideoPageIndex,
|
||||
streams,
|
||||
cameraDockBounds,
|
||||
cameraDock,
|
||||
focusedId,
|
||||
handleVideoFocus,
|
||||
isGridEnabled,
|
||||
@ -1275,7 +1335,7 @@ class VideoProviderGraphql extends Component {
|
||||
streams,
|
||||
swapLayout,
|
||||
currentVideoPageIndex,
|
||||
cameraDockBounds,
|
||||
cameraDock,
|
||||
focusedId,
|
||||
handleVideoFocus,
|
||||
isGridEnabled,
|
||||
@ -1289,6 +1349,4 @@ class VideoProviderGraphql extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
VideoProviderGraphql.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(VideoProviderGraphql);
|
||||
|
@ -1,7 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import { Session } from 'meteor/session';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { useMutation } from '@apollo/client';
|
||||
@ -12,39 +9,48 @@ import Settings from '/imports/ui/services/settings';
|
||||
import {
|
||||
useCurrentVideoPageIndex,
|
||||
useExitVideo,
|
||||
useInfo,
|
||||
useIsUserLocked,
|
||||
useLockUser,
|
||||
useStopVideo,
|
||||
useVideoStreams,
|
||||
} from './hooks';
|
||||
import { CAMERA_BROADCAST_START, CAMERA_BROADCAST_STOP } from './mutations';
|
||||
import { CAMERA_BROADCAST_START } from './mutations';
|
||||
import VideoProvider from './component';
|
||||
import VideoService from './service';
|
||||
import { Output } from '/imports/ui/components/layout/layoutTypes';
|
||||
import { VideoItem } from './types';
|
||||
|
||||
interface VideoProviderContainerGraphqlProps {
|
||||
isGridLayout: boolean;
|
||||
currUserId: string;
|
||||
currentUserId: string;
|
||||
focusedId: string;
|
||||
swapLayout: boolean;
|
||||
isGridEnabled: boolean;
|
||||
paginationEnabled: boolean;
|
||||
isMeteorConnected: boolean;
|
||||
viewParticipantsWebcams: boolean;
|
||||
children: React.ReactNode;
|
||||
cameraDock: Output['cameraDock'];
|
||||
handleVideoFocus:(id: string) => void;
|
||||
}
|
||||
|
||||
const VideoProviderContainerGraphql: React.FC<VideoProviderContainerGraphqlProps> = ({ children, ...props }) => {
|
||||
const VideoProviderContainerGraphql: React.FC<VideoProviderContainerGraphqlProps> = (props) => {
|
||||
const {
|
||||
isGridLayout,
|
||||
currUserId,
|
||||
currentUserId,
|
||||
paginationEnabled,
|
||||
viewParticipantsWebcams,
|
||||
cameraDock,
|
||||
focusedId,
|
||||
handleVideoFocus,
|
||||
isGridEnabled,
|
||||
isMeteorConnected,
|
||||
swapLayout,
|
||||
} = props;
|
||||
const [cameraBroadcastStart] = useMutation(CAMERA_BROADCAST_START);
|
||||
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
|
||||
|
||||
const sendUserShareWebcam = (cameraId: string) => {
|
||||
return cameraBroadcastStart({ variables: { cameraId } });
|
||||
};
|
||||
|
||||
const sendUserUnshareWebcam = (cameraId: string) => {
|
||||
cameraBroadcastStop({ variables: { cameraId } });
|
||||
};
|
||||
|
||||
const playStart = (cameraId: string) => {
|
||||
if (VideoService.isLocalStream(cameraId)) {
|
||||
sendUserShareWebcam(cameraId).then(() => {
|
||||
@ -68,11 +74,11 @@ const VideoProviderContainerGraphql: React.FC<VideoProviderContainerGraphqlProps
|
||||
gridUsers,
|
||||
totalNumberOfStreams,
|
||||
users,
|
||||
} = useVideoStreams(isGridLayout, paginationEnabled, viewParticipantsWebcams);
|
||||
} = useVideoStreams(isGridEnabled, paginationEnabled, viewParticipantsWebcams);
|
||||
|
||||
let usersVideo = streams;
|
||||
let usersVideo: VideoItem[] = streams;
|
||||
|
||||
if (gridUsers.length > 0 && isGridLayout) {
|
||||
if (gridUsers.length > 0 && isGridEnabled) {
|
||||
usersVideo = usersVideo.concat(gridUsers);
|
||||
}
|
||||
|
||||
@ -80,45 +86,71 @@ const VideoProviderContainerGraphql: React.FC<VideoProviderContainerGraphqlProps
|
||||
currentMeeting?.usersPolicies?.webcamsOnlyForModerator
|
||||
&& currentUser?.locked
|
||||
) {
|
||||
usersVideo = usersVideo.filter((uv) => uv.isUserModerator || uv.userId === currUserId);
|
||||
usersVideo = usersVideo.filter(
|
||||
(uv) => (uv.type !== 'connecting' && uv.isModerator) || uv.userId === currentUserId,
|
||||
);
|
||||
}
|
||||
|
||||
const isUserLocked = useIsUserLocked();
|
||||
const currentVideoPageIndex = useCurrentVideoPageIndex();
|
||||
const exitVideo = useExitVideo();
|
||||
const lockUser = useLockUser();
|
||||
const stopVideo = useStopVideo();
|
||||
const info = useInfo();
|
||||
|
||||
if (!usersVideo.length && !isGridEnabled) return null;
|
||||
|
||||
return (
|
||||
!usersVideo.length && !isGridLayout
|
||||
? null
|
||||
: (
|
||||
<VideoProvider
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
playStart={playStart}
|
||||
sendUserUnshareWebcam={sendUserUnshareWebcam}
|
||||
streams={usersVideo}
|
||||
totalNumberOfStreams={totalNumberOfStreams}
|
||||
isUserLocked={isUserLocked}
|
||||
currentVideoPageIndex={currentVideoPageIndex}
|
||||
exitVideo={exitVideo}
|
||||
users={users}
|
||||
>
|
||||
{children}
|
||||
</VideoProvider>
|
||||
)
|
||||
<VideoProvider
|
||||
cameraDock={cameraDock}
|
||||
focusedId={focusedId}
|
||||
handleVideoFocus={handleVideoFocus}
|
||||
isGridEnabled={isGridEnabled}
|
||||
isMeteorConnected={isMeteorConnected}
|
||||
swapLayout={swapLayout}
|
||||
currentUserId={currentUserId}
|
||||
paginationEnabled={paginationEnabled}
|
||||
viewParticipantsWebcams={viewParticipantsWebcams}
|
||||
totalNumberOfStreams={totalNumberOfStreams}
|
||||
isUserLocked={isUserLocked}
|
||||
currentVideoPageIndex={currentVideoPageIndex}
|
||||
streams={usersVideo}
|
||||
users={users}
|
||||
info={info}
|
||||
playStart={playStart}
|
||||
exitVideo={exitVideo}
|
||||
lockUser={lockUser}
|
||||
stopVideo={stopVideo}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTracker(() => {
|
||||
const isGridLayout = Session.get('isGridEnabled');
|
||||
const currUserId = Auth.userID;
|
||||
const isMeteorConnected = Meteor.status().connected;
|
||||
type TrackerData = {
|
||||
currentUserId: string;
|
||||
isMeteorConnected: boolean;
|
||||
paginationEnabled: boolean;
|
||||
viewParticipantsWebcams: boolean;
|
||||
};
|
||||
|
||||
type TrackerProps = {
|
||||
swapLayout: boolean;
|
||||
cameraDock: Output['cameraDock'];
|
||||
focusedId: string;
|
||||
handleVideoFocus:(id: string) => void;
|
||||
isGridEnabled: boolean;
|
||||
};
|
||||
|
||||
export default withTracker<TrackerData, TrackerProps>(() => {
|
||||
const currentUserId = Auth.userID ?? '';
|
||||
const isMeteorConnected = Meteor.status().connected;
|
||||
// @ts-expect-error -> Untyped object.
|
||||
const { paginationEnabled } = Settings.application;
|
||||
// @ts-expect-error -> Untyped object.
|
||||
const { viewParticipantsWebcams } = Settings.dataSaving;
|
||||
return {
|
||||
currUserId,
|
||||
isGridLayout,
|
||||
currentUserId,
|
||||
isMeteorConnected,
|
||||
paginationEnabled: Settings.application.paginationEnabled,
|
||||
viewParticipantsWebcams: Settings.dataSaving.viewParticipantsWebcams,
|
||||
paginationEnabled,
|
||||
viewParticipantsWebcams,
|
||||
};
|
||||
})(VideoProviderContainerGraphql);
|
||||
|
@ -1,12 +1,12 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useSubscription, useReactiveVar, useLazyQuery, useMutation } from '@apollo/client';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
useSubscription,
|
||||
useReactiveVar,
|
||||
useLazyQuery,
|
||||
useMutation,
|
||||
} from '@apollo/client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Users from '/imports/api/users';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
import { partition } from '/imports/utils/array-utils';
|
||||
@ -14,7 +14,7 @@ import { USER_AGGREGATE_COUNT_SUBSCRIPTION } from '/imports/ui/core/graphql/quer
|
||||
import {
|
||||
getSortingMethod,
|
||||
sortVideoStreams,
|
||||
} from '/imports/ui/components/video-provider/stream-sorting';
|
||||
} from '/imports/ui/components/video-provider/video-provider-graphql/stream-sorting';
|
||||
import {
|
||||
useVideoState,
|
||||
setVideoState,
|
||||
@ -28,15 +28,17 @@ import {
|
||||
VIDEO_STREAMS_USERS_SUBSCRIPTION,
|
||||
VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION,
|
||||
VideoStreamsUsersResponse,
|
||||
OwnVideoStreamsResponse,
|
||||
} from '../queries';
|
||||
import videoService from '../service';
|
||||
import { CAMERA_BROADCAST_STOP } from '../mutations';
|
||||
import { GridItem, StreamItem } from '../types';
|
||||
import { DesktopPageSizes, MobilePageSizes } from '/imports/ui/Types/meetingClientSettings';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
|
||||
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
|
||||
const ROLE_VIEWER = window.meetingClientSettings.public.user.role_viewer;
|
||||
const MIRROR_WEBCAM = window.meetingClientSettings.public.app.mirrorOwnWebcam;
|
||||
const {
|
||||
paginationToggleEnabled: PAGINATION_TOGGLE_ENABLED,
|
||||
desktopPageSizes: DESKTOP_PAGE_SIZES,
|
||||
mobilePageSizes: MOBILE_PAGE_SIZES,
|
||||
desktopGridSizes: DESKTOP_GRID_SIZES,
|
||||
@ -52,27 +54,10 @@ const {
|
||||
defaultSorting: DEFAULT_SORTING,
|
||||
} = window.meetingClientSettings.public.kurento.cameraSortingModes;
|
||||
|
||||
export const useFetchedVideoStreams = () => {
|
||||
const { streams: s } = useStreams();
|
||||
let streams = [...s];
|
||||
const connectingStream = useConnectingStream(streams);
|
||||
const isPaginationEnabled = useIsPaginationEnabled();
|
||||
const isPaginationDisabled = !isPaginationEnabled;
|
||||
|
||||
const { viewParticipantsWebcams } = Settings.dataSaving;
|
||||
if (!viewParticipantsWebcams) streams = videoService.filterLocalOnly(streams);
|
||||
|
||||
if (connectingStream) {
|
||||
streams.push(connectingStream);
|
||||
}
|
||||
const pages = useVideoPage(streams);
|
||||
|
||||
if (!isPaginationDisabled) {
|
||||
return pages;
|
||||
}
|
||||
|
||||
return streams;
|
||||
};
|
||||
const FILTER_VIDEO_STATS = [
|
||||
'outbound-rtp',
|
||||
'inbound-rtp',
|
||||
];
|
||||
|
||||
export const useStatus = () => {
|
||||
const videoState = useVideoState()[0];
|
||||
@ -97,25 +82,13 @@ export const useDisableReason = () => {
|
||||
return disableReason;
|
||||
};
|
||||
|
||||
export const useRole = (isLocal: boolean) => {
|
||||
return isLocal ? 'share' : 'viewer';
|
||||
};
|
||||
|
||||
export const useMyStreamId = (deviceId: string) => {
|
||||
const { streams } = useStreams();
|
||||
const videoStream = streams.find(
|
||||
(vs) => vs.userId === Auth.userID && vs.deviceId === deviceId,
|
||||
);
|
||||
return videoStream ? videoStream.stream : null;
|
||||
};
|
||||
|
||||
export const useIsUserLocked = () => {
|
||||
const disableCam = useDisableCam();
|
||||
const { data: currentUser } = useCurrentUser((u) => ({
|
||||
locked: u.locked,
|
||||
isModerator: u.isModerator,
|
||||
}));
|
||||
return currentUser?.locked && !currentUser.isModerator && disableCam;
|
||||
return !!currentUser?.locked && !currentUser.isModerator && disableCam;
|
||||
};
|
||||
|
||||
export const useVideoStreamsCount = () => {
|
||||
@ -134,6 +107,7 @@ export const useLocalVideoStreamsCount = () => {
|
||||
export const useInfo = () => {
|
||||
const { data } = useMeeting((m) => ({
|
||||
voiceSettings: {
|
||||
// @ts-expect-error -> There seems to be a design issue on the projection portion.
|
||||
voiceConf: m.voiceSettings?.voiceConf,
|
||||
},
|
||||
}));
|
||||
@ -147,20 +121,11 @@ export const useInfo = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useMirrorOwnWebcam = (userId = null) => {
|
||||
// only true if setting defined and video ids match
|
||||
const isOwnWebcam = userId ? Auth.userID === userId : true;
|
||||
const isEnabledMirroring = getFromUserSettings(
|
||||
'bbb_mirror_own_webcam',
|
||||
MIRROR_WEBCAM,
|
||||
);
|
||||
return isOwnWebcam && isEnabledMirroring;
|
||||
};
|
||||
|
||||
export const useHasCapReached = () => {
|
||||
const { data: meeting } = useMeeting((m) => ({
|
||||
meetingCameraCap: m.meetingCameraCap,
|
||||
usersPolicies: {
|
||||
// @ts-expect-error -> There seems to be a design issue on the projection portion.
|
||||
userCameraCap: m.usersPolicies?.userCameraCap,
|
||||
},
|
||||
}));
|
||||
@ -175,82 +140,23 @@ export const useHasCapReached = () => {
|
||||
const { meetingCameraCap } = meeting;
|
||||
const { userCameraCap } = meeting.usersPolicies;
|
||||
|
||||
// @ts-expect-error -> There seems to be a design issue on the projection portion.
|
||||
const meetingCap = meetingCameraCap !== 0 && videoStreamsCount >= meetingCameraCap;
|
||||
const userCap = userCameraCap !== 0 && localVideoStreamsCount >= userCameraCap;
|
||||
|
||||
return meetingCap || userCap;
|
||||
};
|
||||
|
||||
export const useWebcamsOnlyForModerator = () => {
|
||||
const { data: meeting } = useMeeting((m) => ({
|
||||
usersPolicies: {
|
||||
webcamsOnlyForModerator: m.usersPolicies?.webcamsOnlyForModerator,
|
||||
},
|
||||
}));
|
||||
const user = Users.findOne(
|
||||
{ userId: Auth.userID },
|
||||
{ fields: { locked: 1, role: 1 } },
|
||||
);
|
||||
|
||||
if (meeting?.usersPolicies && user?.role !== ROLE_MODERATOR && user?.locked) {
|
||||
return meeting.usersPolicies.webcamsOnlyForModerator;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const useDisableCam = () => {
|
||||
const { data: meeting } = useMeeting((m) => ({
|
||||
lockSettings: {
|
||||
// @ts-expect-error -> There seems to be a design issue on the projection portion.
|
||||
disableCam: m.lockSettings?.disableCam,
|
||||
},
|
||||
}));
|
||||
return meeting?.lockSettings ? meeting?.lockSettings.disableCam : false;
|
||||
};
|
||||
|
||||
export const useVideoPinByUser = (userId: string) => {
|
||||
const user = Users.findOne({ userId }, { fields: { pin: 1 } });
|
||||
|
||||
return user?.pin || false;
|
||||
};
|
||||
|
||||
export const useSetNumberOfPages = (
|
||||
numberOfPublishers: number,
|
||||
numberOfSubscribers: number,
|
||||
pageSize: number,
|
||||
) => {
|
||||
let { currentVideoPageIndex, numberOfPages } = useVideoState()[0];
|
||||
|
||||
useEffect(() => {
|
||||
// Page size 0 means no pagination, return itself
|
||||
if (pageSize === 0) return;
|
||||
|
||||
// Page size refers only to the number of subscribers. Publishers are always
|
||||
// shown, hence not accounted for
|
||||
const nOfPages = Math.ceil(numberOfSubscribers / pageSize);
|
||||
|
||||
if (nOfPages !== numberOfPages) {
|
||||
numberOfPages = nOfPages;
|
||||
// Check if we have to page back on the current video page index due to a
|
||||
// page ceasing to exist
|
||||
if (nOfPages === 0) {
|
||||
currentVideoPageIndex = 0;
|
||||
} else if (currentVideoPageIndex + 1 > numberOfPages) {
|
||||
videoService.getPreviousVideoPage();
|
||||
}
|
||||
|
||||
videoService.numberOfPages = nOfPages;
|
||||
videoService.currentVideoPageIndex = currentVideoPageIndex;
|
||||
setVideoState((curr) => ({
|
||||
...curr,
|
||||
numberOfPages,
|
||||
currentVideoPageIndex,
|
||||
}));
|
||||
}
|
||||
}, [numberOfPublishers, numberOfSubscribers, pageSize, currentVideoPageIndex, numberOfPages]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const usePageSizeDictionary = () => {
|
||||
const { data: countData } = useSubscription(
|
||||
USER_AGGREGATE_COUNT_SUBSCRIPTION,
|
||||
@ -265,7 +171,10 @@ export const usePageSizeDictionary = () => {
|
||||
// matching threshold entry, return the val.
|
||||
let targetThreshold;
|
||||
const processThreshold = (
|
||||
threshold = {
|
||||
threshold: {
|
||||
desktopPageSizes?: DesktopPageSizes,
|
||||
mobilePageSizes?: MobilePageSizes,
|
||||
} = {
|
||||
desktopPageSizes: DESKTOP_PAGE_SIZES,
|
||||
mobilePageSizes: MOBILE_PAGE_SIZES,
|
||||
},
|
||||
@ -309,25 +218,28 @@ export const useMyPageSize = () => {
|
||||
let size;
|
||||
switch (myRole) {
|
||||
case ROLE_MODERATOR:
|
||||
size = pageSizes.moderator;
|
||||
size = pageSizes?.moderator;
|
||||
break;
|
||||
case ROLE_VIEWER:
|
||||
default:
|
||||
size = pageSizes.viewer;
|
||||
size = pageSizes?.viewer;
|
||||
}
|
||||
|
||||
return size;
|
||||
return size ?? 0;
|
||||
};
|
||||
|
||||
export const useShouldRenderPaginationToggle = () => PAGINATION_TOGGLE_ENABLED && useMyPageSize() > 0;
|
||||
|
||||
export const useIsPaginationEnabled = (paginationEnabled) => paginationEnabled && useMyPageSize() > 0;
|
||||
export const useIsPaginationEnabled = (paginationEnabled: boolean) => paginationEnabled && useMyPageSize() > 0;
|
||||
|
||||
export const useStreams = () => {
|
||||
const videoStreams = useReactiveVar(streams);
|
||||
return { streams: videoStreams };
|
||||
};
|
||||
|
||||
type StreamUser = VideoStreamsUsersResponse['user'][number] & {
|
||||
pin: boolean;
|
||||
sortName: string;
|
||||
};
|
||||
|
||||
export const useStreamUsers = (isGridEnabled: boolean) => {
|
||||
const { streams } = useStreams();
|
||||
const subscription = isGridEnabled
|
||||
@ -335,24 +247,34 @@ export const useStreamUsers = (isGridEnabled: boolean) => {
|
||||
: VIDEO_STREAMS_USERS_FILTERED_SUBSCRIPTION;
|
||||
const variables = isGridEnabled
|
||||
? {}
|
||||
: { userIds: streams.map((s) => s.userId) }
|
||||
: { userIds: streams.map((s) => s.userId) };
|
||||
const { data, loading, error } = useSubscription<VideoStreamsUsersResponse>(
|
||||
subscription,
|
||||
{ variables },
|
||||
);
|
||||
const users = useMemo(
|
||||
() => (data
|
||||
? data.user.map((user) => ({
|
||||
const [users, setUsers] = useState<StreamUser[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
|
||||
if (error) {
|
||||
logger.error(`Error on load stream users. name=${error.name}`, error);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
const newData = data.user.map((user) => ({
|
||||
...user,
|
||||
pin: user.pinned,
|
||||
sortName: user.nameSortable,
|
||||
}))
|
||||
: []),
|
||||
[data],
|
||||
);
|
||||
}));
|
||||
setUsers(newData);
|
||||
} else {
|
||||
setUsers([]);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return {
|
||||
users: users || [],
|
||||
users,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
@ -367,11 +289,6 @@ export const useSharedDevices = () => {
|
||||
return devices;
|
||||
};
|
||||
|
||||
export const useUserIdsFromVideoStreams = () => {
|
||||
const { streams } = useStreams();
|
||||
return streams.map((s) => s.userId);
|
||||
};
|
||||
|
||||
export const useNumberOfPages = () => {
|
||||
const state = useVideoState()[0];
|
||||
return state.numberOfPages;
|
||||
@ -401,40 +318,6 @@ export const useGridSize = () => {
|
||||
return size;
|
||||
};
|
||||
|
||||
export const useVideoPage = (streams) => {
|
||||
const numberOfPages = useNumberOfPages();
|
||||
const currentVideoPageIndex = useCurrentVideoPageIndex();
|
||||
const pageSize = useMyPageSize();
|
||||
|
||||
// Publishers are taken into account for the page size calculations. They
|
||||
// also appear on every page. Same for pinned user.
|
||||
const [filtered, others] = partition(
|
||||
streams,
|
||||
(vs) => Auth.userID === vs.userId || vs.pin,
|
||||
);
|
||||
|
||||
// Separate pin from local cameras
|
||||
const [pin, mine] = partition(filtered, (vs) => vs.pin);
|
||||
|
||||
// Recalculate total number of pages
|
||||
useSetNumberOfPages(filtered.length, others.length, pageSize);
|
||||
const chunkIndex = currentVideoPageIndex * pageSize;
|
||||
|
||||
// This is an extra check because pagination is globally in effect (hard
|
||||
// limited page sizes, toggles on), but we might still only have one page.
|
||||
// Use the default sorting method if that's the case.
|
||||
const sortingMethod = numberOfPages > 1 ? PAGINATION_SORTING : DEFAULT_SORTING;
|
||||
const paginatedStreams = sortVideoStreams(others, sortingMethod).slice(
|
||||
chunkIndex,
|
||||
chunkIndex + pageSize,
|
||||
) || [];
|
||||
|
||||
if (getSortingMethod(sortingMethod).localFirst) {
|
||||
return [...pin, ...mine, ...paginatedStreams];
|
||||
}
|
||||
return [...pin, ...paginatedStreams, ...mine];
|
||||
};
|
||||
|
||||
export const useVideoStreams = (
|
||||
isGridEnabled: boolean,
|
||||
paginationEnabled: boolean,
|
||||
@ -443,13 +326,13 @@ export const useVideoStreams = (
|
||||
const [state] = useVideoState();
|
||||
const { currentVideoPageIndex, numberOfPages } = state;
|
||||
const { users } = useStreamUsers(isGridEnabled);
|
||||
const { streams: videoStreams} = useStreams();
|
||||
const { streams: videoStreams } = useStreams();
|
||||
const connectingStream = useConnectingStream(videoStreams);
|
||||
const gridSize = useGridSize();
|
||||
const myPageSize = useMyPageSize();
|
||||
const isPaginationEnabled = useIsPaginationEnabled(paginationEnabled);
|
||||
let streams = [...videoStreams];
|
||||
let gridUsers = [];
|
||||
let streams: StreamItem[] = [...videoStreams];
|
||||
let gridUsers: GridItem[] = [];
|
||||
|
||||
if (connectingStream) streams.push(connectingStream);
|
||||
|
||||
@ -458,8 +341,14 @@ export const useVideoStreams = (
|
||||
}
|
||||
|
||||
if (isPaginationEnabled) {
|
||||
const [filtered, others] = partition(streams, (vs) => Auth.userID === vs.userId || vs.pin);
|
||||
const [pin, mine] = partition(filtered, (vs) => vs.pin);
|
||||
const [filtered, others] = partition(
|
||||
streams,
|
||||
(vs: StreamItem) => Auth.userID === vs.userId || (vs.type === 'stream' && vs.pin),
|
||||
);
|
||||
const [pin, mine] = partition(
|
||||
filtered,
|
||||
(vs: StreamItem) => vs.type === 'stream' && vs.pin,
|
||||
);
|
||||
|
||||
if (myPageSize !== 0) {
|
||||
const total = others.length ?? 0;
|
||||
@ -505,7 +394,7 @@ export const useVideoStreams = (
|
||||
(user) => !user.loggedOut && !user.left && !streamUsers.includes(user.userId),
|
||||
)
|
||||
.map((user) => ({
|
||||
isGridItem: true,
|
||||
type: 'grid' as const,
|
||||
...user,
|
||||
}))
|
||||
.slice(0, gridSize - streams.length);
|
||||
@ -519,108 +408,142 @@ export const useVideoStreams = (
|
||||
};
|
||||
};
|
||||
|
||||
export const useGridUsers = (users = []) => {
|
||||
const pageSize = useMyPageSize();
|
||||
const { streams } = useStreams();
|
||||
const paginatedStreams = useVideoPage(streams, pageSize);
|
||||
const isPaginationEnabled = useIsPaginationEnabled();
|
||||
const isPaginationDisabled = !isPaginationEnabled || pageSize === 0;
|
||||
const isGridEnabled = videoService.isGridEnabled();
|
||||
let gridUsers = [];
|
||||
|
||||
if (isPaginationDisabled) {
|
||||
if (isGridEnabled) {
|
||||
const streamUsers = streams.map((stream) => stream.userId);
|
||||
|
||||
gridUsers = users
|
||||
.filter(
|
||||
(user) => !user.loggedOut && !user.left && !streamUsers.includes(user.userId),
|
||||
)
|
||||
.map((user) => ({
|
||||
isGridItem: true,
|
||||
...user,
|
||||
}));
|
||||
}
|
||||
|
||||
return gridUsers;
|
||||
}
|
||||
|
||||
if (isGridEnabled) {
|
||||
const streamUsers = paginatedStreams.map((stream) => stream.userId);
|
||||
|
||||
gridUsers = users
|
||||
.filter(
|
||||
(user) => !user.loggedOut && !user.left && !streamUsers.includes(user.userId),
|
||||
)
|
||||
.map((user) => ({
|
||||
isGridItem: true,
|
||||
...user,
|
||||
}));
|
||||
}
|
||||
return gridUsers;
|
||||
};
|
||||
|
||||
export const useHasVideoStream = () => {
|
||||
const { streams } = useStreams();
|
||||
return streams.some((s) => s.userId === Auth.userID);
|
||||
};
|
||||
|
||||
export const useHasStream = (streams, stream) => {
|
||||
return streams.find((s) => s.stream === stream);
|
||||
};
|
||||
const useOwnVideoStreamsQuery = () => useLazyQuery<OwnVideoStreamsResponse>(
|
||||
OWN_VIDEO_STREAMS_QUERY,
|
||||
{ variables: { userId: Auth.userID } },
|
||||
);
|
||||
|
||||
export const useFilterModeratorOnly = (streams) => {
|
||||
const amIViewer = useMyRole() === ROLE_VIEWER;
|
||||
|
||||
if (amIViewer) {
|
||||
const moderators = Users.find(
|
||||
{
|
||||
role: ROLE_MODERATOR,
|
||||
},
|
||||
{ fields: { userId: 1 } },
|
||||
)
|
||||
.fetch()
|
||||
.map((user) => user.userId);
|
||||
|
||||
return streams.reduce((result, stream) => {
|
||||
const { userId } = stream;
|
||||
|
||||
const isModerator = moderators.includes(userId);
|
||||
const isMe = Auth.userID === userId;
|
||||
|
||||
if (isModerator || isMe) result.push(stream);
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
return streams;
|
||||
};
|
||||
|
||||
export const useExitVideo = () => {
|
||||
export const useExitVideo = (forceExit = false) => {
|
||||
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
|
||||
const [getOwnVideoStreams] = useLazyQuery(OWN_VIDEO_STREAMS_QUERY, { variables: { userId: Auth.userID } });
|
||||
const [getOwnVideoStreams] = useOwnVideoStreamsQuery();
|
||||
|
||||
const exitVideo = useCallback(() => {
|
||||
const exitVideo = useCallback(async () => {
|
||||
const { isConnected } = getVideoState();
|
||||
|
||||
if (isConnected) {
|
||||
if (isConnected || forceExit) {
|
||||
const sendUserUnshareWebcam = (cameraId: string) => {
|
||||
cameraBroadcastStop({ variables: { cameraId } });
|
||||
};
|
||||
|
||||
getOwnVideoStreams().then(({ data }) => {
|
||||
if (!data) return;
|
||||
const streams = data.user_camera || [];
|
||||
streams.forEach((s) => sendUserUnshareWebcam(s.streamId));
|
||||
videoService.exitedVideo();
|
||||
return getOwnVideoStreams().then(({ data }) => {
|
||||
if (data) {
|
||||
const streams = data.user_camera || [];
|
||||
streams.forEach((s: { streamId: string }) => sendUserUnshareWebcam(s.streamId));
|
||||
videoService.exitedVideo();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}, [cameraBroadcastStop]);
|
||||
|
||||
return exitVideo;
|
||||
};
|
||||
|
||||
export const useViewersInWebcamCount = () => {
|
||||
export const useViewersInWebcamCount = (): number => {
|
||||
const { data } = useSubscription(VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION);
|
||||
return data?.user_camera_aggregate?.aggregate?.count || 0;
|
||||
};
|
||||
|
||||
export const useLockUser = () => {
|
||||
const exitVideo = useExitVideo();
|
||||
return useCallback(() => {
|
||||
const { isConnected } = getVideoState();
|
||||
if (isConnected) {
|
||||
exitVideo();
|
||||
}
|
||||
}, [exitVideo]);
|
||||
};
|
||||
|
||||
export const useStopVideo = () => {
|
||||
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
|
||||
const [getOwnVideoStreams] = useOwnVideoStreamsQuery();
|
||||
const exit = useExitVideo(true);
|
||||
|
||||
return useCallback(async (cameraId?: string) => {
|
||||
const { data } = await getOwnVideoStreams();
|
||||
const streams = data?.user_camera ?? [];
|
||||
const hasTargetStream = streams.some((s) => s.streamId === cameraId);
|
||||
const hasOtherStream = streams.some((s) => s.streamId !== cameraId);
|
||||
|
||||
if (hasTargetStream) {
|
||||
cameraBroadcastStop({ variables: { cameraId } });
|
||||
}
|
||||
|
||||
if (!hasOtherStream) {
|
||||
videoService.exitedVideo();
|
||||
} else {
|
||||
exit().then(() => {
|
||||
videoService.exitedVideo();
|
||||
});
|
||||
}
|
||||
}, [cameraBroadcastStop]);
|
||||
};
|
||||
|
||||
export const useActivePeers = (
|
||||
isGridEnabled: boolean,
|
||||
paginationEnabled: boolean,
|
||||
viewParticipantsWebcams: boolean,
|
||||
) => {
|
||||
const videoData = useVideoStreams(
|
||||
isGridEnabled,
|
||||
paginationEnabled,
|
||||
viewParticipantsWebcams,
|
||||
);
|
||||
|
||||
if (!videoData) return null;
|
||||
|
||||
const { streams: activeVideoStreams } = videoData;
|
||||
|
||||
if (!activeVideoStreams) return null;
|
||||
|
||||
const activePeers: Record<string, RTCPeerConnection> = {};
|
||||
|
||||
activeVideoStreams.forEach((stream) => {
|
||||
if (videoService.webRtcPeersRef()[stream.stream]) {
|
||||
activePeers[stream.stream] = videoService.webRtcPeersRef()[stream.stream].peerConnection;
|
||||
}
|
||||
});
|
||||
|
||||
return activePeers;
|
||||
};
|
||||
|
||||
export const useGetStats = (
|
||||
isGridEnabled: boolean,
|
||||
paginationEnabled: boolean,
|
||||
viewParticipantsWebcams: boolean,
|
||||
) => {
|
||||
const peers = useActivePeers(
|
||||
isGridEnabled,
|
||||
paginationEnabled,
|
||||
viewParticipantsWebcams,
|
||||
);
|
||||
|
||||
return useCallback(async () => {
|
||||
if (!peers) return null;
|
||||
|
||||
const stats: Record<string, unknown> = {};
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(peers).map(async (peerId) => {
|
||||
const peerStats = await peers[peerId].getStats();
|
||||
|
||||
const videoStats: Record<string, unknown> = {};
|
||||
|
||||
peerStats.forEach((stat) => {
|
||||
if (FILTER_VIDEO_STATS.includes(stat.type)) {
|
||||
videoStats[stat.type] = stat;
|
||||
}
|
||||
});
|
||||
stats[peerId] = videoStats;
|
||||
}),
|
||||
);
|
||||
|
||||
return stats;
|
||||
}, [peers]);
|
||||
};
|
||||
|
@ -1,10 +1,9 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React, { Component } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { IntlShape, defineMessages, injectIntl } from 'react-intl';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import { toast } from 'react-toastify';
|
||||
import Styled from './styles';
|
||||
import { LockSettings } from '/imports/ui/Types/meeting';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
suggestLockTitle: {
|
||||
@ -27,8 +26,21 @@ const intlMessages = defineMessages({
|
||||
|
||||
const REPEAT_INTERVAL = 120000;
|
||||
|
||||
class LockViewersNotifyComponent extends Component {
|
||||
constructor(props) {
|
||||
interface LockViewersNotifyComponentProps {
|
||||
toggleWebcamsOnlyForModerator: () => void;
|
||||
currentUserIsModerator: boolean;
|
||||
viewersInWebcam: number;
|
||||
limitOfViewersInWebcam: number;
|
||||
limitOfViewersInWebcamIsEnable: boolean;
|
||||
lockSettings: LockSettings;
|
||||
webcamOnlyForModerator: boolean;
|
||||
intl: IntlShape;
|
||||
}
|
||||
|
||||
class LockViewersNotifyComponent extends Component<LockViewersNotifyComponentProps, object> {
|
||||
private interval: NodeJS.Timeout | null;
|
||||
|
||||
constructor(props: LockViewersNotifyComponentProps) {
|
||||
super(props);
|
||||
this.interval = null;
|
||||
this.intervalCallback = this.intervalCallback.bind(this);
|
||||
@ -55,7 +67,7 @@ class LockViewersNotifyComponent extends Component {
|
||||
this.intervalCallback();
|
||||
}
|
||||
if (webcamForViewersIsLocked || (!viwerersInWebcamGreaterThatLimit && this.interval)) {
|
||||
clearInterval(this.interval);
|
||||
if (this.interval) clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
@ -9,8 +7,16 @@ import ManyUsersComponent from './component';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { SET_WEBCAM_ONLY_FOR_MODERATOR } from '/imports/ui/components/lock-viewers/mutations';
|
||||
import { useViewersInWebcamCount } from '../hooks';
|
||||
import { LockSettings } from '/imports/ui/Types/meeting';
|
||||
|
||||
const ManyUsersContainer = (props) => {
|
||||
interface ManyUsersContainerProps {
|
||||
lockSettings: LockSettings,
|
||||
webcamOnlyForModerator: boolean,
|
||||
limitOfViewersInWebcam: number,
|
||||
limitOfViewersInWebcamIsEnable: boolean,
|
||||
}
|
||||
|
||||
const ManyUsersContainer: React.FC<ManyUsersContainerProps> = (props) => {
|
||||
const { data: currentUserData } = useCurrentUser((user) => ({
|
||||
isModerator: user.isModerator,
|
||||
}));
|
||||
@ -27,15 +33,24 @@ const ManyUsersContainer = (props) => {
|
||||
|
||||
const viewersInWebcam = useViewersInWebcamCount();
|
||||
|
||||
const currentUserIsModerator = currentUserData?.isModerator;
|
||||
const currentUserIsModerator = !!currentUserData?.isModerator;
|
||||
|
||||
const {
|
||||
limitOfViewersInWebcam,
|
||||
limitOfViewersInWebcamIsEnable,
|
||||
lockSettings,
|
||||
webcamOnlyForModerator,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ManyUsersComponent
|
||||
{...{
|
||||
toggleWebcamsOnlyForModerator,
|
||||
currentUserIsModerator,
|
||||
viewersInWebcam,
|
||||
...props,
|
||||
}}
|
||||
toggleWebcamsOnlyForModerator={toggleWebcamsOnlyForModerator}
|
||||
currentUserIsModerator={currentUserIsModerator}
|
||||
viewersInWebcam={viewersInWebcam}
|
||||
limitOfViewersInWebcam={limitOfViewersInWebcam}
|
||||
limitOfViewersInWebcamIsEnable={limitOfViewersInWebcamIsEnable}
|
||||
lockSettings={lockSettings}
|
||||
webcamOnlyForModerator={webcamOnlyForModerator}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,6 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import styled from 'styled-components';
|
||||
import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import Styled from '/imports/ui/components/breakout-room/styles';
|
||||
import Styled from '/imports/ui/components/breakout-room/breakout-room/styles';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
const Info = styled.p`
|
||||
@ -17,6 +15,7 @@ const ButtonWrapper = styled(Styled.BreakoutActions)`
|
||||
}
|
||||
`;
|
||||
|
||||
// @ts-expect-error -> Untyped component.
|
||||
const ManyUsersButton = styled(Button)`
|
||||
flex: 0 1 48%;
|
||||
color: ${colorPrimary};
|
||||
|
@ -33,13 +33,20 @@ export interface VideoStreamsUsersResponse {
|
||||
presenter: boolean;
|
||||
clientType: string;
|
||||
raiseHand: boolean;
|
||||
isModerator: boolean
|
||||
isModerator: boolean;
|
||||
left: boolean;
|
||||
reaction: {
|
||||
reactionEmoji: string;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface OwnVideoStreamsResponse {
|
||||
user_camera: {
|
||||
streamId: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const VIDEO_STREAMS_SUBSCRIPTION = gql`
|
||||
subscription VideoStreams {
|
||||
user_camera {
|
||||
@ -142,6 +149,7 @@ export const VIDEO_STREAMS_USERS_FILTERED_SUBSCRIPTION = gql`
|
||||
`;
|
||||
|
||||
export default {
|
||||
OWN_VIDEO_STREAMS_QUERY,
|
||||
VIDEO_STREAMS_SUBSCRIPTION,
|
||||
VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION,
|
||||
VIDEO_STREAMS_USERS_SUBSCRIPTION,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -12,20 +12,21 @@ const [useVideoState, setVideoState, videoState] = createUseLocalState({
|
||||
|
||||
const getVideoState = () => videoState();
|
||||
|
||||
type ConnectingStream = {
|
||||
export type ConnectingStream = {
|
||||
stream: string;
|
||||
name: string;
|
||||
userId: string;
|
||||
type: 'connecting';
|
||||
} | null;
|
||||
|
||||
const connectingStream = makeVar<ConnectingStream>(null);
|
||||
|
||||
const useConnectingStream = (streams: { stream: string }[]) => {
|
||||
const useConnectingStream = (streams?: Stream[]) => {
|
||||
const connecting = useReactiveVar(connectingStream);
|
||||
|
||||
if (!connecting) return null;
|
||||
|
||||
const hasStream = streams.find((s) => s.stream === connecting.stream);
|
||||
const hasStream = streams && streams.find((s) => s.stream === connecting.stream);
|
||||
|
||||
if (hasStream) {
|
||||
return null;
|
||||
@ -47,7 +48,8 @@ export type Stream = {
|
||||
pin: boolean;
|
||||
floor: boolean;
|
||||
lastFloorTime: string;
|
||||
isUserModerator: boolean;
|
||||
isModerator: boolean;
|
||||
type: 'stream';
|
||||
}
|
||||
|
||||
const streams = makeVar<Stream[]>([]);
|
||||
@ -56,6 +58,8 @@ const setStreams = (vs: Stream[]) => {
|
||||
streams(vs);
|
||||
};
|
||||
|
||||
const getStreams = () => streams();
|
||||
|
||||
export {
|
||||
useVideoState,
|
||||
setVideoState,
|
||||
@ -63,6 +67,7 @@ export {
|
||||
useConnectingStream,
|
||||
setConnectingStream,
|
||||
setStreams,
|
||||
getStreams,
|
||||
streams,
|
||||
};
|
||||
|
||||
@ -73,5 +78,6 @@ export default {
|
||||
useConnectingStream,
|
||||
setConnectingStream,
|
||||
setStreams,
|
||||
getStreams,
|
||||
streams,
|
||||
};
|
||||
|
@ -1,12 +1,17 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import { StreamItem } from './types';
|
||||
import UserListService from '/imports/ui/components/user-list/service';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
|
||||
const DEFAULT_SORTING_MODE = 'LOCAL_ALPHABETICAL';
|
||||
|
||||
// pin first
|
||||
export const sortPin = (s1, s2) => {
|
||||
export const sortPin = (s1: StreamItem, s2: StreamItem) => {
|
||||
if (s1.type === 'connecting') {
|
||||
return 1;
|
||||
}
|
||||
if (s2.type === 'connecting') {
|
||||
return -1;
|
||||
}
|
||||
if (s1.pin) {
|
||||
return -1;
|
||||
} if (s2.pin) {
|
||||
@ -15,19 +20,27 @@ export const sortPin = (s1, s2) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const mandatorySorting = (s1, s2) => sortPin(s1, s2);
|
||||
export const mandatorySorting = (s1: StreamItem, s2: StreamItem) => sortPin(s1, s2);
|
||||
|
||||
// lastFloorTime, descending
|
||||
export const sortVoiceActivity = (s1, s2) => {
|
||||
export const sortVoiceActivity = (s1: StreamItem, s2: StreamItem) => {
|
||||
if (s1.type === 'connecting') {
|
||||
return 1;
|
||||
}
|
||||
if (s2.type === 'connecting') {
|
||||
return -1;
|
||||
}
|
||||
if (s2.lastFloorTime < s1.lastFloorTime) {
|
||||
return -1;
|
||||
} else if (s2.lastFloorTime > s1.lastFloorTime) {
|
||||
}
|
||||
if (s2.lastFloorTime > s1.lastFloorTime) {
|
||||
return 1;
|
||||
} else return 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// pin -> lastFloorTime (descending) -> alphabetical -> local
|
||||
export const sortVoiceActivityLocal = (s1, s2) => {
|
||||
export const sortVoiceActivityLocal = (s1: StreamItem, s2: StreamItem) => {
|
||||
if (s1.userId === Auth.userID) {
|
||||
return 1;
|
||||
} if (s2.userId === Auth.userID) {
|
||||
@ -40,26 +53,28 @@ export const sortVoiceActivityLocal = (s1, s2) => {
|
||||
};
|
||||
|
||||
// pin -> local -> lastFloorTime (descending) -> alphabetical
|
||||
export const sortLocalVoiceActivity = (s1, s2) => mandatorySorting(s1, s2)
|
||||
export const sortLocalVoiceActivity = (s1: StreamItem, s2: StreamItem) => mandatorySorting(s1, s2)
|
||||
|| UserListService.sortUsersByCurrent(s1, s2)
|
||||
|| sortVoiceActivity(s1, s2)
|
||||
|| UserListService.sortUsersByName(s1, s2);
|
||||
|
||||
// pin -> local -> alphabetic
|
||||
export const sortLocalAlphabetical = (s1, s2) => mandatorySorting(s1, s2)
|
||||
export const sortLocalAlphabetical = (s1: StreamItem, s2: StreamItem) => mandatorySorting(s1, s2)
|
||||
|| UserListService.sortUsersByCurrent(s1, s2)
|
||||
|| UserListService.sortUsersByName(s1, s2);
|
||||
|
||||
export const sortPresenter = (s1, s2) => {
|
||||
export const sortPresenter = (s1: StreamItem, s2: StreamItem) => {
|
||||
if (UserListService.isUserPresenter(s1.userId)) {
|
||||
return -1;
|
||||
} else if (UserListService.isUserPresenter(s2.userId)) {
|
||||
}
|
||||
if (UserListService.isUserPresenter(s2.userId)) {
|
||||
return 1;
|
||||
} else return 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// pin -> local -> presenter -> alphabetical
|
||||
export const sortLocalPresenterAlphabetical = (s1, s2) => mandatorySorting(s1, s2)
|
||||
export const sortLocalPresenterAlphabetical = (s1: StreamItem, s2: StreamItem) => mandatorySorting(s1, s2)
|
||||
|| UserListService.sortUsersByCurrent(s1, s2)
|
||||
|| sortPresenter(s1, s2)
|
||||
|| UserListService.sortUsersByName(s1, s2);
|
||||
@ -95,12 +110,13 @@ const SORTING_METHODS = Object.freeze({
|
||||
LOCAL_ALPHABETICAL: {
|
||||
sortingMethod: sortLocalAlphabetical,
|
||||
neededDataTypes: MANDATORY_DATA_TYPES,
|
||||
filter: false,
|
||||
localFirst: true,
|
||||
},
|
||||
VOICE_ACTIVITY_LOCAL: {
|
||||
sortingMethod: sortVoiceActivityLocal,
|
||||
neededDataTypes: {
|
||||
lastFloorTime: 1, floor: 1, ...MANDATORY_DATA_TYPES,
|
||||
lastFloorTime: 1, ...MANDATORY_DATA_TYPES,
|
||||
},
|
||||
filter: true,
|
||||
localFirst: false,
|
||||
@ -108,7 +124,7 @@ const SORTING_METHODS = Object.freeze({
|
||||
LOCAL_VOICE_ACTIVITY: {
|
||||
sortingMethod: sortLocalVoiceActivity,
|
||||
neededDataTypes: {
|
||||
lastFloorTime: 1, floor: 1, ...MANDATORY_DATA_TYPES,
|
||||
lastFloorTime: 1, ...MANDATORY_DATA_TYPES,
|
||||
},
|
||||
filter: true,
|
||||
localFirst: true,
|
||||
@ -116,27 +132,17 @@ const SORTING_METHODS = Object.freeze({
|
||||
LOCAL_PRESENTER_ALPHABETICAL: {
|
||||
sortingMethod: sortLocalPresenterAlphabetical,
|
||||
neededDataTypes: MANDATORY_DATA_TYPES,
|
||||
filter: false,
|
||||
localFirst: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const getSortingMethod = (identifier) => {
|
||||
return SORTING_METHODS[identifier] || SORTING_METHODS[DEFAULT_SORTING_MODE];
|
||||
export const getSortingMethod = (identifier: string) => {
|
||||
return SORTING_METHODS[identifier as keyof typeof SORTING_METHODS] || SORTING_METHODS[DEFAULT_SORTING_MODE];
|
||||
};
|
||||
|
||||
export const sortVideoStreams = (streams, mode) => {
|
||||
const { sortingMethod, filter } = getSortingMethod(mode);
|
||||
export const sortVideoStreams = (streams: StreamItem[], mode: string) => {
|
||||
const { sortingMethod } = getSortingMethod(mode);
|
||||
const sorted = streams.sort(sortingMethod);
|
||||
|
||||
if (!filter) return sorted;
|
||||
|
||||
return sorted.map(videoStream => ({
|
||||
stream: videoStream.stream,
|
||||
isGridItem: videoStream?.isGridItem,
|
||||
userId: videoStream.userId,
|
||||
name: videoStream.name,
|
||||
sortName: videoStream.sortName,
|
||||
floor: videoStream.floor,
|
||||
pin: videoStream.pin,
|
||||
}));
|
||||
return sorted;
|
||||
};
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { VideoStreamsUsersResponse } from './queries';
|
||||
import { ConnectingStream, Stream } from './state';
|
||||
|
||||
export type StreamUser = VideoStreamsUsersResponse['user'][number];
|
||||
export type StreamItem = Stream | NonNullable<ConnectingStream>;
|
||||
export type GridItem = StreamUser & { type: 'grid' };
|
||||
export type VideoItem = StreamItem | GridItem;
|
@ -1,11 +1,7 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FetchResult } from '@apollo/client';
|
||||
import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji';
|
||||
import VideoService from '../service';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Styled from './styles';
|
||||
import { IntlShape, defineMessages, injectIntl } from 'react-intl';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import { debounce } from '/imports/utils/debounce';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
@ -14,6 +10,9 @@ import Button from '/imports/ui/components/common/button/component';
|
||||
import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
|
||||
import { CameraSettingsDropdownItemType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/camera-settings-dropdown-item/enums';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import { CameraSettingsDropdownInterface } from 'bigbluebutton-html-plugin-sdk';
|
||||
import VideoService from '../service';
|
||||
import Styled from './styles';
|
||||
|
||||
const ENABLE_WEBCAM_SELECTOR_BUTTON = window.meetingClientSettings.public.app.enableWebcamSelectorButton;
|
||||
const ENABLE_CAMERA_BRIGHTNESS = window.meetingClientSettings.public.app.enableCameraBrightness;
|
||||
@ -59,29 +58,32 @@ const intlMessages = defineMessages({
|
||||
|
||||
const JOIN_VIDEO_DELAY_MILLISECONDS = 500;
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasVideoStream: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
cameraSettingsDropdownItems: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
})).isRequired,
|
||||
sendUserUnshareWebcam: PropTypes.func.isRequired,
|
||||
setLocalSettings: PropTypes.func.isRequired,
|
||||
};
|
||||
interface JoinVideoButtonProps {
|
||||
cameraSettingsDropdownItems: CameraSettingsDropdownInterface[];
|
||||
hasVideoStream: boolean;
|
||||
updateSettings: (
|
||||
obj: object,
|
||||
msgDescriptor: object | null,
|
||||
mutation: (settings: Record<string, unknown>) => void,
|
||||
) => void;
|
||||
disableReason: string | undefined;
|
||||
status: string;
|
||||
setLocalSettings: (settings: Record<string, unknown>) => Promise<FetchResult<object>>;
|
||||
exitVideo: () => void;
|
||||
stopVideo: (cameraId?: string | undefined) => void;
|
||||
intl: IntlShape;
|
||||
}
|
||||
|
||||
const JoinVideoButton = ({
|
||||
const JoinVideoButton: React.FC<JoinVideoButtonProps> = ({
|
||||
intl,
|
||||
hasVideoStream,
|
||||
status,
|
||||
disableReason,
|
||||
updateSettings,
|
||||
cameraSettingsDropdownItems,
|
||||
sendUserUnshareWebcam,
|
||||
setLocalSettings,
|
||||
streams,
|
||||
exitVideo: exit,
|
||||
stopVideo,
|
||||
}) => {
|
||||
const { isMobile } = deviceInfo;
|
||||
const isMobileSharingCamera = hasVideoStream && isMobile;
|
||||
@ -95,18 +97,20 @@ const JoinVideoButton = ({
|
||||
const exitVideo = () => isDesktopSharingCamera && (!VideoService.isMultipleCamerasEnabled()
|
||||
|| shouldEnableWebcamSelectorButton);
|
||||
|
||||
const [propsToPassModal, setPropsToPassModal] = useState({});
|
||||
const [propsToPassModal, setPropsToPassModal] = useState<{ isVisualEffects?: boolean }>({});
|
||||
const [forceOpen, setForceOpen] = useState(false);
|
||||
const [isVideoPreviewModalOpen, setVideoPreviewModalIsOpen] = useState(false);
|
||||
const [wasSelfViewDisabled, setWasSelfViewDisabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error -> Untyped object.
|
||||
const isSelfViewDisabled = Settings.application.selfViewDisable;
|
||||
|
||||
if (isVideoPreviewModalOpen && isSelfViewDisabled) {
|
||||
setWasSelfViewDisabled(true);
|
||||
const obj = {
|
||||
application:
|
||||
// @ts-expect-error -> Untyped object.
|
||||
{ ...Settings.application, selfViewDisable: false },
|
||||
};
|
||||
updateSettings(obj, null, setLocalSettings);
|
||||
@ -116,7 +120,7 @@ const JoinVideoButton = ({
|
||||
const handleOnClick = debounce(() => {
|
||||
switch (status) {
|
||||
case 'videoConnecting':
|
||||
VideoService.stopVideo(undefined, sendUserUnshareWebcam, streams);
|
||||
stopVideo();
|
||||
break;
|
||||
case 'connected':
|
||||
default:
|
||||
@ -129,7 +133,7 @@ const JoinVideoButton = ({
|
||||
}
|
||||
}, JOIN_VIDEO_DELAY_MILLISECONDS);
|
||||
|
||||
const handleOpenAdvancedOptions = (callback) => {
|
||||
const handleOpenAdvancedOptions = (callback?: () => void) => {
|
||||
if (callback) callback();
|
||||
setForceOpen(isDesktopSharingCamera);
|
||||
setVideoPreviewModalIsOpen(true);
|
||||
@ -144,8 +148,8 @@ const JoinVideoButton = ({
|
||||
};
|
||||
|
||||
const label = disableReason
|
||||
? intl.formatMessage(intlMessages[disableReason])
|
||||
: intl.formatMessage(intlMessages[getMessageFromStatus()]);
|
||||
? intl.formatMessage(intlMessages[disableReason as keyof typeof intlMessages])
|
||||
: intl.formatMessage(intlMessages[getMessageFromStatus() as keyof typeof intlMessages]);
|
||||
|
||||
const isSharing = hasVideoStream || status === 'videoConnecting';
|
||||
|
||||
@ -168,8 +172,7 @@ const JoinVideoButton = ({
|
||||
{
|
||||
key: 'virtualBgSelection',
|
||||
label: intl.formatMessage(intlMessages.visualEffects),
|
||||
onClick: () => handleOpenAdvancedOptions((
|
||||
) => setPropsToPassModal({ isVisualEffects: true })),
|
||||
onClick: () => handleOpenAdvancedOptions(() => setPropsToPassModal({ isVisualEffects: true })),
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -182,8 +185,11 @@ const JoinVideoButton = ({
|
||||
case CameraSettingsDropdownItemType.OPTION:
|
||||
actions.push({
|
||||
key: plugin.id,
|
||||
// @ts-expect-error -> Plugin-related.
|
||||
label: plugin.label,
|
||||
// @ts-expect-error -> Plugin-related.
|
||||
onClick: plugin.onClick,
|
||||
// @ts-expect-error -> Plugin-related.
|
||||
icon: plugin.icon,
|
||||
});
|
||||
break;
|
||||
@ -249,8 +255,11 @@ const JoinVideoButton = ({
|
||||
if (wasSelfViewDisabled) {
|
||||
setTimeout(() => {
|
||||
const obj = {
|
||||
application:
|
||||
{ ...Settings.application, selfViewDisable: true },
|
||||
application: {
|
||||
// @ts-expect-error -> Untyped object.
|
||||
...Settings.application,
|
||||
selfViewDisable: true,
|
||||
},
|
||||
};
|
||||
updateSettings(obj, null, setLocalSettings);
|
||||
setWasSelfViewDisabled(false);
|
||||
@ -264,13 +273,11 @@ const JoinVideoButton = ({
|
||||
setIsOpen: setVideoPreviewModalIsOpen,
|
||||
isOpen: isVideoPreviewModalOpen,
|
||||
}}
|
||||
{...propsToPassModal}
|
||||
isVisualEffects={propsToPassModal.isVisualEffects}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
JoinVideoButton.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(memo(JoinVideoButton));
|
||||
|
@ -1,41 +1,20 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import JoinVideoButton from './component';
|
||||
import VideoService from '../service';
|
||||
import {
|
||||
updateSettings,
|
||||
} from '/imports/ui/components/settings/service';
|
||||
import React, { useContext } from 'react';
|
||||
import { CameraSettingsDropdownInterface } from 'bigbluebutton-html-plugin-sdk';
|
||||
import { updateSettings } from '/imports/ui/components/settings/service';
|
||||
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
|
||||
import { CAMERA_BROADCAST_STOP } from '../mutations';
|
||||
import useUserChangedLocalSettings from '/imports/ui/services/settings/hooks/useUserChangedLocalSettings';
|
||||
import {
|
||||
useDisableReason, useExitVideo, useHasVideoStream, useStatus, useStreams,
|
||||
useDisableReason, useExitVideo, useHasVideoStream, useStatus, useStopVideo,
|
||||
} from '/imports/ui/components/video-provider/video-provider-graphql/hooks';
|
||||
import JoinVideoButton from './component';
|
||||
|
||||
const JoinVideoOptionsContainer = (props) => {
|
||||
const {
|
||||
updateSettings,
|
||||
intl,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const { streams } = useStreams();
|
||||
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
|
||||
const JoinVideoOptionsContainer: React.FC = () => {
|
||||
const setLocalSettings = useUserChangedLocalSettings();
|
||||
|
||||
const sendUserUnshareWebcam = (cameraId) => {
|
||||
cameraBroadcastStop({ variables: { cameraId } });
|
||||
};
|
||||
|
||||
const {
|
||||
pluginsExtensibleAreasAggregatedState,
|
||||
} = useContext(PluginsContext);
|
||||
let cameraSettingsDropdownItems = [];
|
||||
let cameraSettingsDropdownItems: CameraSettingsDropdownInterface[] = [];
|
||||
if (pluginsExtensibleAreasAggregatedState.cameraSettingsDropdownItems) {
|
||||
cameraSettingsDropdownItems = [
|
||||
...pluginsExtensibleAreasAggregatedState.cameraSettingsDropdownItems,
|
||||
@ -45,25 +24,21 @@ const JoinVideoOptionsContainer = (props) => {
|
||||
const hasVideoStream = useHasVideoStream();
|
||||
const disableReason = useDisableReason();
|
||||
const status = useStatus();
|
||||
const exitVideo = useExitVideo()
|
||||
const exitVideo = useExitVideo();
|
||||
const stopVideo = useStopVideo();
|
||||
|
||||
return (
|
||||
<JoinVideoButton {...{
|
||||
cameraSettingsDropdownItems,
|
||||
hasVideoStream,
|
||||
updateSettings,
|
||||
disableReason,
|
||||
status,
|
||||
sendUserUnshareWebcam,
|
||||
setLocalSettings,
|
||||
streams,
|
||||
exitVideo,
|
||||
...restProps,
|
||||
}}
|
||||
<JoinVideoButton
|
||||
cameraSettingsDropdownItems={cameraSettingsDropdownItems}
|
||||
hasVideoStream={hasVideoStream}
|
||||
updateSettings={updateSettings}
|
||||
disableReason={disableReason}
|
||||
status={status}
|
||||
setLocalSettings={setLocalSettings}
|
||||
exitVideo={exitVideo}
|
||||
stopVideo={stopVideo}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(withTracker(() => ({
|
||||
updateSettings,
|
||||
}))(JoinVideoOptionsContainer));
|
||||
export default JoinVideoOptionsContainer;
|
||||
|
@ -1,8 +1,5 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { IntlShape, defineMessages, injectIntl } from 'react-intl';
|
||||
import { throttle } from '/imports/utils/throttle';
|
||||
import { range } from '/imports/utils/array-utils';
|
||||
import Styled from './styles';
|
||||
@ -12,16 +9,8 @@ import logger from '/imports/startup/client/logger';
|
||||
import playAndRetry from '/imports/utils/mediaElementPlayRetry';
|
||||
import VideoService from '../service';
|
||||
import { ACTIONS } from '/imports/ui/components/layout/enums';
|
||||
|
||||
const propTypes = {
|
||||
streams: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onVideoItemMount: PropTypes.func.isRequired,
|
||||
onVideoItemUnmount: PropTypes.func.isRequired,
|
||||
intl: PropTypes.objectOf(Object).isRequired,
|
||||
swapLayout: PropTypes.bool.isRequired,
|
||||
numberOfPages: PropTypes.number.isRequired,
|
||||
currentVideoPageIndex: PropTypes.number.isRequired,
|
||||
};
|
||||
import { Output } from '../../../layout/layoutTypes';
|
||||
import { StreamUser, VideoItem } from '../types';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
autoplayBlockedDesc: {
|
||||
@ -38,7 +27,20 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const findOptimalGrid = (canvasWidth, canvasHeight, gutter, aspectRatio, numItems, columns = 1) => {
|
||||
declare global {
|
||||
interface WindowEventMap {
|
||||
'videoPlayFailed': CustomEvent<{ mediaElement: HTMLVideoElement }>;
|
||||
}
|
||||
}
|
||||
|
||||
const findOptimalGrid = (
|
||||
canvasWidth: number,
|
||||
canvasHeight: number,
|
||||
gutter: number,
|
||||
aspectRatio: number,
|
||||
numItems: number,
|
||||
columns = 1,
|
||||
) => {
|
||||
const rows = Math.ceil(numItems / columns);
|
||||
const gutterTotalWidth = (columns - 1) * gutter;
|
||||
const gutterTotalHeight = (rows - 1) * gutter;
|
||||
@ -62,8 +64,48 @@ const findOptimalGrid = (canvasWidth, canvasHeight, gutter, aspectRatio, numItem
|
||||
const ASPECT_RATIO = 4 / 3;
|
||||
// const ACTION_NAME_BACKGROUND = 'blurBackground';
|
||||
|
||||
class VideoList extends Component {
|
||||
constructor(props) {
|
||||
interface VideoListProps {
|
||||
layoutType: string;
|
||||
layoutContextDispatch: (...args: unknown[]) => void;
|
||||
numberOfPages: number;
|
||||
swapLayout: boolean;
|
||||
currentVideoPageIndex: number;
|
||||
cameraDock: Output['cameraDock'];
|
||||
focusedId: string;
|
||||
handleVideoFocus: (id: string) => void;
|
||||
isGridEnabled: boolean;
|
||||
users: StreamUser[];
|
||||
streams: VideoItem[];
|
||||
intl: IntlShape;
|
||||
onVideoItemMount: (stream: string, video: HTMLVideoElement) => void;
|
||||
onVideoItemUnmount: (stream: string) => void;
|
||||
onVirtualBgDrop: (stream: string, type: string, name: string, data: string) => Promise<unknown>;
|
||||
}
|
||||
|
||||
interface VideoListState {
|
||||
optimalGrid: {
|
||||
cols: number,
|
||||
rows: number,
|
||||
filledArea: number,
|
||||
width: number;
|
||||
height: number;
|
||||
columns: number;
|
||||
},
|
||||
autoplayBlocked: boolean,
|
||||
}
|
||||
|
||||
class VideoList extends Component<VideoListProps, VideoListState> {
|
||||
private ticking: boolean;
|
||||
|
||||
private grid: HTMLDivElement | null;
|
||||
|
||||
private canvas: HTMLDivElement | null;
|
||||
|
||||
private failedMediaElements: unknown[];
|
||||
|
||||
private autoplayWasHandled: boolean;
|
||||
|
||||
constructor(props: VideoListProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@ -71,6 +113,9 @@ class VideoList extends Component {
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
filledArea: 0,
|
||||
columns: 0,
|
||||
height: 0,
|
||||
width: 0,
|
||||
},
|
||||
autoplayBlocked: false,
|
||||
};
|
||||
@ -96,8 +141,10 @@ class VideoList extends Component {
|
||||
window.addEventListener('videoPlayFailed', this.handlePlayElementFailed);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { layoutType, cameraDock, streams, focusedId } = this.props;
|
||||
componentDidUpdate(prevProps: VideoListProps) {
|
||||
const {
|
||||
layoutType, cameraDock, streams, focusedId,
|
||||
} = this.props;
|
||||
const { width: cameraDockWidth, height: cameraDockHeight } = cameraDock;
|
||||
const {
|
||||
layoutType: prevLayoutType,
|
||||
@ -148,7 +195,7 @@ class VideoList extends Component {
|
||||
if (autoplayBlocked) { this.setState({ autoplayBlocked: false }); }
|
||||
}
|
||||
|
||||
handlePlayElementFailed(e) {
|
||||
handlePlayElementFailed(e: CustomEvent<{ mediaElement: HTMLVideoElement }>) {
|
||||
const { mediaElement } = e.detail;
|
||||
const { autoplayBlocked } = this.state;
|
||||
|
||||
@ -190,7 +237,7 @@ class VideoList extends Component {
|
||||
const gridGutter = parseInt(window.getComputedStyle(this.grid)
|
||||
.getPropertyValue('grid-row-gap'), 10);
|
||||
|
||||
const hasFocusedItem = streams.filter(s => s.stream === focusedId).length && numItems > 2;
|
||||
const hasFocusedItem = streams.filter((s) => s.type !== 'grid' && s.stream === focusedId).length && numItems > 2;
|
||||
|
||||
// Has a focused item so we need +3 cells
|
||||
if (hasFocusedItem) {
|
||||
@ -303,23 +350,24 @@ class VideoList extends Component {
|
||||
} = this.props;
|
||||
const numOfStreams = streams.length;
|
||||
|
||||
let videoItems = streams;
|
||||
const videoItems = streams;
|
||||
|
||||
return videoItems.map((item) => {
|
||||
const { userId, name } = item;
|
||||
const isStream = !!item.stream;
|
||||
const isStream = item.type !== 'grid';
|
||||
const stream = isStream ? item.stream : null;
|
||||
const key = isStream ? stream : userId;
|
||||
const isFocused = isStream && focusedId === stream && numOfStreams > 2;
|
||||
const user = users.find((u) => u.userId === userId) || {};
|
||||
|
||||
return (
|
||||
<Styled.VideoListItem
|
||||
key={key}
|
||||
focused={isFocused}
|
||||
$focused={isFocused}
|
||||
data-test="webcamVideoItem"
|
||||
>
|
||||
<VideoListItemContainer
|
||||
users={users}
|
||||
user={user}
|
||||
numOfStreams={numOfStreams}
|
||||
cameraId={stream}
|
||||
userId={userId}
|
||||
@ -329,12 +377,16 @@ class VideoList extends Component {
|
||||
onHandleVideoFocus={isStream ? handleVideoFocus : null}
|
||||
onVideoItemMount={(videoRef) => {
|
||||
this.handleCanvasResize();
|
||||
if (isStream) onVideoItemMount(stream, videoRef);
|
||||
if (isStream) onVideoItemMount(item.stream, videoRef);
|
||||
}}
|
||||
stream={streams.find((s) => s.userId === userId) || {}}
|
||||
stream={streams.find((s) => s.userId === userId)}
|
||||
onVideoItemUnmount={onVideoItemUnmount}
|
||||
swapLayout={swapLayout}
|
||||
onVirtualBgDrop={(type, name, data) => { return isStream ? onVirtualBgDrop(stream, type, name, data) : null; }}
|
||||
onVirtualBgDrop={
|
||||
(type, name, data) => {
|
||||
return isStream ? onVirtualBgDrop(item.stream, type, name, data) : Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Styled.VideoListItem>
|
||||
);
|
||||
@ -353,7 +405,7 @@ class VideoList extends Component {
|
||||
|
||||
return (
|
||||
<Styled.VideoCanvas
|
||||
position={position}
|
||||
$position={position}
|
||||
ref={(ref) => {
|
||||
this.canvas = ref;
|
||||
}}
|
||||
@ -397,6 +449,4 @@ class VideoList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
VideoList.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(VideoList);
|
||||
|
@ -1,43 +1,64 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import VideoList from '/imports/ui/components/video-provider/video-provider-graphql/video-list/component';
|
||||
import VideoService from '/imports/ui/components/video-provider/video-provider-graphql/service';
|
||||
import { layoutSelect, layoutSelectOutput, layoutDispatch } from '/imports/ui/components/layout/context';
|
||||
import Users from '/imports/api/users';
|
||||
import { layoutSelect, layoutDispatch } from '/imports/ui/components/layout/context';
|
||||
import { useNumberOfPages } from '../hooks';
|
||||
import { StreamUser, VideoItem } from '../types';
|
||||
import { Layout, Output } from '../../../layout/layoutTypes';
|
||||
|
||||
const VideoListContainer = ({ children, ...props }) => {
|
||||
const layoutType = layoutSelect((i) => i.layoutType);
|
||||
const cameraDock = layoutSelectOutput((i) => i.cameraDock);
|
||||
interface VideoListContainerProps {
|
||||
streams: VideoItem[];
|
||||
swapLayout: boolean;
|
||||
currentVideoPageIndex: number;
|
||||
cameraDock: Output['cameraDock'];
|
||||
focusedId: string;
|
||||
handleVideoFocus: (id: string) => void;
|
||||
isGridEnabled: boolean;
|
||||
users: StreamUser[];
|
||||
onVideoItemMount: (stream: string, video: HTMLVideoElement) => void;
|
||||
onVideoItemUnmount: (stream: string) => void;
|
||||
onVirtualBgDrop: (stream: string, type: string, name: string, data: string) => Promise<unknown>;
|
||||
}
|
||||
|
||||
const VideoListContainer: React.FC<VideoListContainerProps> = (props) => {
|
||||
const layoutType = layoutSelect((i: Layout) => i.layoutType);
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
const { streams } = props;
|
||||
const {
|
||||
streams,
|
||||
cameraDock,
|
||||
currentVideoPageIndex,
|
||||
focusedId,
|
||||
handleVideoFocus,
|
||||
isGridEnabled,
|
||||
swapLayout,
|
||||
users,
|
||||
onVideoItemMount,
|
||||
onVideoItemUnmount,
|
||||
onVirtualBgDrop,
|
||||
} = props;
|
||||
const numberOfPages = useNumberOfPages();
|
||||
|
||||
return (
|
||||
!streams.length
|
||||
? null
|
||||
: (
|
||||
<VideoList {...{
|
||||
layoutType,
|
||||
cameraDock,
|
||||
layoutContextDispatch,
|
||||
numberOfPages,
|
||||
...props,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</VideoList>
|
||||
<VideoList
|
||||
layoutType={layoutType}
|
||||
layoutContextDispatch={layoutContextDispatch}
|
||||
numberOfPages={numberOfPages}
|
||||
swapLayout={swapLayout}
|
||||
currentVideoPageIndex={currentVideoPageIndex}
|
||||
cameraDock={cameraDock}
|
||||
focusedId={focusedId}
|
||||
handleVideoFocus={handleVideoFocus}
|
||||
isGridEnabled={isGridEnabled}
|
||||
users={users}
|
||||
streams={streams}
|
||||
onVideoItemMount={onVideoItemMount}
|
||||
onVideoItemUnmount={onVideoItemUnmount}
|
||||
onVirtualBgDrop={onVirtualBgDrop}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default withTracker((props) => {
|
||||
const { streams } = props;
|
||||
|
||||
return {
|
||||
...props,
|
||||
streams,
|
||||
};
|
||||
})(VideoListContainer);
|
||||
export default VideoListContainer;
|
||||
|
@ -1,12 +1,10 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import styled from 'styled-components';
|
||||
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { mdPaddingX } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
import { actionsBarHeight, navbarHeight } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { actionsBarHeight, navbarHeight, mdPaddingX } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
// @ts-expect-error -> Untyped component.
|
||||
const NextPageButton = styled(Button)`
|
||||
color: ${colorWhite};
|
||||
width: ${mdPaddingX};
|
||||
@ -33,6 +31,7 @@ const NextPageButton = styled(Button)`
|
||||
`}
|
||||
`;
|
||||
|
||||
// @ts-expect-error -> Untyped component.
|
||||
const PreviousPageButton = styled(Button)`
|
||||
color: ${colorWhite};
|
||||
width: ${mdPaddingX};
|
||||
@ -59,19 +58,23 @@ const PreviousPageButton = styled(Button)`
|
||||
`}
|
||||
`;
|
||||
|
||||
const VideoListItem = styled.div`
|
||||
const VideoListItem = styled.div<{
|
||||
$focused: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
${({ focused }) => focused && `
|
||||
${({ $focused }) => $focused && `
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 1 / span 2;
|
||||
`}
|
||||
`;
|
||||
|
||||
const VideoCanvas = styled.div`
|
||||
const VideoCanvas = styled.div<{
|
||||
$position: string;
|
||||
}>`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
min-height: calc((100vh - calc(${navbarHeight} + ${actionsBarHeight})) * 0.2);
|
||||
@ -84,7 +87,7 @@ const VideoCanvas = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
${({ position }) => (position === 'contentRight' || position === 'contentLeft') && `
|
||||
${({ $position }) => ($position === 'contentRight' || $position === 'contentLeft') && `
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
order: 0;
|
||||
|
@ -1,8 +1,6 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { injectIntl, defineMessages, useIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Session } from 'meteor/session';
|
||||
import UserActions from '/imports/ui/components/video-provider/video-provider-graphql/video-list/video-list-item/user-actions/component';
|
||||
import UserStatus from '/imports/ui/components/video-provider/video-provider-graphql/video-list/video-list-item/user-status/component';
|
||||
import PinArea from '/imports/ui/components/video-provider/video-provider-graphql/video-list/video-list-item/pin-area/component';
|
||||
@ -16,8 +14,9 @@ import {
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import VideoService from '/imports/ui/components/video-provider/video-provider-graphql/service';
|
||||
import Styled from './styles';
|
||||
import { withDragAndDrop } from './drag-and-drop/component';
|
||||
import withDragAndDrop from './drag-and-drop/component';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { StreamUser, VideoItem } from '../../types';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
disableDesc: {
|
||||
@ -27,7 +26,39 @@ const intlMessages = defineMessages({
|
||||
|
||||
const VIDEO_CONTAINER_WIDTH_BOUND = 125;
|
||||
|
||||
const VideoListItem = (props) => {
|
||||
interface VideoListItemProps {
|
||||
isFullscreenContext: boolean;
|
||||
layoutContextDispatch: (...args: unknown[]) => void;
|
||||
isRTL: boolean;
|
||||
amIModerator: boolean;
|
||||
cameraId: string;
|
||||
disabledCams: string[];
|
||||
focused: boolean;
|
||||
isStream: boolean;
|
||||
name: string;
|
||||
numOfStreams: number;
|
||||
onHandleVideoFocus: ((id: string) => void) | null;
|
||||
onVideoItemMount: (ref: HTMLVideoElement) => void;
|
||||
onVideoItemUnmount: (stream: string) => void;
|
||||
settingsSelfViewDisable: boolean;
|
||||
stream: VideoItem | undefined;
|
||||
user: Partial<StreamUser>;
|
||||
makeDragOperations: (userId?: string) => {
|
||||
onDragOver: (e: DragEvent) => void,
|
||||
onDrop: (e: DragEvent) => void,
|
||||
onDragLeave: (e: DragEvent) => void,
|
||||
};
|
||||
dragging: boolean;
|
||||
draggingOver: boolean;
|
||||
voiceUser: {
|
||||
muted: boolean;
|
||||
listenOnly: boolean;
|
||||
talking: boolean;
|
||||
joined: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const VideoListItem: React.FC<VideoListItemProps> = (props) => {
|
||||
const {
|
||||
name, voiceUser, isFullscreenContext, layoutContextDispatch, user, onHandleVideoFocus,
|
||||
cameraId, numOfStreams, focused, onVideoItemMount, onVideoItemUnmount,
|
||||
@ -39,7 +70,7 @@ const VideoListItem = (props) => {
|
||||
|
||||
const [videoDataLoaded, setVideoDataLoaded] = useState(false);
|
||||
const [isStreamHealthy, setIsStreamHealthy] = useState(false);
|
||||
const [isMirrored, setIsMirrored] = useState(VideoService.mirrorOwnWebcam(user?.userId));
|
||||
const [isMirrored, setIsMirrored] = useState<boolean>(VideoService.mirrorOwnWebcam(user?.userId));
|
||||
const [isVideoSqueezed, setIsVideoSqueezed] = useState(false);
|
||||
const [isSelfViewDisabled, setIsSelfViewDisabled] = useState(false);
|
||||
|
||||
@ -50,14 +81,15 @@ const VideoListItem = (props) => {
|
||||
return setIsVideoSqueezed(false);
|
||||
});
|
||||
|
||||
const videoTag = useRef();
|
||||
const videoContainer = useRef();
|
||||
const videoTag = useRef<HTMLVideoElement | null>(null);
|
||||
const videoContainer = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const videoIsReady = isStreamHealthy && videoDataLoaded && !isSelfViewDisabled;
|
||||
// @ts-expect-error -> Untyped object.
|
||||
const { animations, webcamBorderHighlightColor } = Settings.application;
|
||||
const talking = voiceUser?.talking;
|
||||
|
||||
const onStreamStateChange = (e) => {
|
||||
const onStreamStateChange = (e: CustomEvent) => {
|
||||
const { streamState } = e.detail;
|
||||
const newHealthState = !isStreamStateUnhealthy(streamState);
|
||||
e.stopPropagation();
|
||||
@ -78,8 +110,8 @@ const VideoListItem = (props) => {
|
||||
// component did mount
|
||||
useEffect(() => {
|
||||
subscribeToStreamStateChange(cameraId, onStreamStateChange);
|
||||
onVideoItemMount(videoTag.current);
|
||||
resizeObserver.observe(videoContainer.current);
|
||||
onVideoItemMount(videoTag.current!);
|
||||
if (videoContainer.current) resizeObserver.observe(videoContainer.current);
|
||||
videoTag?.current?.addEventListener('loadeddata', onLoadedData);
|
||||
|
||||
return () => {
|
||||
@ -90,7 +122,7 @@ const VideoListItem = (props) => {
|
||||
|
||||
// component will mount
|
||||
useEffect(() => {
|
||||
const playElement = (elem) => {
|
||||
const playElement = (elem: HTMLVideoElement) => {
|
||||
if (elem.paused) {
|
||||
elem.play().catch((error) => {
|
||||
// NotAllowedError equals autoplay issues, fire autoplay handling event
|
||||
@ -102,10 +134,10 @@ const VideoListItem = (props) => {
|
||||
}
|
||||
};
|
||||
if (!isSelfViewDisabled && videoDataLoaded) {
|
||||
playElement(videoTag.current);
|
||||
playElement(videoTag.current!);
|
||||
}
|
||||
if ((isSelfViewDisabled && user.userId === Auth.userID) || disabledCams?.includes(cameraId)) {
|
||||
videoTag.current.pause();
|
||||
videoTag.current?.pause?.();
|
||||
}
|
||||
}, [isSelfViewDisabled, videoDataLoaded]);
|
||||
|
||||
@ -122,7 +154,8 @@ const VideoListItem = (props) => {
|
||||
const renderSqueezedButton = () => (
|
||||
<UserActions
|
||||
name={name}
|
||||
user={{ ...user, ...stream }}
|
||||
user={user}
|
||||
stream={stream}
|
||||
videoContainer={videoContainer}
|
||||
isVideoSqueezed={isVideoSqueezed}
|
||||
cameraId={cameraId}
|
||||
@ -153,7 +186,8 @@ const VideoListItem = (props) => {
|
||||
<Styled.BottomBar>
|
||||
<UserActions
|
||||
name={name}
|
||||
user={{ ...user, ...stream }}
|
||||
user={user}
|
||||
stream={stream}
|
||||
cameraId={cameraId}
|
||||
numOfStreams={numOfStreams}
|
||||
onHandleVideoFocus={onHandleVideoFocus}
|
||||
@ -180,7 +214,7 @@ const VideoListItem = (props) => {
|
||||
animations={animations}
|
||||
>
|
||||
<UserAvatarVideo
|
||||
user={{ ...user, ...stream }}
|
||||
user={user}
|
||||
unhealthyStream={videoDataLoaded && !isStreamHealthy}
|
||||
squeezed
|
||||
/>
|
||||
@ -192,7 +226,8 @@ const VideoListItem = (props) => {
|
||||
<>
|
||||
<Styled.TopBar>
|
||||
<PinArea
|
||||
user={{ ...user, ...stream }}
|
||||
user={user}
|
||||
stream={stream}
|
||||
amIModerator={amIModerator}
|
||||
/>
|
||||
<ViewActions
|
||||
@ -207,7 +242,8 @@ const VideoListItem = (props) => {
|
||||
<Styled.BottomBar>
|
||||
<UserActions
|
||||
name={name}
|
||||
user={{ ...user, ...stream }}
|
||||
user={user}
|
||||
stream={stream}
|
||||
cameraId={cameraId}
|
||||
numOfStreams={numOfStreams}
|
||||
onHandleVideoFocus={onHandleVideoFocus}
|
||||
@ -228,7 +264,14 @@ const VideoListItem = (props) => {
|
||||
</>
|
||||
);
|
||||
|
||||
const {
|
||||
onDragLeave,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
} = makeDragOperations(user?.userId);
|
||||
|
||||
return (
|
||||
// @ts-expect-error -> Until everything in Typescript.
|
||||
<Styled.Content
|
||||
ref={videoContainer}
|
||||
talking={talking}
|
||||
@ -238,7 +281,9 @@ const VideoListItem = (props) => {
|
||||
animations={animations}
|
||||
isStream={isStream}
|
||||
{...{
|
||||
...makeDragOperations(user?.userId),
|
||||
onDragLeave,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
dragging,
|
||||
draggingOver,
|
||||
}}
|
||||
@ -253,7 +298,7 @@ const VideoListItem = (props) => {
|
||||
unhealthyStream={videoDataLoaded && !isStreamHealthy}
|
||||
data-test={isMirrored ? 'mirroredVideoContainer' : 'videoContainer'}
|
||||
ref={videoTag}
|
||||
muted="muted"
|
||||
muted
|
||||
autoPlay
|
||||
playsInline
|
||||
/>
|
||||
@ -280,37 +325,5 @@ const VideoListItem = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default withDragAndDrop(injectIntl(VideoListItem));
|
||||
|
||||
VideoListItem.defaultProps = {
|
||||
numOfStreams: 0,
|
||||
onVideoItemMount: () => { },
|
||||
onVideoItemUnmount: () => { },
|
||||
onVirtualBgDrop: () => { },
|
||||
};
|
||||
|
||||
VideoListItem.propTypes = {
|
||||
cameraId: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
numOfStreams: PropTypes.number,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
onHandleVideoFocus: PropTypes.func.isRequired,
|
||||
onVideoItemMount: PropTypes.func,
|
||||
onVideoItemUnmount: PropTypes.func,
|
||||
onVirtualBgDrop: PropTypes.func,
|
||||
isFullscreenContext: PropTypes.bool.isRequired,
|
||||
layoutContextDispatch: PropTypes.func.isRequired,
|
||||
user: PropTypes.shape({
|
||||
pin: PropTypes.bool.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
voiceUser: PropTypes.shape({
|
||||
muted: PropTypes.bool.isRequired,
|
||||
listenOnly: PropTypes.bool.isRequired,
|
||||
talking: PropTypes.bool.isRequired,
|
||||
joined: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
};
|
||||
// @ts-expect-error -> Until everything in Typescript.
|
||||
export default withDragAndDrop(VideoListItem);
|
||||
|
@ -1,23 +1,69 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { Session } from 'meteor/session';
|
||||
import VoiceUsers from '/imports/api/voice-users/';
|
||||
import Users from '/imports/api/users/';
|
||||
import VideoListItem from './component';
|
||||
import { layoutSelect, layoutDispatch } from '/imports/ui/components/layout/context';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import { layoutSelect, layoutDispatch } from '/imports/ui/components/layout/context';
|
||||
import VideoListItem from './component';
|
||||
import { StreamUser, VideoItem } from '../../types';
|
||||
import { Layout } from '/imports/ui/components/layout/layoutTypes';
|
||||
|
||||
const VideoListItemContainer = (props) => {
|
||||
const { cameraId, user } = props;
|
||||
type TrackerData = {
|
||||
disabledCams: string[];
|
||||
settingsSelfViewDisable: boolean;
|
||||
user: Partial<StreamUser>;
|
||||
stream: VideoItem | undefined;
|
||||
voiceUser: {
|
||||
muted: boolean;
|
||||
listenOnly: boolean;
|
||||
talking: boolean;
|
||||
joined: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const fullscreen = layoutSelect((i) => i.fullscreen);
|
||||
type TrackerProps = {
|
||||
user: Partial<StreamUser>;
|
||||
numOfStreams: number;
|
||||
cameraId: string | null;
|
||||
userId: string;
|
||||
name: string;
|
||||
focused: boolean;
|
||||
isStream: boolean;
|
||||
onHandleVideoFocus: ((id: string) => void) | null;
|
||||
stream: VideoItem | undefined;
|
||||
onVideoItemUnmount: (stream: string) => void;
|
||||
swapLayout: boolean;
|
||||
onVirtualBgDrop: (type: string, name: string, data: string) => void;
|
||||
onVideoItemMount: (ref: HTMLVideoElement) => void;
|
||||
}
|
||||
|
||||
type VideoListItemContainerProps = TrackerData & Omit<TrackerProps, 'userId'>;
|
||||
|
||||
const VideoListItemContainer: React.FC<VideoListItemContainerProps> = (props) => {
|
||||
const {
|
||||
cameraId,
|
||||
disabledCams,
|
||||
focused,
|
||||
isStream,
|
||||
name,
|
||||
numOfStreams,
|
||||
onHandleVideoFocus,
|
||||
onVideoItemMount,
|
||||
onVideoItemUnmount,
|
||||
onVirtualBgDrop,
|
||||
settingsSelfViewDisable,
|
||||
stream,
|
||||
user,
|
||||
voiceUser,
|
||||
} = props;
|
||||
|
||||
const fullscreen = layoutSelect((i: Layout) => i.fullscreen);
|
||||
const { element } = fullscreen;
|
||||
const isFullscreenContext = (element === cameraId);
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
const isRTL = layoutSelect((i) => i.isRTL);
|
||||
const isRTL = layoutSelect((i: Layout) => i.isRTL);
|
||||
|
||||
const { data: currentUserData } = useCurrentUser((user) => ({
|
||||
isModerator: user.isModerator,
|
||||
@ -29,25 +75,39 @@ const VideoListItemContainer = (props) => {
|
||||
|
||||
return (
|
||||
<VideoListItem
|
||||
{...props}
|
||||
{...{
|
||||
isFullscreenContext,
|
||||
layoutContextDispatch,
|
||||
isRTL,
|
||||
amIModerator,
|
||||
}}
|
||||
cameraId={cameraId}
|
||||
disabledCams={disabledCams}
|
||||
focused={focused}
|
||||
isStream={isStream}
|
||||
name={name}
|
||||
numOfStreams={numOfStreams}
|
||||
onHandleVideoFocus={onHandleVideoFocus}
|
||||
onVideoItemMount={onVideoItemMount}
|
||||
onVideoItemUnmount={onVideoItemUnmount}
|
||||
onVirtualBgDrop={onVirtualBgDrop}
|
||||
settingsSelfViewDisable={settingsSelfViewDisable}
|
||||
stream={stream}
|
||||
user={user}
|
||||
voiceUser={voiceUser}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTracker((props) => {
|
||||
export default withTracker<TrackerData, TrackerProps>((props) => {
|
||||
const {
|
||||
userId,
|
||||
users,
|
||||
user,
|
||||
stream,
|
||||
} = props;
|
||||
|
||||
return {
|
||||
// @ts-expect-error -> Untyped object.
|
||||
settingsSelfViewDisable: Settings.application.selfViewDisable,
|
||||
voiceUser: VoiceUsers.findOne({ userId },
|
||||
{
|
||||
@ -55,9 +115,9 @@ export default withTracker((props) => {
|
||||
muted: 1, listenOnly: 1, talking: 1, joined: 1,
|
||||
},
|
||||
}),
|
||||
user: (users?.find((u) => u.userId === userId) || {}),
|
||||
disabledCams: Session.get('disabledCams') || [],
|
||||
user,
|
||||
stream,
|
||||
disabledCams: Session.get('disabledCams') || [],
|
||||
};
|
||||
})(VideoListItemContainer);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React, { useContext, useEffect, useState, useCallback } from 'react';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import React, {
|
||||
useContext, useEffect, useState, useCallback,
|
||||
} from 'react';
|
||||
import { injectIntl, defineMessages, IntlShape } from 'react-intl';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import ConfirmationModal from '/imports/ui/components/common/modal/confirmation/component';
|
||||
import { CustomVirtualBackgroundsContext } from '/imports/ui/components/video-preview/virtual-background/context';
|
||||
@ -9,6 +9,7 @@ import { EFFECT_TYPES } from '/imports/ui/services/virtual-background/service';
|
||||
import VirtualBgService from '/imports/ui/components/video-preview/virtual-background/service';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import withFileReader from '/imports/ui/components/common/file-reader/component';
|
||||
import { Session } from 'meteor/session';
|
||||
|
||||
const { MIME_TYPES_ALLOWED, MAX_FILE_SIZE } = VirtualBgService;
|
||||
|
||||
@ -23,33 +24,51 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const ENABLE_WEBCAM_BACKGROUND_UPLOAD = window.meetingClientSettings.public.virtualBackgrounds.enableVirtualBackgroundUpload;
|
||||
const PUBLIC_CONFIG = window.meetingClientSettings.public;
|
||||
const ENABLE_WEBCAM_BACKGROUND_UPLOAD = PUBLIC_CONFIG.virtualBackgrounds.enableVirtualBackgroundUpload;
|
||||
|
||||
const DragAndDrop = (props) => {
|
||||
const { children, intl, readFile, onVirtualBgDrop: onAction, isStream } = props;
|
||||
interface DragAndDropProps {
|
||||
readFile: (
|
||||
file: File,
|
||||
onSuccess: (data: {
|
||||
filename: string,
|
||||
data: string,
|
||||
uniqueId: string,
|
||||
}) => void,
|
||||
onError: (error: Error) => void
|
||||
) => void;
|
||||
children: React.ReactElement;
|
||||
intl: IntlShape;
|
||||
isStream: boolean;
|
||||
onVirtualBgDrop: (type: string, name: string, data: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const DragAndDrop: React.FC<DragAndDropProps> = (props) => {
|
||||
const {
|
||||
children, intl, readFile, onVirtualBgDrop: onAction, isStream,
|
||||
} = props;
|
||||
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [draggingOver, setDraggingOver] = useState(false);
|
||||
const [isConfirmModalOpen, setConfirmModalIsOpen] = useState(false);
|
||||
const [file, setFile] = useState(false);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const { dispatch: dispatchCustomBackground } = useContext(CustomVirtualBackgroundsContext);
|
||||
|
||||
let callback;
|
||||
const resetEvent = (e) => {
|
||||
const resetEvent = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onDragOver = (e) => {
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
resetEvent(e);
|
||||
setDragging(true);
|
||||
};
|
||||
const onDragLeave = (e) => {
|
||||
const onDragLeave = (e: DragEvent) => {
|
||||
resetEvent(e);
|
||||
setDragging(false);
|
||||
};
|
||||
const onDrop = (e) => {
|
||||
const onDrop = (e: DragEvent) => {
|
||||
resetEvent(e);
|
||||
setDragging(false);
|
||||
};
|
||||
@ -65,8 +84,12 @@ const DragAndDrop = (props) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleStartAndSaveVirtualBackground = (file) => {
|
||||
const onSuccess = (background) => {
|
||||
const handleStartAndSaveVirtualBackground = (file: File) => {
|
||||
const onSuccess = (background: {
|
||||
filename: string,
|
||||
data: string,
|
||||
uniqueId: string,
|
||||
}) => {
|
||||
const { filename, data } = background;
|
||||
if (onAction) {
|
||||
onAction(EFFECT_TYPES.IMAGE_TYPE, filename, data).then(() => {
|
||||
@ -79,17 +102,19 @@ const DragAndDrop = (props) => {
|
||||
},
|
||||
});
|
||||
});
|
||||
} else dispatchCustomBackground({
|
||||
type: 'new',
|
||||
background: {
|
||||
...background,
|
||||
custom: true,
|
||||
lastActivityDate: Date.now(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
dispatchCustomBackground({
|
||||
type: 'new',
|
||||
background: {
|
||||
...background,
|
||||
custom: true,
|
||||
lastActivityDate: Date.now(),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onError = (error) => {
|
||||
const onError = (error: Error) => {
|
||||
logger.warn({
|
||||
logCode: 'read_file_error',
|
||||
extraInfo: {
|
||||
@ -102,39 +127,43 @@ const DragAndDrop = (props) => {
|
||||
readFile(file, onSuccess, onError);
|
||||
};
|
||||
|
||||
callback = (checked) => {
|
||||
const callback = (checked: boolean) => {
|
||||
if (!file) return;
|
||||
handleStartAndSaveVirtualBackground(file);
|
||||
Session.set('skipBackgroundDropConfirmation', checked);
|
||||
};
|
||||
|
||||
const makeDragOperations = useCallback((userId) => {
|
||||
const makeDragOperations = useCallback((userId: string) => {
|
||||
if (!userId || Auth.userID !== userId || !ENABLE_WEBCAM_BACKGROUND_UPLOAD || !isStream) return {};
|
||||
|
||||
const startAndSaveVirtualBackground = (file) => handleStartAndSaveVirtualBackground(file);
|
||||
const startAndSaveVirtualBackground = (file: File) => handleStartAndSaveVirtualBackground(file);
|
||||
|
||||
const onDragOverHandler = (e) => {
|
||||
const onDragOverHandler = (e: DragEvent) => {
|
||||
resetEvent(e);
|
||||
setDraggingOver(true);
|
||||
setDragging(false);
|
||||
};
|
||||
|
||||
const onDropHandler = (e) => {
|
||||
const onDropHandler = (e: DragEvent) => {
|
||||
resetEvent(e);
|
||||
setDraggingOver(false);
|
||||
setDragging(false);
|
||||
|
||||
const { files } = e.dataTransfer;
|
||||
const file = files[0];
|
||||
if (e.dataTransfer) {
|
||||
const { files } = e.dataTransfer;
|
||||
const file = files[0];
|
||||
|
||||
if (Session.get('skipBackgroundDropConfirmation')) {
|
||||
return startAndSaveVirtualBackground(file);
|
||||
if (Session.get('skipBackgroundDropConfirmation')) {
|
||||
return startAndSaveVirtualBackground(file);
|
||||
}
|
||||
|
||||
setFile(file);
|
||||
setConfirmModalIsOpen(true);
|
||||
}
|
||||
|
||||
setFile(file);
|
||||
setConfirmModalIsOpen(true);
|
||||
return null;
|
||||
};
|
||||
|
||||
const onDragLeaveHandler = (e) => {
|
||||
const onDragLeaveHandler = (e: DragEvent) => {
|
||||
resetEvent(e);
|
||||
setDragging(false);
|
||||
setDraggingOver(false);
|
||||
@ -147,29 +176,47 @@ const DragAndDrop = (props) => {
|
||||
};
|
||||
}, [Auth.userID]);
|
||||
|
||||
return <>
|
||||
{React.cloneElement(children, { ...props, dragging, draggingOver, makeDragOperations })}
|
||||
{isConfirmModalOpen ? <ConfirmationModal
|
||||
intl={intl}
|
||||
onConfirm={callback}
|
||||
title={intl.formatMessage(intlMessages.confirmationTitle)}
|
||||
description={intl.formatMessage(intlMessages.confirmationDescription, { 0: file.name })}
|
||||
checkboxMessageId="app.confirmation.skipConfirm"
|
||||
{...{
|
||||
onRequestClose: () => setConfirmModalIsOpen(false),
|
||||
priority: "low",
|
||||
setIsOpen: setConfirmModalIsOpen,
|
||||
isOpen: isConfirmModalOpen
|
||||
}}
|
||||
/> : null}
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(
|
||||
children,
|
||||
{
|
||||
...props, dragging, draggingOver, makeDragOperations,
|
||||
},
|
||||
)}
|
||||
{isConfirmModalOpen ? (
|
||||
<ConfirmationModal
|
||||
intl={intl}
|
||||
onConfirm={callback}
|
||||
title={intl.formatMessage(intlMessages.confirmationTitle)}
|
||||
description={intl.formatMessage(intlMessages.confirmationDescription, { 0: file?.name })}
|
||||
checkboxMessageId="app.confirmation.skipConfirm"
|
||||
{...{
|
||||
onRequestClose: () => setConfirmModalIsOpen(false),
|
||||
priority: 'low',
|
||||
setIsOpen: setConfirmModalIsOpen,
|
||||
isOpen: isConfirmModalOpen,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = (Component) => (props) => (
|
||||
<DragAndDrop {...props} >
|
||||
const Wrapper = (Component: (props: object) => JSX.Element) => (props: DragAndDropProps) => (
|
||||
<DragAndDrop
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
>
|
||||
<Component />
|
||||
</DragAndDrop>
|
||||
);
|
||||
|
||||
export const withDragAndDrop = (Component) =>
|
||||
injectIntl(withFileReader(Wrapper(Component), MIME_TYPES_ALLOWED, MAX_FILE_SIZE));
|
||||
const withDragAndDrop = (Component: (props: object) => JSX.Element) => injectIntl(
|
||||
withFileReader(
|
||||
Wrapper(Component),
|
||||
MIME_TYPES_ALLOWED, MAX_FILE_SIZE,
|
||||
),
|
||||
);
|
||||
|
||||
export default withDragAndDrop;
|
||||
|
@ -1,12 +1,10 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import VideoService from '/imports/ui/components/video-provider/video-provider-graphql/service';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import Styled from './styles';
|
||||
import { SET_CAMERA_PINNED } from '/imports/ui/core/graphql/mutations/userMutations';
|
||||
import { StreamUser, VideoItem } from '../../../types';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
unpinLabel: {
|
||||
@ -17,11 +15,17 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const PinArea = (props) => {
|
||||
interface PinAreaProps {
|
||||
user: Partial<StreamUser>;
|
||||
stream: VideoItem | undefined;
|
||||
amIModerator: boolean;
|
||||
}
|
||||
|
||||
const PinArea: React.FC<PinAreaProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { user, amIModerator } = props;
|
||||
const pinned = user?.pin;
|
||||
const { stream, user, amIModerator } = props;
|
||||
const pinned = stream?.type === 'stream' && stream?.pin;
|
||||
const userId = user?.userId;
|
||||
const shouldRenderPinButton = pinned && userId;
|
||||
const videoPinActionAvailable = VideoService.isVideoPinEnabledForCurrentUser(amIModerator);
|
||||
@ -56,10 +60,3 @@ const PinArea = (props) => {
|
||||
};
|
||||
|
||||
export default PinArea;
|
||||
|
||||
PinArea.propTypes = {
|
||||
user: PropTypes.shape({
|
||||
pin: PropTypes.bool.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
@ -29,7 +29,15 @@ const fade = keyframes`
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
const Content = styled.div<{
|
||||
isStream: boolean;
|
||||
talking: boolean;
|
||||
customHighlight: boolean;
|
||||
animations: boolean;
|
||||
dragging: boolean;
|
||||
draggingOver: boolean;
|
||||
fullscreen: boolean;
|
||||
}>`
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 100%;
|
||||
@ -88,7 +96,9 @@ const Content = styled.div`
|
||||
`}
|
||||
`;
|
||||
|
||||
const WebcamConnecting = styled.div`
|
||||
const WebcamConnecting = styled.div<{
|
||||
animations: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -120,7 +130,9 @@ const LoadingText = styled(TextElipsis)`
|
||||
font-size: 100%;
|
||||
`;
|
||||
|
||||
const VideoContainer = styled.div`
|
||||
const VideoContainer = styled.div<{
|
||||
$selfViewDisabled: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
@ -129,7 +141,10 @@ const VideoContainer = styled.div`
|
||||
${({ $selfViewDisabled }) => $selfViewDisabled && 'display: none'}
|
||||
`;
|
||||
|
||||
const Video = styled.video`
|
||||
const Video = styled.video<{
|
||||
mirrored: boolean;
|
||||
unhealthyStream: boolean;
|
||||
}>`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: calc(100% - 1px);
|
||||
@ -147,7 +162,7 @@ const Video = styled.video`
|
||||
`;
|
||||
|
||||
const VideoDisabled = styled.div`
|
||||
color: white;
|
||||
color: white;
|
||||
width: 100%;
|
||||
height: 20%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
@ -164,7 +179,7 @@ color: white;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
}`;
|
||||
`;
|
||||
|
||||
const TopBar = styled.div`
|
||||
position: absolute;
|
||||
|
@ -1,19 +1,19 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React, { useContext } from 'react';
|
||||
import React, { MutableRefObject, useContext } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { Session } from 'meteor/session';
|
||||
import { UserCameraDropdownInterface } from 'bigbluebutton-html-plugin-sdk';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import VideoService from '/imports/ui/components/video-provider/video-provider-graphql/service';
|
||||
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import PropTypes from 'prop-types';
|
||||
import { UserCameraDropdownItemType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/user-camera-dropdown-item/enums';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import Styled from './styles';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import { SET_CAMERA_PINNED } from '/imports/ui/core/graphql/mutations/userMutations';
|
||||
import { StreamUser, VideoItem } from '../../../types';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
focusLabel: {
|
||||
@ -74,16 +74,35 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const UserActions = (props) => {
|
||||
interface UserActionProps {
|
||||
name: string;
|
||||
user: Partial<StreamUser>;
|
||||
stream: VideoItem | undefined;
|
||||
cameraId: string;
|
||||
numOfStreams: number;
|
||||
onHandleVideoFocus: ((id: string) => void) | null;
|
||||
focused: boolean;
|
||||
onHandleMirror: () => void;
|
||||
isMirrored: boolean;
|
||||
isRTL: boolean;
|
||||
isStream: boolean;
|
||||
onHandleDisableCam: () => void;
|
||||
isSelfViewDisabled: boolean;
|
||||
amIModerator: boolean;
|
||||
isVideoSqueezed?: boolean,
|
||||
videoContainer?: MutableRefObject<HTMLDivElement | null>,
|
||||
}
|
||||
|
||||
const UserActions: React.FC<UserActionProps> = (props) => {
|
||||
const {
|
||||
name, cameraId, numOfStreams, onHandleVideoFocus, user, focused, onHandleMirror,
|
||||
isVideoSqueezed, videoContainer, isRTL, isStream, isSelfViewDisabled, isMirrored,
|
||||
name, cameraId, numOfStreams, onHandleVideoFocus, user, stream, focused, onHandleMirror,
|
||||
isVideoSqueezed = false, videoContainer, isRTL, isStream, isSelfViewDisabled, isMirrored,
|
||||
amIModerator,
|
||||
} = props;
|
||||
|
||||
const { pluginsExtensibleAreasAggregatedState } = useContext(PluginsContext);
|
||||
|
||||
let userCameraDropdownItems = [];
|
||||
let userCameraDropdownItems: UserCameraDropdownInterface[] = [];
|
||||
if (pluginsExtensibleAreasAggregatedState.userCameraDropdownItems) {
|
||||
userCameraDropdownItems = [
|
||||
...pluginsExtensibleAreasAggregatedState.userCameraDropdownItems,
|
||||
@ -97,7 +116,7 @@ const UserActions = (props) => {
|
||||
const [setCameraPinned] = useMutation(SET_CAMERA_PINNED);
|
||||
|
||||
const getAvailableActions = () => {
|
||||
const pinned = user?.pin;
|
||||
const pinned = stream?.type === 'stream' && stream?.pin;
|
||||
const userId = user?.userId;
|
||||
const isPinnedIntlKey = !pinned ? 'pin' : 'unpin';
|
||||
const isFocusedIntlKey = !focused ? 'focus' : 'unfocus';
|
||||
@ -113,7 +132,7 @@ const UserActions = (props) => {
|
||||
Session.set('disabledCams', [...disabledCams, cameraId]);
|
||||
notify(intl.formatMessage(intlMessages.disableWarning), 'info', 'warning');
|
||||
} else {
|
||||
Session.set('disabledCams', disabledCams.filter((cId) => cId !== cameraId));
|
||||
Session.set('disabledCams', disabledCams.filter((cId: string) => cId !== cameraId));
|
||||
}
|
||||
};
|
||||
|
||||
@ -132,7 +151,8 @@ const UserActions = (props) => {
|
||||
key: `${cameraId}-fullscreen`,
|
||||
label: intl.formatMessage(intlMessages.fullscreenLabel),
|
||||
description: intl.formatMessage(intlMessages.fullscreenLabel),
|
||||
onClick: () => FullscreenService.toggleFullScreen(videoContainer.current),
|
||||
// @ts-expect-error -> Until everything in Typescript.
|
||||
onClick: () => FullscreenService.toggleFullScreen(videoContainer?.current),
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -142,7 +162,7 @@ const UserActions = (props) => {
|
||||
key: `${cameraId}-disable`,
|
||||
label: intl.formatMessage(intlMessages[`${enableSelfCamIntlKey}Label`]),
|
||||
description: intl.formatMessage(intlMessages[`${enableSelfCamIntlKey}Label`]),
|
||||
onClick: () => toggleDisableCam(cameraId),
|
||||
onClick: () => toggleDisableCam(),
|
||||
dataTest: 'selfViewDisableBtn',
|
||||
});
|
||||
}
|
||||
@ -152,7 +172,7 @@ const UserActions = (props) => {
|
||||
key: `${cameraId}-mirror`,
|
||||
label: intl.formatMessage(intlMessages[`${isMirroredIntlKey}Label`]),
|
||||
description: intl.formatMessage(intlMessages[`${isMirroredIntlKey}Desc`]),
|
||||
onClick: () => onHandleMirror(cameraId),
|
||||
onClick: () => onHandleMirror(),
|
||||
dataTest: 'mirrorWebcamBtn',
|
||||
});
|
||||
}
|
||||
@ -162,7 +182,7 @@ const UserActions = (props) => {
|
||||
key: `${cameraId}-focus`,
|
||||
label: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Label`]),
|
||||
description: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Desc`]),
|
||||
onClick: () => onHandleVideoFocus(cameraId),
|
||||
onClick: () => onHandleVideoFocus?.(cameraId),
|
||||
dataTest: 'FocusWebcamBtn',
|
||||
});
|
||||
}
|
||||
@ -189,8 +209,11 @@ const UserActions = (props) => {
|
||||
case UserCameraDropdownItemType.OPTION:
|
||||
menuItems.push({
|
||||
key: pluginItem.id,
|
||||
// @ts-expect-error -> Plugin-related.
|
||||
label: pluginItem.label,
|
||||
// @ts-expect-error -> Plugin-related.
|
||||
onClick: pluginItem.onClick,
|
||||
// @ts-expect-error -> Plugin-related.
|
||||
icon: pluginItem.icon,
|
||||
});
|
||||
break;
|
||||
@ -238,7 +261,7 @@ const UserActions = (props) => {
|
||||
<Styled.DropdownTrigger
|
||||
tabIndex={0}
|
||||
data-test="dropdownWebcamButton"
|
||||
isRTL={isRTL}
|
||||
$isRTL={isRTL}
|
||||
>
|
||||
{name}
|
||||
</Styled.DropdownTrigger>
|
||||
@ -257,8 +280,8 @@ const UserActions = (props) => {
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Styled.Dropdown isFirefox={isFirefox}>
|
||||
<Styled.UserName noMenu={numOfStreams < 3}>
|
||||
<Styled.Dropdown $isFirefox={isFirefox}>
|
||||
<Styled.UserName $noMenu={numOfStreams < 3}>
|
||||
{name}
|
||||
</Styled.UserName>
|
||||
</Styled.Dropdown>
|
||||
@ -274,29 +297,3 @@ const UserActions = (props) => {
|
||||
};
|
||||
|
||||
export default UserActions;
|
||||
|
||||
UserActions.defaultProps = {
|
||||
focused: false,
|
||||
isVideoSqueezed: false,
|
||||
videoContainer: () => { },
|
||||
onHandleVideoFocus: () => {},
|
||||
};
|
||||
|
||||
UserActions.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
cameraId: PropTypes.string.isRequired,
|
||||
numOfStreams: PropTypes.number.isRequired,
|
||||
onHandleVideoFocus: PropTypes.func,
|
||||
user: PropTypes.shape({
|
||||
pin: PropTypes.bool.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
focused: PropTypes.bool,
|
||||
isVideoSqueezed: PropTypes.bool,
|
||||
videoContainer: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
onHandleMirror: PropTypes.func.isRequired,
|
||||
onHandleDisableCam: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -7,7 +7,9 @@ import { landscape, mediumUp } from '/imports/ui/stylesheets/styled-components/b
|
||||
import { fontSizeSmaller } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
const DropdownTrigger = styled(DivElipsis)`
|
||||
const DropdownTrigger = styled(DivElipsis)<{
|
||||
$isRTL: boolean;
|
||||
}>`
|
||||
user-select: none;
|
||||
position: relative;
|
||||
// Keep the background with 0.5 opacity, but leave the text with 1
|
||||
@ -26,7 +28,7 @@ const DropdownTrigger = styled(DivElipsis)`
|
||||
content: "\\203a";
|
||||
position: absolute;
|
||||
transform: rotate(90deg);
|
||||
${({ isRTL }) => isRTL && `
|
||||
${({ $isRTL }) => $isRTL && `
|
||||
transform: rotate(-90deg);
|
||||
`}
|
||||
top: 45%;
|
||||
@ -36,19 +38,23 @@ const DropdownTrigger = styled(DivElipsis)`
|
||||
}
|
||||
`;
|
||||
|
||||
const UserName = styled(TextElipsis)`
|
||||
const UserName = styled(TextElipsis)<{
|
||||
$noMenu: boolean;
|
||||
}>`
|
||||
position: relative;
|
||||
// Keep the background with 0.5 opacity, but leave the text with 1
|
||||
color: ${colorOffWhite};
|
||||
padding: 0 1rem 0 .5rem !important;
|
||||
font-size: 80%;
|
||||
|
||||
${({ noMenu }) => noMenu && `
|
||||
${({ $noMenu }) => $noMenu && `
|
||||
padding: 0 .5rem 0 .5rem !important;
|
||||
`}
|
||||
`;
|
||||
|
||||
const Dropdown = styled.div`
|
||||
const Dropdown = styled.div<{
|
||||
$isFirefox: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
outline: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
@ -68,7 +74,7 @@ const Dropdown = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
${({ isFirefox }) => isFirefox && `
|
||||
${({ $isFirefox }) => $isFirefox && `
|
||||
max-width: 100%;
|
||||
`}
|
||||
`;
|
||||
|
@ -1,15 +1,28 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Styled from './styles';
|
||||
import Icon from '/imports/ui/components/common/icon/component';
|
||||
import UserListService from '/imports/ui/components/user-list/service';
|
||||
import { StreamUser } from '../../../types';
|
||||
|
||||
const UserAvatarVideo = (props) => {
|
||||
const { user, unhealthyStream, squeezed, voiceUser } = props;
|
||||
interface UserAvatarVideoProps {
|
||||
user: Partial<StreamUser>;
|
||||
// eslint-disable-next-line react/require-default-props
|
||||
voiceUser?: {
|
||||
muted?: boolean;
|
||||
listenOnly?: boolean;
|
||||
talking?: boolean;
|
||||
joined?: boolean;
|
||||
};
|
||||
squeezed: boolean;
|
||||
unhealthyStream: boolean;
|
||||
}
|
||||
|
||||
const UserAvatarVideo: React.FC<UserAvatarVideoProps> = (props) => {
|
||||
const {
|
||||
name, color, avatar, role, emoji,
|
||||
user, unhealthyStream, squeezed, voiceUser = {},
|
||||
} = props;
|
||||
const {
|
||||
name = '', color, avatar, role, emoji,
|
||||
} = user;
|
||||
let {
|
||||
presenter, clientType,
|
||||
@ -21,6 +34,7 @@ const UserAvatarVideo = (props) => {
|
||||
|
||||
const handleUserIcon = () => {
|
||||
if (emoji !== 'none') {
|
||||
// @ts-expect-error -> Untyped component.
|
||||
return <Icon iconName={UserListService.normalizeEmojiName(emoji)} />;
|
||||
}
|
||||
return name.toLowerCase().slice(0, 2);
|
||||
@ -29,7 +43,7 @@ const UserAvatarVideo = (props) => {
|
||||
// hide icons when squeezed
|
||||
if (squeezed) {
|
||||
presenter = false;
|
||||
clientType = false;
|
||||
clientType = '';
|
||||
}
|
||||
|
||||
return (
|
||||
@ -42,6 +56,7 @@ const UserAvatarVideo = (props) => {
|
||||
avatar={avatar}
|
||||
unhealthyStream={unhealthyStream}
|
||||
talking={talking}
|
||||
whiteboardAccess={undefined}
|
||||
>
|
||||
{handleUserIcon()}
|
||||
</Styled.UserAvatarStyled>
|
||||
@ -49,17 +64,3 @@ const UserAvatarVideo = (props) => {
|
||||
};
|
||||
|
||||
export default UserAvatarVideo;
|
||||
|
||||
UserAvatarVideo.propTypes = {
|
||||
user: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
avatar: PropTypes.string.isRequired,
|
||||
role: PropTypes.string.isRequired,
|
||||
emoji: PropTypes.string.isRequired,
|
||||
presenter: PropTypes.bool.isRequired,
|
||||
clientType: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
unhealthyStream: PropTypes.bool.isRequired,
|
||||
squeezed: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
@ -1,5 +1,3 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import UserAvatar from '/imports/ui/components/user-avatar/component';
|
||||
import {
|
||||
userIndicatorsOffset,
|
||||
@ -10,7 +8,11 @@ import {
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const UserAvatarStyled = styled(UserAvatar)`
|
||||
const UserAvatarStyled = styled(UserAvatar)<{
|
||||
unhealthyStream: boolean;
|
||||
dialIn: boolean;
|
||||
presenter: boolean;
|
||||
}>`
|
||||
height: 60%;
|
||||
width: 45%;
|
||||
max-width: 66px;
|
||||
|
@ -1,10 +1,18 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Styled from './styles';
|
||||
import { StreamUser } from '../../../types';
|
||||
|
||||
const UserStatus = (props) => {
|
||||
interface UserStatusProps {
|
||||
user: Partial<StreamUser>;
|
||||
voiceUser: {
|
||||
muted: boolean;
|
||||
listenOnly: boolean;
|
||||
talking: boolean;
|
||||
joined: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const UserStatus: React.FC<UserStatusProps> = (props) => {
|
||||
const { voiceUser, user } = props;
|
||||
|
||||
const listenOnly = voiceUser?.listenOnly;
|
||||
@ -26,11 +34,3 @@ const UserStatus = (props) => {
|
||||
};
|
||||
|
||||
export default UserStatus;
|
||||
|
||||
UserStatus.propTypes = {
|
||||
voiceUser: PropTypes.shape({
|
||||
listenOnly: PropTypes.bool.isRequired,
|
||||
muted: PropTypes.bool.isRequired,
|
||||
joined: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
@ -1,11 +1,18 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { MutableRefObject, useEffect } from 'react';
|
||||
import { ACTIONS } from '/imports/ui/components/layout/enums';
|
||||
import FullscreenButtonContainer from '/imports/ui/components/common/fullscreen-button/container';
|
||||
import Styled from './styles';
|
||||
|
||||
const ViewActions = (props) => {
|
||||
interface ViewActionsProps {
|
||||
name: string;
|
||||
cameraId: string;
|
||||
videoContainer: MutableRefObject<HTMLDivElement | null>;
|
||||
isFullscreenContext: boolean;
|
||||
layoutContextDispatch: (...args: unknown[]) => void;
|
||||
isStream: boolean;
|
||||
}
|
||||
|
||||
const ViewActions: React.FC<ViewActionsProps> = (props) => {
|
||||
const {
|
||||
name, cameraId, videoContainer, isFullscreenContext, layoutContextDispatch, isStream,
|
||||
} = props;
|
||||
@ -13,7 +20,6 @@ const ViewActions = (props) => {
|
||||
const ALLOW_FULLSCREEN = window.meetingClientSettings.public.app.allowFullscreen;
|
||||
|
||||
useEffect(() => () => {
|
||||
// exit fullscreen when component is unmounted
|
||||
if (isFullscreenContext) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_FULLSCREEN_ELEMENT,
|
||||
@ -43,14 +49,3 @@ const ViewActions = (props) => {
|
||||
};
|
||||
|
||||
export default ViewActions;
|
||||
|
||||
ViewActions.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
cameraId: PropTypes.string.isRequired,
|
||||
videoContainer: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]).isRequired,
|
||||
isFullscreenContext: PropTypes.bool.isRequired,
|
||||
layoutContextDispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -1,8 +1,6 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Resizable from 're-resizable';
|
||||
import Draggable from 'react-draggable';
|
||||
import Draggable, { DraggableEvent } from 'react-draggable';
|
||||
import { Session } from 'meteor/session';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { useSubscription } from '@apollo/client';
|
||||
@ -15,17 +13,29 @@ import {
|
||||
} from '/imports/ui/components/layout/context';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import { LAYOUT_TYPE, ACTIONS, CAMERADOCK_POSITION } from '/imports/ui/components/layout/enums';
|
||||
import { sortVideoStreams } from '/imports/ui/components/video-provider/stream-sorting';
|
||||
import { CURRENT_PRESENTATION_PAGE_SUBSCRIPTION } from '/imports/ui/components/whiteboard/queries';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import DropAreaContainer from './drop-areas/container';
|
||||
import VideoProviderContainer from '/imports/ui/components/video-provider/video-provider-graphql/container';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import Styled from '../styles';
|
||||
import Styled from './styles';
|
||||
import { Input, Layout, Output } from '/imports/ui/components/layout/layoutTypes';
|
||||
import { VideoItem } from '/imports/ui/components/video-provider/video-provider-graphql/types';
|
||||
|
||||
const { defaultSorting: DEFAULT_SORTING } = window.meetingClientSettings.public.kurento.cameraSortingModes;
|
||||
interface WebcamComponentGraphqlProps {
|
||||
cameraDock: Output['cameraDock'];
|
||||
swapLayout: boolean;
|
||||
focusedId: string;
|
||||
layoutContextDispatch: (...args: unknown[]) => void;
|
||||
fullscreen: Layout['fullscreen'];
|
||||
isPresenter: boolean;
|
||||
displayPresentation: boolean;
|
||||
cameraOptimalGridSize: Input['cameraDock']['cameraOptimalGridSize'];
|
||||
isRTL: boolean;
|
||||
isGridEnabled: boolean;
|
||||
}
|
||||
|
||||
const WebcamComponentGraphql = ({
|
||||
const WebcamComponentGraphql: React.FC<WebcamComponentGraphqlProps> = ({
|
||||
cameraDock,
|
||||
swapLayout,
|
||||
focusedId,
|
||||
@ -45,7 +55,7 @@ const WebcamComponentGraphql = ({
|
||||
const [draggedAtLeastOneTime, setDraggedAtLeastOneTime] = useState(false);
|
||||
|
||||
const lastSize = Storage.getItem('webcamSize') || { width: 0, height: 0 };
|
||||
const { width: lastWidth, height: lastHeight } = lastSize;
|
||||
const { height: lastHeight } = lastSize as { width: number, height: number };
|
||||
|
||||
const isCameraTopOrBottom = cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP
|
||||
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM;
|
||||
@ -72,7 +82,9 @@ const WebcamComponentGraphql = ({
|
||||
}, [fullscreen]);
|
||||
|
||||
useEffect(() => {
|
||||
const newCameraMaxWidth = (isPresenter && cameraDock.presenterMaxWidth) ? cameraDock.presenterMaxWidth : cameraDock.maxWidth;
|
||||
const newCameraMaxWidth = (isPresenter && cameraDock.presenterMaxWidth)
|
||||
? cameraDock.presenterMaxWidth
|
||||
: cameraDock.maxWidth;
|
||||
setCameraMaxWidth(newCameraMaxWidth);
|
||||
|
||||
if (isCameraLeftOrRight && cameraDock.width > newCameraMaxWidth) {
|
||||
@ -91,18 +103,18 @@ const WebcamComponentGraphql = ({
|
||||
}
|
||||
|
||||
const cams = document.getElementById('cameraDock');
|
||||
cams?.setAttribute("data-position", cameraDock.position);
|
||||
cams?.setAttribute('data-position', cameraDock.position);
|
||||
}, [cameraDock.position, cameraDock.maxWidth, isPresenter, displayPresentation]);
|
||||
|
||||
const handleVideoFocus = (id) => {
|
||||
const handleVideoFocus = (id: string) => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_FOCUSED_CAMERA_ID,
|
||||
value: focusedId !== id ? id : false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onResizeHandle = (deltaWidth, deltaHeight) => {
|
||||
if (cameraDock.resizableEdge.top || cameraDock.resizableEdge.bottom) {
|
||||
const onResizeHandle = (deltaWidth: number, deltaHeight: number) => {
|
||||
if (cameraDock.resizableEdge?.top || cameraDock.resizableEdge?.bottom) {
|
||||
layoutContextDispatch(
|
||||
{
|
||||
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
|
||||
@ -115,7 +127,7 @@ const WebcamComponentGraphql = ({
|
||||
},
|
||||
);
|
||||
}
|
||||
if (cameraDock.resizableEdge.left || cameraDock.resizableEdge.right) {
|
||||
if (cameraDock.resizableEdge?.left || cameraDock.resizableEdge?.right) {
|
||||
layoutContextDispatch(
|
||||
{
|
||||
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
|
||||
@ -139,18 +151,19 @@ const WebcamComponentGraphql = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleWebcamDragStop = (e) => {
|
||||
const handleWebcamDragStop = (e: DraggableEvent) => {
|
||||
setIsDragging(false);
|
||||
setDraggedAtLeastOneTime(false);
|
||||
document.body.style.overflow = 'auto';
|
||||
const dropAreaId = (e.target as HTMLDivElement).id;
|
||||
|
||||
if (Object.values(CAMERADOCK_POSITION).includes(e.target.id) && draggedAtLeastOneTime) {
|
||||
if (Object.values(CAMERADOCK_POSITION).includes(dropAreaId) && draggedAtLeastOneTime) {
|
||||
const layout = document.getElementById('layout');
|
||||
layout?.setAttribute("data-cam-position", e?.target?.id);
|
||||
layout?.setAttribute('data-cam-position', dropAreaId);
|
||||
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_CAMERA_DOCK_POSITION,
|
||||
value: e.target.id,
|
||||
value: dropAreaId,
|
||||
});
|
||||
}
|
||||
|
||||
@ -160,32 +173,33 @@ const WebcamComponentGraphql = ({
|
||||
});
|
||||
};
|
||||
|
||||
let draggableOffset = {
|
||||
const draggableOffset = {
|
||||
left: isDragging && (isCameraTopOrBottom || isCameraSidebar)
|
||||
? ((cameraDock.width - cameraSize.width) / 2)
|
||||
? ((cameraDock.width - (cameraSize?.width ?? 0)) / 2)
|
||||
: 0,
|
||||
top: isDragging && isCameraLeftOrRight
|
||||
? ((cameraDock.height - cameraSize.height) / 2)
|
||||
? ((cameraDock.height - (cameraSize?.height ?? 0)) / 2)
|
||||
: 0,
|
||||
};
|
||||
|
||||
if (isRTL) {
|
||||
draggableOffset.left = draggableOffset.left * -1;
|
||||
draggableOffset.left *= -1;
|
||||
}
|
||||
const isIphone = !!(navigator.userAgent.match(/iPhone/i));
|
||||
|
||||
const mobileWidth = `${isDragging ? cameraSize.width : cameraDock.width}pt`;
|
||||
const mobileHeight = `${isDragging ? cameraSize.height : cameraDock.height}pt`;
|
||||
const isDesktopWidth = isDragging ? cameraSize.width : cameraDock.width;
|
||||
const isDesktopHeight = isDragging ? cameraSize.height : cameraDock.height;
|
||||
const isIphone = !!(navigator.userAgent.match(/iPhone/i));
|
||||
const mobileWidth = `${isDragging ? cameraSize?.width : cameraDock.width}pt`;
|
||||
const mobileHeight = `${isDragging ? cameraSize?.height : cameraDock.height}pt`;
|
||||
const isDesktopWidth = isDragging ? cameraSize?.width : cameraDock.width;
|
||||
const isDesktopHeight = isDragging ? cameraSize?.height : cameraDock.height;
|
||||
const camOpacity = isDragging ? 0.5 : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDragging ? <DropAreaContainer /> : null}
|
||||
<Styled.ResizableWrapper
|
||||
horizontal={cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP
|
||||
$horizontal={cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP
|
||||
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM}
|
||||
vertical={cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT
|
||||
$vertical={cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT
|
||||
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT}
|
||||
>
|
||||
<Draggable
|
||||
@ -210,13 +224,13 @@ const WebcamComponentGraphql = ({
|
||||
}
|
||||
>
|
||||
<Resizable
|
||||
minWidth={isDragging ? cameraSize.width : cameraDock.minWidth}
|
||||
minHeight={isDragging ? cameraSize.height : cameraDock.minHeight}
|
||||
maxWidth={isDragging ? cameraSize.width : cameraMaxWidth}
|
||||
maxHeight={isDragging ? cameraSize.height : cameraDock.maxHeight}
|
||||
minWidth={isDragging ? cameraSize?.width : cameraDock.minWidth}
|
||||
minHeight={isDragging ? cameraSize?.height : cameraDock.minHeight}
|
||||
maxWidth={isDragging ? cameraSize?.width : cameraMaxWidth}
|
||||
maxHeight={isDragging ? cameraSize?.height : cameraDock.maxHeight}
|
||||
size={{
|
||||
width: isDragging ? cameraSize.width : cameraDock.width,
|
||||
height: isDragging ? cameraSize.height : cameraDock.height,
|
||||
width: isDragging ? cameraSize?.width : cameraDock.width,
|
||||
height: isDragging ? cameraSize?.height : cameraDock.height,
|
||||
}}
|
||||
onResizeStart={() => {
|
||||
setIsResizing(true);
|
||||
@ -227,7 +241,7 @@ const WebcamComponentGraphql = ({
|
||||
value: true,
|
||||
});
|
||||
}}
|
||||
onResize={(e, direction, ref, d) => {
|
||||
onResize={(_, __, ___, d) => {
|
||||
onResizeHandle(d.width, d.height);
|
||||
}}
|
||||
onResizeStop={() => {
|
||||
@ -239,11 +253,11 @@ const WebcamComponentGraphql = ({
|
||||
});
|
||||
}}
|
||||
enable={{
|
||||
top: !isFullscreen && !isDragging && !swapLayout && cameraDock.resizableEdge.top,
|
||||
top: !isFullscreen && !isDragging && !swapLayout && cameraDock?.resizableEdge?.top,
|
||||
bottom: !isFullscreen && !isDragging && !swapLayout
|
||||
&& cameraDock.resizableEdge.bottom,
|
||||
left: !isFullscreen && !isDragging && !swapLayout && cameraDock.resizableEdge.left,
|
||||
right: !isFullscreen && !isDragging && !swapLayout && cameraDock.resizableEdge.right,
|
||||
&& cameraDock?.resizableEdge?.bottom,
|
||||
left: !isFullscreen && !isDragging && !swapLayout && cameraDock?.resizableEdge?.left,
|
||||
right: !isFullscreen && !isDragging && !swapLayout && cameraDock?.resizableEdge?.right,
|
||||
topLeft: false,
|
||||
topRight: false,
|
||||
bottomLeft: false,
|
||||
@ -251,12 +265,12 @@ const WebcamComponentGraphql = ({
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: isCameraSidebar && !isDragging ? 0 : cameraDock.zIndex,
|
||||
zIndex: isCameraSidebar && !isDragging ? 0 : cameraDock?.zIndex,
|
||||
}}
|
||||
>
|
||||
<Styled.Draggable
|
||||
isDraggable={cameraDock.isDraggable && !isFullscreen && !isDragging}
|
||||
isDragging={isDragging}
|
||||
$isDraggable={!!cameraDock.isDraggable && !isFullscreen && !isDragging}
|
||||
$isDragging={isDragging}
|
||||
id="cameraDock"
|
||||
role="region"
|
||||
draggable={cameraDock.isDraggable && !isFullscreen ? 'true' : undefined}
|
||||
@ -264,7 +278,7 @@ const WebcamComponentGraphql = ({
|
||||
width: isIphone ? mobileWidth : isDesktopWidth,
|
||||
height: isIphone ? mobileHeight : isDesktopHeight,
|
||||
opacity: camOpacity,
|
||||
background: null,
|
||||
background: 'none',
|
||||
}}
|
||||
>
|
||||
<VideoProviderContainer
|
||||
@ -299,11 +313,11 @@ const WebcamContainerGraphql: React.FC<WebcamContainerGraphqlProps> = ({
|
||||
paginationEnabled,
|
||||
viewParticipantsWebcams,
|
||||
}) => {
|
||||
const fullscreen = layoutSelect((i) => i.fullscreen);
|
||||
const isRTL = layoutSelect((i) => i.isRTL);
|
||||
const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
|
||||
const presentation = layoutSelectOutput((i) => i.presentation);
|
||||
const cameraDock = layoutSelectOutput((i) => i.cameraDock);
|
||||
const fullscreen = layoutSelect((i: Layout) => i.fullscreen);
|
||||
const isRTL = layoutSelect((i: Layout) => i.isRTL);
|
||||
const cameraDockInput = layoutSelectInput((i: Input) => i.cameraDock);
|
||||
const presentation = layoutSelectOutput((i: Output) => i.presentation);
|
||||
const cameraDock = layoutSelectOutput((i: Output) => i.cameraDock);
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
const { data: presentationPageData } = useSubscription(CURRENT_PRESENTATION_PAGE_SUBSCRIPTION);
|
||||
const presentationPage = presentationPageData?.pres_page_curr[0] || {};
|
||||
@ -330,9 +344,12 @@ const WebcamContainerGraphql: React.FC<WebcamContainerGraphqlProps> = ({
|
||||
|
||||
const { streams: videoUsers, gridUsers } = useVideoStreams(isGridEnabled, paginationEnabled, viewParticipantsWebcams);
|
||||
|
||||
let usersVideo;
|
||||
let usersVideo: VideoItem[];
|
||||
if (gridUsers.length > 0) {
|
||||
usersVideo = videoUsers.concat(gridUsers);
|
||||
usersVideo = [
|
||||
...videoUsers,
|
||||
...gridUsers,
|
||||
];
|
||||
} else {
|
||||
usersVideo = videoUsers;
|
||||
}
|
||||
@ -348,7 +365,7 @@ const WebcamContainerGraphql: React.FC<WebcamContainerGraphqlProps> = ({
|
||||
cameraOptimalGridSize,
|
||||
layoutContextDispatch,
|
||||
fullscreen,
|
||||
isPresenter: currentUserData?.presenter,
|
||||
isPresenter: currentUserData?.presenter ?? false,
|
||||
displayPresentation,
|
||||
isRTL,
|
||||
isGridEnabled,
|
||||
@ -360,14 +377,26 @@ const WebcamContainerGraphql: React.FC<WebcamContainerGraphqlProps> = ({
|
||||
: null;
|
||||
};
|
||||
|
||||
export default withTracker<{
|
||||
audioModalIsOpen: boolean,
|
||||
}, WebcamContainerGraphqlProps>(() => {
|
||||
const data = {
|
||||
audioModalIsOpen: Session.get('audioModalIsOpen'),
|
||||
paginationEnabled: Settings.application.paginationEnabled,
|
||||
viewParticipantsWebcams: Settings.dataSaving.viewParticipantsWebcams,
|
||||
};
|
||||
type TrackerData = {
|
||||
audioModalIsOpen: boolean;
|
||||
paginationEnabled: boolean;
|
||||
viewParticipantsWebcams: boolean;
|
||||
};
|
||||
|
||||
return data;
|
||||
type TrackerProps = {
|
||||
isLayoutSwapped: boolean;
|
||||
layoutType: string;
|
||||
};
|
||||
|
||||
export default withTracker<TrackerData, TrackerProps>(() => {
|
||||
const audioModalIsOpen = Session.get('audioModalIsOpen');
|
||||
// @ts-expect-error -> Untyped object.
|
||||
const { paginationEnabled } = Settings.application;
|
||||
// @ts-expect-error -> Untyped object.
|
||||
const { viewParticipantsWebcams } = Settings.dataSaving;
|
||||
return {
|
||||
audioModalIsOpen,
|
||||
paginationEnabled,
|
||||
viewParticipantsWebcams,
|
||||
};
|
||||
})(WebcamContainerGraphql);
|
||||
|
@ -1,7 +1,5 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Styled from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -11,31 +9,42 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const DropArea = ({
|
||||
id, dataTest, style, intl,
|
||||
}) => (
|
||||
<>
|
||||
<Styled.DropZoneArea
|
||||
id={id}
|
||||
data-test={dataTest}
|
||||
style={
|
||||
{
|
||||
...style,
|
||||
zIndex: style.zIndex + 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Styled.DropZoneBg
|
||||
style={
|
||||
{
|
||||
...style,
|
||||
zIndex: style.zIndex,
|
||||
}
|
||||
}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.dropZoneLabel)}
|
||||
</Styled.DropZoneBg>
|
||||
</>
|
||||
);
|
||||
interface DropAreaProps {
|
||||
id: string;
|
||||
dataTest: string;
|
||||
style: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export default injectIntl(DropArea);
|
||||
const DropArea: React.FC<DropAreaProps> = ({
|
||||
id,
|
||||
dataTest,
|
||||
style,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<>
|
||||
<Styled.DropZoneArea
|
||||
id={id}
|
||||
data-test={dataTest}
|
||||
style={
|
||||
{
|
||||
...style,
|
||||
zIndex: (style.zIndex as number) + 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Styled.DropZoneBg
|
||||
style={
|
||||
{
|
||||
...style,
|
||||
zIndex: (style.zIndex as number),
|
||||
}
|
||||
}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.dropZoneLabel)}
|
||||
</Styled.DropZoneBg>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropArea;
|
||||
|
@ -1,15 +1,19 @@
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import { layoutSelectOutput } from '/imports/ui/components/layout/context';
|
||||
import { Output } from '/imports/ui/components/layout/layoutTypes';
|
||||
import DropArea from './component';
|
||||
|
||||
const DropAreaContainer = () => {
|
||||
const dropZoneAreas = layoutSelectOutput((i) => i.dropZoneAreas);
|
||||
const dropZoneAreas = layoutSelectOutput((i: Output) => i.dropZoneAreas);
|
||||
|
||||
return (
|
||||
Object.keys(dropZoneAreas).map((objectKey) => (
|
||||
<DropArea dataTest={`dropArea-${objectKey}`} key={objectKey} id={objectKey} style={dropZoneAreas[objectKey]} />
|
||||
<DropArea
|
||||
dataTest={`dropArea-${objectKey}`}
|
||||
key={objectKey}
|
||||
id={objectKey}
|
||||
style={dropZoneAreas[objectKey]}
|
||||
/>
|
||||
))
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,44 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
const Draggable = styled.div<{
|
||||
$isDraggable: boolean;
|
||||
$isDragging: boolean;
|
||||
}>`
|
||||
${({ $isDraggable }) => $isDraggable && css`
|
||||
& > video {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`}
|
||||
|
||||
${({ $isDragging }) => $isDragging && css`
|
||||
background-color: rgba(200, 200, 200, 0.5);
|
||||
`}
|
||||
`;
|
||||
|
||||
const ResizableWrapper = styled.div<{
|
||||
$horizontal: boolean;
|
||||
$vertical: boolean;
|
||||
}>`
|
||||
${({ $horizontal }) => $horizontal && css`
|
||||
& > div span div {
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, .3);
|
||||
}
|
||||
width: 100% !important;
|
||||
}
|
||||
`}
|
||||
|
||||
${({ $vertical }) => $vertical && css`
|
||||
& > div span div {
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, .3);
|
||||
}
|
||||
height: 100% !important;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export default {
|
||||
Draggable,
|
||||
ResizableWrapper,
|
||||
};
|
@ -3,8 +3,7 @@ import { ReactiveVar, makeVar, useReactiveVar } from '@apollo/client';
|
||||
function createUseLocalState<T>(initialValue: T):
|
||||
[
|
||||
() => [T, (value: T) => void], // hook that returns [state, setter]
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
(value: T | Function) => void, // setter
|
||||
(value: T | ((curr: T) => T)) => void, // setter
|
||||
ReactiveVar<T> // state
|
||||
] {
|
||||
const localState = makeVar(initialValue);
|
||||
@ -15,7 +14,7 @@ function createUseLocalState<T>(initialValue: T):
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
function changeLocalState(value: T | Function) {
|
||||
function changeLocalState(value: T | ((curr: T) => T)) {
|
||||
if (value instanceof Function) {
|
||||
return localState(value(localState()));
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ class Page {
|
||||
}
|
||||
await this.waitForSelector(e.webcamContainer, VIDEO_LOADING_WAIT_TIME);
|
||||
await this.waitForSelector(e.leaveVideo, VIDEO_LOADING_WAIT_TIME);
|
||||
await this.wasRemoved(e.webcamConnecting);
|
||||
await this.wasRemoved(e.webcamConnecting, VIDEO_LOADING_WAIT_TIME);
|
||||
}
|
||||
|
||||
getLocator(selector) {
|
||||
|
Loading…
Reference in New Issue
Block a user