1import {setupSimpleApp} from '../bumble.js';
2
3(function () {
4    'use strict';
5
6    let codecText;
7    let packetsReceivedText;
8    let bytesReceivedText;
9    let streamStateText;
10    let connectionStateText;
11    let audioOnButton;
12    let mediaSource;
13    let sourceBuffer;
14    let audioElement;
15    let audioContext;
16    let audioAnalyzer;
17    let audioFrequencyBinCount;
18    let audioFrequencyData;
19    let packetsReceived = 0;
20    let bytesReceived = 0;
21    let audioState = 'stopped';
22    let streamState = 'IDLE';
23    let fftCanvas;
24    let fftCanvasContext;
25    let bandwidthCanvas;
26    let bandwidthCanvasContext;
27    let bandwidthBinCount;
28    let bandwidthBins = [];
29
30    const FFT_WIDTH = 800;
31    const FFT_HEIGHT = 256;
32    const BANDWIDTH_WIDTH = 500;
33    const BANDWIDTH_HEIGHT = 100;
34
35
36    function init() {
37        initUI();
38        initMediaSource();
39        initAudioElement();
40        initAnalyzer();
41        initBumble();
42    }
43
44    function initUI() {
45        audioOnButton = document.getElementById('audioOnButton');
46        codecText = document.getElementById('codecText');
47        packetsReceivedText = document.getElementById('packetsReceivedText');
48        bytesReceivedText = document.getElementById('bytesReceivedText');
49        streamStateText = document.getElementById('streamStateText');
50        connectionStateText = document.getElementById('connectionStateText');
51
52        audioOnButton.onclick = startAudio;
53
54        codecText.innerText = 'AAC';
55
56        requestAnimationFrame(onAnimationFrame);
57    }
58
59    function initMediaSource() {
60        mediaSource = new MediaSource();
61        mediaSource.onsourceopen = onMediaSourceOpen;
62        mediaSource.onsourceclose = onMediaSourceClose;
63        mediaSource.onsourceended = onMediaSourceEnd;
64    }
65
66    function initAudioElement() {
67        audioElement = document.getElementById('audio');
68        audioElement.src = URL.createObjectURL(mediaSource);
69        // audioElement.controls = true;
70    }
71
72    function initAnalyzer() {
73        fftCanvas = document.getElementById('fftCanvas');
74        fftCanvas.width = FFT_WIDTH
75        fftCanvas.height = FFT_HEIGHT
76        fftCanvasContext = fftCanvas.getContext('2d');
77        fftCanvasContext.fillStyle = 'rgb(0, 0, 0)';
78        fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT);
79
80        bandwidthCanvas = document.getElementById('bandwidthCanvas');
81        bandwidthCanvas.width = BANDWIDTH_WIDTH
82        bandwidthCanvas.height = BANDWIDTH_HEIGHT
83        bandwidthCanvasContext = bandwidthCanvas.getContext('2d');
84        bandwidthCanvasContext.fillStyle = 'rgb(255, 255, 255)';
85        bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
86    }
87
88    async function initBumble() {
89        const bumbleControls = document.querySelector('#bumble-controls');
90        const app = await setupSimpleApp('speaker.py', bumbleControls, console.log);
91        app.on('start', onStart);
92        app.on('stop', onStop);
93        app.on('suspend', onSuspend);
94        app.on('connection', onConnection);
95        app.on('disconnection', onDisconnection);
96        app.on('audio', onAudio);
97    }
98
99    function 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
117    function setStreamState(state) {
118        streamState = state;
119        streamStateText.innerText = streamState;
120    }
121
122    function onAnimationFrame() {
123        // FFT
124        if (audioAnalyzer !== undefined) {
125            audioAnalyzer.getByteFrequencyData(audioFrequencyData);
126            fftCanvasContext.fillStyle = 'rgb(0, 0, 0)';
127            fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT);
128            const barCount = audioFrequencyBinCount;
129            const barWidth = (FFT_WIDTH / audioFrequencyBinCount) - 1;
130            for (let bar = 0; bar < barCount; bar++) {
131                const barHeight = audioFrequencyData[bar];
132                fftCanvasContext.fillStyle = `rgb(${barHeight / 256 * 200 + 50}, 50, ${50 + 2 * bar})`;
133                fftCanvasContext.fillRect(bar * (barWidth + 1), FFT_HEIGHT - barHeight, barWidth, barHeight);
134            }
135        }
136
137        // Bandwidth
138        bandwidthCanvasContext.fillStyle = 'rgb(255, 255, 255)';
139        bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
140        bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`;
141        for (let t = 0; t < bandwidthBins.length; t++) {
142            const lineHeight = (bandwidthBins[t] / 1000) * BANDWIDTH_HEIGHT;
143            bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight);
144        }
145
146        // Display again at the next frame
147        requestAnimationFrame(onAnimationFrame);
148    }
149
150    function onMediaSourceOpen() {
151        console.log(this.readyState);
152        sourceBuffer = mediaSource.addSourceBuffer('audio/aac');
153    }
154
155    function onMediaSourceClose() {
156        console.log(this.readyState);
157    }
158
159    function onMediaSourceEnd() {
160        console.log(this.readyState);
161    }
162
163    async function startAudio() {
164        try {
165            console.log('starting audio...');
166            audioOnButton.disabled = true;
167            audioState = 'starting';
168            await audioElement.play();
169            console.log('audio started');
170            audioState = 'playing';
171            startAnalyzer();
172        } catch (error) {
173            console.error(`play failed: ${error}`);
174            audioState = 'stopped';
175            audioOnButton.disabled = false;
176        }
177    }
178
179    function onStart() {
180        setStreamState('STARTED');
181    }
182
183    function onStop() {
184        setStreamState('STOPPED');
185    }
186
187    function onSuspend() {
188        setStreamState('SUSPENDED');
189    }
190
191    function onConnection(params) {
192        connectionStateText.innerText = `CONNECTED: ${params.get('peer_name')} (${params.get('peer_address')})`;
193    }
194
195    function onDisconnection(params) {
196        connectionStateText.innerText = 'DISCONNECTED';
197    }
198
199    function onAudio(python_packet) {
200        const packet = python_packet.toJs({create_proxies : false});
201        python_packet.destroy();
202        if (audioState != 'stopped') {
203            // Queue the audio packet.
204            sourceBuffer.appendBuffer(packet);
205        }
206
207        packetsReceived += 1;
208        packetsReceivedText.innerText = packetsReceived;
209        bytesReceived += packet.byteLength;
210        bytesReceivedText.innerText = bytesReceived;
211
212        bandwidthBins[bandwidthBins.length] = packet.byteLength;
213        if (bandwidthBins.length > bandwidthBinCount) {
214            bandwidthBins.shift();
215        }
216    }
217
218    window.onload = (event) => {
219        init();
220    }
221}());