pearCast/app.js
2024-11-23 05:06:43 -05:00

340 lines
11 KiB
JavaScript

import Hyperswarm from 'hyperswarm';
import crypto from 'hypercore-crypto';
import b4a from 'b4a';
let swarm;
let micStream;
let audioContext;
let isBroadcasting = false;
let conns = [];
let currentDeviceId = null; // To store the selected audio input device ID
let accumulatedBuffer = b4a.alloc(0); // Buffer for accumulating received audio data
let stationKey = crypto.randomBytes(32); // Default random key for the station
document.addEventListener("DOMContentLoaded", () => {
document.getElementById('create-station').addEventListener('click', () => {
const createStationModal = new bootstrap.Modal(document.getElementById('createStationModal'));
createStationModal.show();
});
document.getElementById('generate-new-key').addEventListener('click', () => {
stationKey = crypto.randomBytes(32);
document.getElementById('existing-key').value = b4a.toString(stationKey, 'hex');
});
document.getElementById('create-station-button').addEventListener('click', () => {
const existingKey = document.getElementById('existing-key').value.trim();
stationKey = existingKey ? b4a.from(existingKey, 'hex') : stationKey;
setupStation(stationKey);
const createStationModal = bootstrap.Modal.getInstance(document.getElementById('createStationModal'));
createStationModal.hide();
});
document.getElementById('leave-stream').addEventListener('click', () => {
stopBroadcast();
leaveStation();
});
document.getElementById('join-station-button').addEventListener('click', joinStation);
document.getElementById('apply-audio-source').addEventListener('click', applyAudioSource);
// document.getElementById('apply-output-device').addEventListener('click', applyOutputDevice);
populateAudioInputSources();
populateAudioOutputSources();
});
// Update peer count in the UI
function updatePeerCount() {
const peerCount = conns.length;
const stationInfoElement = document.getElementById('station-info');
if (isBroadcasting) {
stationInfoElement.textContent = `Station ID: ${b4a.toString(stationKey, 'hex')}\nConnected Peers: ${peerCount}`;
} else {
stationInfoElement.textContent = `Station ID: ${b4a.toString(stationKey, 'hex')}\nConnected Peers: ${peerCount}`;
}
console.log(`Peer count updated: ${peerCount}`);
}
// Populate audio input sources
async function populateAudioInputSources() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const audioInputSelect = document.getElementById('audio-input-select');
audioInputSelect.innerHTML = '';
devices.forEach((device) => {
if (device.kind === 'audioinput') {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || `Microphone ${audioInputSelect.length + 1}`;
audioInputSelect.appendChild(option);
}
});
currentDeviceId = audioInputSelect.value;
} catch (err) {
console.error("Error enumerating devices:", err);
}
}
// Populate audio output sources
async function populateAudioOutputSources() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const audioOutputSelect = document.getElementById('audio-output-select');
audioOutputSelect.innerHTML = '';
devices.forEach((device) => {
if (device.kind === 'audiooutput') {
// const option = document.createElement('option');
// option.value = device.deviceId;
// option.textContent = device.label || `Speaker ${audioOutputSelect.length + 1}`;
// audioOutputSelect.appendChild(option);
}
});
} catch (err) {
console.error("Error enumerating output devices:", err);
}
}
// Apply the selected audio output device
// Apply the selected audio output device
async function applyOutputDevice() {
const selectedDeviceId = document.getElementById('audio-output-select').value;
try {
if (!selectedDeviceId) {
console.warn("No output device selected.");
return;
}
// Ensure there is a valid audio element
const audioElement = document.createElement('audio');
audioElement.autoplay = true; // Play automatically when data is received
audioElement.controls = true; // Debugging purposes (can be removed)
document.body.appendChild(audioElement); // Add temporarily for testing
// Set the audio sink to the selected device
if (typeof audioElement.setSinkId === 'function') {
await audioElement.setSinkId(selectedDeviceId);
console.log(`Audio output device applied: ${selectedDeviceId}`);
} else {
console.error("setSinkId is not supported in this browser.");
}
} catch (err) {
console.error("Error applying audio output device:", err);
}
}
// Apply the selected audio input source
async function applyAudioSource() {
const selectedDeviceId = document.getElementById('audio-input-select').value;
if (selectedDeviceId !== currentDeviceId) {
currentDeviceId = selectedDeviceId;
stopBroadcast();
startBroadcast();
}
}
// Start broadcasting from the microphone
async function startBroadcast() {
if (isBroadcasting) stopBroadcast();
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
micStream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: currentDeviceId ? { exact: currentDeviceId } : undefined },
});
const source = audioContext.createMediaStreamSource(micStream);
const processor = audioContext.createScriptProcessor(4096, 1, 1);
source.connect(processor);
processor.connect(audioContext.destination);
processor.onaudioprocess = (event) => {
const audioData = event.inputBuffer.getChannelData(0);
const buffer = b4a.from(new Float32Array(audioData).buffer);
for (const conn of conns) {
conn.write(buffer);
}
};
isBroadcasting = true;
console.log("Broadcasting started.");
} catch (err) {
console.error("Error starting broadcast:", err);
}
}
// Stop broadcasting
function stopBroadcast() {
if (!isBroadcasting) return;
broadcastStopSignal();
if (micStream) {
micStream.getTracks().forEach((track) => track.stop());
micStream = null;
}
if (audioContext) {
audioContext.close();
audioContext = null;
}
accumulatedBuffer = b4a.alloc(0);
isBroadcasting = false;
console.log("Broadcasting stopped.");
}
// Broadcast a stop signal to all peers
function broadcastStopSignal() {
for (const conn of conns) {
conn.write(Buffer.alloc(0));
}
}
function processIncomingAudioData(data) {
// Ensure audioContext is initialized
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
console.log("AudioContext initialized.");
}
// Accumulate incoming data
accumulatedBuffer = b4a.concat([accumulatedBuffer, data]);
// Debug log for accumulated buffer size
console.log("Accumulated buffer size:", accumulatedBuffer.byteLength);
// Process audio data in chunks
while (accumulatedBuffer.byteLength >= 4096) {
try {
const chunkSize = 4096; // Process fixed-size chunks
const audioData = new Float32Array(accumulatedBuffer.slice(0, chunkSize).buffer);
accumulatedBuffer = accumulatedBuffer.slice(chunkSize); // Remove processed data
const sampleRate = audioContext.sampleRate || 44100; // Use context sample rate or default
const buffer = audioContext.createBuffer(1, audioData.length, sampleRate);
// Copy data to the audio buffer
buffer.copyToChannel(audioData, 0);
// Create and configure audio buffer source
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start();
// Debug log for processed audio chunk
console.log("Audio chunk processed and played. Chunk size:", chunkSize);
} catch (err) {
console.error("Error processing audio data:", err);
break; // Stop processing on error to prevent cascading issues
}
}
}
// Setup a broadcasting station
async function setupStation(key) {
swarm = new Hyperswarm();
swarm.join(key, { client: false, server: true });
document.getElementById('broadcaster-controls').classList.remove('d-none');
document.getElementById('setup').classList.add('d-none');
document.getElementById('controls').classList.remove('d-none');
startBroadcast();
updatePeerCount();
swarm.on('connection', (conn) => {
conns.push(conn);
console.log("Peer connected. Total peers:", conns.length);
updatePeerCount();
conn.once('close', () => {
conns.splice(conns.indexOf(conn), 1);
console.log("Peer disconnected. Total peers:", conns.length);
updatePeerCount();
});
conn.on('data', handleData);
conn.on('error', (err) => {
if (err.code === 'ECONNRESET') {
console.log("Peer connection reset by remote peer.");
} else {
console.error("Connection error:", err);
}
conns.splice(conns.indexOf(conn), 1);
updatePeerCount();
});
});
}
// Leave the station
function leaveStation() {
if (swarm) swarm.destroy();
document.getElementById('setup').classList.remove('d-none');
document.getElementById('controls').classList.add('d-none');
document.getElementById('broadcaster-controls').classList.add('d-none');
stopBroadcast();
console.log("Left the station.");
}
// Handle incoming data from peers
function handleData(data) {
if (data.length === 0) {
stopBroadcast();
} else {
processIncomingAudioData(data);
}
}
// Join an existing station
async function joinStation() {
const stationId = document.getElementById('station-id').value;
if (!stationId) {
alert("Please enter a station ID.");
return;
}
const topicBuffer = b4a.from(stationId, 'hex');
swarm = new Hyperswarm();
swarm.join(topicBuffer, { client: true, server: false });
document.getElementById('station-info').textContent = `Connected to Station: ${stationId}`;
document.getElementById('setup').classList.add('d-none');
document.getElementById('controls').classList.remove('d-none');
document.getElementById('broadcaster-controls').classList.add('d-none');
// document.getElementById('listener-controls').classList.remove('d-none'); // Ensure listener controls are visible
// Populate audio output devices
await populateAudioOutputSources();
swarm.on('connection', (conn) => {
conns.push(conn);
console.log("Peer connected. Total peers:", conns.length);
updatePeerCount();
conn.on('data', (data) => processIncomingAudioData(data));
conn.once('close', () => {
conns.splice(conns.indexOf(conn), 1);
console.log("Peer disconnected. Total peers:", conns.length);
updatePeerCount();
});
conn.on('error', (err) => {
if (err.code === 'ECONNRESET') {
console.log("Peer connection reset by remote peer.");
} else {
console.error("Connection error:", err);
}
conns.splice(conns.indexOf(conn), 1);
updatePeerCount();
});
});
const joinModal = bootstrap.Modal.getInstance(document.getElementById('joinModal'));
if (joinModal) joinModal.hide();
updatePeerCount();
}