Created
June 20, 2021 03:52
-
-
Save kwindla/9b03f93f0282a3360d2b3afd83f22719 to your computer and use it in GitHub Desktop.
Daily video API -- simple audio-only sample code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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: ⬆️ ${sendKbs} ⬇️ ${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