DailyParticipantPermissions
Every participant object exposes apermissions field of type DailyParticipantPermissions:
See the DailyParticipantPermissions reference for the full interface definition and field descriptions.
Updating permissions at runtime
UseupdateParticipant() to change a participant’s permissions mid-call. The caller must themselves have canAdmin: 'participants' (or canAdmin: true) or be an owner.
call.updateParticipant(sessionId, {
updatePermissions: {
hasPresence: true,
canSend: new Set(['audio', 'video']),
canAdmin: false,
canReceive: {
base: true,
},
},
});
updatePermissions field accepts a DailyParticipantPermissionsUpdate, where every field is optional — only those provided are changed:
type DailyParticipantPermissionsUpdate = {
hasPresence?: boolean;
canSend?: Array<string> | Set<string> | boolean;
canReceive?: Partial<DailyParticipantCanReceivePermission>;
canAdmin?: Array<string> | Set<string> | boolean;
};
Restricting what a participant can send
The example below restricts a participant to audio-only (no camera or screen share):call.updateParticipant(participantSessionId, {
updatePermissions: {
canSend: new Set(['audio']),
},
});
call.updateParticipant(participantSessionId, {
updatePermissions: {
canSend: new Set(['audio', 'video']),
},
});
Restricting what a participant can receive
The example below allows a participant to receive audio from everyone but video only from a specific user:call.updateParticipant(participantSessionId, {
updatePermissions: {
canReceive: {
base: {
video: false,
audio: true,
screenVideo: false,
screenAudio: false,
customVideo: { '*': false },
customAudio: { '*': false },
},
byUserId: {
'presenter-user-id': { video: true },
},
},
},
});
Events
Fired when a participant’s permissions change. Check
participant.permissions to see the updated values.call.on('participant-updated', ({ participant }) => {
console.log('Updated permissions:', participant.permissions);
});
Complete example
Demonstrates the full permissions lifecycle: new participants join locked out, request presence via an app message, get admitted by the owner, and then have theircanSend permissions managed through a panel.
- Open
https://localhost?t=YOUR_OWNER_TOKENfor the owner view - Open
https://localhost(no token) for a participant view
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 });
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Permissions Demo</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="app">
<!-- Main area: video tiles for all visible participants -->
<div id="tracks"></div>
<!-- Owner panel -->
<aside id="owner-panel" hidden>
<h2>Participants</h2>
<select id="participant-select">
<option value="">— select participant —</option>
</select>
<div id="permission-checkboxes">
<label
><input type="checkbox" id="check-hasPresence" /> Has
presence</label
>
<label><input type="checkbox" id="check-audio" /> Send audio</label>
<label><input type="checkbox" id="check-video" /> Send video</label>
</div>
<button id="update-btn">Update permissions</button>
<!-- Popup shown when a participant requests presence -->
<div id="request-popup" hidden>
<p id="request-text"></p>
<button id="approve-btn">Approve</button>
<button id="deny-btn">Deny</button>
</div>
</aside>
<!-- Participant panel -->
<aside id="participant-panel" hidden>
<h2>Your permissions</h2>
<ul id="perms-list"></ul>
<button id="request-btn">Request presence</button>
</aside>
</div>
<script src="https://unpkg.com/@daily-co/daily-js"></script>
<script type="module" src="app.js"></script>
</body>
</html>
*,
*::before,
*::after {
box-sizing: border-box;
}
[hidden] {
display: none !important;
}
body {
font-family: sans-serif;
background: #0f0f1a;
color: #e0e0e0;
margin: 0;
height: 100vh;
overflow: hidden;
}
#app {
display: flex;
height: 100%;
}
/* ── Tracks area ── */
#tracks {
flex: 1;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 0.5rem;
padding: 1rem;
overflow-y: auto;
}
.tile {
position: relative;
width: 280px;
height: 210px;
background: #1a1a2e;
border-radius: 6px;
overflow: hidden;
}
.tile video {
width: 100%;
height: 100%;
object-fit: cover;
}
.tile-label {
position: absolute;
bottom: 6px;
left: 8px;
font-size: 0.75rem;
background: rgba(0, 0, 0, 0.55);
padding: 2px 6px;
border-radius: 3px;
}
/* ── Side panel ── */
aside {
width: 260px;
flex-shrink: 0;
background: #13131f;
border-left: 1px solid #2a2a3e;
padding: 1rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
h2 {
margin: 0;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #a0a0c0;
}
select {
width: 100%;
background: #1a1a2e;
color: #e0e0e0;
border: 1px solid #2a2a3e;
border-radius: 4px;
padding: 0.4rem;
font-size: 0.875rem;
}
#permission-checkboxes {
display: flex;
flex-direction: column;
gap: 0.4rem;
font-size: 0.875rem;
}
#permission-checkboxes label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
button {
padding: 0.45rem 0.9rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
background: #e94560;
color: #fff;
}
button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
#update-btn {
background: #2ecc71;
color: #fff;
}
/* ── Request popup ── */
#request-popup {
background: #1a1a2e;
border: 1px solid #2a2a3e;
border-radius: 6px;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
#request-text {
margin: 0;
font-size: 0.875rem;
}
#request-popup div {
display: flex;
gap: 0.5rem;
}
#approve-btn {
background: #2ecc71;
}
#deny-btn {
background: #e94560;
}
/* ── Permissions list ── */
#perms-list {
list-style: none;
margin: 0;
padding: 0;
font-size: 0.875rem;
display: flex;
flex-direction: column;
gap: 0.3rem;
}
#perms-list li.on {
color: #2ecc71;
}
#perms-list li.off {
color: #555577;
}