Skip to content

Instantly share code, notes, and snippets.

@kwindla
Created June 20, 2021 03:52
Show Gist options
  • Save kwindla/9b03f93f0282a3360d2b3afd83f22719 to your computer and use it in GitHub Desktop.
Save kwindla/9b03f93f0282a3360d2b3afd83f22719 to your computer and use it in GitHub Desktop.
Daily video API -- simple audio-only sample code
<html>
<head>
<title>audio track subs demo</title>
<script src="https://unpkg.com/@daily-co/daily-js"></script>
</head>
<!--
audio-only sample code
-->
<body onload="main()">
<div id="local-controls">
<div id="join-leave"></div>
<hr />
</div>
<div id="participants"></div>
<script>
// CHANGE THIS TO A ROOM AND API KEY WITHIN YOUR DAILY ACCOUNT
// THAT HAS THE sfu_switchover PROPERTY SET TO 1, SO WE ARE ALWAYS
// IN MEDIA SERVER (NOT PEER-TO-PEER) MODE. TRACK SUBSCRIPTIONS ARE
// SUPPORTED ONLY IN MEDIA SERVER MODE
const ROOM_URL = 'https://YOUR-TEAM.daily.co/ROOM';
async function main() {
// set up join-meeting UI elements
leftMeeting();
// create the Daily call object
window.call = DailyIframe.createCallObject({
videoSource: false,
subscribeToTracksAutomatically: false,
url: ROOM_URL,
});
// set up event handlers
call.on('error', (e) => console.error(e));
call.on('joined-meeting', () => {
console.log('[joined-meeting]');
joinedMeeting();
});
call.on('left-meeting', () => {
console.log('[left-meeting]');
leftMeeting();
});
call.on('participant-joined', (e) => {
console.log('[participant-joined]', e);
addParticipant(e);
});
call.on('participant-updated', (e) => {
console.log('[participant-updated]', e);
updateParticipant(e);
});
call.on('participant-left', (e) => {
console.log('[participant-left]', e);
removeParticipant(e);
});
// when an audio track is playable, we will get a track-started event.
call.on('track-started', (e) => {
console.log('[track-started]', e);
playAudio(e);
});
// don't do anything when a track becomes unplayable. this could happen
// because of packet loss on the network, or because we unsubscribe
// or the participant leaves. if we unsubscribe or the participant leaves,
// we'll clean the audio element up.
call.on('track-stopped', (e) => console.log('[track-stopped]', e));
}
// ----
// called by 'joined-meeting' event handler when the local client connects to the session
function joinedMeeting() {
const joinEl = document.getElementById('join-leave');
joinEl.innerHTML = `
<div>${call.participants().local.session_id}</div>
<button id="toggle-mute" onclick="toggleMute()">mute</button> |
<button onclick="document.getElementById('join-leave').innerHTML=''; call.leave()">leave</button>
<div id="send-recv-kbs"></div>
`;
window.statsDisplayFunc = window.setInterval(async () => {
const stats = (await call.getNetworkStats()).stats.latest;
const sendKbs = Math.round(stats.sendBitsPerSecond / 1000);
const recvKbs = Math.round(stats.recvBitsPerSecond / 1000);
document.getElementById('send-recv-kbs').innerHTML = `
bitrates: &#11014;&#65039; ${sendKbs} &nbsp;&nbsp;&nbsp; &#11015;&#65039; ${recvKbs}
`;
Object.values(call.participants()).forEach(async (p) => {
if (p.local) {
return;
}
const audioLevelEl = document.querySelector(
`#participants div[data-participant-id='${p.session_id}'] .tx-audio-level`
);
if (audioLevelEl) {
audioLevelEl.innerHTML =
'tx audio level: ' +
((await getParticipantAudioLevel(p.session_id)) || 0);
}
});
}, 2000);
}
// called by 'left-meeting' event handler called when the local client leaves the session
function leftMeeting() {
clearInterval(window.statsDisplayFunc);
const joinEl = document.getElementById('join-leave');
joinEl.innerHTML = `
<button onclick="document.getElementById('join-leave').innerHTML=''; call.join()">join</button>
`;
const participantsEl = document.getElementById('participants');
participants.innerHTML = '';
if (window.call) {
window.call.leave();
}
}
// ----
function toggleMute() {
const muteButtonEl = document.getElementById('toggle-mute');
if (call.localAudio()) {
call.setLocalAudio(false);
muteButtonEl.innerHTML = 'unmute';
} else {
call.setLocalAudio(true);
muteButtonEl.innerHTML = 'mute';
}
}
// ----
// called by 'participant-joined' event handler when a new participant has joined the session
function addParticipant(e) {
const id = e.participant.session_id;
const el = document.createElement('div');
el.dataset.participantId = id;
el.innerHTML = `
<div class="participant-id">${id}</div>
<div class="subscribe-toggle"><button onclick="toggleSubscribe(this, '${id}')">subscribe</button></div>
<input class="volume-slider" type="range" min="0" max="100" value="100" oninput="changeVolume('${id}')"></input>
<div class="tx-audio-level">0</div>
<hr />
`;
document.getElementById('participants').appendChild(el);
}
// called by 'participant-updated' event handler when any information in the
// participants() data structure for a participant changes
function updateParticipant(e) {
// nothing to do in this very simple ux. we might want to
// monitor for participant updates to get the remote
// audio mute state, for example.
}
// called by 'participant-left' event handler whean a participant has left the session
function removeParticipant(e) {
const participantEl = document.querySelector(
`#participants div[data-participant-id='${e.participant.session_id}']`
);
if (!participantEl) {
return;
}
participantEl.remove();
}
function toggleSubscribe(buttonEl, id) {
const p = call.participants()[id];
if (!p) {
return;
}
if (p.tracks.audio.subscribed) {
console.log('unsubscribing from audio for', id);
call.updateParticipant(id, { setSubscribedTracks: { audio: false } });
buttonEl.innerText = 'subscribe';
} else {
console.log('subscribing to audio for', id);
call.updateParticipant(id, { setSubscribedTracks: { audio: true } });
buttonEl.innerText = 'unsubscribe';
}
}
function changeVolume(id) {
const rangeEl = document.querySelector(
`#participants div[data-participant-id='${id}'] .volume-slider`
);
const volume = rangeEl.value / 100;
console.log('changing audio volume', volume, id);
const audioEl = findAudioElement(id);
audioEl && (audioEl.volume = volume);
}
// ----
// called by 'track-started' event handler when a track become playable
function playAudio(e) {
// ignore the local audio track (we never want to play the local mic audio)
if (e.participant.local) {
return;
}
// sanity check
if (e.track.kind !== 'audio') {
return;
}
let audioEl = findAudioElement(e.participant.session_id);
if (!audioEl) {
// create audio element for this participant if we don't already
// have one
console.log('creating the audio element');
audioEl = document.createElement('audio');
audioEl.dataset.participantId = e.participant.session_id;
document.body.appendChild(audioEl);
}
// if the track already matches, we don't need to do anything. the
// browser machinery will play the track as the bytes start to
// come in again
if (
audioEl.srcObject &&
audioEl.srcObject.getAudioTracks()[0] === e.track
) {
console.log('already have the track');
return;
}
console.log('attaching the track and playing it');
audioEl.srcObject = new MediaStream([e.track]);
audioEl.play().catch((e) => console.error(e));
changeVolume(e.participant.session_id);
}
function findAudioElement(participantId) {
return document.querySelector(
`audio[data-participant-id='${participantId}']`
);
}
// It's possible to get the audio level of an incoming track in several ways.
// In the past, the best practice was to attach a little worker process to
// the audio track and do the calculation in javascript. That uses a fair amount
// of CPU. Chrome and Safari now have support for a WebRTC stats entry with the
// audio level calculation. This uses much less CPU. But note that the audio track
// must be attached to an <audio> element for the browser to fill in the
// audioLevel stat.
//
// We also do a version of this calculation on the media server, but exposing that
// to all clients is not implemented.
//
// We will add a public API for this, soon. In the meantime, you can reach
// into the daily-js internals in the following way ...
async function getParticipantAudioLevel(participantId) {
try {
if (!(window.rtcpeers && window.rtcpeers.sfu)) {
return;
}
const consumer =
window.rtcpeers.sfu.consumers[participantId + '/cam-audio'];
if (!(consumer && consumer.getStats)) {
return;
}
return Array.from((await consumer.getStats()).values()).find(
(s) => 'audioLevel' in s
).audioLevel;
} catch (e) {
console.error(e);
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment