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:
3nd3r
2026-01-03 10:29:10 -06:00
parent 5e06254e1a
commit 81120ac7ea
4 changed files with 222 additions and 21 deletions

101
server.py
View File

@@ -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.