/* Galene client example. */ /** * The main function. * * @param {string} url */ async function start(url) { // fetch the group information let r = await fetch(url + ".status"); if(!r.ok) { throw new Error(`${r.status} ${r.statusText}`); } let status = await r.json(); // parse a token in the URL. let token = null; let parms = new URLSearchParams(window.location.search); if(parms.has('token')) token = parms.get('token'); // connect to the server if(token) { serverConnect(status, token); } else if(status.authPortal) { window.location.href = groupStatus.authPortal return; } else { serverConnect(status, null); } } /** * Display the connection status. * * @parm {string} status */ function displayStatus(status) { let c = document.getElementById('status'); c.textContent = status; } /** * Connect to the server. * * @parm {Object} status * @parm {string} token */ function serverConnect(status, token) { // create the connection to the server let conn = new ServerConnection(); conn.onconnected = async function() { displayStatus('Connected'); let creds = token ? {type: 'token', token: token} : {type: 'password', password: ''}; // join the group and wait for the onjoined callback await this.join("public", "example-user", creds); }; conn.onchat = onChat; conn.onusermessage = onUserMessage; conn.ondownstream = onDownStream; conn.onclose = function() { displayStatus('Disconnected'); } conn.onjoined = onJoined; // connect and wait for the onconnected callback conn.connect(status.endpoint); } /** * Called whenever we receive a chat message. * * @this {ServerConnection} * @parm {string} username * @parm {string} message */ function onChat(id, dest, username, time, privileged, history, kind, message) { let p = document.createElement('p'); p.textContent = `${username}${dest ? ' → ' + dest : ''}: ${message}`; let container = document.getElementById('chat'); container.appendChild(p); } /** * Called whenever we receive a user message. * * @this {ServerConnection} * @parm {string} username * @parm {string} message * @parm {string} kind */ function onUserMessage(id, dest, username, time, privileged, kind, error, message) { switch(kind) { case 'kicked': case 'error': case 'warning': case 'info': if(!privileged) { console.error(`Got unprivileged message of kind ${kind}`); return; } displayError(message); break; case 'clearchat': if(!privileged) { console.error(`Got unprivileged message of kind ${kind}`); return; } document.getElementById('chat').textContent = ''; break; } } /** * Find the camera stream, if any. * * @parm {string} conn * @returns {Stream} */ function cameraStream(conn) { for(let id in conn.up) { let s = conn.up[id]; if(s.label === 'camera') return s; } return null; } /** * Enable or disable the show/hide button. * * @parm{ServerConnection} conn * @parm{boolean} enable */ function enableShow(conn, enable) { let b = /** @type{HTMLButtonElement} */(document.getElementById('show')); if(enable) { b.onclick = function() { let s = cameraStream(conn); if(!s) showCamera(conn); else hide(conn, s); } b.disabled = false; } else { b.disabled = true; b.onclick = null; } } /** * Called when we join or leave a group. * * @this {ServerConnection} * @parm {string} kind * @parm {string} message} */ async function onJoined(kind, group, perms, status, data, error, message) { switch(kind) { case 'fail': displayError(message); enableShow(this, false); this.close(); break; case 'redirect': this.close(); document.location.href = message; return; case 'leave': displayStatus('Connected'); enableShow(this, false); this.close(); break; case 'join': case 'change': displayStatus(`Connected as ${this.username} in group ${this.group}.`); enableShow(this, true); // request videos from the server this.request({'': ['audio', 'video']}); break; default: displayError(`Unexpected state ${kind}.`); this.close(); break; } } /** * Create a video element. We encode the stream's id in the element's id * in order to avoid having a global hash table that maps ids to video * elements. * * @parm {string} id * @returns {HTMLVideoElement} */ function makeVideoElement(id) { let v = document.createElement('video'); v.id = 'video-' + id; let container = document.getElementById('videos'); container.appendChild(v); return v; } /** * Find the video element that shows a given id. * * @parm {string} id * @returns {HTMLVideoElement} */ function getVideoElement(id) { let v = document.getElementById('video-' + id); return /** @type{HTMLVideoElement} */(v); } /** * Enable the camera and broadcast yourself to the group. * * @parm {ServerConnection} conn */ async function showCamera(conn) { let ms = await navigator.mediaDevices.getUserMedia({audio: true, video: true}); /* Send the new stream to the server */ let s = conn.newUpStream(); s.label = 'camera'; s.setStream(ms); let v = makeVideoElement(s.localId); s.onclose = function(replace) { s.stream.getTracks().forEach(t => t.stop()); v.srcObject = null; v.parentNode.removeChild(v); } function addTrack(t) { t.oneneded = function(e) { ms.onaddtrack = null; s.onremovetrack = null; s.close(); } s.pc.addTransceiver(t, { direction: 'sendonly', streams: [ms], }); } // Make sure all future tracks are added. s.onaddtrack = function(e) { addTrack(e.track); } // Add any existing tracks. ms.getTracks().forEach(addTrack); // Connect the MediaStream to the video element and start playing. v.srcObject = ms; v.muted = true; v.play(); } /** * Stop broadcasting. * * @parm {ServerConnection} conn * @parm {Stream} s */ async function hide(conn, s) { s.stream.getTracks().forEach(t => t.stop()); s.close(); } /** * Called when the server pushes a stream. * * @this {ServerConnection} * @parm {Stream} c */ function onDownStream(s) { s.onclose = function(replace) { let v = getVideoElement(s.localId); v.srcObject = null; v.parentNode.removeChild(v); } s.ondowntrack = function(track, transceiver, stream) { let v = getVideoElement(s.localId); if(v.srcObject !== stream) v.srcObject = stream; } let v = makeVideoElement(s.localId); v.srcObject = s.stream; v.play(); } /** * Display an error message. * * @parm {string} message */ function displayError(message) { document.getElementById('error').textContent = message; } document.getElementById('start').onclick = async function(e) { let button = /** @type{HTMLButtonElement} */(this); button.hidden = true; try { await start("/group/public/"); } catch(e) { displayError(e); }; }