@whereby.com/browser-sdk 1.8.0 → 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.
Files changed (64) hide show
  1. package/.eslintrc +4 -2
  2. package/.storybook/main.cjs +13 -2
  3. package/README.md +60 -7
  4. package/jest.config.js +2 -0
  5. package/package.json +38 -12
  6. package/rollup.config.js +37 -13
  7. package/src/lib/RoomConnection.ts +516 -0
  8. package/src/lib/RoomParticipant.ts +77 -0
  9. package/src/lib/__tests__/{index.unit.js → embed.unit.ts} +3 -9
  10. package/src/lib/api/ApiClient.ts +111 -0
  11. package/src/lib/api/Credentials.ts +45 -0
  12. package/src/lib/api/HttpClient.ts +95 -0
  13. package/src/lib/api/MultipartHttpClient.ts +53 -0
  14. package/src/lib/api/OrganizationApiClient.ts +64 -0
  15. package/src/lib/api/Response.ts +34 -0
  16. package/src/lib/api/credentialsService/index.ts +159 -0
  17. package/src/lib/api/credentialsService/test/index.spec.ts +181 -0
  18. package/src/lib/api/deviceService/index.ts +42 -0
  19. package/src/lib/api/deviceService/tests/index.spec.ts +74 -0
  20. package/src/lib/api/extractUtils.ts +160 -0
  21. package/src/lib/api/index.ts +8 -0
  22. package/src/lib/api/localStorageWrapper/index.ts +15 -0
  23. package/src/lib/api/models/Account.ts +48 -0
  24. package/src/lib/api/models/Meeting.ts +42 -0
  25. package/src/lib/api/models/Organization.ts +186 -0
  26. package/src/lib/api/models/Room.ts +44 -0
  27. package/src/lib/api/models/account/EmbeddedFreeTierStatus.ts +34 -0
  28. package/src/lib/api/models/tests/Account.spec.ts +128 -0
  29. package/src/lib/api/models/tests/Organization.spec.ts +161 -0
  30. package/src/lib/api/models/tests/Room.spec.ts +74 -0
  31. package/src/lib/api/modules/AbstractStore.ts +18 -0
  32. package/src/lib/api/modules/ChromeStorageStore.ts +44 -0
  33. package/src/lib/api/modules/LocalStorageStore.ts +57 -0
  34. package/src/lib/api/modules/tests/ChromeStorageStore.spec.ts +67 -0
  35. package/src/lib/api/modules/tests/LocalStorageStore.spec.ts +79 -0
  36. package/src/lib/api/modules/tests/__mocks__/storage.ts +24 -0
  37. package/src/lib/api/organizationService/index.ts +284 -0
  38. package/src/lib/api/organizationService/tests/index.spec.ts +781 -0
  39. package/src/lib/api/organizationServiceCache/index.ts +28 -0
  40. package/src/lib/api/organizationServiceCache/tests/index.spec.ts +101 -0
  41. package/src/lib/api/parameterAssertUtils.ts +166 -0
  42. package/src/lib/api/roomService/index.ts +310 -0
  43. package/src/lib/api/roomService/tests/index.spec.ts +668 -0
  44. package/src/lib/api/test/ApiClient.spec.ts +139 -0
  45. package/src/lib/api/test/HttpClient.spec.ts +120 -0
  46. package/src/lib/api/test/MultipartHttpClient.spec.ts +145 -0
  47. package/src/lib/api/test/OrganizationApiClient.spec.ts +132 -0
  48. package/src/lib/api/test/extractUtils.spec.ts +357 -0
  49. package/src/lib/api/test/helpers.ts +41 -0
  50. package/src/lib/api/test/parameterAssertUtils.spec.ts +265 -0
  51. package/src/lib/api/types.ts +6 -0
  52. package/src/lib/{index.js → embed.ts} +53 -27
  53. package/src/lib/index.ts +3 -0
  54. package/src/lib/react/VideoElement.tsx +16 -0
  55. package/src/lib/react/index.ts +3 -0
  56. package/src/lib/react/useLocalMedia.ts +25 -0
  57. package/src/lib/react/useRoomConnection.ts +92 -0
  58. package/src/lib/reducer.ts +142 -0
  59. package/src/stories/custom-ui.stories.tsx +133 -0
  60. package/src/stories/prebuilt-ui.stories.tsx +131 -0
  61. package/src/stories/styles.css +74 -0
  62. package/src/types.d.ts +175 -0
  63. package/tsconfig.json +30 -0
  64. package/src/index.stories.js +0 -105
@@ -1,9 +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",
6
- "cameraAccess",
35
+ "cameraaccess",
7
36
  "chat",
8
37
  "people",
9
38
  "embed",
