diff --git a/static/example/example.html b/static/example/example.html new file mode 100644 index 0000000..acdfca8 --- /dev/null +++ b/static/example/example.html @@ -0,0 +1,22 @@ + + +
+Disconnected
+ + + + + + + + + + + + diff --git a/static/example/example.js b/static/example/example.js new file mode 100644 index 0000000..6fbce10 --- /dev/null +++ b/static/example/example.js @@ -0,0 +1,255 @@ +/* Galene client example. Send-only for now. */ + +/** + * 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) { + await serverConnect(status, token); + } else if(status.authPortal) { + window.location.href = groupStatus.authPortal + } 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 + */ +async 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 = function(s) { + // This should not happen, since we didn't ask to receive streams. + console.warn('Received unexpected stream from server'); + s.abort(); + } + conn.onclose = function() { + displayStatus('Disconnected'); + } + conn.onjoined = onJoined; + + // connect and wait for the onconnected callback + await 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); + break; + default: + displayError(`Unexpected state ${kind}.`); + this.close(); + break; + } +} + +/** + * Enable the camera and broadcast yourself to the group. + * + * @parm {ServerConnection} conn + */ +async function showCamera(conn) { + let v = /** @type HTMLVideoElement */(document.getElementById('video')); + 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); + s.onclose = function(replace) { + v.srcObject = null; + } + + 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(); +} + +/** + * Display an error message. + * + * @parm {string} message + */ +function displayError(message) { + document.getElementById('error').textContent = message; +} + +start("/group/public/").catch(e => displayError(e)); diff --git a/static/tsconfig.json b/static/tsconfig.json index fdfcc72..b329ede 100644 --- a/static/tsconfig.json +++ b/static/tsconfig.json @@ -15,6 +15,7 @@ "files": [ "protocol.js", "galene.js", - "management.js" + "management.js", + "example/example.js" ] }