@whereby.com/browser-sdk 1.7.1 → 2.0.0-alpha
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/.eslintrc +4 -2
- package/.storybook/main.cjs +13 -2
- package/README.md +60 -7
- package/jest.config.js +2 -0
- package/package.json +38 -12
- package/rollup.config.js +37 -13
- package/src/lib/RoomConnection.ts +516 -0
- package/src/lib/RoomParticipant.ts +77 -0
- package/src/lib/__tests__/{index.unit.js → embed.unit.ts} +4 -8
- package/src/lib/api/ApiClient.ts +111 -0
- package/src/lib/api/Credentials.ts +45 -0
- package/src/lib/api/HttpClient.ts +95 -0
- package/src/lib/api/MultipartHttpClient.ts +53 -0
- package/src/lib/api/OrganizationApiClient.ts +64 -0
- package/src/lib/api/Response.ts +34 -0
- package/src/lib/api/credentialsService/index.ts +159 -0
- package/src/lib/api/credentialsService/test/index.spec.ts +181 -0
- package/src/lib/api/deviceService/index.ts +42 -0
- package/src/lib/api/deviceService/tests/index.spec.ts +74 -0
- package/src/lib/api/extractUtils.ts +160 -0
- package/src/lib/api/index.ts +8 -0
- package/src/lib/api/localStorageWrapper/index.ts +15 -0
- package/src/lib/api/models/Account.ts +48 -0
- package/src/lib/api/models/Meeting.ts +42 -0
- package/src/lib/api/models/Organization.ts +186 -0
- package/src/lib/api/models/Room.ts +44 -0
- package/src/lib/api/models/account/EmbeddedFreeTierStatus.ts +34 -0
- package/src/lib/api/models/tests/Account.spec.ts +128 -0
- package/src/lib/api/models/tests/Organization.spec.ts +161 -0
- package/src/lib/api/models/tests/Room.spec.ts +74 -0
- package/src/lib/api/modules/AbstractStore.ts +18 -0
- package/src/lib/api/modules/ChromeStorageStore.ts +44 -0
- package/src/lib/api/modules/LocalStorageStore.ts +57 -0
- package/src/lib/api/modules/tests/ChromeStorageStore.spec.ts +67 -0
- package/src/lib/api/modules/tests/LocalStorageStore.spec.ts +79 -0
- package/src/lib/api/modules/tests/__mocks__/storage.ts +24 -0
- package/src/lib/api/organizationService/index.ts +284 -0
- package/src/lib/api/organizationService/tests/index.spec.ts +781 -0
- package/src/lib/api/organizationServiceCache/index.ts +28 -0
- package/src/lib/api/organizationServiceCache/tests/index.spec.ts +101 -0
- package/src/lib/api/parameterAssertUtils.ts +166 -0
- package/src/lib/api/roomService/index.ts +310 -0
- package/src/lib/api/roomService/tests/index.spec.ts +668 -0
- package/src/lib/api/test/ApiClient.spec.ts +139 -0
- package/src/lib/api/test/HttpClient.spec.ts +120 -0
- package/src/lib/api/test/MultipartHttpClient.spec.ts +145 -0
- package/src/lib/api/test/OrganizationApiClient.spec.ts +132 -0
- package/src/lib/api/test/extractUtils.spec.ts +357 -0
- package/src/lib/api/test/helpers.ts +41 -0
- package/src/lib/api/test/parameterAssertUtils.spec.ts +265 -0
- package/src/lib/api/types.ts +6 -0
- package/src/lib/{index.js → embed.ts} +53 -26
- package/src/lib/index.ts +3 -0
- package/src/lib/react/VideoElement.tsx +16 -0
- package/src/lib/react/index.ts +3 -0
- package/src/lib/react/useLocalMedia.ts +25 -0
- package/src/lib/react/useRoomConnection.ts +92 -0
- package/src/lib/reducer.ts +142 -0
- package/src/stories/custom-ui.stories.tsx +133 -0
- package/src/stories/prebuilt-ui.stories.tsx +131 -0
- package/src/stories/styles.css +74 -0
- package/src/types.d.ts +175 -0
- package/tsconfig.json +30 -0
- package/src/index.stories.js +0 -101
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
import { define, ref } from "heresy";
|
|
2
2
|
|
|
3
|
+
interface WherebyEmbedAttributes {
|
|
4
|
+
audio: string;
|
|
5
|
+
avatarUrl: string;
|
|
6
|
+
background: string;
|
|
7
|
+
cameraAccess: string;
|
|
8
|
+
chat: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
emptyRoomInvitation: string;
|
|
11
|
+
floatSelf: string;
|
|
12
|
+
help: string;
|
|
13
|
+
leaveButton: string;
|
|
14
|
+
logo: string;
|
|
15
|
+
people: string;
|
|
16
|
+
precallReview: string;
|
|
17
|
+
recording: string;
|
|
18
|
+
screenshare: string;
|
|
19
|
+
video: string;
|
|
20
|
+
virtualBackgroundUrl: string;
|
|
21
|
+
room: string;
|
|
22
|
+
style: { [key: string]: string };
|
|
23
|
+
}
|
|
24
|
+
declare global {
|
|
25
|
+
namespace JSX {
|
|
26
|
+
interface IntrinsicElements {
|
|
27
|
+
["whereby-embed"]: Partial<WherebyEmbedAttributes>;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
3
32
|
const boolAttrs = [
|
|
4
33
|
"audio",
|
|
5
34
|
"background",
|
|
35
|
+
"cameraaccess",
|
|
6
36
|
"chat",
|
|
7
37
|
"people",
|
|
8
38
|
"embed",
|
|
@@ -31,10 +61,10 @@ define("WherebyEmbed", {
|
|
|
31
61
|
this.iframe = ref();
|
|
32
62
|
},
|
|
33
63
|
onconnected() {
|
|
34
|
-
window.addEventListener("message", this);
|
|
64
|
+
window.addEventListener("message", this.onmessage);
|
|
35
65
|
},
|
|
36
66
|
ondisconnected() {
|
|
37
|
-
window.removeEventListener("message", this);
|
|
67
|
+
window.removeEventListener("message", this.onmessage);
|
|
38
68
|
},
|
|
39
69
|
observedAttributes: [
|
|
40
70
|
"displayName",
|
|
@@ -48,11 +78,11 @@ define("WherebyEmbed", {
|
|
|
48
78
|
"avatarUrl",
|
|
49
79
|
...boolAttrs,
|
|
50
80
|
].map((a) => a.toLowerCase()),
|
|
51
|
-
onattributechanged({ attributeName, oldValue }) {
|
|
81
|
+
onattributechanged({ attributeName, oldValue }: { attributeName: string; oldValue: string | boolean }) {
|
|
52
82
|
if (["room", "subdomain"].includes(attributeName) && oldValue == null) return;
|
|
53
83
|
this.render();
|
|
54
84
|
},
|
|
55
|
-
style(self) {
|
|
85
|
+
style(self: string) {
|
|
56
86
|
return `
|
|
57
87
|
${self} {
|
|
58
88
|
display: block;
|
|
@@ -66,7 +96,7 @@ define("WherebyEmbed", {
|
|
|
66
96
|
},
|
|
67
97
|
|
|
68
98
|
// Commands
|
|
69
|
-
_postCommand(command, args = []) {
|
|
99
|
+
_postCommand(command: string, args = []) {
|
|
70
100
|
if (this.iframe.current) {
|
|
71
101
|
const url = new URL(this.room, `https://${this.subdomain}.whereby.com`);
|
|
72
102
|
this.iframe.current.contentWindow.postMessage({ command, args }, url.origin);
|
|
@@ -78,17 +108,16 @@ define("WherebyEmbed", {
|
|
|
78
108
|
stopRecording() {
|
|
79
109
|
this._postCommand("stop_recording");
|
|
80
110
|
},
|
|
81
|
-
toggleCamera(enabled) {
|
|
111
|
+
toggleCamera(enabled: boolean) {
|
|
82
112
|
this._postCommand("toggle_camera", [enabled]);
|
|
83
113
|
},
|
|
84
|
-
toggleMicrophone(enabled) {
|
|
114
|
+
toggleMicrophone(enabled: boolean) {
|
|
85
115
|
this._postCommand("toggle_microphone", [enabled]);
|
|
86
116
|
},
|
|
87
|
-
toggleScreenshare(enabled) {
|
|
117
|
+
toggleScreenshare(enabled: boolean) {
|
|
88
118
|
this._postCommand("toggle_screenshare", [enabled]);
|
|
89
119
|
},
|
|
90
|
-
|
|
91
|
-
onmessage({ origin, data }) {
|
|
120
|
+
onmessage({ origin, data }: { origin: string; data: { type: string; payload: string } }) {
|
|
92
121
|
const url = new URL(this.room, `https://${this.subdomain}.whereby.com`);
|
|
93
122
|
if (origin !== url.origin) return;
|
|
94
123
|
const { type, payload: detail } = data;
|
|
@@ -105,9 +134,9 @@ define("WherebyEmbed", {
|
|
|
105
134
|
groups,
|
|
106
135
|
virtualbackgroundurl: virtualBackgroundUrl,
|
|
107
136
|
} = this;
|
|
108
|
-
if (!room) return this.html`Whereby: Missing room
|
|
137
|
+
if (!room) return this.html`Whereby: Missing room attribute.`;
|
|
109
138
|
// Get subdomain from room URL, or use it specified
|
|
110
|
-
|
|
139
|
+
const m = /https:\/\/([^.]+)\.whereby.com\/.+/.exec(room);
|
|
111
140
|
const subdomain = (m && m[1]) || this.subdomain;
|
|
112
141
|
if (!subdomain) return this.html`Whereby: Missing subdomain attr.`;
|
|
113
142
|
const url = new URL(room, `https://${subdomain}.whereby.com`);
|
|
@@ -116,11 +145,11 @@ define("WherebyEmbed", {
|
|
|
116
145
|
we: "__SDK_VERSION__",
|
|
117
146
|
iframeSource: subdomain,
|
|
118
147
|
...(displayName && { displayName }),
|
|
119
|
-
...(lang && { lang
|
|
120
|
-
...(metadata && { metadata
|
|
121
|
-
...(groups && { groups
|
|
122
|
-
...(virtualBackgroundUrl && { virtualBackgroundUrl
|
|
123
|
-
...(avatarUrl && { avatarUrl
|
|
148
|
+
...(lang && { lang }),
|
|
149
|
+
...(metadata && { metadata }),
|
|
150
|
+
...(groups && { groups }),
|
|
151
|
+
...(virtualBackgroundUrl && { virtualBackgroundUrl }),
|
|
152
|
+
...(avatarUrl && { avatarUrl }),
|
|
124
153
|
// the original ?embed name was confusing, so we give minimal
|
|
125
154
|
...(minimal != null && { embed: minimal }),
|
|
126
155
|
...boolAttrs.reduce(
|
|
@@ -129,17 +158,15 @@ define("WherebyEmbed", {
|
|
|
129
158
|
{}
|
|
130
159
|
),
|
|
131
160
|
}).forEach(([k, v]) => {
|
|
132
|
-
if (!url.searchParams.has(k)) {
|
|
161
|
+
if (!url.searchParams.has(k) && typeof v === "string") {
|
|
133
162
|
url.searchParams.set(k, v);
|
|
134
163
|
}
|
|
135
164
|
});
|
|
136
|
-
this.html`
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
return this.html`
|
|
166
|
+
<iframe
|
|
167
|
+
ref=${this.iframe}
|
|
168
|
+
src=${url}
|
|
169
|
+
allow="autoplay; camera; microphone; fullscreen; speaker; display-capture" />
|
|
170
|
+
`;
|
|
142
171
|
},
|
|
143
172
|
});
|
|
144
|
-
|
|
145
|
-
export default { sdkVersion: "__SDK_VERSION__" };
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React, { useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
interface VideoElProps {
|
|
4
|
+
stream: MediaStream;
|
|
5
|
+
style?: React.CSSProperties;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default ({ stream, style }: VideoElProps) => {
|
|
9
|
+
const videoEl = useCallback<(node: HTMLVideoElement) => void>((node) => {
|
|
10
|
+
if (node !== null && node.srcObject !== stream) {
|
|
11
|
+
node.srcObject = stream;
|
|
12
|
+
}
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
return <video ref={videoEl} autoPlay playsInline style={style} />;
|
|
16
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export default function useLocalMedia() {
|
|
4
|
+
const [localStream, setLocalStream] = useState<MediaStream | undefined>(undefined);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const getLocalStream = async () => {
|
|
8
|
+
if (!localStream) {
|
|
9
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
|
10
|
+
setLocalStream(stream);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
getLocalStream();
|
|
15
|
+
|
|
16
|
+
// Stop tracks on unmount
|
|
17
|
+
return () => {
|
|
18
|
+
localStream?.getTracks().forEach((t) => {
|
|
19
|
+
t.stop();
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
}, [localStream]);
|
|
23
|
+
|
|
24
|
+
return [localStream];
|
|
25
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useEffect, useReducer, useState } from "react";
|
|
2
|
+
import RoomConnection, { RoomConnectionOptions } from "../RoomConnection";
|
|
3
|
+
import reducer, { RoomState } from "../reducer";
|
|
4
|
+
import VideoElement from "./VideoElement";
|
|
5
|
+
|
|
6
|
+
interface RoomConnectionActions {
|
|
7
|
+
toggleCamera(enabled?: boolean): void;
|
|
8
|
+
toggleMicrophone(enabled?: boolean): void;
|
|
9
|
+
setDisplayName(displayName: string): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface RoomConnectionComponents {
|
|
13
|
+
VideoView: typeof VideoElement;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function useRoomConnection(
|
|
17
|
+
roomUrl: string,
|
|
18
|
+
roomConnectionOptions: RoomConnectionOptions
|
|
19
|
+
): [state: RoomState, actions: RoomConnectionActions, components: RoomConnectionComponents] {
|
|
20
|
+
const [roomConnection, setRoomConnection] = useState<RoomConnection | null>(null);
|
|
21
|
+
const [state, dispatch] = useReducer(reducer, { remoteParticipants: [] });
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setRoomConnection(new RoomConnection(roomUrl, roomConnectionOptions));
|
|
25
|
+
}, [roomUrl]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!roomConnection) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
roomConnection.addEventListener("participant_audio_enabled", (e) => {
|
|
33
|
+
const { participantId, isAudioEnabled } = e.detail;
|
|
34
|
+
dispatch({ type: "PARTICIPANT_AUDIO_ENABLED", payload: { participantId, isAudioEnabled } });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
roomConnection.addEventListener("participant_joined", (e) => {
|
|
38
|
+
const { remoteParticipant } = e.detail;
|
|
39
|
+
dispatch({ type: "PARTICIPANT_JOINED", payload: { paritipant: remoteParticipant } });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
roomConnection.addEventListener("participant_left", (e) => {
|
|
43
|
+
const { participantId } = e.detail;
|
|
44
|
+
dispatch({ type: "PARTICIPANT_LEFT", payload: { participantId } });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
roomConnection.addEventListener("participant_stream_added", (e) => {
|
|
48
|
+
const { participantId, stream } = e.detail;
|
|
49
|
+
dispatch({ type: "PARTICIPANT_STREAM_ADDED", payload: { participantId, stream } });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
roomConnection.addEventListener("room_joined", (e) => {
|
|
53
|
+
const { localParticipant, remoteParticipants } = e.detail;
|
|
54
|
+
dispatch({ type: "ROOM_JOINED", payload: { localParticipant, remoteParticipants } });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
roomConnection.addEventListener("participant_video_enabled", (e) => {
|
|
58
|
+
const { participantId, isVideoEnabled } = e.detail;
|
|
59
|
+
dispatch({ type: "PARTICIPANT_VIDEO_ENABLED", payload: { participantId, isVideoEnabled } });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
roomConnection.addEventListener("participant_metadata_changed", (e) => {
|
|
63
|
+
const { participantId, displayName } = e.detail;
|
|
64
|
+
dispatch({ type: "PARTICIPANT_METADATA_CHANGED", payload: { participantId, displayName } });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
roomConnection.join();
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
roomConnection.leave();
|
|
71
|
+
};
|
|
72
|
+
}, [roomConnection]);
|
|
73
|
+
|
|
74
|
+
return [
|
|
75
|
+
state,
|
|
76
|
+
{
|
|
77
|
+
toggleCamera: (enabled) => {
|
|
78
|
+
roomConnection?.toggleCamera(enabled);
|
|
79
|
+
},
|
|
80
|
+
toggleMicrophone: (enabled) => {
|
|
81
|
+
roomConnection?.toggleMicrophone(enabled);
|
|
82
|
+
},
|
|
83
|
+
setDisplayName: (displayName) => {
|
|
84
|
+
roomConnection?.setDisplayName(displayName);
|
|
85
|
+
dispatch({ type: "LOCAL_CLIENT_DISPLAY_NAME_CHANGED", payload: { displayName } });
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
VideoView: VideoElement,
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { LocalParticipant, RemoteParticipant } from "./RoomParticipant";
|
|
2
|
+
|
|
3
|
+
type RemoteParticipantState = Omit<RemoteParticipant, "updateStreamState">;
|
|
4
|
+
|
|
5
|
+
export interface RoomState {
|
|
6
|
+
localParticipant?: LocalParticipant;
|
|
7
|
+
roomConnectionStatus?: "connecting" | "connected" | "disconnected";
|
|
8
|
+
remoteParticipants: RemoteParticipantState[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type Action =
|
|
12
|
+
| {
|
|
13
|
+
type: "ROOM_JOINED";
|
|
14
|
+
payload: {
|
|
15
|
+
localParticipant: LocalParticipant;
|
|
16
|
+
remoteParticipants: RemoteParticipantState[];
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
type: "PARTICIPANT_AUDIO_ENABLED";
|
|
21
|
+
payload: {
|
|
22
|
+
participantId: string;
|
|
23
|
+
isAudioEnabled: boolean;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
type: "PARTICIPANT_JOINED";
|
|
28
|
+
payload: {
|
|
29
|
+
paritipant: RemoteParticipantState;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
type: "PARTICIPANT_LEFT";
|
|
34
|
+
payload: {
|
|
35
|
+
participantId: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
type: "PARTICIPANT_STREAM_ADDED";
|
|
40
|
+
payload: {
|
|
41
|
+
participantId: string;
|
|
42
|
+
stream: MediaStream;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
type: "PARTICIPANT_VIDEO_ENABLED";
|
|
47
|
+
payload: {
|
|
48
|
+
participantId: string;
|
|
49
|
+
isVideoEnabled: boolean;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
type: "PARTICIPANT_METADATA_CHANGED";
|
|
54
|
+
payload: {
|
|
55
|
+
participantId: string;
|
|
56
|
+
displayName: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
type: "LOCAL_CLIENT_DISPLAY_NAME_CHANGED";
|
|
61
|
+
payload: {
|
|
62
|
+
displayName: string;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function updateParticipant(
|
|
67
|
+
remoteParticipants: RemoteParticipantState[],
|
|
68
|
+
participantId: string,
|
|
69
|
+
updates: Partial<RemoteParticipantState>
|
|
70
|
+
): RemoteParticipantState[] {
|
|
71
|
+
const existingParticipant = remoteParticipants.find((p) => p.id === participantId);
|
|
72
|
+
if (!existingParticipant) {
|
|
73
|
+
return remoteParticipants;
|
|
74
|
+
}
|
|
75
|
+
const index = remoteParticipants.indexOf(existingParticipant);
|
|
76
|
+
|
|
77
|
+
return [
|
|
78
|
+
...remoteParticipants.slice(0, index),
|
|
79
|
+
{ ...existingParticipant, ...updates },
|
|
80
|
+
...remoteParticipants.slice(index + 1),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default function reducer(state: RoomState, action: Action): RoomState {
|
|
85
|
+
switch (action.type) {
|
|
86
|
+
case "ROOM_JOINED":
|
|
87
|
+
return {
|
|
88
|
+
...state,
|
|
89
|
+
localParticipant: action.payload.localParticipant,
|
|
90
|
+
remoteParticipants: action.payload.remoteParticipants,
|
|
91
|
+
roomConnectionStatus: "connected",
|
|
92
|
+
};
|
|
93
|
+
case "PARTICIPANT_AUDIO_ENABLED":
|
|
94
|
+
return {
|
|
95
|
+
...state,
|
|
96
|
+
remoteParticipants: updateParticipant(state.remoteParticipants, action.payload.participantId, {
|
|
97
|
+
isAudioEnabled: action.payload.isAudioEnabled,
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
case "PARTICIPANT_JOINED":
|
|
101
|
+
return {
|
|
102
|
+
...state,
|
|
103
|
+
remoteParticipants: [...state.remoteParticipants, action.payload.paritipant],
|
|
104
|
+
};
|
|
105
|
+
case "PARTICIPANT_LEFT":
|
|
106
|
+
return {
|
|
107
|
+
...state,
|
|
108
|
+
remoteParticipants: [...state.remoteParticipants.filter((p) => p.id !== action.payload.participantId)],
|
|
109
|
+
};
|
|
110
|
+
case "PARTICIPANT_STREAM_ADDED":
|
|
111
|
+
return {
|
|
112
|
+
...state,
|
|
113
|
+
remoteParticipants: updateParticipant(state.remoteParticipants, action.payload.participantId, {
|
|
114
|
+
stream: action.payload.stream,
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
case "PARTICIPANT_VIDEO_ENABLED":
|
|
118
|
+
return {
|
|
119
|
+
...state,
|
|
120
|
+
remoteParticipants: updateParticipant(state.remoteParticipants, action.payload.participantId, {
|
|
121
|
+
isVideoEnabled: action.payload.isVideoEnabled,
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
case "PARTICIPANT_METADATA_CHANGED":
|
|
125
|
+
return {
|
|
126
|
+
...state,
|
|
127
|
+
remoteParticipants: [
|
|
128
|
+
...state.remoteParticipants.map((p) =>
|
|
129
|
+
p.id === action.payload.participantId ? { ...p, displayName: action.payload.displayName } : p
|
|
130
|
+
),
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
case "LOCAL_CLIENT_DISPLAY_NAME_CHANGED":
|
|
134
|
+
if (!state.localParticipant) return state;
|
|
135
|
+
return {
|
|
136
|
+
...state,
|
|
137
|
+
localParticipant: { ...state.localParticipant, displayName: action.payload.displayName },
|
|
138
|
+
};
|
|
139
|
+
default:
|
|
140
|
+
throw state;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useLocalMedia, useRoomConnection, VideoElement } from "../lib/react";
|
|
3
|
+
import "./styles.css";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: "Examples/Custom UI",
|
|
7
|
+
argTypes: {
|
|
8
|
+
displayName: { control: "text", defaultValue: "SDK" },
|
|
9
|
+
roomUrl: { control: "text", defaultValue: process.env.STORYBOOK_ROOM, type: { required: true } },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DisplayNameForm = ({
|
|
14
|
+
initialDisplayName,
|
|
15
|
+
onSetDisplayName,
|
|
16
|
+
}: {
|
|
17
|
+
initialDisplayName?: string;
|
|
18
|
+
onSetDisplayName: (displayName: string) => void;
|
|
19
|
+
}) => {
|
|
20
|
+
const [displayName, setDisplayName] = useState(initialDisplayName || "");
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div>
|
|
24
|
+
<label htmlFor="displayName">Display name: </label>
|
|
25
|
+
<input
|
|
26
|
+
type="text"
|
|
27
|
+
name="displayName"
|
|
28
|
+
id="displayName"
|
|
29
|
+
value={displayName}
|
|
30
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
31
|
+
/>
|
|
32
|
+
<button onClick={() => onSetDisplayName(displayName || "")}>Save</button>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const VideoExperience = ({
|
|
38
|
+
displayName,
|
|
39
|
+
roomName,
|
|
40
|
+
localStream,
|
|
41
|
+
}: {
|
|
42
|
+
displayName?: string;
|
|
43
|
+
roomName: string;
|
|
44
|
+
localStream?: MediaStream;
|
|
45
|
+
}) => {
|
|
46
|
+
const [state, actions, components] = useRoomConnection(roomName, {
|
|
47
|
+
displayName,
|
|
48
|
+
localMediaConstraints: {
|
|
49
|
+
audio: true,
|
|
50
|
+
video: true,
|
|
51
|
+
},
|
|
52
|
+
localStream,
|
|
53
|
+
logger: console,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { localParticipant, remoteParticipants } = state;
|
|
57
|
+
const { setDisplayName, toggleCamera, toggleMicrophone } = actions;
|
|
58
|
+
const { VideoView } = components;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
<div className="container">
|
|
63
|
+
{[localParticipant, ...remoteParticipants].map((participant, i) => (
|
|
64
|
+
<div className="participantWrapper" key={participant?.id || i}>
|
|
65
|
+
{participant ? (
|
|
66
|
+
<>
|
|
67
|
+
<div
|
|
68
|
+
className="bouncingball"
|
|
69
|
+
style={{
|
|
70
|
+
animationDelay: `${Math.random() * 1000}ms`,
|
|
71
|
+
...(participant.isAudioEnabled
|
|
72
|
+
? {
|
|
73
|
+
border: "2px solid grey",
|
|
74
|
+
}
|
|
75
|
+
: null),
|
|
76
|
+
...(!participant.isVideoEnabled
|
|
77
|
+
? {
|
|
78
|
+
backgroundColor: "green",
|
|
79
|
+
}
|
|
80
|
+
: null),
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{participant.stream && participant.isVideoEnabled && (
|
|
84
|
+
<VideoView
|
|
85
|
+
stream={participant.stream}
|
|
86
|
+
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="displayName">{participant.displayName || "Guest"}</div>
|
|
91
|
+
</>
|
|
92
|
+
) : null}
|
|
93
|
+
</div>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
<div className="controls">
|
|
97
|
+
<button onClick={() => toggleCamera()}>Toggle camera</button>
|
|
98
|
+
<button onClick={() => toggleMicrophone()}>Toggle microphone</button>
|
|
99
|
+
<DisplayNameForm initialDisplayName={displayName} onSetDisplayName={setDisplayName} />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const roomRegEx = new RegExp(/^https:\/\/.*\/.*/);
|
|
106
|
+
|
|
107
|
+
export const Simplistic = ({ roomUrl, displayName }: { roomUrl: string; displayName?: string }) => {
|
|
108
|
+
if (!roomUrl || !roomUrl.match(roomRegEx)) {
|
|
109
|
+
return <p>Set room url on the Controls panel</p>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return <VideoExperience displayName={displayName} roomName={roomUrl} />;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const WithPreCall = ({ roomUrl, displayName }: { roomUrl: string; displayName?: string }) => {
|
|
116
|
+
const [localStream] = useLocalMedia();
|
|
117
|
+
const [shouldJoin, setShouldJoin] = useState(false);
|
|
118
|
+
|
|
119
|
+
if (!roomUrl || !roomUrl.match(roomRegEx)) {
|
|
120
|
+
return <p>Set room url on the Controls panel</p>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
{shouldJoin ? (
|
|
126
|
+
<VideoExperience displayName={displayName} roomName={roomUrl} localStream={localStream} />
|
|
127
|
+
) : (
|
|
128
|
+
<div>{localStream && <VideoElement stream={localStream} />}</div>
|
|
129
|
+
)}
|
|
130
|
+
<button onClick={() => setShouldJoin(!shouldJoin)}>{shouldJoin ? "Leave room" : "Join room"}</button>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
};
|