const ROOM_URL = 'https://your-domain.daily.co/room';
const token = new URLSearchParams(window.location.search).get('t') ?? undefined;
let canAdmin = false;
const call = Daily.createCallObject();
window.call = call; // for debugging
const participantIds = new Set();
window.participantIds = participantIds; // for debugging
const SEND_TRACKS = ['audio', 'video'];
// ── Helpers ──────────────────────────────────────────────────────────────────
function _canAdmin(permissions) {
const ca = permissions?.canAdmin;
if (ca === true) return true;
if (!ca) return false;
return ca instanceof Set ? ca.has('participants') : false;
}
function canSendSet(permissions) {
const cs = permissions?.canSend;
if (cs === true) return new Set(SEND_TRACKS);
if (!cs) return new Set();
return cs instanceof Set ? new Set(cs) : new Set(cs);
}
// ── Track rendering (shared) ─────────────────────────────────────────────────
function addParticipantTile(participant) {
const sessionId = participant.session_id;
let tile = document.getElementById(`tile-${sessionId}`);
if (tile) return tile;
tile = document.createElement('div');
tile.id = `tile-${sessionId}`;
tile.className = 'tile';
const label = document.createElement('span');
label.className = 'tile-label';
console.log(call.participants());
label.textContent = participant.local ? 'You' : sessionId.slice(0, 8);
tile.append(label);
document.getElementById('tracks').appendChild(tile);
return tile;
}
function addVideoTile(participant, track) {
const tile = addParticipantTile(participant);
if (!tile) return;
let video = tile.querySelector('video');
if (video) {
video.srcObject = new MediaStream([track]);
return;
}
video = document.createElement('video');
video.autoplay = true;
video.playsInline = true;
video.srcObject = new MediaStream([track]);
tile.append(video);
}
function addAudioTile(participant, track) {
const tile = addParticipantTile(participant);
if (!tile) return;
let audio = tile.querySelector('audio');
if (audio) {
audio.srcObject = new MediaStream([track]);
return;
}
audio = document.createElement('audio');
audio.autoplay = true;
audio.srcObject = new MediaStream([track]);
tile.append(audio);
}
call.on('track-started', ({ participant, track, type }) => {
console.log('track started', { participant, track, type });
if (type === 'video') addVideoTile(participant, track);
if (type === 'audio' && !participant.local) addAudioTile(participant, track);
});
call.on('track-stopped', ({ participant, type }) => {
if (type === 'video')
document.getElementById(`tile-${participant.session_id}`)?.remove();
});
// ── Owner panel ──────────────────────────────────────────────────────────────
let pendingSessionId = null;
function populateDropdown() {
const select = document.getElementById('participant-select');
let prev = select.value;
console.log(`Select.value: ${select.value}`);
select.innerHTML = '<option value="">— select participant —</option>';
for (const [id, p] of Object.entries(call.participants())) {
if (p.local) continue;
const opt = document.createElement('option');
opt.value = id;
opt.textContent = p.user_name || id.slice(0, 8);
select.appendChild(opt);
}
select.value = prev;
syncCheckboxes();
}
function syncCheckboxes() {
const id = document.getElementById('participant-select').value;
const participant = id ? call.participants()[id] : null;
const perms = participant?.permissions;
const sending = canSendSet(perms);
document.getElementById('check-hasPresence').checked =
perms?.hasPresence ?? false;
for (const track of SEND_TRACKS) {
document.getElementById(`check-${track}`).checked = sending.has(track);
}
}
document
.getElementById('participant-select')
.addEventListener('change', syncCheckboxes);
document.getElementById('update-btn').addEventListener('click', async () => {
const id = document.getElementById('participant-select').value;
if (!id) return;
const hasPresence = document.getElementById('check-hasPresence').checked;
const canSend = new Set(
SEND_TRACKS.filter((t) => document.getElementById(`check-${t}`).checked),
);
console.log('Updating permissions for', id, { hasPresence, canSend });
await call.updateParticipant(id, {
updatePermissions: { hasPresence, canSend },
});
});
call.on('app-message', ({ data }) => {
if (data?.type !== 'request-presence') return;
pendingSessionId = data.sessionId;
document.getElementById('request-text').textContent =
`Participant ${data.sessionId.slice(0, 8)} is requesting presence.`;
document.getElementById('request-popup').hidden = false;
});
document.getElementById('approve-btn').addEventListener('click', async () => {
if (!pendingSessionId) return;
// Grant presence only — canSend stays false until the owner enables it via the panel
console.log('Granting presence to', pendingSessionId);
await call.updateParticipant(pendingSessionId, {
updatePermissions: { hasPresence: true },
});
});
document.getElementById('deny-btn').addEventListener('click', () => {
pendingSessionId = null;
document.getElementById('request-popup').hidden = true;
});
// ── Participant panel ─────────────────────────────────────────────────────────
function updatePermsDisplay(permissions) {
const sending = canSendSet(permissions);
console.log('canSend:', sending);
const rows = [
['Has presence', permissions?.hasPresence],
['Audio', sending.has('audio')],
['Video', sending.has('video')],
];
document.getElementById('perms-list').innerHTML = rows
.map(
([label, on]) =>
`<li class="${on ? 'on' : 'off'}">${label}: ${on ? '✓' : '✗'}</li>`,
)
.join('');
}
document.getElementById('request-btn').addEventListener('click', () => {
call.sendAppMessage(
{
type: 'request-presence',
sessionId: call.participants().local.session_id,
},
'*',
);
document.getElementById('request-btn').textContent = 'Request sent…';
document.getElementById('request-btn').disabled = true;
});
// ── Shared event handlers ─────────────────────────────────────────────────────
call.on('participant-joined', (e) => {
const participant = e.participant;
console.log('Participant joined', e);
if (canAdmin) {
console.log(
'Participant joined:',
participantIds,
_canAdmin(participant.permissions),
);
if (!participantIds.has(participant.session_id)) {
if (!_canAdmin(participant.permissions)) {
// Lock out every new non-admin participant immediately
console.log('Denying presence to', participant.session_id);
call.updateParticipant(participant.session_id, {
updatePermissions: { hasPresence: false, canSend: false },
});
}
participantIds.add(participant.session_id);
} else if (pendingSessionId === participant.session_id) {
pendingSessionId = null;
document.getElementById('request-popup').hidden = true;
}
populateDropdown();
}
});
call.on('joined-meeting', () => {
canAdmin = _canAdmin(call.participants().local.permissions);
if (canAdmin) {
document.getElementById('owner-panel').hidden = false;
populateDropdown();
} else {
document.getElementById('participant-panel').hidden = false;
updatePermsDisplay(call.participants().local?.permissions);
}
});
call.on('participant-updated', ({ participant }) => {
if (canAdmin && !participant.local) {
populateDropdown();
}
if (!canAdmin && participant.local) {
updatePermsDisplay(participant.permissions);
// Re-enable request button if presence was revoked
if (!participant.permissions?.hasPresence) {
document.getElementById('request-btn').textContent = 'Request presence';
document.getElementById('request-btn').disabled = false;
}
const canSend = canSendSet(participant.permissions);
if (canSend.has('video') && !call.localVideo()) call.setLocalVideo(true);
if (canSend.has('audio') && !call.localAudio()) call.setLocalAudio(true);
}
});
call.on('participant-left', ({ participant, reason }) => {
document.getElementById(`tile-${participant.session_id}`)?.remove();
if (canAdmin) {
if (reason !== 'hidden') {
participantIds.delete(participant.session_id);
}
populateDropdown();
}
});
// ── Join ──────────────────────────────────────────────────────────────────────
if (token) {
await call.join({ url: ROOM_URL, token });
} else {
await call.join({ url: ROOM_URL });
}