1(function () { 2 'use strict'; 3 4const channelUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/channel"; 5let channelSocket; 6let connectionText; 7let codecText; 8let packetsReceivedText; 9let bytesReceivedText; 10let streamStateText; 11let connectionStateText; 12let controlsDiv; 13let audioOnButton; 14let mediaSource; 15let sourceBuffer; 16let audioElement; 17let audioContext; 18let audioAnalyzer; 19let audioFrequencyBinCount; 20let audioFrequencyData; 21let packetsReceived = 0; 22let bytesReceived = 0; 23let audioState = "stopped"; 24let streamState = "IDLE"; 25let audioSupportMessageText; 26let fftCanvas; 27let fftCanvasContext; 28let bandwidthCanvas; 29let bandwidthCanvasContext; 30let bandwidthBinCount; 31let bandwidthBins = []; 32 33const FFT_WIDTH = 800; 34const FFT_HEIGHT = 256; 35const BANDWIDTH_WIDTH = 500; 36const BANDWIDTH_HEIGHT = 100; 37 38function hexToBytes(hex) { 39 return Uint8Array.from(hex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); 40} 41 42function init() { 43 initUI(); 44 initMediaSource(); 45 initAudioElement(); 46 initAnalyzer(); 47 48 connect(); 49} 50 51function initUI() { 52 controlsDiv = document.getElementById("controlsDiv"); 53 controlsDiv.style.visibility = "hidden"; 54 connectionText = document.getElementById("connectionText"); 55 audioOnButton = document.getElementById("audioOnButton"); 56 codecText = document.getElementById("codecText"); 57 packetsReceivedText = document.getElementById("packetsReceivedText"); 58 bytesReceivedText = document.getElementById("bytesReceivedText"); 59 streamStateText = document.getElementById("streamStateText"); 60 connectionStateText = document.getElementById("connectionStateText"); 61 audioSupportMessageText = document.getElementById("audioSupportMessageText"); 62 63 audioOnButton.onclick = () => startAudio(); 64 65 setConnectionText(""); 66 67 requestAnimationFrame(onAnimationFrame); 68} 69 70function initMediaSource() { 71 mediaSource = new MediaSource(); 72 mediaSource.onsourceopen = onMediaSourceOpen; 73 mediaSource.onsourceclose = onMediaSourceClose; 74 mediaSource.onsourceended = onMediaSourceEnd; 75} 76 77function initAudioElement() { 78 audioElement = document.getElementById("audio"); 79 audioElement.src = URL.createObjectURL(mediaSource); 80 // audioElement.controls = true; 81} 82 83function initAnalyzer() { 84 fftCanvas = document.getElementById("fftCanvas"); 85 fftCanvas.width = FFT_WIDTH 86 fftCanvas.height = FFT_HEIGHT 87 fftCanvasContext = fftCanvas.getContext('2d'); 88 fftCanvasContext.fillStyle = "rgb(0, 0, 0)"; 89 fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT); 90 91 bandwidthCanvas = document.getElementById("bandwidthCanvas"); 92 bandwidthCanvas.width = BANDWIDTH_WIDTH 93 bandwidthCanvas.height = BANDWIDTH_HEIGHT 94 bandwidthCanvasContext = bandwidthCanvas.getContext('2d'); 95 bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)"; 96 bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT); 97} 98 99function startAnalyzer() { 100 // FFT 101 if (audioElement.captureStream !== undefined) { 102 audioContext = new AudioContext(); 103 audioAnalyzer = audioContext.createAnalyser(); 104 audioAnalyzer.fftSize = 128; 105 audioFrequencyBinCount = audioAnalyzer.frequencyBinCount; 106 audioFrequencyData = new Uint8Array(audioFrequencyBinCount); 107 const stream = audioElement.captureStream(); 108 const source = audioContext.createMediaStreamSource(stream); 109 source.connect(audioAnalyzer); 110 } 111 112 // Bandwidth 113 bandwidthBinCount = BANDWIDTH_WIDTH / 2; 114 bandwidthBins = []; 115} 116 117function setConnectionText(message) { 118 connectionText.innerText = message; 119 if (message.length == 0) { 120 connectionText.style.display = "none"; 121 } else { 122 connectionText.style.display = "inline-block"; 123 } 124} 125 126function setStreamState(state) { 127 streamState = state; 128 streamStateText.innerText = streamState; 129} 130 131function onAnimationFrame() { 132 // FFT 133 if (audioAnalyzer !== undefined) { 134 audioAnalyzer.getByteFrequencyData(audioFrequencyData); 135 fftCanvasContext.fillStyle = "rgb(0, 0, 0)"; 136 fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT); 137 const barCount = audioFrequencyBinCount; 138 const barWidth = (FFT_WIDTH / audioFrequencyBinCount) - 1; 139 for (let bar = 0; bar < barCount; bar++) { 140 const barHeight = audioFrequencyData[bar]; 141 fftCanvasContext.fillStyle = `rgb(${barHeight / 256 * 200 + 50}, 50, ${50 + 2 * bar})`; 142 fftCanvasContext.fillRect(bar * (barWidth + 1), FFT_HEIGHT - barHeight, barWidth, barHeight); 143 } 144 } 145 146 // Bandwidth 147 bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)"; 148 bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT); 149 bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`; 150 for (let t = 0; t < bandwidthBins.length; t++) { 151 const lineHeight = (bandwidthBins[t] / 1000) * BANDWIDTH_HEIGHT; 152 bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight); 153 } 154 155 // Display again at the next frame 156 requestAnimationFrame(onAnimationFrame); 157} 158 159function onMediaSourceOpen() { 160 console.log(this.readyState); 161 sourceBuffer = mediaSource.addSourceBuffer("audio/aac"); 162} 163 164function onMediaSourceClose() { 165 console.log(this.readyState); 166} 167 168function onMediaSourceEnd() { 169 console.log(this.readyState); 170} 171 172async function startAudio() { 173 try { 174 console.log("starting audio..."); 175 audioOnButton.disabled = true; 176 audioState = "starting"; 177 await audioElement.play(); 178 console.log("audio started"); 179 audioState = "playing"; 180 startAnalyzer(); 181 } catch(error) { 182 console.error(`play failed: ${error}`); 183 audioState = "stopped"; 184 audioOnButton.disabled = false; 185 } 186} 187 188function onAudioPacket(packet) { 189 if (audioState != "stopped") { 190 // Queue the audio packet. 191 sourceBuffer.appendBuffer(packet); 192 } 193 194 packetsReceived += 1; 195 packetsReceivedText.innerText = packetsReceived; 196 bytesReceived += packet.byteLength; 197 bytesReceivedText.innerText = bytesReceived; 198 199 bandwidthBins[bandwidthBins.length] = packet.byteLength; 200 if (bandwidthBins.length > bandwidthBinCount) { 201 bandwidthBins.shift(); 202 } 203} 204 205function onChannelOpen() { 206 console.log('channel OPEN'); 207 setConnectionText(""); 208 controlsDiv.style.visibility = "visible"; 209 210 // Handshake with the backend. 211 sendMessage({ 212 type: "hello" 213 }); 214} 215 216function onChannelClose() { 217 console.log('channel CLOSED'); 218 setConnectionText("Connection to CLI app closed, restart it and reload this page."); 219 controlsDiv.style.visibility = "hidden"; 220} 221 222function onChannelError(error) { 223 console.log(`channel ERROR: ${error}`); 224 setConnectionText(`Connection to CLI app error ({${error}}), restart it and reload this page.`); 225 controlsDiv.style.visibility = "hidden"; 226} 227 228function onChannelMessage(message) { 229 if (typeof message.data === 'string' || message.data instanceof String) { 230 // JSON message. 231 const jsonMessage = JSON.parse(message.data); 232 console.log(`channel MESSAGE: ${message.data}`); 233 234 // Dispatch the message. 235 const handlerName = `on${jsonMessage.type.charAt(0).toUpperCase()}${jsonMessage.type.slice(1)}Message` 236 const handler = messageHandlers[handlerName]; 237 if (handler !== undefined) { 238 const params = jsonMessage.params; 239 if (params === undefined) { 240 params = {}; 241 } 242 handler(params); 243 } else { 244 console.warn(`unhandled message: ${jsonMessage.type}`) 245 } 246 } else { 247 // BINARY audio data. 248 onAudioPacket(message.data); 249 } 250} 251 252function onHelloMessage(params) { 253 codecText.innerText = params.codec; 254 if (params.codec != "aac") { 255 audioOnButton.disabled = true; 256 audioSupportMessageText.innerText = "Only AAC can be played, audio will be disabled"; 257 audioSupportMessageText.style.display = "inline-block"; 258 } else { 259 audioSupportMessageText.innerText = ""; 260 audioSupportMessageText.style.display = "none"; 261 } 262 if (params.streamState) { 263 setStreamState(params.streamState); 264 } 265} 266 267function onStartMessage(params) { 268 setStreamState("STARTED"); 269} 270 271function onStopMessage(params) { 272 setStreamState("STOPPED"); 273} 274 275function onSuspendMessage(params) { 276 setStreamState("SUSPENDED"); 277} 278 279function onConnectionMessage(params) { 280 connectionStateText.innerText = `CONNECTED: ${params.peer_name} (${params.peer_address})`; 281} 282 283function onDisconnectionMessage(params) { 284 connectionStateText.innerText = "DISCONNECTED"; 285} 286 287function sendMessage(message) { 288 channelSocket.send(JSON.stringify(message)); 289} 290 291function connect() { 292 console.log("connecting to CLI app"); 293 294 channelSocket = new WebSocket(channelUrl); 295 channelSocket.binaryType = "arraybuffer"; 296 channelSocket.onopen = onChannelOpen; 297 channelSocket.onclose = onChannelClose; 298 channelSocket.onerror = onChannelError; 299 channelSocket.onmessage = onChannelMessage; 300} 301 302const messageHandlers = { 303 onHelloMessage, 304 onStartMessage, 305 onStopMessage, 306 onSuspendMessage, 307 onConnectionMessage, 308 onDisconnectionMessage 309} 310 311window.onload = (event) => { 312 init(); 313} 314 315}());