@whereby.com/browser-sdk 2.0.0-alpha → 2.0.0-alpha10

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