@@ -32,10 +61,10 @@ define("WherebyEmbed", {
32
61
  this.iframe = ref();
33
62
  },
34
63
  onconnected() {
35
- window.addEventListener("message", this);
64
+ window.addEventListener("message", this.onmessage);
36
65
  },
37
66
  ondisconnected() {
38
- window.removeEventListener("message", this);
67
+ window.removeEventListener("message", this.onmessage);
39
68
  },
40
69
  observedAttributes: [
41
70
  "displayName",
@@ -49,11 +78,11 @@ define("WherebyEmbed", {
49
78
  "avatarUrl",
50
79
  ...boolAttrs,
51
80
  ].map((a) => a.toLowerCase()),
52
- onattributechanged({ attributeName, oldValue }) {
81
+ onattributechanged({ attributeName, oldValue }: { attributeName: string; oldValue: string | boolean }) {
53
82
  if (["room", "subdomain"].includes(attributeName) && oldValue == null) return;
54
83
  this.render();
55
84
  },
56
- style(self) {
85
+ style(self: string) {
57
86
  return `
58
87
  ${self} {
59
88
  display: block;
@@ -67,7 +96,7 @@ define("WherebyEmbed", {
67
96
  },
68
97
 
69
98
  // Commands
70
- _postCommand(command, args = []) {
99
+ _postCommand(command: string, args = []) {
71
100
  if (this.iframe.current) {
72
101
  const url = new URL(this.room, `https://${this.subdomain}.whereby.com`);
73
102
  this.iframe.current.contentWindow.postMessage({ command, args }, url.origin);
@@ -79,17 +108,16 @@ define("WherebyEmbed", {
79
108
  stopRecording() {
80
109
  this._postCommand("stop_recording");
81
110
  },
82
- toggleCamera(enabled) {
111
+ toggleCamera(enabled: boolean) {
83
112
  this._postCommand("toggle_camera", [enabled]);
84
113
  },
85
- toggleMicrophone(enabled) {
114
+ toggleMicrophone(enabled: boolean) {
86
115
  this._postCommand("toggle_microphone", [enabled]);
87
116
  },
88
- toggleScreenshare(enabled) {
117
+ toggleScreenshare(enabled: boolean) {
89
118
  this._postCommand("toggle_screenshare", [enabled]);
90
119
  },
91
-
92
- onmessage({ origin, data }) {
120
+ onmessage({ origin, data }: { origin: string; data: { type: string; payload: string } }) {
93
121
  const url = new URL(this.room, `https://${this.subdomain}.whereby.com`);
94
122
  if (origin !== url.origin) return;
95
123
  const { type, payload: detail } = data;
@@ -106,9 +134,9 @@ define("WherebyEmbed", {
106
134
  groups,
107
135
  virtualbackgroundurl: virtualBackgroundUrl,
108
136
  } = this;
109
- if (!room) return this.html`Whereby: Missing room attr.`;
137
+ if (!room) return this.html`Whereby: Missing room attribute.`;
110
138
  // Get subdomain from room URL, or use it specified
111
- let m = /https:\/\/([^.]+)\.whereby.com\/.+/.exec(room);
139
+ const m = /https:\/\/([^.]+)\.whereby.com\/.+/.exec(room);
112
140
  const subdomain = (m && m[1]) || this.subdomain;
113
141
  if (!subdomain) return this.html`Whereby: Missing subdomain attr.`;
114
142
  const url = new URL(room, `https://${subdomain}.whereby.com`);
@@ -117,11 +145,11 @@ define("WherebyEmbed", {
117
145
  we: "__SDK_VERSION__",
118
146
  iframeSource: subdomain,
119
147
  ...(displayName && { displayName }),
120
- ...(lang && { lang: lang }),
121
- ...(metadata && { metadata: metadata }),
122
- ...(groups && { groups: groups }),
123
- ...(virtualBackgroundUrl && { virtualBackgroundUrl: virtualBackgroundUrl }),
124
- ...(avatarUrl && { avatarUrl: avatarUrl }),
148
+ ...(lang && { lang }),
149
+ ...(metadata && { metadata }),
150
+ ...(groups && { groups }),
151
+ ...(virtualBackgroundUrl && { virtualBackgroundUrl }),
152
+ ...(avatarUrl && { avatarUrl }),
125
153
  // the original ?embed name was confusing, so we give minimal
126
154
  ...(minimal != null && { embed: minimal }),
127
155
  ...boolAttrs.reduce(
@@ -130,17 +158,15 @@ define("WherebyEmbed", {
130
158
  {}
131
159
  ),
132
160
  }).forEach(([k, v]) => {
133
- if (!url.searchParams.has(k)) {
161
+ if (!url.searchParams.has(k) && typeof v === "string") {
134
162
  url.searchParams.set(k, v);
135
163
  }
136
164
  });
137
- this.html`
138
- <iframe
139
- ref=${this.iframe}
140
- src=${url}
141
- allow="autoplay; camera; microphone; fullscreen; speaker; display-capture" />
142
- `;
165
+ return this.html`
166
+ <iframe
167
+ ref=${this.iframe}
168
+ src=${url}
169
+ allow="autoplay; camera; microphone; fullscreen; speaker; display-capture" />
170
+ `;
143
171
  },
144
172
  });
145
-
146
- export default { sdkVersion: "__SDK_VERSION__" };
@@ -0,0 +1,3 @@
1
+ import "./embed";
2
+ export { useRoomConnection } from "./react";
3
+ export const sdkVersion = "__SDK_VERSION__";
@@ -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,3 @@
1
+ export { default as VideoElement } from "./VideoElement";
2
+ export { default as useLocalMedia } from "./useLocalMedia";
3
+ export { default as useRoomConnection } from "./useRoomConnection";
@@ -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
+ };