alpha1ct commited on
Commit
bec56cb
·
verified ·
1 Parent(s): 9fe2a67

Upload 3 files

Browse files
Files changed (3) hide show
  1. index.html +50 -0
  2. index.js +563 -0
  3. style.css +74 -0
index.html ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Daily Call Example</title>
6
+ <link href="style.css" rel="stylesheet" />
7
+ </head>
8
+ <body>
9
+ <div>
10
+ <label for="room-url">Room URL:</label>
11
+ <input
12
+ type="text"
13
+ id="room-url"
14
+ size="50"
15
+ placeholder="https://yourcompany.daily.co/hello"
16
+ />
17
+ </div>
18
+ <div>
19
+ <input type="text" id="join-token" size="50" placeholder="Optional" />
20
+ </div>
21
+
22
+ <div class="controls">
23
+ <button id="join-btn">Join Room</button>
24
+ <button id="leave-btn" disabled>Leave</button>
25
+ <button id="toggle-camera" disabled="true">Toggle Camera</button>
26
+ <button id="toggle-mic" disabled="true">Toggle Microphone</button>
27
+ </div>
28
+
29
+ <div class="controls">
30
+ <select id="camera-selector">
31
+ <option value="" disabled selected>Select a camera</option>
32
+ </select>
33
+ <select id="mic-selector">
34
+ <option value="" disabled selected>Select a microphone</option>
35
+ </select>
36
+ </div>
37
+
38
+ <div id="status">
39
+ <div id="camera-state">Camera: Off</div>
40
+ <div id="mic-state">Mic: Off</div>
41
+ <div id="participant-count">Participants: 0</div>
42
+ <div id="active-speaker">Active Speaker: None</div>
43
+ </div>
44
+
45
+ <div id="videos"></div>
46
+
47
+ <script src="https://unpkg.com/@daily-co/daily-js"></script>
48
+ <script src="index.js"></script>
49
+ </body>
50
+ </html>
index.js ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Initializes a new instance of the `DailyCallManager` class, creating
3
+ * a Daily.co call object and setting initial states for camera and
4
+ * microphone muting, as well as the current room URL. It then calls the
5
+ * `initialize` method to set up event listeners and UI interactions.
6
+ */
7
+ class DailyCallManager {
8
+ constructor() {
9
+ this.call = Daily.createCallObject();
10
+ this.currentRoomUrl = null;
11
+ this.initialize();
12
+ }
13
+
14
+ /**
15
+ * Performs initial setup of event listeners and UI component interactions.
16
+ */
17
+ async initialize() {
18
+ this.setupEventListeners();
19
+ document
20
+ .getElementById("toggle-camera")
21
+ .addEventListener("click", () => this.toggleCamera());
22
+ document
23
+ .getElementById("toggle-mic")
24
+ .addEventListener("click", () => this.toggleMicrophone());
25
+ }
26
+
27
+ /**
28
+ * Configures event listeners for various call-related events.
29
+ */
30
+ setupEventListeners() {
31
+ const events = {
32
+ "active-speaker-change": this.handleActiveSpeakerChange.bind(this),
33
+ error: this.handleError.bind(this),
34
+ "joined-meeting": this.handleJoin.bind(this),
35
+ "left-meeting": this.handleLeave.bind(this),
36
+ "participant-joined": this.handleParticipantJoinedOrUpdated.bind(this),
37
+ "participant-left": this.handleParticipantLeft.bind(this),
38
+ "participant-updated": this.handleParticipantJoinedOrUpdated.bind(this),
39
+ };
40
+
41
+ Object.entries(events).forEach(([event, handler]) => {
42
+ this.call.on(event, handler);
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Handler for the local participant joining:
48
+ * - Prints the room URL
49
+ * - Enables the toggle camera, toggle mic, and leave buttons
50
+ * - Gets the initial track states
51
+ * - Sets up and enables the device selectors
52
+ * @param {Object} event - The joined-meeting event object.
53
+ */
54
+ handleJoin(event) {
55
+ const tracks = event.participants.local.tracks;
56
+
57
+ console.log(`Successfully joined: ${this.currentRoomUrl}`);
58
+
59
+ // Update the participant count
60
+ this.updateAndDisplayParticipantCount();
61
+
62
+ // Enable the leave button
63
+ document.getElementById("leave-btn").disabled = false;
64
+
65
+ // Enable the toggle camera and mic buttons and selectors
66
+ document.getElementById("toggle-camera").disabled = false;
67
+ document.getElementById("toggle-mic").disabled = false;
68
+ document.getElementById("camera-selector").disabled = false;
69
+ document.getElementById("mic-selector").disabled = false;
70
+
71
+ // Set up the camera and mic selectors
72
+ this.setupDeviceSelectors();
73
+
74
+ // Initialize the camera and microphone states and UI for the local
75
+ // participant
76
+ Object.entries(tracks).forEach(([trackType, trackInfo]) => {
77
+ this.updateUiForDevicesState(trackType, trackInfo);
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Handler for participant leave events:
83
+ * - Confirms leaving with a console message
84
+ * - Disable the toggle camera and mic buttons
85
+ * - Resets the camera and mic selectors
86
+ * - Updates the call state in the UI
87
+ * - Removes all video containers
88
+ */
89
+ handleLeave() {
90
+ console.log("Successfully left the call");
91
+
92
+ // Update the join and leave button states
93
+ document.getElementById("leave-btn").disabled = true;
94
+ document.getElementById("join-btn").disabled = false;
95
+
96
+ // Disable the toggle camera and mic buttons
97
+ document.getElementById("toggle-camera").disabled = true;
98
+ document.getElementById("toggle-mic").disabled = true;
99
+
100
+ // Reset and disable the camera and mic selectors
101
+ const cameraSelector = document.getElementById("camera-selector");
102
+ const micSelector = document.getElementById("mic-selector");
103
+ cameraSelector.selectedIndex = 0;
104
+ micSelector.selectedIndex = 0;
105
+ cameraSelector.disabled = true;
106
+ micSelector.disabled = true;
107
+
108
+ // Update the call state in the UI
109
+ document.getElementById("camera-state").textContent = "Camera: Off";
110
+ document.getElementById("mic-state").textContent = "Mic: Off";
111
+ document.getElementById(
112
+ "participant-count"
113
+ ).textContent = `Participants: 0`;
114
+ document.getElementById(
115
+ "active-speaker"
116
+ ).textContent = `Active Speaker: None`;
117
+
118
+ // Remove all video containers
119
+ const videosDiv = document.getElementById("videos");
120
+ while (videosDiv.firstChild) {
121
+ videosDiv.removeChild(videosDiv.firstChild);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Handles fatal errors emitted from the Daily call object.
127
+ * These errors result in the participant leaving the meeting. A
128
+ * `left-meeting` event will also be sent, so we still rely on that event
129
+ * for cleanup.
130
+ * @param {Object} e - The error event object.
131
+ */
132
+ handleError(e) {
133
+ console.error("DAILY SENT AN ERROR!", e.error ? e.error : e.errorMsg);
134
+ }
135
+
136
+ /**
137
+ * Handles participant-left event:
138
+ * - Cleans up the video and audio tracks for the participant
139
+ * - Removes the related UI elements
140
+ * @param {Object} event - The participant-left event object.
141
+ */
142
+ handleParticipantLeft(event) {
143
+ const participantId = event.participant.session_id;
144
+
145
+ // Clean up the video and audio tracks for the participant
146
+ this.destroyTracks(["video", "audio"], participantId);
147
+
148
+ // Now, remove the related video UI
149
+ document.getElementById(`video-container-${participantId}`)?.remove();
150
+
151
+ // Update the participant count
152
+ this.updateAndDisplayParticipantCount();
153
+ }
154
+
155
+ /**
156
+ * Handles participant-joined and participant-updated events:
157
+ * - Updates the participant count
158
+ * - Creates a video container for new participants
159
+ * - Creates an audio element for new participants
160
+ * - Manages video and audio tracks based on their current state
161
+ * - Updates device states for the local participant
162
+ * @param {Object} event - The participant-joined, participant-updated
163
+ * event object.
164
+ */
165
+ handleParticipantJoinedOrUpdated(event) {
166
+ const { participant } = event;
167
+ const participantId = participant.session_id;
168
+ const isLocal = participant.local;
169
+ const tracks = participant.tracks;
170
+
171
+ // Always update the participant count regardless of the event action
172
+ this.updateAndDisplayParticipantCount();
173
+
174
+ // Create a video container if one doesn't exist
175
+ if (!document.getElementById(`video-container-${participantId}`)) {
176
+ this.createVideoContainer(participantId);
177
+ }
178
+
179
+ // Create an audio element for non-local participants if one doesn't exist
180
+ if (!document.getElementById(`audio-${participantId}`) && !isLocal) {
181
+ this.createAudioElement(participantId);
182
+ }
183
+
184
+ Object.entries(tracks).forEach(([trackType, trackInfo]) => {
185
+ // If a persistentTrack exists...
186
+ if (trackInfo.persistentTrack) {
187
+ // Check if this is the local participant's audio track.
188
+ // If so, we will skip playing it, as it's already being played.
189
+ // We'll start or update tracks in all other cases.
190
+ if (!(isLocal && trackType === "audio")) {
191
+ this.startOrUpdateTrack(trackType, trackInfo, participantId);
192
+ }
193
+ } else {
194
+ // If the track is not available, remove the media element
195
+ this.destroyTracks([trackType], participantId);
196
+ }
197
+
198
+ // Update the video UI based on the track's state
199
+ if (trackType === "video") {
200
+ this.updateVideoUi(trackInfo, participantId);
201
+ }
202
+
203
+ // Update the camera and microphone states for the local user based on
204
+ // the track's state
205
+ if (isLocal) {
206
+ this.updateUiForDevicesState(trackType, trackInfo);
207
+ }
208
+ });
209
+ }
210
+
211
+ /**
212
+ * Updates the UI with the current active speaker's identity.
213
+ * @param {Object} event - The active speaker change event object.
214
+ */
215
+ handleActiveSpeakerChange(event) {
216
+ document.getElementById(
217
+ "active-speaker"
218
+ ).textContent = `Active Speaker: ${event.activeSpeaker.peerId}`;
219
+ }
220
+
221
+ /**
222
+ * Tries to join a call with provided room URL and optional join token.
223
+ * @param {string} roomUrl - The URL of the room to join.
224
+ * @param {string|null} joinToken - An optional token for joining the room.
225
+ */
226
+ async joinRoom(roomUrl, joinToken = null) {
227
+ if (!roomUrl) {
228
+ console.error("Room URL is required to join a room.");
229
+ return;
230
+ }
231
+
232
+ this.currentRoomUrl = roomUrl;
233
+
234
+ const joinOptions = { url: roomUrl };
235
+ if (joinToken) {
236
+ joinOptions.token = joinToken;
237
+ console.log("Joining with a token.");
238
+ } else {
239
+ console.log("Joining without a token.");
240
+ }
241
+
242
+ try {
243
+ // Disable the join button to prevent multiple attempts to join
244
+ document.getElementById("join-btn").disabled = true;
245
+ // Join the room
246
+ await this.call.join(joinOptions);
247
+ } catch (e) {
248
+ console.error("Join failed:", e);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Creates and sets up a new video container for a specific participant. This
254
+ * function dynamically generates a video element along with a container and
255
+ * an overlay displaying the participant's ID. The newly created elements are
256
+ * appended to a designated parent in the DOM, preparing them for video
257
+ * streaming or playback related to the specified participant.
258
+ *
259
+ * @param {string} participantId - The unique identifier for the participant.
260
+ */
261
+ createVideoContainer(participantId) {
262
+ // Create a video container for the participant
263
+ const videoContainer = document.createElement("div");
264
+ videoContainer.id = `video-container-${participantId}`;
265
+ videoContainer.className = "video-container";
266
+ document.getElementById("videos").appendChild(videoContainer);
267
+
268
+ // Add an overlay to display the participant's session ID
269
+ // const sessionIdOverlay = document.createElement("div");
270
+ // sessionIdOverlay.className = "session-id-overlay";
271
+ // sessionIdOverlay.textContent = participantId;
272
+ // videoContainer.appendChild(sessionIdOverlay);
273
+
274
+ // Create a video element for the participant
275
+ const videoEl = document.createElement("video");
276
+ videoEl.className = "video-element";
277
+ videoContainer.appendChild(videoEl);
278
+ }
279
+
280
+ /**
281
+ * Creates an audio element for a particular participant. This function is
282
+ * responsible for dynamically generating a standalone audio element that can
283
+ * be used to play audio streams associated with the specified participant.
284
+ * The audio element is appended directly to the document body or a relevant
285
+ * container, thereby preparing it for playback of the participant's audio.
286
+ *
287
+ * @param {string} participantId - A unique identifier corresponding to the participant.
288
+ */
289
+ createAudioElement(participantId) {
290
+ // Create an audio element for the participant
291
+ const audioEl = document.createElement("audio");
292
+ audioEl.id = `audio-${participantId}`;
293
+ document.body.appendChild(audioEl);
294
+ }
295
+
296
+ /**
297
+ * Updates the media track (audio or video) source for a specific participant
298
+ * and plays the updated track. It checks if the source track needs to be
299
+ * updated and performs the update if necessary, ensuring playback of the
300
+ * media track.
301
+ *
302
+ * @param {string} trackType - Specifies the type of track to update ('audio'
303
+ * or 'video'), allowing the function to dynamically adapt to the track being
304
+ * processed.
305
+ * @param {Object} track - Contains the media track data, including the
306
+ * `persistentTrack` property which holds the actual MediaStreamTrack to be
307
+ * played or updated.
308
+ * @param {string} participantId - Identifies the participant whose media
309
+ * track is being updated.
310
+ */
311
+ startOrUpdateTrack(trackType, track, participantId) {
312
+ // Construct the selector string or ID based on the trackType.
313
+ const selector =
314
+ trackType === "video"
315
+ ? `#video-container-${participantId} video.video-element`
316
+ : `audio-${participantId}`;
317
+
318
+ // Retrieve the specific media element from the DOM.
319
+ const trackEl =
320
+ trackType === "video"
321
+ ? document.querySelector(selector)
322
+ : document.getElementById(selector);
323
+
324
+ // Error handling if the target media element does not exist.
325
+ if (!trackEl) {
326
+ console.error(
327
+ `${trackType} element does not exist for participant: ${participantId}`
328
+ );
329
+ return;
330
+ }
331
+
332
+ // Check for the need to update the media source. This is determined by
333
+ // checking whether the existing srcObject's tracks include the new
334
+ // persistentTrack. If there are no existing tracks or the new track is not
335
+ // among them, an update is necessary.
336
+ const existingTracks = trackEl.srcObject?.getTracks();
337
+ const needsUpdate = !existingTracks?.includes(track.persistentTrack);
338
+
339
+ // Perform the media source update if needed by setting the srcObject of
340
+ // the target element to a new MediaStream containing the provided
341
+ // persistentTrack.
342
+ if (needsUpdate) {
343
+ trackEl.srcObject = new MediaStream([track.persistentTrack]);
344
+
345
+ // Once the media metadata is loaded, attempts to play the track. Error
346
+ // handling for play failures is included to catch and log issues such as
347
+ // autoplay policies blocking playback.
348
+ trackEl.onloadedmetadata = () => {
349
+ trackEl
350
+ .play()
351
+ .catch((e) =>
352
+ console.error(
353
+ `Error playing ${trackType} for participant ${participantId}:`,
354
+ e
355
+ )
356
+ );
357
+ };
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Shows or hides the video element for a participant, including managing
363
+ * the visibility of the video based on the track state.
364
+ * @param {Object} track - The video track object.
365
+ * @param {string} participantId - The ID of the participant.
366
+ */
367
+ updateVideoUi(track, participantId) {
368
+ let videoEl = document
369
+ .getElementById(`video-container-${participantId}`)
370
+ .querySelector("video.video-element");
371
+
372
+ switch (track.state) {
373
+ case "off":
374
+ case "interrupted":
375
+ case "blocked":
376
+ videoEl.style.display = "none"; // Hide video but keep container
377
+ break;
378
+ case "playable":
379
+ default:
380
+ // Here we handle all other states the same as we handle 'playable'.
381
+ // In your code, you may choose to handle them differently.
382
+ videoEl.style.display = "";
383
+ break;
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Cleans up specified media track types (e.g., 'video', 'audio') for a given
389
+ * participant by stopping the tracks and removing their corresponding
390
+ * elements from the DOM. This is essential for properly managing resources
391
+ * when participants leave or change their track states.
392
+ * @param {Array} trackTypes - An array of track types to destroy, e.g.,
393
+ * ['video', 'audio'].
394
+ * @param {string} participantId - The ID of the participant.
395
+ */
396
+ destroyTracks(trackTypes, participantId) {
397
+ trackTypes.forEach((trackType) => {
398
+ const elementId = `${trackType}-${participantId}`;
399
+ const element = document.getElementById(elementId);
400
+ if (element) {
401
+ element.srcObject = null; // Release media resources
402
+ element.parentNode.removeChild(element); // Remove element from the DOM
403
+ }
404
+ });
405
+ }
406
+
407
+ /**
408
+ * Toggles the local video track's mute state.
409
+ */
410
+ toggleCamera() {
411
+ this.call.setLocalVideo(!this.call.localVideo());
412
+ }
413
+
414
+ /**
415
+ * Toggles the local audio track's mute state.
416
+ */
417
+ toggleMicrophone() {
418
+ this.call.setLocalAudio(!this.call.localAudio());
419
+ }
420
+
421
+ /**
422
+ * Updates the UI to reflect the current states of the local participant's
423
+ * camera and microphone.
424
+ * @param {string} trackType - The type of track, either 'video' for cameras
425
+ * or 'audio' for microphones.
426
+ * @param {Object} trackInfo - The track object.
427
+ */
428
+ updateUiForDevicesState(trackType, trackInfo) {
429
+ // For video, set the camera state
430
+ if (trackType === "video") {
431
+ document.getElementById("camera-state").textContent = `Camera: ${
432
+ this.call.localVideo() ? "On" : "Off"
433
+ }`;
434
+ } else if (trackType === "audio") {
435
+ // For audio, set the mic state
436
+ document.getElementById("mic-state").textContent = `Mic: ${
437
+ this.call.localAudio() ? "On" : "Off"
438
+ }`;
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Sets up device selectors for cameras and microphones by dynamically
444
+ * populating them with available devices and attaching event listeners to
445
+ * handle device selection changes.
446
+ */
447
+ async setupDeviceSelectors() {
448
+ // Fetch current input devices settings and an array of available devices.
449
+ const selectedDevices = await this.call.getInputDevices();
450
+ const { devices: allDevices } = await this.call.enumerateDevices();
451
+
452
+ // Element references for camera and microphone selectors.
453
+ const selectors = {
454
+ videoinput: document.getElementById("camera-selector"),
455
+ audioinput: document.getElementById("mic-selector"),
456
+ };
457
+
458
+ // Prepare selectors by clearing existing options and adding a
459
+ // non-selectable prompt.
460
+ Object.values(selectors).forEach((selector) => {
461
+ selector.innerHTML = "";
462
+ const promptOption = new Option(
463
+ `Select a ${selector.id.includes("camera") ? "camera" : "microphone"}`,
464
+ "",
465
+ true,
466
+ true
467
+ );
468
+ promptOption.disabled = true;
469
+ selector.appendChild(promptOption);
470
+ });
471
+
472
+ // Create and append options to the selectors based on available devices.
473
+ allDevices.forEach((device) => {
474
+ if (device.label && selectors[device.kind]) {
475
+ const isSelected =
476
+ selectedDevices[device.kind === "videoinput" ? "camera" : "mic"]
477
+ .deviceId === device.deviceId;
478
+ const option = new Option(
479
+ device.label,
480
+ device.deviceId,
481
+ isSelected,
482
+ isSelected
483
+ );
484
+ selectors[device.kind].appendChild(option);
485
+ }
486
+ });
487
+
488
+ // Listen for user device change requests.
489
+ Object.entries(selectors).forEach(([deviceKind, selector]) => {
490
+ selector.addEventListener("change", async (e) => {
491
+ const deviceId = e.target.value;
492
+ const deviceOptions = {
493
+ [deviceKind === "videoinput" ? "videoDeviceId" : "audioDeviceId"]:
494
+ deviceId,
495
+ };
496
+ await this.call.setInputDevicesAsync(deviceOptions);
497
+ });
498
+ });
499
+ }
500
+
501
+ /**
502
+ * Updates the UI with the current number of participants.
503
+ * This method combines getting the participant count and updating the UI.
504
+ */
505
+ updateAndDisplayParticipantCount() {
506
+ const participantCount =
507
+ this.call.participantCounts().present +
508
+ this.call.participantCounts().hidden;
509
+ document.getElementById(
510
+ "participant-count"
511
+ ).textContent = `Participants: ${participantCount}`;
512
+ }
513
+
514
+ /**
515
+ * Leaves the call and performs necessary cleanup operations like removing
516
+ * video elements.
517
+ */
518
+ async leave() {
519
+ try {
520
+ await this.call.leave();
521
+ document.querySelectorAll("#videos video, audio").forEach((el) => {
522
+ el.srcObject = null; // Release media resources
523
+ el.remove(); // Remove the element from the DOM
524
+ });
525
+ } catch (e) {
526
+ console.error("Leaving failed", e);
527
+ }
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Main entry point: Setup and event listener bindings after the DOM is fully
533
+ * loaded.
534
+ */ /**
535
+ * Main entry point: Setup and event listener bindings after the DOM is fully loaded.
536
+ */
537
+
538
+ document.addEventListener("DOMContentLoaded", async () => {
539
+ const dailyCallManager = new DailyCallManager();
540
+
541
+ // Extract the room URL from query parameters if available
542
+ const urlParams = new URLSearchParams(window.location.search);
543
+ const roomUrlParam = urlParams.get("room_url");
544
+
545
+ if (roomUrlParam) {
546
+ document.getElementById("room-url").value = roomUrlParam.trim();
547
+ }
548
+
549
+ // Bind the join call action to the join button.
550
+ document
551
+ .getElementById("join-btn")
552
+ .addEventListener("click", async function () {
553
+ const roomUrl = document.getElementById("room-url").value.trim();
554
+ const joinToken =
555
+ document.getElementById("join-token").value.trim() || null;
556
+ await dailyCallManager.joinRoom(roomUrl, joinToken);
557
+ });
558
+
559
+ // Bind the leave call action to the leave button.
560
+ document.getElementById("leave-btn").addEventListener("click", function () {
561
+ dailyCallManager.leave();
562
+ });
563
+ });
style.css ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* General styling for the body - sets the base font and margins for the page */
2
+ body {
3
+ font-family: Arial, sans-serif;
4
+ margin: 20px;
5
+ text-align: center;
6
+ }
7
+
8
+ /* Styles for controlling the layout and spacing of control buttons (e.g., 'Join', 'Leave', etc.) */
9
+ .controls {
10
+ margin-top: 10px;
11
+ margin-bottom: 10px;
12
+ }
13
+
14
+ /* Container for holding all video elements. It uses Flexbox for a flexible and responsive layout */
15
+ #videos {
16
+ display: flex;
17
+ flex-wrap: wrap;
18
+ gap: 10px;
19
+
20
+ justify-content: center;
21
+ }
22
+
23
+ /* Styles for individual video elements within the #videos container */
24
+ #videos video {
25
+ max-width: 500px;
26
+ aspect-ratio: 16 / 9;
27
+ width: 100%;
28
+ object-fit: cover;
29
+ }
30
+
31
+ /* Styling for the outer container of each video, providing a uniform appearance and layout */
32
+ .video-container {
33
+ position: relative;
34
+ display: inline-block;
35
+ background-color: lightgray;
36
+ padding: 0.5%; /* Adjusts size of video relatively */
37
+ margin: 5px;
38
+ aspect-ratio: 16 / 9;
39
+ width: 400px;
40
+ }
41
+
42
+ /* Styles for displaying the session ID as an overlay on the bottom left corner of the video */
43
+ .session-id-overlay {
44
+ position: absolute;
45
+ left: 0;
46
+ bottom: 0;
47
+ padding: 5px;
48
+ color: white;
49
+ background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */
50
+ font-size: 0.8em; /* Adjust based on your UI requirements */
51
+ }
52
+
53
+ /* Additional styling specifics for video elements, ensuring they fit well within their containers */
54
+ .video-element {
55
+ width: 100%;
56
+ height: auto;
57
+ background-color: black;
58
+ }
59
+
60
+ #status {
61
+ display: none;
62
+ }
63
+
64
+ #toggle-camera {
65
+ display: none;
66
+ }
67
+
68
+ #camera-selector {
69
+ display: none;
70
+ }
71
+
72
+ #join-token {
73
+ display: none;
74
+ }