Add peer coutns
This commit is contained in:
parent
18d282cd6c
commit
461884f5e6
228
app.js
228
app.js
@ -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');
|
|
||||||
|
|
||||||
// Update UI
|
document.getElementById('broadcaster-controls').classList.remove('d-none');
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
50
index.html
50
index.html
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user