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}());