@stream-io/video-react-sdk 0.4.25 → 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.
Files changed (91) hide show
  1. package/CHANGELOG.md +301 -236
  2. package/README.md +5 -5
  3. package/dist/css/styles.css +952 -481
  4. package/dist/css/styles.css.map +1 -1
  5. package/dist/index.cjs.js +946 -639
  6. package/dist/index.cjs.js.map +1 -1
  7. package/dist/index.es.js +939 -639
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/src/components/Button/CompositeButton.d.ts +9 -11
  10. package/dist/src/components/Button/index.d.ts +0 -1
  11. package/dist/src/components/CallControls/CallStatsButton.d.ts +3 -0
  12. package/dist/src/components/CallControls/CancelCallButton.d.ts +1 -0
  13. package/dist/src/components/CallControls/ReactionsButton.d.ts +2 -1
  14. package/dist/src/components/CallControls/RecordCallButton.d.ts +4 -1
  15. package/dist/src/components/CallControls/ToggleAudioButton.d.ts +3 -9
  16. package/dist/src/components/CallControls/ToggleAudioOutputButton.d.ts +2 -5
  17. package/dist/src/components/CallControls/ToggleVideoButton.d.ts +3 -9
  18. package/dist/src/components/CallParticipantsList/CallParticipantListHeader.d.ts +3 -1
  19. package/dist/src/components/CallParticipantsList/CallParticipantListingItem.d.ts +0 -5
  20. package/dist/src/components/CallStats/CallStats.d.ts +25 -2
  21. package/dist/src/components/DeviceSettings/DeviceSelector.d.ts +6 -1
  22. package/dist/src/components/DeviceSettings/DeviceSelectorAudio.d.ts +4 -2
  23. package/dist/src/components/DeviceSettings/DeviceSelectorVideo.d.ts +2 -1
  24. package/dist/src/components/DeviceSettings/DeviceSettings.d.ts +5 -1
  25. package/dist/src/components/DropdownSelect/DropdownSelect.d.ts +14 -0
  26. package/dist/src/components/DropdownSelect/index.d.ts +1 -0
  27. package/dist/src/components/Icon/Icon.d.ts +2 -1
  28. package/dist/src/components/Menu/GenericMenu.d.ts +4 -2
  29. package/dist/src/components/Menu/MenuToggle.d.ts +15 -2
  30. package/dist/src/components/Notification/Notification.d.ts +1 -0
  31. package/dist/src/components/Notification/RecordingInProgressNotification.d.ts +5 -0
  32. package/dist/src/components/Notification/SpeakingWhileMutedNotification.d.ts +3 -1
  33. package/dist/src/components/Notification/index.d.ts +1 -0
  34. package/dist/src/components/index.d.ts +2 -0
  35. package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.d.ts +7 -1
  36. package/dist/src/core/components/ParticipantView/ParticipantActionsContextMenu.d.ts +1 -0
  37. package/dist/src/core/components/ParticipantView/ParticipantViewContext.d.ts +3 -3
  38. package/dist/src/core/components/ParticipantView/index.d.ts +1 -0
  39. package/dist/src/hooks/useFloatingUIPreset.d.ts +4 -1
  40. package/dist/src/translations/index.d.ts +9 -0
  41. package/package.json +7 -9
  42. package/src/components/Button/CompositeButton.tsx +78 -26
  43. package/src/components/Button/IconButton.tsx +22 -21
  44. package/src/components/Button/index.ts +0 -1
  45. package/src/components/CallControls/AcceptCallButton.tsx +1 -0
  46. package/src/components/CallControls/CallControls.tsx +2 -2
  47. package/src/components/CallControls/CallStatsButton.tsx +24 -7
  48. package/src/components/CallControls/CancelCallButton.tsx +102 -3
  49. package/src/components/CallControls/ReactionsButton.tsx +37 -17
  50. package/src/components/CallControls/RecordCallButton.tsx +131 -21
  51. package/src/components/CallControls/ScreenShareButton.tsx +29 -15
  52. package/src/components/CallControls/ToggleAudioButton.tsx +76 -31
  53. package/src/components/CallControls/ToggleAudioOutputButton.tsx +14 -10
  54. package/src/components/CallControls/ToggleVideoButton.tsx +83 -33
  55. package/src/components/CallParticipantsList/CallParticipantListHeader.tsx +9 -6
  56. package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +17 -281
  57. package/src/components/CallParticipantsList/CallParticipantsList.tsx +2 -32
  58. package/src/components/CallRecordingList/CallRecordingList.tsx +24 -6
  59. package/src/components/CallRecordingList/CallRecordingListHeader.tsx +6 -2
  60. package/src/components/CallRecordingList/CallRecordingListItem.tsx +18 -41
  61. package/src/components/CallStats/CallStats.tsx +167 -10
  62. package/src/components/CallStats/CallStatsLatencyChart.tsx +73 -44
  63. package/src/components/DeviceSettings/DeviceSelector.tsx +107 -12
  64. package/src/components/DeviceSettings/DeviceSelectorAudio.tsx +13 -5
  65. package/src/components/DeviceSettings/DeviceSelectorVideo.tsx +10 -4
  66. package/src/components/DeviceSettings/DeviceSettings.tsx +40 -28
  67. package/src/components/DropdownSelect/DropdownSelect.tsx +214 -0
  68. package/src/components/DropdownSelect/index.ts +1 -0
  69. package/src/components/Icon/Icon.tsx +7 -2
  70. package/src/components/Menu/GenericMenu.tsx +25 -3
  71. package/src/components/Menu/MenuToggle.tsx +79 -14
  72. package/src/components/Notification/Notification.tsx +8 -0
  73. package/src/components/Notification/PermissionNotification.tsx +2 -1
  74. package/src/components/Notification/RecordingInProgressNotification.tsx +40 -0
  75. package/src/components/Notification/SpeakingWhileMutedNotification.tsx +9 -1
  76. package/src/components/Notification/index.ts +1 -0
  77. package/src/components/Permissions/PermissionRequests.tsx +9 -21
  78. package/src/components/Search/hooks/useSearch.ts +5 -1
  79. package/src/components/index.ts +2 -0
  80. package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +71 -57
  81. package/src/core/components/ParticipantView/ParticipantActionsContextMenu.tsx +241 -0
  82. package/src/core/components/ParticipantView/ParticipantView.tsx +2 -2
  83. package/src/core/components/ParticipantView/ParticipantViewContext.tsx +3 -3
  84. package/src/core/components/ParticipantView/index.ts +1 -0
  85. package/src/core/components/Video/BaseVideo.tsx +1 -1
  86. package/src/core/components/Video/DefaultVideoPlaceholder.tsx +19 -5
  87. package/src/hooks/useFloatingUIPreset.ts +3 -2
  88. package/src/hooks/useRequestPermission.ts +2 -1
  89. package/src/translations/en.json +9 -0
  90. package/dist/src/components/Button/CopyToClipboardButton.d.ts +0 -27
  91. 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
