forked from auracaster/bumble_mirror
221 lines
7.1 KiB
JavaScript
221 lines
7.1 KiB
JavaScript
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();
|
|
}
|
|
}()); |