Upload 3 files
Browse files- index.html +50 -0
- index.js +563 -0
- 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 |
+
}
|