| |
| |
| |
| |
| |
| |
| class DailyCallManager { |
| constructor() { |
| this.call = Daily.createCallObject(); |
| this.currentRoomUrl = null; |
| this.initialize(); |
| } |
|
|
| |
| |
| |
| async initialize() { |
| this.setupEventListeners(); |
| document |
| .getElementById("toggle-camera") |
| .addEventListener("click", () => this.toggleCamera()); |
| document |
| .getElementById("toggle-mic") |
| .addEventListener("click", () => this.toggleMicrophone()); |
| } |
|
|
| |
| |
| |
| setupEventListeners() { |
| const events = { |
| "active-speaker-change": this.handleActiveSpeakerChange.bind(this), |
| error: this.handleError.bind(this), |
| "joined-meeting": this.handleJoin.bind(this), |
| "left-meeting": this.handleLeave.bind(this), |
| "participant-joined": this.handleParticipantJoinedOrUpdated.bind(this), |
| "participant-left": this.handleParticipantLeft.bind(this), |
| "participant-updated": this.handleParticipantJoinedOrUpdated.bind(this), |
| }; |
|
|
| Object.entries(events).forEach(([event, handler]) => { |
| this.call.on(event, handler); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| handleJoin(event) { |
| const tracks = event.participants.local.tracks; |
|
|
| console.log(`Successfully joined: ${this.currentRoomUrl}`); |
|
|
| |
| this.updateAndDisplayParticipantCount(); |
|
|
| |
| document.getElementById("leave-btn").disabled = false; |
|
|
| |
| document.getElementById("toggle-camera").disabled = false; |
| document.getElementById("toggle-mic").disabled = false; |
| document.getElementById("camera-selector").disabled = false; |
| document.getElementById("mic-selector").disabled = false; |
|
|
| |
| this.setupDeviceSelectors(); |
|
|
| |
| |
| Object.entries(tracks).forEach(([trackType, trackInfo]) => { |
| this.updateUiForDevicesState(trackType, trackInfo); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| handleLeave() { |
| console.log("Successfully left the call"); |
|
|
| |
| document.getElementById("leave-btn").disabled = true; |
| document.getElementById("join-btn").disabled = false; |
|
|
| |
| document.getElementById("toggle-camera").disabled = true; |
| document.getElementById("toggle-mic").disabled = true; |
|
|
| |
| const cameraSelector = document.getElementById("camera-selector"); |
| const micSelector = document.getElementById("mic-selector"); |
| cameraSelector.selectedIndex = 0; |
| micSelector.selectedIndex = 0; |
| cameraSelector.disabled = true; |
| micSelector.disabled = true; |
|
|
| |
| document.getElementById("camera-state").textContent = "Camera: Off"; |
| document.getElementById("mic-state").textContent = "Mic: Off"; |
| document.getElementById( |
| "participant-count" |
| ).textContent = `Participants: 0`; |
| document.getElementById( |
| "active-speaker" |
| ).textContent = `Active Speaker: None`; |
|
|
| |
| const videosDiv = document.getElementById("videos"); |
| while (videosDiv.firstChild) { |
| videosDiv.removeChild(videosDiv.firstChild); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| handleError(e) { |
| console.error("DAILY SENT AN ERROR!", e.error ? e.error : e.errorMsg); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| handleParticipantLeft(event) { |
| const participantId = event.participant.session_id; |
|
|
| |
| this.destroyTracks(["video", "audio"], participantId); |
|
|
| |
| document.getElementById(`video-container-${participantId}`)?.remove(); |
|
|
| |
| this.updateAndDisplayParticipantCount(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| handleParticipantJoinedOrUpdated(event) { |
| const { participant } = event; |
| const participantId = participant.session_id; |
| const isLocal = participant.local; |
| const tracks = participant.tracks; |
|
|
| |
| this.updateAndDisplayParticipantCount(); |
|
|
| |
| if (!document.getElementById(`video-container-${participantId}`)) { |
| this.createVideoContainer(participantId); |
| } |
|
|
| |
| if (!document.getElementById(`audio-${participantId}`) && !isLocal) { |
| this.createAudioElement(participantId); |
| } |
|
|
| Object.entries(tracks).forEach(([trackType, trackInfo]) => { |
| |
| if (trackInfo.persistentTrack) { |
| |
| |
| |
| if (!(isLocal && trackType === "audio")) { |
| this.startOrUpdateTrack(trackType, trackInfo, participantId); |
| } |
| } else { |
| |
| this.destroyTracks([trackType], participantId); |
| } |
|
|
| |
| if (trackType === "video") { |
| this.updateVideoUi(trackInfo, participantId); |
| } |
|
|
| |
| |
| if (isLocal) { |
| this.updateUiForDevicesState(trackType, trackInfo); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| handleActiveSpeakerChange(event) { |
| document.getElementById( |
| "active-speaker" |
| ).textContent = `Active Speaker: ${event.activeSpeaker.peerId}`; |
| } |
|
|
| |
| |
| |
| |
| |
| async joinRoom(roomUrl, joinToken = null) { |
| if (!roomUrl) { |
| console.error("Room URL is required to join a room."); |
| return; |
| } |
|
|
| this.currentRoomUrl = roomUrl; |
|
|
| const joinOptions = { url: roomUrl }; |
| if (joinToken) { |
| joinOptions.token = joinToken; |
| console.log("Joining with a token."); |
| } else { |
| console.log("Joining without a token."); |
| } |
|
|
| try { |
| |
| document.getElementById("join-btn").disabled = true; |
| |
| await this.call.join(joinOptions); |
| } catch (e) { |
| console.error("Join failed:", e); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| createVideoContainer(participantId) { |
| |
| const videoContainer = document.createElement("div"); |
| videoContainer.id = `video-container-${participantId}`; |
| videoContainer.className = "video-container"; |
| document.getElementById("videos").appendChild(videoContainer); |
|
|
| |
| |
| |
| |
| |
|
|
| |
| const videoEl = document.createElement("video"); |
| videoEl.className = "video-element"; |
| videoContainer.appendChild(videoEl); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| createAudioElement(participantId) { |
| |
| const audioEl = document.createElement("audio"); |
| audioEl.id = `audio-${participantId}`; |
| document.body.appendChild(audioEl); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| startOrUpdateTrack(trackType, track, participantId) { |
| |
| const selector = |
| trackType === "video" |
| ? `#video-container-${participantId} video.video-element` |
| : `audio-${participantId}`; |
|
|
| |
| const trackEl = |
| trackType === "video" |
| ? document.querySelector(selector) |
| : document.getElementById(selector); |
|
|
| |
| if (!trackEl) { |
| console.error( |
| `${trackType} element does not exist for participant: ${participantId}` |
| ); |
| return; |
| } |
|
|
| |
| |
| |
| |
| const existingTracks = trackEl.srcObject?.getTracks(); |
| const needsUpdate = !existingTracks?.includes(track.persistentTrack); |
|
|
| |
| |
| |
| if (needsUpdate) { |
| trackEl.srcObject = new MediaStream([track.persistentTrack]); |
|
|
| |
| |
| |
| trackEl.onloadedmetadata = () => { |
| trackEl |
| .play() |
| .catch((e) => |
| console.error( |
| `Error playing ${trackType} for participant ${participantId}:`, |
| e |
| ) |
| ); |
| }; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| updateVideoUi(track, participantId) { |
| let videoEl = document |
| .getElementById(`video-container-${participantId}`) |
| .querySelector("video.video-element"); |
|
|
| switch (track.state) { |
| case "off": |
| case "interrupted": |
| case "blocked": |
| videoEl.style.display = "none"; |
| break; |
| case "playable": |
| default: |
| |
| |
| videoEl.style.display = ""; |
| break; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| destroyTracks(trackTypes, participantId) { |
| trackTypes.forEach((trackType) => { |
| const elementId = `${trackType}-${participantId}`; |
| const element = document.getElementById(elementId); |
| if (element) { |
| element.srcObject = null; |
| element.parentNode.removeChild(element); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| toggleCamera() { |
| this.call.setLocalVideo(!this.call.localVideo()); |
| } |
|
|
| |
| |
| |
| toggleMicrophone() { |
| this.call.setLocalAudio(!this.call.localAudio()); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| updateUiForDevicesState(trackType, trackInfo) { |
| |
| if (trackType === "video") { |
| document.getElementById("camera-state").textContent = `Camera: ${ |
| this.call.localVideo() ? "On" : "Off" |
| }`; |
| } else if (trackType === "audio") { |
| |
| document.getElementById("mic-state").textContent = `Mic: ${ |
| this.call.localAudio() ? "On" : "Off" |
| }`; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async setupDeviceSelectors() { |
| |
| const selectedDevices = await this.call.getInputDevices(); |
| const { devices: allDevices } = await this.call.enumerateDevices(); |
|
|
| |
| const selectors = { |
| videoinput: document.getElementById("camera-selector"), |
| audioinput: document.getElementById("mic-selector"), |
| }; |
|
|
| |
| |
| Object.values(selectors).forEach((selector) => { |
| selector.innerHTML = ""; |
| const promptOption = new Option( |
| `Select a ${selector.id.includes("camera") ? "camera" : "microphone"}`, |
| "", |
| true, |
| true |
| ); |
| promptOption.disabled = true; |
| selector.appendChild(promptOption); |
| }); |
|
|
| |
| allDevices.forEach((device) => { |
| if (device.label && selectors[device.kind]) { |
| const isSelected = |
| selectedDevices[device.kind === "videoinput" ? "camera" : "mic"] |
| .deviceId === device.deviceId; |
| const option = new Option( |
| device.label, |
| device.deviceId, |
| isSelected, |
| isSelected |
| ); |
| selectors[device.kind].appendChild(option); |
| } |
| }); |
|
|
| |
| Object.entries(selectors).forEach(([deviceKind, selector]) => { |
| selector.addEventListener("change", async (e) => { |
| const deviceId = e.target.value; |
| const deviceOptions = { |
| [deviceKind === "videoinput" ? "videoDeviceId" : "audioDeviceId"]: |
| deviceId, |
| }; |
| await this.call.setInputDevicesAsync(deviceOptions); |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| |
| updateAndDisplayParticipantCount() { |
| const participantCount = |
| this.call.participantCounts().present + |
| this.call.participantCounts().hidden; |
| document.getElementById( |
| "participant-count" |
| ).textContent = `Participants: ${participantCount}`; |
| } |
|
|
| |
| |
| |
| |
| async leave() { |
| try { |
| await this.call.leave(); |
| document.querySelectorAll("#videos video, audio").forEach((el) => { |
| el.srcObject = null; |
| el.remove(); |
| }); |
| } catch (e) { |
| console.error("Leaving failed", e); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
|
|
| document.addEventListener("DOMContentLoaded", async () => { |
| const dailyCallManager = new DailyCallManager(); |
|
|
| |
| const urlParams = new URLSearchParams(window.location.search); |
| const roomUrlParam = urlParams.get("room_url"); |
|
|
| if (roomUrlParam) { |
| document.getElementById("room-url").value = roomUrlParam.trim(); |
| } |
|
|
| |
| document |
| .getElementById("join-btn") |
| .addEventListener("click", async function () { |
| const roomUrl = document.getElementById("room-url").value.trim(); |
| const joinToken = |
| document.getElementById("join-token").value.trim() || null; |
| await dailyCallManager.joinRoom(roomUrl, joinToken); |
| }); |
|
|
| |
| document.getElementById("leave-btn").addEventListener("click", function () { |
| dailyCallManager.leave(); |
| }); |
| }); |
|
|