import {setupSimpleApp} from '../bumble.js'; (function () { 'use strict'; let codecText; let packetsReceivedText; let bytesReceivedText; let streamStateText; let connectionStateText; let audioOnButton; let mediaSource; let sourceBuffer; let audioElement; let audioContext; let audioAnalyzer; let audioFrequencyBinCount; let audioFrequencyData; let packetsReceived = 0; let bytesReceived = 0; let audioState = 'stopped'; let streamState = 'IDLE'; let fftCanvas; let fftCanvasContext; let bandwidthCanvas; let bandwidthCanvasContext; let bandwidthBinCount; let bandwidthBins = []; const FFT_WIDTH = 800; const FFT_HEIGHT = 256; const BANDWIDTH_WIDTH = 500; const BANDWIDTH_HEIGHT = 100; function init() { initUI(); initMediaSource(); initAudioElement(); initAnalyzer(); initBumble(); } function initUI() { audioOnButton = document.getElementById('audioOnButton'); codecText = document.getElementById('codecText'); packetsReceivedText = document.getElementById('packetsReceivedText'); bytesReceivedText = document.getElementById('bytesReceivedText'); streamStateText = document.getElementById('streamStateText'); connectionStateText = document.getElementById('connectionStateText'); audioOnButton.onclick = startAudio; codecText.innerText = 'AAC'; requestAnimationFrame(onAnimationFrame); } function initMediaSource() { mediaSource = new MediaSource(); mediaSource.onsourceopen = onMediaSourceOpen; mediaSource.onsourceclose = onMediaSourceClose; mediaSource.onsourceended = onMediaSourceEnd; } function initAudioElement() { audioElement = document.getElementById('audio'); audioElement.src = URL.createObjectURL(mediaSource); // audioElement.controls = true; } function initAnalyzer() { fftCanvas = document.getElementById('fftCanvas'); fftCanvas.width = FFT_WIDTH fftCanvas.height = FFT_HEIGHT fftCanvasContext = fftCanvas.getContext('2d'); fftCanvasContext.fillStyle = 'rgb(0, 0, 0)'; fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT); bandwidthCanvas = document.getElementById('bandwidthCanvas'); bandwidthCanvas.width = BANDWIDTH_WIDTH bandwidthCanvas.height = BANDWIDTH_HEIGHT bandwidthCanvasContext = bandwidthCanvas.getContext('2d'); bandwidthCanvasContext.fillStyle = 'rgb(255, 255, 255)'; bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT); } async function initBumble() { const bumbleControls = document.querySelector('#bumble-controls'); const app = await setupSimpleApp('speaker.py', bumbleControls, console.log); app.on('start', onStart); app.on('stop', onStop); app.on('suspend', onSuspend); app.on('connection', onConnection); app.on('disconnection', onDisconnection); app.on('audio', onAudio); } function startAnalyzer() { // FFT if (audioElement.captureStream !== undefined) { audioContext = new AudioContext(); audioAnalyzer = audioContext.createAnalyser(); audioAnalyzer.fftSize = 128; audioFrequencyBinCount = audioAnalyzer.frequencyBinCount; audioFrequencyData = new Uint8Array(audioFrequencyBinCount); const stream = audioElement.captureStream(); const source = audioContext.createMediaStreamSource(stream); source.connect(audioAnalyzer); } // Bandwidth bandwidthBinCount = BANDWIDTH_WIDTH / 2; bandwidthBins = []; } function setStreamState(state) { streamState = state; streamStateText.innerText = streamState; } function onAnimationFrame() { // FFT if (audioAnalyzer !== undefined) { audioAnalyzer.getByteFrequencyData(audioFrequencyData); fftCanvasContext.fillStyle = 'rgb(0, 0, 0)'; fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT); const barCount = audioFrequencyBinCount; const barWidth = (FFT_WIDTH / audioFrequencyBinCount) - 1; for (let bar = 0; bar < barCount; bar++) { const barHeight = audioFrequencyData[bar]; fftCanvasContext.fillStyle = `rgb(${barHeight / 256 * 200 + 50}, 50, ${50 + 2 * bar})`; fftCanvasContext.fillRect(bar * (barWidth + 1), FFT_HEIGHT - barHeight, barWidth, barHeight); } } // Bandwidth bandwidthCanvasContext.fillStyle = 'rgb(255, 255, 255)'; bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT); bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`; for (let t = 0; t < bandwidthBins.length; t++) { const lineHeight = (bandwidthBins[t] / 1000) * BANDWIDTH_HEIGHT; bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight); } // Display again at the next frame requestAnimationFrame(onAnimationFrame); } function onMediaSourceOpen() { console.log(this.readyState); sourceBuffer = mediaSource.addSourceBuffer('audio/aac'); } function onMediaSourceClose() { console.log(this.readyState); } function onMediaSourceEnd() { console.log(this.readyState); } async function startAudio() { try { console.log('starting audio...'); audioOnButton.disabled = true; audioState = 'starting'; await audioElement.play(); console.log('audio started'); audioState = 'playing'; startAnalyzer(); } catch (error) { console.error(`play failed: ${error}`); audioState = 'stopped'; audioOnButton.disabled = false; } } function onStart() { setStreamState('STARTED'); } function onStop() { setStreamState('STOPPED'); } function onSuspend() { setStreamState('SUSPENDED'); } function onConnection(params) { connectionStateText.innerText = `CONNECTED: ${params.get('peer_name')} (${params.get('peer_address')})`; } function onDisconnection(params) { connectionStateText.innerText = 'DISCONNECTED'; } function onAudio(python_packet) { const packet = python_packet.toJs({create_proxies : false}); python_packet.destroy(); if (audioState != 'stopped') { // Queue the audio packet. sourceBuffer.appendBuffer(packet); } packetsReceived += 1; packetsReceivedText.innerText = packetsReceived; bytesReceived += packet.byteLength; bytesReceivedText.innerText = bytesReceived; bandwidthBins[bandwidthBins.length] = packet.byteLength; if (bandwidthBins.length > bandwidthBinCount) { bandwidthBins.shift(); } } window.onload = (event) => { init(); } }());