@stream-io/video-react-sdk 0.4.26 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +297 -238
- package/README.md +5 -5
- package/dist/css/styles.css +952 -481
- package/dist/css/styles.css.map +1 -1
- package/dist/index.cjs.js +946 -639
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +939 -639
- package/dist/index.es.js.map +1 -1
- package/dist/src/components/Button/CompositeButton.d.ts +9 -11
- package/dist/src/components/Button/index.d.ts +0 -1
- package/dist/src/components/CallControls/CallStatsButton.d.ts +3 -0
- package/dist/src/components/CallControls/CancelCallButton.d.ts +1 -0
- package/dist/src/components/CallControls/ReactionsButton.d.ts +2 -1
- package/dist/src/components/CallControls/RecordCallButton.d.ts +4 -1
- package/dist/src/components/CallControls/ToggleAudioButton.d.ts +3 -9
- package/dist/src/components/CallControls/ToggleAudioOutputButton.d.ts +2 -5
- package/dist/src/components/CallControls/ToggleVideoButton.d.ts +3 -9
- package/dist/src/components/CallParticipantsList/CallParticipantListHeader.d.ts +3 -1
- package/dist/src/components/CallParticipantsList/CallParticipantListingItem.d.ts +0 -5
- package/dist/src/components/CallStats/CallStats.d.ts +25 -2
- package/dist/src/components/DeviceSettings/DeviceSelector.d.ts +6 -1
- package/dist/src/components/DeviceSettings/DeviceSelectorAudio.d.ts +4 -2
- package/dist/src/components/DeviceSettings/DeviceSelectorVideo.d.ts +2 -1
- package/dist/src/components/DeviceSettings/DeviceSettings.d.ts +5 -1
- package/dist/src/components/DropdownSelect/DropdownSelect.d.ts +14 -0
- package/dist/src/components/DropdownSelect/index.d.ts +1 -0
- package/dist/src/components/Icon/Icon.d.ts +2 -1
- package/dist/src/components/Menu/GenericMenu.d.ts +4 -2
- package/dist/src/components/Menu/MenuToggle.d.ts +15 -2
- package/dist/src/components/Notification/Notification.d.ts +1 -0
- package/dist/src/components/Notification/RecordingInProgressNotification.d.ts +5 -0
- package/dist/src/components/Notification/SpeakingWhileMutedNotification.d.ts +3 -1
- package/dist/src/components/Notification/index.d.ts +1 -0
- package/dist/src/components/index.d.ts +2 -0
- package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.d.ts +7 -1
- package/dist/src/core/components/ParticipantView/ParticipantActionsContextMenu.d.ts +1 -0
- package/dist/src/core/components/ParticipantView/ParticipantViewContext.d.ts +3 -3
- package/dist/src/core/components/ParticipantView/index.d.ts +1 -0
- package/dist/src/hooks/useFloatingUIPreset.d.ts +4 -1
- package/dist/src/translations/index.d.ts +9 -0
- package/package.json +7 -9
- package/src/components/Button/CompositeButton.tsx +78 -26
- package/src/components/Button/IconButton.tsx +22 -21
- package/src/components/Button/index.ts +0 -1
- package/src/components/CallControls/AcceptCallButton.tsx +1 -0
- package/src/components/CallControls/CallControls.tsx +2 -2
- package/src/components/CallControls/CallStatsButton.tsx +24 -7
- package/src/components/CallControls/CancelCallButton.tsx +102 -3
- package/src/components/CallControls/ReactionsButton.tsx +37 -17
- package/src/components/CallControls/RecordCallButton.tsx +131 -21
- package/src/components/CallControls/ScreenShareButton.tsx +29 -15
- package/src/components/CallControls/ToggleAudioButton.tsx +76 -31
- package/src/components/CallControls/ToggleAudioOutputButton.tsx +14 -10
- package/src/components/CallControls/ToggleVideoButton.tsx +83 -33
- package/src/components/CallParticipantsList/CallParticipantListHeader.tsx +9 -6
- package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +17 -281
- package/src/components/CallParticipantsList/CallParticipantsList.tsx +2 -32
- package/src/components/CallRecordingList/CallRecordingList.tsx +24 -6
- package/src/components/CallRecordingList/CallRecordingListHeader.tsx +6 -2
- package/src/components/CallRecordingList/CallRecordingListItem.tsx +18 -41
- package/src/components/CallStats/CallStats.tsx +167 -10
- package/src/components/CallStats/CallStatsLatencyChart.tsx +73 -44
- package/src/components/DeviceSettings/DeviceSelector.tsx +107 -12
- package/src/components/DeviceSettings/DeviceSelectorAudio.tsx +13 -5
- package/src/components/DeviceSettings/DeviceSelectorVideo.tsx +10 -4
- package/src/components/DeviceSettings/DeviceSettings.tsx +40 -28
- package/src/components/DropdownSelect/DropdownSelect.tsx +214 -0
- package/src/components/DropdownSelect/index.ts +1 -0
- package/src/components/Icon/Icon.tsx +7 -2
- package/src/components/Menu/GenericMenu.tsx +25 -3
- package/src/components/Menu/MenuToggle.tsx +79 -14
- package/src/components/Notification/Notification.tsx +8 -0
- package/src/components/Notification/PermissionNotification.tsx +2 -1
- package/src/components/Notification/RecordingInProgressNotification.tsx +40 -0
- package/src/components/Notification/SpeakingWhileMutedNotification.tsx +9 -1
- package/src/components/Notification/index.ts +1 -0
- package/src/components/Permissions/PermissionRequests.tsx +9 -21
- package/src/components/Search/hooks/useSearch.ts +5 -1
- package/src/components/index.ts +2 -0
- package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +71 -57
- package/src/core/components/ParticipantView/ParticipantActionsContextMenu.tsx +241 -0
- package/src/core/components/ParticipantView/ParticipantView.tsx +2 -2
- package/src/core/components/ParticipantView/ParticipantViewContext.tsx +3 -3
- package/src/core/components/ParticipantView/index.ts +1 -0
- package/src/core/components/Video/BaseVideo.tsx +1 -1
- package/src/core/components/Video/DefaultVideoPlaceholder.tsx +19 -5
- package/src/hooks/useFloatingUIPreset.ts +3 -2
- package/src/hooks/useRequestPermission.ts +2 -1
- package/src/translations/en.json +9 -0
- package/dist/src/components/Button/CopyToClipboardButton.d.ts +0 -27
- package/src/components/Button/CopyToClipboardButton.tsx +0 -129
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CallRecording } from '@stream-io/video-client';
|
|
2
|
+
import { useI18n } from '@stream-io/video-react-bindings';
|
|
2
3
|
import { IconButton } from '../Button';
|
|
3
4
|
|
|
4
5
|
export type CallRecordingListHeaderProps = {
|
|
@@ -12,13 +13,16 @@ export const CallRecordingListHeader = ({
|
|
|
12
13
|
callRecordings,
|
|
13
14
|
onRefresh,
|
|
14
15
|
}: CallRecordingListHeaderProps) => {
|
|
16
|
+
const { t } = useI18n();
|
|
15
17
|
return (
|
|
16
18
|
<div className="str-video__call-recording-list__header">
|
|
17
19
|
<div className="str-video__call-recording-list__title">
|
|
18
|
-
<span>Call Recordings</span>
|
|
20
|
+
<span>{t('Call Recordings')}</span>
|
|
19
21
|
{callRecordings.length ? <span>({callRecordings.length})</span> : null}
|
|
20
22
|
</div>
|
|
21
|
-
|
|
23
|
+
{onRefresh && (
|
|
24
|
+
<IconButton icon="refresh" title={t('Refresh')} onClick={onRefresh} />
|
|
25
|
+
)}
|
|
22
26
|
</div>
|
|
23
27
|
);
|
|
24
28
|
};
|
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import { ComponentProps, ForwardedRef, forwardRef } from 'react';
|
|
3
2
|
import { CallRecording } from '@stream-io/video-client';
|
|
4
|
-
import {
|
|
3
|
+
import { Icon } from '../Icon';
|
|
5
4
|
|
|
6
5
|
export type CallRecordingListItemProps = {
|
|
7
6
|
/** CallRecording object to represent */
|
|
8
7
|
recording: CallRecording;
|
|
9
8
|
};
|
|
9
|
+
|
|
10
|
+
const dateFormat = (date: string) => {
|
|
11
|
+
const format = new Date(date);
|
|
12
|
+
return format.toTimeString().split(' ')[0];
|
|
13
|
+
};
|
|
10
14
|
export const CallRecordingListItem = ({
|
|
11
15
|
recording,
|
|
12
16
|
}: CallRecordingListItemProps) => {
|
|
13
17
|
return (
|
|
14
|
-
<
|
|
15
|
-
<div className="str-video__call-recording-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
<li className="str-video__call-recording-list__item">
|
|
19
|
+
<div className="str-video__call-recording-list__table-cell str-video__call-recording-list__filename">
|
|
20
|
+
{recording.filename}
|
|
21
|
+
</div>
|
|
22
|
+
<div className="str-video__call-recording-list__table-cell str-video__call-recording-list__time">
|
|
23
|
+
{dateFormat(recording.start_time)}
|
|
19
24
|
</div>
|
|
20
|
-
<div className="str-video__call-recording-
|
|
25
|
+
<div className="str-video__call-recording-list__table-cell str-video__call-recording-list__time">
|
|
26
|
+
{dateFormat(recording.end_time)}
|
|
27
|
+
</div>
|
|
28
|
+
<div className="str-video__call-recording-list__table-cell str-video__call-recording-list__download">
|
|
21
29
|
<a
|
|
22
30
|
className={clsx(
|
|
23
31
|
'str-video__call-recording-list-item__action-button',
|
|
@@ -28,40 +36,9 @@ export const CallRecordingListItem = ({
|
|
|
28
36
|
download={recording.filename}
|
|
29
37
|
title="Download the recording"
|
|
30
38
|
>
|
|
31
|
-
<
|
|
32
|
-
className={clsx(
|
|
33
|
-
'str-video__call-recording-list-item__action-button-icon',
|
|
34
|
-
'str-video__download-button--icon',
|
|
35
|
-
)}
|
|
36
|
-
/>
|
|
39
|
+
<Icon icon="download" />
|
|
37
40
|
</a>
|
|
38
|
-
<CopyToClipboardButtonWithPopup
|
|
39
|
-
Button={CopyUrlButton}
|
|
40
|
-
copyValue={recording.url}
|
|
41
|
-
/>
|
|
42
41
|
</div>
|
|
43
|
-
</
|
|
42
|
+
</li>
|
|
44
43
|
);
|
|
45
44
|
};
|
|
46
|
-
const CopyUrlButton = forwardRef(
|
|
47
|
-
(props: ComponentProps<'button'>, ref: ForwardedRef<HTMLButtonElement>) => {
|
|
48
|
-
return (
|
|
49
|
-
<button
|
|
50
|
-
{...props}
|
|
51
|
-
className={clsx(
|
|
52
|
-
'str-video__call-recording-list-item__action-button',
|
|
53
|
-
'str-video__call-recording-list-item__action-button--copy-link',
|
|
54
|
-
)}
|
|
55
|
-
ref={ref}
|
|
56
|
-
title="Copy the recording link"
|
|
57
|
-
>
|
|
58
|
-
<span
|
|
59
|
-
className={clsx(
|
|
60
|
-
'str-video__call-recording-list-item__action-button-icon',
|
|
61
|
-
'str-video__copy-button--icon',
|
|
62
|
-
)}
|
|
63
|
-
/>
|
|
64
|
-
</button>
|
|
65
|
-
);
|
|
66
|
-
},
|
|
67
|
-
);
|
|
@@ -1,12 +1,52 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { ReactNode, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
2
3
|
import {
|
|
3
4
|
AggregatedStatsReport,
|
|
4
5
|
CallStatsReport,
|
|
5
6
|
} from '@stream-io/video-client';
|
|
6
|
-
import { useCallStateHooks } from '@stream-io/video-react-bindings';
|
|
7
|
+
import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings';
|
|
8
|
+
|
|
9
|
+
import { useFloating, useHover, useInteractions } from '@floating-ui/react';
|
|
10
|
+
|
|
7
11
|
import { CallStatsLatencyChart } from './CallStatsLatencyChart';
|
|
12
|
+
import { Icon } from '../Icon';
|
|
13
|
+
|
|
14
|
+
export enum Statuses {
|
|
15
|
+
GOOD = 'Good',
|
|
16
|
+
OK = 'Ok',
|
|
17
|
+
BAD = 'Bad',
|
|
18
|
+
}
|
|
19
|
+
export type Status = Statuses.GOOD | Statuses.OK | Statuses.BAD;
|
|
20
|
+
|
|
21
|
+
const statsStatus = ({
|
|
22
|
+
value,
|
|
23
|
+
lowBound,
|
|
24
|
+
highBound,
|
|
25
|
+
}: {
|
|
26
|
+
value: number;
|
|
27
|
+
lowBound: number;
|
|
28
|
+
highBound: number;
|
|
29
|
+
}): Status => {
|
|
30
|
+
if (value <= lowBound) {
|
|
31
|
+
return Statuses.GOOD;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (value >= lowBound && value <= highBound) {
|
|
35
|
+
return Statuses.OK;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (value >= highBound) {
|
|
39
|
+
return Statuses.BAD;
|
|
40
|
+
}
|
|
8
41
|
|
|
9
|
-
|
|
42
|
+
return Statuses.GOOD;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const CallStats = (props: {
|
|
46
|
+
latencyLowBound?: number;
|
|
47
|
+
latencyHighBound?: number;
|
|
48
|
+
}) => {
|
|
49
|
+
const { latencyLowBound = 75, latencyHighBound = 400 } = props;
|
|
10
50
|
const [latencyBuffer, setLatencyBuffer] = useState<
|
|
11
51
|
Array<{ x: number; y: number }>
|
|
12
52
|
>(() => {
|
|
@@ -14,6 +54,7 @@ export const CallStats = () => {
|
|
|
14
54
|
return Array.from({ length: 20 }, (_, i) => ({ x: now + i, y: 0 }));
|
|
15
55
|
});
|
|
16
56
|
|
|
57
|
+
const { t } = useI18n();
|
|
17
58
|
const [publishBitrate, setPublishBitrate] = useState('-');
|
|
18
59
|
const [subscribeBitrate, setSubscribeBitrate] = useState('-');
|
|
19
60
|
const previousStats = useRef<CallStatsReport>();
|
|
@@ -49,27 +90,72 @@ export const CallStats = () => {
|
|
|
49
90
|
previousStats.current = callStatsReport;
|
|
50
91
|
}, [callStatsReport]);
|
|
51
92
|
|
|
93
|
+
const latencyComparison = {
|
|
94
|
+
lowBound: latencyLowBound,
|
|
95
|
+
highBound: latencyHighBound,
|
|
96
|
+
value: callStatsReport?.publisherStats.averageRoundTripTimeInMs || 0,
|
|
97
|
+
};
|
|
98
|
+
|
|
52
99
|
return (
|
|
53
100
|
<div className="str-video__call-stats">
|
|
54
101
|
{callStatsReport && (
|
|
55
102
|
<>
|
|
56
|
-
<
|
|
57
|
-
|
|
103
|
+
<div className="str-video__call-stats__header">
|
|
104
|
+
<h3 className="str-video__call-stats__heading">
|
|
105
|
+
<Icon
|
|
106
|
+
className="str-video__call-stats__icon"
|
|
107
|
+
icon="call-latency"
|
|
108
|
+
/>
|
|
109
|
+
{t('Call Latency')}
|
|
110
|
+
</h3>
|
|
111
|
+
<p className="str-video__call-stats__description">
|
|
112
|
+
{t(
|
|
113
|
+
'Very high latency values may reduce call quality, cause lag, and make the call less enjoyable.',
|
|
114
|
+
)}
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="str-video__call-stats__latencychart">
|
|
119
|
+
<CallStatsLatencyChart values={latencyBuffer} />
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="str-video__call-stats__header">
|
|
123
|
+
<h3 className="str-video__call-stats__heading">
|
|
124
|
+
<Icon
|
|
125
|
+
className="str-video__call-stats__icon"
|
|
126
|
+
icon="network-quality"
|
|
127
|
+
/>
|
|
128
|
+
{t('Call performance')}
|
|
129
|
+
</h3>
|
|
130
|
+
<p className="str-video__call-stats__description">
|
|
131
|
+
{t(
|
|
132
|
+
'Very high latency values may reduce call quality, cause lag, and make the call less enjoyable.',
|
|
133
|
+
)}
|
|
134
|
+
</p>
|
|
135
|
+
</div>
|
|
58
136
|
|
|
59
|
-
<h3>Call performance</h3>
|
|
60
137
|
<div className="str-video__call-stats__card-container">
|
|
61
138
|
<StatCard label="Region" value={callStatsReport.datacenter} />
|
|
62
139
|
<StatCard
|
|
63
140
|
label="Latency"
|
|
64
141
|
value={`${callStatsReport.publisherStats.averageRoundTripTimeInMs} ms.`}
|
|
142
|
+
comparison={latencyComparison}
|
|
65
143
|
/>
|
|
66
144
|
<StatCard
|
|
67
145
|
label="Receive jitter"
|
|
68
146
|
value={`${callStatsReport.subscriberStats.averageJitterInMs} ms.`}
|
|
147
|
+
comparison={{
|
|
148
|
+
...latencyComparison,
|
|
149
|
+
value: callStatsReport.subscriberStats.averageJitterInMs,
|
|
150
|
+
}}
|
|
69
151
|
/>
|
|
70
152
|
<StatCard
|
|
71
153
|
label="Publish jitter"
|
|
72
154
|
value={`${callStatsReport.publisherStats.averageJitterInMs} ms.`}
|
|
155
|
+
comparison={{
|
|
156
|
+
...latencyComparison,
|
|
157
|
+
value: callStatsReport.publisherStats.averageJitterInMs,
|
|
158
|
+
}}
|
|
73
159
|
/>
|
|
74
160
|
<StatCard
|
|
75
161
|
label="Publish resolution"
|
|
@@ -96,12 +182,83 @@ export const CallStats = () => {
|
|
|
96
182
|
);
|
|
97
183
|
};
|
|
98
184
|
|
|
99
|
-
export const
|
|
100
|
-
const {
|
|
185
|
+
export const StatCardExplanation = (props: { description: string }) => {
|
|
186
|
+
const { description } = props;
|
|
187
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
188
|
+
|
|
189
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
190
|
+
open: isOpen,
|
|
191
|
+
onOpenChange: setIsOpen,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const hover = useHover(context);
|
|
195
|
+
|
|
196
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([hover]);
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<>
|
|
200
|
+
<div
|
|
201
|
+
className="str-video__call-explanation"
|
|
202
|
+
ref={refs.setReference}
|
|
203
|
+
{...getReferenceProps()}
|
|
204
|
+
>
|
|
205
|
+
<Icon className="str-video__call-explanation__icon" icon="info" />
|
|
206
|
+
</div>
|
|
207
|
+
{isOpen && (
|
|
208
|
+
<div
|
|
209
|
+
className="str-video__call-explanation__description"
|
|
210
|
+
ref={refs.setFloating}
|
|
211
|
+
style={floatingStyles}
|
|
212
|
+
{...getFloatingProps()}
|
|
213
|
+
>
|
|
214
|
+
{description}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
</>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const StatsTag = ({
|
|
222
|
+
children,
|
|
223
|
+
status = Statuses.GOOD,
|
|
224
|
+
}: {
|
|
225
|
+
children: ReactNode;
|
|
226
|
+
status: Statuses.GOOD | Statuses.OK | Statuses.BAD;
|
|
227
|
+
}) => {
|
|
228
|
+
return (
|
|
229
|
+
<div
|
|
230
|
+
className={clsx('str-video__call-stats__tag', {
|
|
231
|
+
'str-video__call-stats__tag--good': status === Statuses.GOOD,
|
|
232
|
+
'str-video__call-stats__tag--ok': status === Statuses.OK,
|
|
233
|
+
'str-video__call-stats__tag--bad': status === Statuses.BAD,
|
|
234
|
+
})}
|
|
235
|
+
>
|
|
236
|
+
<div className="str-video__call-stats__tag__text">{children}</div>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export const StatCard = (props: {
|
|
242
|
+
label: string;
|
|
243
|
+
value: string | ReactNode;
|
|
244
|
+
description?: string;
|
|
245
|
+
comparison?: { value: number; highBound: number; lowBound: number };
|
|
246
|
+
}) => {
|
|
247
|
+
const { label, value, description, comparison } = props;
|
|
248
|
+
|
|
249
|
+
const { t } = useI18n();
|
|
250
|
+
const status = comparison ? statsStatus(comparison) : undefined;
|
|
251
|
+
|
|
101
252
|
return (
|
|
102
253
|
<div className="str-video__call-stats__card">
|
|
103
|
-
<div className="str-video__call-
|
|
104
|
-
|
|
254
|
+
<div className="str-video__call-stats__card-content">
|
|
255
|
+
<div className="str-video__call-stats__card-label">
|
|
256
|
+
{label}
|
|
257
|
+
{description && <StatCardExplanation description={description} />}
|
|
258
|
+
</div>
|
|
259
|
+
<div className="str-video__call-stats__card-value">{value}</div>
|
|
260
|
+
</div>
|
|
261
|
+
{comparison && status && <StatsTag status={status}>{t(status)}</StatsTag>}
|
|
105
262
|
</div>
|
|
106
263
|
);
|
|
107
264
|
};
|
|
@@ -1,57 +1,86 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CategoryScale,
|
|
3
|
+
Chart as ChartJS,
|
|
4
|
+
ChartData,
|
|
5
|
+
ChartOptions,
|
|
6
|
+
LinearScale,
|
|
7
|
+
LineElement,
|
|
8
|
+
PointElement,
|
|
9
|
+
} from 'chart.js';
|
|
10
|
+
import { Line } from 'react-chartjs-2';
|
|
11
|
+
import { useMemo } from 'react';
|
|
12
|
+
|
|
13
|
+
ChartJS.register(CategoryScale, LinearScale, LineElement, PointElement);
|
|
2
14
|
|
|
3
15
|
export const CallStatsLatencyChart = (props: {
|
|
4
16
|
values: Array<{ x: number; y: number }>;
|
|
5
17
|
}) => {
|
|
6
18
|
const { values } = props;
|
|
7
19
|
let max = 0;
|
|
8
|
-
const data =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
const data: ChartData<'line'> = {
|
|
21
|
+
labels: values.map((point) => {
|
|
22
|
+
const date = new Date(point.x * 1000);
|
|
23
|
+
return `${date.getHours()}:${date.getMinutes()}`;
|
|
24
|
+
}),
|
|
25
|
+
datasets: [
|
|
26
|
+
{
|
|
27
|
+
data: values.map((point) => {
|
|
28
|
+
const { y } = point;
|
|
29
|
+
max = Math.max(max, y);
|
|
30
|
+
return point;
|
|
31
|
+
}),
|
|
32
|
+
borderColor: '#00e2a1',
|
|
33
|
+
backgroundColor: '#00e2a1',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const options = useMemo<ChartOptions<'line'>>(() => {
|
|
39
|
+
return {
|
|
40
|
+
maintainAspectRatio: false,
|
|
41
|
+
animation: {
|
|
42
|
+
duration: 0,
|
|
43
|
+
},
|
|
44
|
+
elements: {
|
|
45
|
+
line: {
|
|
46
|
+
borderWidth: 1,
|
|
47
|
+
},
|
|
48
|
+
point: {
|
|
49
|
+
radius: 2,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
scales: {
|
|
53
|
+
y: {
|
|
54
|
+
position: 'right',
|
|
55
|
+
stacked: true,
|
|
34
56
|
min: 0,
|
|
35
|
-
max: max
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
axis: {
|
|
40
|
-
ticks: {
|
|
41
|
-
text: {
|
|
42
|
-
fill: '#FCFCFD',
|
|
43
|
-
},
|
|
44
|
-
line: {
|
|
45
|
-
stroke: '#FCFCFD',
|
|
46
|
-
},
|
|
47
|
-
},
|
|
57
|
+
max: Math.max(180, Math.ceil((max + 10) / 10) * 10),
|
|
58
|
+
grid: {
|
|
59
|
+
display: true,
|
|
60
|
+
color: '#979ca0',
|
|
48
61
|
},
|
|
62
|
+
ticks: {
|
|
63
|
+
stepSize: 30,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
x: {
|
|
49
67
|
grid: {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
display: false,
|
|
69
|
+
},
|
|
70
|
+
ticks: {
|
|
71
|
+
display: false,
|
|
53
72
|
},
|
|
54
|
-
}
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}, [max]);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="str-video__call-stats-line-chart-container">
|
|
80
|
+
<Line
|
|
81
|
+
options={options}
|
|
82
|
+
data={data}
|
|
83
|
+
className="str-video__call-stats__latencychart"
|
|
55
84
|
/>
|
|
56
85
|
</div>
|
|
57
86
|
);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import { ChangeEventHandler } from 'react';
|
|
2
|
+
import { ChangeEventHandler, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
import { DropDownSelect, DropDownSelectOption } from '../DropdownSelect';
|
|
3
5
|
|
|
4
6
|
type DeviceSelectorOptionProps = {
|
|
5
7
|
id: string;
|
|
6
8
|
label: string;
|
|
7
|
-
name
|
|
9
|
+
name?: string;
|
|
8
10
|
selected?: boolean;
|
|
9
11
|
value: string;
|
|
10
12
|
disabled?: boolean;
|
|
@@ -44,19 +46,23 @@ const DeviceSelectorOption = ({
|
|
|
44
46
|
</label>
|
|
45
47
|
);
|
|
46
48
|
};
|
|
47
|
-
|
|
49
|
+
|
|
50
|
+
export type DeviceSelectorType = 'audioinput' | 'audiooutput' | 'videoinput';
|
|
51
|
+
|
|
52
|
+
const DeviceSelectorList = (props: {
|
|
48
53
|
devices: MediaDeviceInfo[];
|
|
54
|
+
type: DeviceSelectorType;
|
|
49
55
|
selectedDeviceId?: string;
|
|
50
|
-
title
|
|
56
|
+
title?: string;
|
|
51
57
|
onChange?: (deviceId: string) => void;
|
|
52
58
|
}) => {
|
|
53
59
|
const {
|
|
54
60
|
devices = [],
|
|
55
61
|
selectedDeviceId: selectedDeviceFromProps,
|
|
56
62
|
title,
|
|
63
|
+
type,
|
|
57
64
|
onChange,
|
|
58
65
|
} = props;
|
|
59
|
-
const inputGroupName = title.replace(' ', '-').toLowerCase();
|
|
60
66
|
|
|
61
67
|
// sometimes the browser (Chrome) will report the system-default device
|
|
62
68
|
// with an id of 'default'. In case when it doesn't, we'll select the first
|
|
@@ -71,14 +77,16 @@ export const DeviceSelector = (props: {
|
|
|
71
77
|
|
|
72
78
|
return (
|
|
73
79
|
<div className="str-video__device-settings__device-kind">
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
{title && (
|
|
81
|
+
<div className="str-video__device-settings__device-selector-title">
|
|
82
|
+
{title}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
77
85
|
{!devices.length ? (
|
|
78
86
|
<DeviceSelectorOption
|
|
79
|
-
id={`${
|
|
87
|
+
id={`${type}--default`}
|
|
80
88
|
label="Default"
|
|
81
|
-
name={
|
|
89
|
+
name={type}
|
|
82
90
|
defaultChecked
|
|
83
91
|
value="default"
|
|
84
92
|
/>
|
|
@@ -86,14 +94,14 @@ export const DeviceSelector = (props: {
|
|
|
86
94
|
devices.map((device) => {
|
|
87
95
|
return (
|
|
88
96
|
<DeviceSelectorOption
|
|
89
|
-
id={`${
|
|
97
|
+
id={`${type}--${device.deviceId}`}
|
|
90
98
|
value={device.deviceId}
|
|
91
99
|
label={device.label}
|
|
92
100
|
key={device.deviceId}
|
|
93
101
|
onChange={(e) => {
|
|
94
102
|
onChange?.(e.target.value);
|
|
95
103
|
}}
|
|
96
|
-
name={
|
|
104
|
+
name={type}
|
|
97
105
|
selected={
|
|
98
106
|
device.deviceId === selectedDeviceId || devices.length === 1
|
|
99
107
|
}
|
|
@@ -104,3 +112,90 @@ export const DeviceSelector = (props: {
|
|
|
104
112
|
</div>
|
|
105
113
|
);
|
|
106
114
|
};
|
|
115
|
+
|
|
116
|
+
const DeviceSelectorDropdown = (props: {
|
|
117
|
+
devices: MediaDeviceInfo[];
|
|
118
|
+
selectedDeviceId?: string;
|
|
119
|
+
title?: string;
|
|
120
|
+
onChange?: (deviceId: string) => void;
|
|
121
|
+
visualType?: 'list' | 'dropdown';
|
|
122
|
+
icon: string;
|
|
123
|
+
placeholder?: string;
|
|
124
|
+
}) => {
|
|
125
|
+
const {
|
|
126
|
+
devices = [],
|
|
127
|
+
selectedDeviceId: selectedDeviceFromProps,
|
|
128
|
+
title,
|
|
129
|
+
onChange,
|
|
130
|
+
icon,
|
|
131
|
+
} = props;
|
|
132
|
+
|
|
133
|
+
// sometimes the browser (Chrome) will report the system-default device
|
|
134
|
+
// with an id of 'default'. In case when it doesn't, we'll select the first
|
|
135
|
+
// available device.
|
|
136
|
+
let selectedDeviceId = selectedDeviceFromProps;
|
|
137
|
+
if (
|
|
138
|
+
devices.length > 0 &&
|
|
139
|
+
!devices.find((d) => d.deviceId === selectedDeviceId)
|
|
140
|
+
) {
|
|
141
|
+
selectedDeviceId = devices[0].deviceId;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const selectedIndex = devices.findIndex(
|
|
145
|
+
(d) => d.deviceId === selectedDeviceId,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const handleSelect = useCallback(
|
|
149
|
+
(index: number) => {
|
|
150
|
+
onChange?.(devices[index].deviceId);
|
|
151
|
+
},
|
|
152
|
+
[devices, onChange],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div className="str-video__device-settings__device-kind">
|
|
157
|
+
<div className="str-video__device-settings__device-selector-title">
|
|
158
|
+
{title}
|
|
159
|
+
</div>
|
|
160
|
+
<DropDownSelect
|
|
161
|
+
icon={icon}
|
|
162
|
+
defaultSelectedIndex={selectedIndex}
|
|
163
|
+
defaultSelectedLabel={devices[selectedIndex]?.label}
|
|
164
|
+
handleSelect={handleSelect}
|
|
165
|
+
>
|
|
166
|
+
{devices.map((device) => {
|
|
167
|
+
return (
|
|
168
|
+
<DropDownSelectOption
|
|
169
|
+
key={device.deviceId}
|
|
170
|
+
icon={icon}
|
|
171
|
+
label={device.label}
|
|
172
|
+
selected={
|
|
173
|
+
device.deviceId === selectedDeviceId || devices.length === 1
|
|
174
|
+
}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
})}
|
|
178
|
+
</DropDownSelect>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const DeviceSelector = (props: {
|
|
184
|
+
devices: MediaDeviceInfo[];
|
|
185
|
+
icon: string;
|
|
186
|
+
type: DeviceSelectorType;
|
|
187
|
+
selectedDeviceId?: string;
|
|
188
|
+
title?: string;
|
|
189
|
+
onChange?: (deviceId: string) => void;
|
|
190
|
+
visualType?: 'list' | 'dropdown';
|
|
191
|
+
placeholder?: string;
|
|
192
|
+
}) => {
|
|
193
|
+
const { visualType = 'list', icon, placeholder, ...rest } = props;
|
|
194
|
+
|
|
195
|
+
if (visualType === 'list') {
|
|
196
|
+
return <DeviceSelectorList {...rest} />;
|
|
197
|
+
}
|
|
198
|
+
return (
|
|
199
|
+
<DeviceSelectorDropdown {...rest} icon={icon} placeholder={placeholder} />
|
|
200
|
+
);
|
|
201
|
+
};
|