- <IconButton icon="refresh" title="Refresh" onClick={onRefresh} />
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 { CopyToClipboardButtonWithPopup } from '../Button';
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
- <div className="str-video__call-recording-list-item">
15
- <div className="str-video__call-recording-list-item__info">
16
- <div className="str-video__call-recording-list-item__created">
17
- {new Date(recording.end_time).toLocaleString()}
18
- </div>
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-list-item__actions">
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
- <span
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
- </div>
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
- export const CallStats = () => {
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
- <h3>Call Latency</h3>
57
- <CallStatsLatencyChart values={latencyBuffer} />
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 StatCard = (props: { label: string; value: string }) => {
100
- const { label, value } = props;
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-stats__card_label">{label}</div>
104
- <div className="str-video__call-stats__card_value">{value}</div>
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 { ResponsiveLine } from '@nivo/line';
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 = values.map((point) => {
9
- const { y } = point;
10
- max = Math.max(max, y);
11
- return point;
12
- });
13
- return (
14
- <div className="str-video__call-stats-line-chart-container">
15
- <ResponsiveLine
16
- colors={{ scheme: 'blues' }}
17
- data={[
18
- {
19
- id: 'Latency',
20
- data: data,
21
- },
22
- ]}
23
- animate={false}
24
- margin={{ top: 10, right: 5, bottom: 5, left: 30 }}
25
- enablePoints
26
- enableGridX={false}
27
- enableGridY
28
- enableSlices="x"
29
- isInteractive
30
- useMesh={false}
31
- xScale={{ type: 'point' }}
32
- yScale={{
33
- type: 'linear',
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 < 220 ? 220 : max + 30,
36
- nice: true,
37
- }}
38
- theme={{
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
- line: {
51
- strokeWidth: 0.1,
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: string;
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
- export const DeviceSelector = (props: {
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: string;
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
- <div className="str-video__device-settings__device-selector-title">
75
- {title}
76
- </div>
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={`${inputGroupName}--default`}
87
+ id={`${type}--default`}
80
88
  label="Default"
81
- name={inputGroupName}
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={`${inputGroupName}--${device.deviceId}`}
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={inputGroupName}
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
+ };