Add remote stream relay feature: relay remote DJ streams to listeners
- Server-side: Added remote URL support in ffmpeg transcoder - UI: Added relay controls in streaming panel with URL input - Client: Added start/stop relay functions with socket communication - Listener: Shows remote relay status in stream indicator
This commit is contained in:
10
index.html
10
index.html
@@ -398,6 +398,16 @@
|
||||
<span class="quality-hint">Lower = more stable on poor connections</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="remote-relay-section">
|
||||
<h4>🔗 Remote Stream Relay</h4>
|
||||
<div class="relay-controls">
|
||||
<input type="text" id="remote-stream-url" placeholder="Paste remote stream URL (e.g., http://remote.dj/stream.mp3)" class="relay-url-input">
|
||||
<button class="relay-btn" id="start-relay-btn" onclick="startRemoteRelay()">START RELAY</button>
|
||||
<button class="relay-btn stop" id="stop-relay-btn" onclick="stopRemoteRelay()" style="display: none;">STOP RELAY</button>
|
||||
</div>
|
||||
<div class="relay-status" id="relay-status"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
63
script.js
63
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...';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
101
server.py
101
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.
|
||||
|
||||
69
style.css
69
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 {
|
||||
|
||||
Reference in New Issue
Block a user