diff --git a/index.html b/index.html
index 38945ae..505270a 100644
--- a/index.html
+++ b/index.html
@@ -398,6 +398,16 @@
Lower = more stable on poor connections
+
+
+
🔗 Remote Stream Relay
+
+
+
+
+
+
+
diff --git a/script.js b/script.js
index d87db82..15a2ed8 100644
--- a/script.js
+++ b/script.js
@@ -1664,10 +1664,20 @@ function initSocket() {
socket.on('broadcast_started', () => {
console.log('🎙️ Broadcast started notification received');
+ // Update relay UI if it's a relay
+ const relayStatus = document.getElementById('relay-status');
+ if (relayStatus && relayStatus.textContent.includes('Connecting')) {
+ relayStatus.textContent = 'Relay active - streaming to listeners';
+ relayStatus.style.color = '#00ff00';
+ }
});
socket.on('broadcast_stopped', () => {
console.log('🛑 Broadcast stopped notification received');
+ // Reset relay UI if it was active
+ document.getElementById('start-relay-btn').style.display = 'inline-block';
+ document.getElementById('stop-relay-btn').style.display = 'none';
+ document.getElementById('relay-status').textContent = '';
});
socket.on('mixer_status', (data) => {
@@ -1682,6 +1692,10 @@ function initSocket() {
socket.on('error', (data) => {
console.error('📡 Server error:', data.message);
alert(`SERVER ERROR: ${data.message}`);
+ // Reset relay UI on error
+ document.getElementById('start-relay-btn').style.display = 'inline-block';
+ document.getElementById('stop-relay-btn').style.display = 'none';
+ document.getElementById('relay-status').textContent = '';
});
return socket;
@@ -2151,6 +2165,48 @@ function toggleAutoStream(enabled) {
localStorage.setItem('autoStartStream', enabled);
}
+// ========== REMOTE RELAY FUNCTIONS ==========
+
+function startRemoteRelay() {
+ const urlInput = document.getElementById('remote-stream-url');
+ const url = urlInput.value.trim();
+
+ if (!url) {
+ alert('Please enter a remote stream URL');
+ return;
+ }
+
+ if (!socket) initSocket();
+
+ // Stop any existing broadcast first
+ if (isBroadcasting) {
+ stopBroadcast();
+ }
+
+ console.log('🔗 Starting remote relay for:', url);
+
+ // Update UI
+ document.getElementById('start-relay-btn').style.display = 'none';
+ document.getElementById('stop-relay-btn').style.display = 'inline-block';
+ document.getElementById('relay-status').textContent = 'Connecting to remote stream...';
+ document.getElementById('relay-status').style.color = '#00f3ff';
+
+ socket.emit('start_remote_relay', { url: url });
+}
+
+function stopRemoteRelay() {
+ if (!socket) return;
+
+ console.log('🛑 Stopping remote relay');
+
+ socket.emit('stop_remote_relay');
+
+ // Update UI
+ document.getElementById('start-relay-btn').style.display = 'inline-block';
+ document.getElementById('stop-relay-btn').style.display = 'none';
+ document.getElementById('relay-status').textContent = '';
+}
+
// ========== LISTENER MODE ==========
function initListenerMode() {
@@ -2272,7 +2328,12 @@ function initListenerMode() {
socket.on('stream_status', (data) => {
const nowPlayingEl = document.getElementById('listener-now-playing');
if (nowPlayingEl) {
- nowPlayingEl.textContent = data.active ? '🎵 Stream is live!' : 'Stream offline - waiting for DJ...';
+ if (data.active) {
+ const status = data.remote_relay ? '🔗 Remote stream is live!' : '🎵 DJ stream is live!';
+ nowPlayingEl.textContent = status;
+ } else {
+ nowPlayingEl.textContent = 'Stream offline - waiting for DJ...';
+ }
}
});
diff --git a/server.py b/server.py
index 142104d..bb1c009 100644
--- a/server.py
+++ b/server.py
@@ -53,6 +53,7 @@ _transcode_threads_started = False
_transcoder_bytes_out = 0
_transcoder_last_error = None
_last_audio_chunk_ts = 0.0
+_remote_stream_url = None # For relaying remote streams
def _start_transcoder_if_needed():
@@ -61,32 +62,55 @@ def _start_transcoder_if_needed():
if _ffmpeg_proc is not None and _ffmpeg_proc.poll() is None:
return
- cmd = [
- 'ffmpeg',
- '-hide_banner',
- '-loglevel', 'error',
- '-i', 'pipe:0',
- '-vn',
- '-acodec', 'libmp3lame',
- '-b:a', '192k',
- '-f', 'mp3',
- 'pipe:1',
- ]
+ if _remote_stream_url:
+ # Remote relay mode: input from URL
+ cmd = [
+ 'ffmpeg',
+ '-hide_banner',
+ '-loglevel', 'error',
+ '-i', _remote_stream_url,
+ '-vn',
+ '-acodec', 'libmp3lame',
+ '-b:a', '192k',
+ '-f', 'mp3',
+ 'pipe:1',
+ ]
+ else:
+ # Local broadcast mode: input from pipe
+ cmd = [
+ 'ffmpeg',
+ '-hide_banner',
+ '-loglevel', 'error',
+ '-i', 'pipe:0',
+ '-vn',
+ '-acodec', 'libmp3lame',
+ '-b:a', '192k',
+ '-f', 'mp3',
+ 'pipe:1',
+ ]
try:
- _ffmpeg_proc = subprocess.Popen(
- cmd,
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- bufsize=0,
- )
+ if _remote_stream_url:
+ _ffmpeg_proc = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ bufsize=0,
+ )
+ else:
+ _ffmpeg_proc = subprocess.Popen(
+ cmd,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ bufsize=0,
+ )
except FileNotFoundError:
_ffmpeg_proc = None
print('⚠️ ffmpeg not found; /stream.mp3 fallback disabled')
return
- print('🎛️ ffmpeg transcoder started for /stream.mp3')
+ print(f'🎛️ ffmpeg transcoder started for /stream.mp3 ({ "remote relay" if _remote_stream_url else "local broadcast" })')
def _writer():
global _transcoder_last_error
@@ -152,7 +176,7 @@ def _stop_transcoder():
def _feed_transcoder(data: bytes):
global _last_audio_chunk_ts
- if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None:
+ if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None or _remote_stream_url:
return
_last_audio_chunk_ts = time.time()
try:
@@ -480,6 +504,43 @@ def dj_stop():
listener_socketio.emit('broadcast_stopped', namespace='/')
listener_socketio.emit('stream_status', {'active': False}, namespace='/')
+@dj_socketio.on('start_remote_relay')
+def dj_start_remote_relay(data):
+ global _remote_stream_url
+ url = data.get('url', '').strip()
+ if not url:
+ dj_socketio.emit('error', {'message': 'No URL provided for remote relay'})
+ return
+
+ # Stop any existing broadcast/relay
+ if broadcast_state['active']:
+ dj_stop()
+
+ _remote_stream_url = url
+ broadcast_state['active'] = True
+ broadcast_state['remote_relay'] = True
+ session['is_dj'] = True
+ print(f"🔗 Starting remote relay from: {url}")
+
+ _start_transcoder_if_needed()
+
+ listener_socketio.emit('broadcast_started', namespace='/')
+ listener_socketio.emit('stream_status', {'active': True, 'remote_relay': True}, namespace='/')
+
+@dj_socketio.on('stop_remote_relay')
+def dj_stop_remote_relay():
+ global _remote_stream_url
+ _remote_stream_url = None
+ broadcast_state['active'] = False
+ broadcast_state['remote_relay'] = False
+ session['is_dj'] = False
+ print("🛑 Remote relay stopped")
+
+ _stop_transcoder()
+
+ listener_socketio.emit('broadcast_stopped', namespace='/')
+ listener_socketio.emit('stream_status', {'active': False}, namespace='/')
+
@dj_socketio.on('audio_chunk')
def dj_audio(data):
# MP3-only mode: do not relay raw chunks to listeners; feed transcoder only.
diff --git a/style.css b/style.css
index b3463a3..6c68af9 100644
--- a/style.css
+++ b/style.css
@@ -2320,6 +2320,75 @@ input[type=range] {
opacity: 0.7;
}
+/* Remote Relay Section */
+.remote-relay-section {
+ padding: 15px;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 8px;
+ border: 1px solid rgba(0, 243, 255, 0.3);
+}
+
+.remote-relay-section h4 {
+ margin: 0 0 15px 0;
+ color: var(--primary-cyan);
+ font-family: 'Orbitron', sans-serif;
+ font-size: 1rem;
+}
+
+.relay-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.relay-url-input {
+ padding: 10px;
+ background: rgba(0, 0, 0, 0.5);
+ border: 1px solid var(--primary-cyan);
+ color: var(--text-main);
+ border-radius: 5px;
+ font-family: 'Rajdhani', monospace;
+ font-size: 0.85rem;
+}
+
+.relay-btn {
+ padding: 12px;
+ background: linear-gradient(145deg, #1a1a1a, #0a0a0a);
+ border: 2px solid var(--primary-cyan);
+ color: var(--primary-cyan);
+ font-family: 'Orbitron', sans-serif;
+ font-size: 0.9rem;
+ font-weight: bold;
+ cursor: pointer;
+ border-radius: 5px;
+ transition: all 0.3s;
+ box-shadow: 0 0 15px rgba(0, 243, 255, 0.2);
+}
+
+.relay-btn:hover {
+ background: linear-gradient(145deg, #2a2a2a, #1a1a1a);
+ box-shadow: 0 0 25px rgba(0, 243, 255, 0.4);
+ transform: translateY(-1px);
+}
+
+.relay-btn.stop {
+ border-color: #ff4444;
+ color: #ff4444;
+ box-shadow: 0 0 15px rgba(255, 68, 68, 0.2);
+}
+
+.relay-btn.stop:hover {
+ background: rgba(255, 68, 68, 0.1);
+ box-shadow: 0 0 25px rgba(255, 68, 68, 0.4);
+}
+
+.relay-status {
+ margin-top: 10px;
+ font-size: 0.85rem;
+ color: var(--text-dim);
+ min-height: 20px;
+}
+
/* ========== LISTENER MODE ========== */
.listener-mode {