| | |
| | |
| | |
| | |
| | |
| | |
| | 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(); |
| | }); |
| | }); |
| |
|