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

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 (72) hide show
  1. package/dist/lib.cjs.js +5467 -0
  2. package/dist/lib.esm.js +5451 -0
  3. package/dist/types.d.ts +104 -0
  4. package/dist/v2-alpha2.js +105 -0
  5. package/package.json +6 -1
  6. package/.eslintrc +0 -23
  7. package/.github/actions/build/action.yml +0 -17
  8. package/.github/workflows/deploy.yml +0 -102
  9. package/.github/workflows/test.yml +0 -24
  10. package/.prettierignore +0 -7
  11. package/.prettierrc +0 -4
  12. package/.storybook/main.cjs +0 -16
  13. package/.storybook/preview.js +0 -9
  14. package/jest.config.js +0 -6
  15. package/rollup.config.js +0 -70
  16. package/src/lib/RoomConnection.ts +0 -516
  17. package/src/lib/RoomParticipant.ts +0 -77
  18. package/src/lib/__tests__/embed.unit.ts +0 -77
  19. package/src/lib/api/ApiClient.ts +0 -111
  20. package/src/lib/api/Credentials.ts +0 -45
  21. package/src/lib/api/HttpClient.ts +0 -95
  22. package/src/lib/api/MultipartHttpClient.ts +0 -53
  23. package/src/lib/api/OrganizationApiClient.ts +0 -64
  24. package/src/lib/api/Response.ts +0 -34
  25. package/src/lib/api/credentialsService/index.ts +0 -159
  26. package/src/lib/api/credentialsService/test/index.spec.ts +0 -181
  27. package/src/lib/api/deviceService/index.ts +0 -42
  28. package/src/lib/api/deviceService/tests/index.spec.ts +0 -74
  29. package/src/lib/api/extractUtils.ts +0 -160
  30. package/src/lib/api/index.ts +0 -8
  31. package/src/lib/api/localStorageWrapper/index.ts +0 -15
  32. package/src/lib/api/models/Account.ts +0 -48
  33. package/src/lib/api/models/Meeting.ts +0 -42
  34. package/src/lib/api/models/Organization.ts +0 -186
  35. package/src/lib/api/models/Room.ts +0 -44
  36. package/src/lib/api/models/account/EmbeddedFreeTierStatus.ts +0 -34
  37. package/src/lib/api/models/tests/Account.spec.ts +0 -128
  38. package/src/lib/api/models/tests/Organization.spec.ts +0 -161
  39. package/src/lib/api/models/tests/Room.spec.ts +0 -74
  40. package/src/lib/api/modules/AbstractStore.ts +0 -18
  41. package/src/lib/api/modules/ChromeStorageStore.ts +0 -44
  42. package/src/lib/api/modules/LocalStorageStore.ts +0 -57
  43. package/src/lib/api/modules/tests/ChromeStorageStore.spec.ts +0 -67
  44. package/src/lib/api/modules/tests/LocalStorageStore.spec.ts +0 -79
  45. package/src/lib/api/modules/tests/__mocks__/storage.ts +0 -24
  46. package/src/lib/api/organizationService/index.ts +0 -284
  47. package/src/lib/api/organizationService/tests/index.spec.ts +0 -781
  48. package/src/lib/api/organizationServiceCache/index.ts +0 -28
  49. package/src/lib/api/organizationServiceCache/tests/index.spec.ts +0 -101
  50. package/src/lib/api/parameterAssertUtils.ts +0 -166
  51. package/src/lib/api/roomService/index.ts +0 -310
  52. package/src/lib/api/roomService/tests/index.spec.ts +0 -668
  53. package/src/lib/api/test/ApiClient.spec.ts +0 -139
  54. package/src/lib/api/test/HttpClient.spec.ts +0 -120
  55. package/src/lib/api/test/MultipartHttpClient.spec.ts +0 -145
  56. package/src/lib/api/test/OrganizationApiClient.spec.ts +0 -132
  57. package/src/lib/api/test/extractUtils.spec.ts +0 -357
  58. package/src/lib/api/test/helpers.ts +0 -41
  59. package/src/lib/api/test/parameterAssertUtils.spec.ts +0 -265
  60. package/src/lib/api/types.ts +0 -6
  61. package/src/lib/embed.ts +0 -172
  62. package/src/lib/index.ts +0 -3
  63. package/src/lib/react/VideoElement.tsx +0 -16
  64. package/src/lib/react/index.ts +0 -3
  65. package/src/lib/react/useLocalMedia.ts +0 -25
  66. package/src/lib/react/useRoomConnection.ts +0 -92
  67. package/src/lib/reducer.ts +0 -142
  68. package/src/stories/custom-ui.stories.tsx +0 -133
  69. package/src/stories/prebuilt-ui.stories.tsx +0 -131
  70. package/src/stories/styles.css +0 -74
  71. package/src/types.d.ts +0 -175
  72. 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
- };