Add peer coutns

This commit is contained in:
Raven Scott 2024-11-23 05:06:43 -05:00
parent 18d282cd6c
commit 461884f5e6
2 changed files with 180 additions and 98 deletions

222
app.js
View File

@ -7,32 +7,27 @@ let micStream;
let audioContext; let audioContext;
let isBroadcasting = false; let isBroadcasting = false;
let conns = []; let conns = [];
let currentDeviceId = null; // To store the selected audio device ID let currentDeviceId = null; // To store the selected audio input device ID
let accumulatedBuffer = b4a.alloc(0); // Buffer for accumulating received audio data let accumulatedBuffer = b4a.alloc(0); // Buffer for accumulating received audio data
let stationKey = crypto.randomBytes(32); // Default random key for the station let stationKey = crypto.randomBytes(32); // Default random key for the station
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
document.getElementById('create-station').addEventListener('click', () => { document.getElementById('create-station').addEventListener('click', () => {
// Show the Create Station modal when clicking "Create Station" button
const createStationModal = new bootstrap.Modal(document.getElementById('createStationModal')); const createStationModal = new bootstrap.Modal(document.getElementById('createStationModal'));
createStationModal.show(); createStationModal.show();
}); });
document.getElementById('generate-new-key').addEventListener('click', () => { document.getElementById('generate-new-key').addEventListener('click', () => {
// Generate a new station key automatically
stationKey = crypto.randomBytes(32); stationKey = crypto.randomBytes(32);
document.getElementById('existing-key').value = b4a.toString(stationKey, 'hex'); // Display the new key in the text box document.getElementById('existing-key').value = b4a.toString(stationKey, 'hex');
}); });
document.getElementById('create-station-button').addEventListener('click', () => { document.getElementById('create-station-button').addEventListener('click', () => {
// Check if the user provided an existing key or use the generated one
const existingKey = document.getElementById('existing-key').value.trim(); const existingKey = document.getElementById('existing-key').value.trim();
stationKey = existingKey ? b4a.from(existingKey, 'hex') : stationKey; stationKey = existingKey ? b4a.from(existingKey, 'hex') : stationKey;
// Set up the station with the chosen key
setupStation(stationKey); setupStation(stationKey);
// Hide the modal after setting up the station
const createStationModal = bootstrap.Modal.getInstance(document.getElementById('createStationModal')); const createStationModal = bootstrap.Modal.getInstance(document.getElementById('createStationModal'));
createStationModal.hide(); createStationModal.hide();
}); });
@ -44,17 +39,30 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById('join-station-button').addEventListener('click', joinStation); document.getElementById('join-station-button').addEventListener('click', joinStation);
document.getElementById('apply-audio-source').addEventListener('click', applyAudioSource); document.getElementById('apply-audio-source').addEventListener('click', applyAudioSource);
// document.getElementById('apply-output-device').addEventListener('click', applyOutputDevice);
// Populate the audio input source dropdown for the broadcaster
populateAudioInputSources(); populateAudioInputSources();
populateAudioOutputSources();
}); });
// Function to populate audio input sources // 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() { async function populateAudioInputSources() {
try { try {
const devices = await navigator.mediaDevices.enumerateDevices(); const devices = await navigator.mediaDevices.enumerateDevices();
const audioInputSelect = document.getElementById('audio-input-select'); const audioInputSelect = document.getElementById('audio-input-select');
audioInputSelect.innerHTML = ''; // Clear existing options audioInputSelect.innerHTML = '';
devices.forEach((device) => { devices.forEach((device) => {
if (device.kind === 'audioinput') { if (device.kind === 'audioinput') {
const option = document.createElement('option'); const option = document.createElement('option');
@ -63,26 +71,72 @@ async function populateAudioInputSources() {
audioInputSelect.appendChild(option); audioInputSelect.appendChild(option);
} }
}); });
// Set default device ID to the first option
currentDeviceId = audioInputSelect.value; currentDeviceId = audioInputSelect.value;
} catch (err) { } catch (err) {
console.error("Error enumerating devices:", err); console.error("Error enumerating devices:", err);
} }
} }
// Function to apply selected audio source // 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() { async function applyAudioSource() {
const selectedDeviceId = document.getElementById('audio-input-select').value; const selectedDeviceId = document.getElementById('audio-input-select').value;
if (selectedDeviceId !== currentDeviceId) { if (selectedDeviceId !== currentDeviceId) {
currentDeviceId = selectedDeviceId; currentDeviceId = selectedDeviceId;
stopBroadcast(); // Stop current stream stopBroadcast();
startBroadcast(); // Restart stream with new device startBroadcast();
} }
} }
// Function to start broadcasting from the microphone // Start broadcasting from the microphone
async function startBroadcast() { async function startBroadcast() {
if (isBroadcasting) stopBroadcast(); // Stop any existing broadcast if (isBroadcasting) stopBroadcast();
try { try {
audioContext = new (window.AudioContext || window.webkitAudioContext)(); audioContext = new (window.AudioContext || window.webkitAudioContext)();
@ -98,8 +152,6 @@ async function startBroadcast() {
processor.onaudioprocess = (event) => { processor.onaudioprocess = (event) => {
const audioData = event.inputBuffer.getChannelData(0); const audioData = event.inputBuffer.getChannelData(0);
const buffer = b4a.from(new Float32Array(audioData).buffer); const buffer = b4a.from(new Float32Array(audioData).buffer);
// Send audio data to all connections
for (const conn of conns) { for (const conn of conns) {
conn.write(buffer); conn.write(buffer);
} }
@ -108,11 +160,11 @@ async function startBroadcast() {
isBroadcasting = true; isBroadcasting = true;
console.log("Broadcasting started."); console.log("Broadcasting started.");
} catch (err) { } catch (err) {
console.error("Error accessing microphone:", err); console.error("Error starting broadcast:", err);
} }
} }
// Function to stop broadcasting and clean up resources // Stop broadcasting
function stopBroadcast() { function stopBroadcast() {
if (!isBroadcasting) return; if (!isBroadcasting) return;
@ -125,7 +177,7 @@ function stopBroadcast() {
audioContext.close(); audioContext.close();
audioContext = null; audioContext = null;
} }
accumulatedBuffer = b4a.alloc(0); // Reset accumulated buffer accumulatedBuffer = b4a.alloc(0);
isBroadcasting = false; isBroadcasting = false;
console.log("Broadcasting stopped."); console.log("Broadcasting stopped.");
} }
@ -133,93 +185,108 @@ function stopBroadcast() {
// Broadcast a stop signal to all peers // Broadcast a stop signal to all peers
function broadcastStopSignal() { function broadcastStopSignal() {
for (const conn of conns) { for (const conn of conns) {
conn.write(Buffer.alloc(0)); // Send an empty buffer as a stop signal conn.write(Buffer.alloc(0));
} }
} }
// Function to create a broadcasting station 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) { async function setupStation(key) {
swarm = new Hyperswarm(); swarm = new Hyperswarm();
swarm.join(key, { client: false, server: true }); swarm.join(key, { client: false, server: true });
// Show broadcaster controls
document.getElementById('broadcaster-controls').classList.remove('d-none'); document.getElementById('broadcaster-controls').classList.remove('d-none');
// Update UI
document.getElementById('station-info').textContent = `Station ID: ${b4a.toString(key, 'hex')}`;
document.getElementById('setup').classList.add('d-none'); document.getElementById('setup').classList.add('d-none');
document.getElementById('controls').classList.remove('d-none'); document.getElementById('controls').classList.remove('d-none');
// Start broadcasting as soon as the station is created
startBroadcast(); startBroadcast();
updatePeerCount();
// Listen for incoming connections
swarm.on('connection', (conn) => { swarm.on('connection', (conn) => {
conns.push(conn); conns.push(conn);
console.log("Peer connected. Total peers:", conns.length);
updatePeerCount();
conn.once('close', () => { conn.once('close', () => {
conns.splice(conns.indexOf(conn), 1); conns.splice(conns.indexOf(conn), 1);
console.log("Peer disconnected."); console.log("Peer disconnected. Total peers:", conns.length);
updatePeerCount();
}); });
conn.on('data', handleData); conn.on('data', handleData);
// Add error handler to log disconnects and suppress crashes
conn.on('error', (err) => { conn.on('error', (err) => {
if (err.code === 'ECONNRESET') { if (err.code === 'ECONNRESET') {
console.log("Peer connection reset by remote peer."); console.log("Peer connection reset by remote peer.");
} else { } else {
console.error("Connection error:", err); console.error("Connection error:", err);
} }
conns.splice(conns.indexOf(conn), 1);
updatePeerCount();
}); });
}); });
} }
// Function to leave the station and stop broadcasting // Leave the station
function leaveStation() { function leaveStation() {
if (swarm) swarm.destroy(); if (swarm) swarm.destroy();
document.getElementById('setup').classList.remove('d-none'); document.getElementById('setup').classList.remove('d-none');
document.getElementById('controls').classList.add('d-none'); document.getElementById('controls').classList.add('d-none');
// Hide broadcaster controls
document.getElementById('broadcaster-controls').classList.add('d-none'); document.getElementById('broadcaster-controls').classList.add('d-none');
stopBroadcast(); stopBroadcast();
console.log("Left the station."); console.log("Left the station.");
} }
// Function to handle incoming data from peers // Handle incoming data from peers
function handleData(data) { function handleData(data) {
if (data.length === 0) { if (data.length === 0) {
console.log("Received stop command from peer");
stopBroadcast(); stopBroadcast();
} else { } else {
processIncomingAudioData(data); processIncomingAudioData(data);
} }
} }
// Function to process and play incoming audio data // Join an existing station
function processIncomingAudioData(data) {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
accumulatedBuffer = b4a.concat([accumulatedBuffer, data]);
while (accumulatedBuffer.byteLength >= 4) {
const chunkSize = accumulatedBuffer.byteLength;
const audioData = new Float32Array(accumulatedBuffer.slice(0, chunkSize).buffer);
accumulatedBuffer = accumulatedBuffer.slice(chunkSize);
const buffer = audioContext.createBuffer(1, audioData.length, audioContext.sampleRate);
buffer.copyToChannel(audioData, 0);
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start();
}
}
// Function to join an existing station
async function joinStation() { async function joinStation() {
const stationId = document.getElementById('station-id').value; const stationId = document.getElementById('station-id').value;
if (!stationId) { if (!stationId) {
@ -227,7 +294,6 @@ async function joinStation() {
return; return;
} }
// Convert the station ID to a topic buffer
const topicBuffer = b4a.from(stationId, 'hex'); const topicBuffer = b4a.from(stationId, 'hex');
swarm = new Hyperswarm(); swarm = new Hyperswarm();
swarm.join(topicBuffer, { client: true, server: false }); swarm.join(topicBuffer, { client: true, server: false });
@ -235,29 +301,39 @@ async function joinStation() {
document.getElementById('station-info').textContent = `Connected to Station: ${stationId}`; document.getElementById('station-info').textContent = `Connected to Station: ${stationId}`;
document.getElementById('setup').classList.add('d-none'); document.getElementById('setup').classList.add('d-none');
document.getElementById('controls').classList.remove('d-none'); document.getElementById('controls').classList.remove('d-none');
// Hide broadcaster controls for listener
document.getElementById('broadcaster-controls').classList.add('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) => { swarm.on('connection', (conn) => {
conn.on('data', (data) => { conns.push(conn);
processIncomingAudioData(data); 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();
}); });
// Add error handler for listener connections
conn.on('error', (err) => { conn.on('error', (err) => {
if (err.code === 'ECONNRESET') { if (err.code === 'ECONNRESET') {
console.log("Peer connection reset by remote peer."); console.log("Peer connection reset by remote peer.");
} else { } else {
console.error("Connection error:", err); console.error("Connection error:", err);
} }
conns.splice(conns.indexOf(conn), 1);
updatePeerCount();
}); });
}); });
// Hide the modal after joining const joinModal = bootstrap.Modal.getInstance(document.getElementById('joinModal'));
const joinModal = document.getElementById('joinModal'); if (joinModal) joinModal.hide();
const modalInstance = bootstrap.Modal.getInstance(joinModal);
if (modalInstance) { updatePeerCount();
modalInstance.hide();
}
} }

View File

@ -16,13 +16,11 @@
margin: 0; margin: 0;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
} }
pear-ctrl[data-platform="darwin"] { pear-ctrl[data-platform="darwin"] {
float: left; float: left;
margin-top: 15px; margin-top: 15px;
margin-left: 5px; margin-left: 5px;
} }
</style> </style>
</head> </head>
<body class="bg-dark text-light"> <body class="bg-dark text-light">
@ -45,6 +43,14 @@
<select id="audio-input-select" class="form-select"></select> <select id="audio-input-select" class="form-select"></select>
<button id="apply-audio-source" class="btn btn-success mt-2">Apply</button> <button id="apply-audio-source" class="btn btn-success mt-2">Apply</button>
</div> </div>
<!-- Listener-only Controls -->
<div id="listener-controls" class="mt-3 d-none">
<select id="audio-output-select" class="form-select" style="display: none;"></select>
<!-- <label for="audio-output-select" class="form-label">Select Audio Output Device:</label>
<button id="apply-output-device" class="btn btn-success mt-2">Apply</button> -->
</div>
</div> </div>
</div> </div>
@ -68,25 +74,25 @@
</div> </div>
<!-- Create Station Modal --> <!-- Create Station Modal -->
<div class="modal fade" id="createStationModal" tabindex="-1" aria-labelledby="createStationModalLabel" aria-hidden="true"> <div class="modal fade" id="createStationModal" tabindex="-1" aria-labelledby="createStationModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content bg-dark text-light"> <div class="modal-content bg-dark text-light">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="createStationModalLabel">Create Station</h5> <h5 class="modal-title" id="createStationModalLabel">Create Station</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>Generate a new station ID or use an existing ID?</p> <p>Generate a new station ID or use an existing ID?</p>
<button id="generate-new-key" class="btn btn-primary mt-2">Generate New ID</button> <button id="generate-new-key" class="btn btn-primary mt-2">Generate New ID</button>
<input type="text" id="existing-key" class="form-control mt-3" placeholder="You may manually enter an ID here"> <input type="text" id="existing-key" class="form-control mt-3" placeholder="You may manually enter an ID here">
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="create-station-button">Create Station</button> <button type="button" class="btn btn-primary" id="create-station-button">Create Station</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Bootstrap JavaScript Bundle (includes Popper) --> <!-- Bootstrap JavaScript Bundle (includes Popper) -->
<script src="./assets/bootstrap.bundle.min.js"></script> <script src="./assets/bootstrap.bundle.min.js"></script>