diff --git a/index.html b/index.html index ef8a413..cd411c2 100644 --- a/index.html +++ b/index.html @@ -369,15 +369,6 @@ -
-

🔗 Remote Stream Relay

-
- - - -
-
-
@@ -442,6 +433,12 @@ +
+ +
@@ -452,4 +449,4 @@ - + \ No newline at end of file diff --git a/script.js b/script.js index 9e048dc..70c4af6 100644 --- a/script.js +++ b/script.js @@ -2042,49 +2042,6 @@ 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'; - - const bitrateValue = document.getElementById('stream-quality').value + 'k'; - socket.emit('start_remote_relay', { url: url, bitrate: bitrateValue }); -} - -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() { @@ -2692,7 +2649,7 @@ function checkAndLoadNextFromQueue(deckId) { // ========================================== // Default keyboard mappings (can be customized) -let keyboardMappings = { +const DEFAULT_KEYBOARD_MAPPINGS = { // Deck A Controls 'q': { action: 'playDeckA', label: 'Play Deck A' }, 'a': { action: 'pauseDeckA', label: 'Pause Deck A' }, @@ -2735,23 +2692,36 @@ let keyboardMappings = { 'Escape': { action: 'closeAllPanels', label: 'Close All Panels' } }; -// Load custom mappings from localStorage -function loadKeyboardMappings() { - const saved = localStorage.getItem('keyboardMappings'); - if (saved) { - try { - keyboardMappings = JSON.parse(saved); - console.log('✅ Loaded custom keyboard mappings'); - } catch (e) { - console.error('Failed to load keyboard mappings:', e); +let keyboardMappings = { ...DEFAULT_KEYBOARD_MAPPINGS }; + +// Load custom mappings from server +async function loadKeyboardMappings() { + try { + const response = await fetch('/load_keymaps'); + const data = await response.json(); + if (data.success && data.keymaps) { + keyboardMappings = data.keymaps; + console.log('✅ Loaded custom keyboard mappings from server'); + } else { + console.log('â„šī¸ Using default keyboard mappings'); } + } catch (e) { + console.error('Failed to load keyboard mappings from server:', e); } } -// Save custom mappings to localStorage -function saveKeyboardMappings() { - localStorage.setItem('keyboardMappings', JSON.stringify(keyboardMappings)); - console.log('💾 Saved keyboard mappings'); +// Save custom mappings to server +async function saveKeyboardMappings() { + try { + await fetch('/save_keymaps', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(keyboardMappings) + }); + console.log('💾 Saved keyboard mappings to server'); + } catch (e) { + console.error('Failed to save keyboard mappings to server:', e); + } } // Execute action based on key @@ -2968,10 +2938,11 @@ function reassignKey(oldKey) { } // Reset to default mappings -function resetKeyboardMappings() { +async function resetKeyboardMappings() { if (confirm('Reset all keyboard shortcuts to defaults?')) { - localStorage.removeItem('keyboardMappings'); - location.reload(); + keyboardMappings = { ...DEFAULT_KEYBOARD_MAPPINGS }; + await saveKeyboardMappings(); + renderKeyboardMappings(); } } diff --git a/server.py b/server.py index abd2a8a..6e63e8b 100644 --- a/server.py +++ b/server.py @@ -55,7 +55,6 @@ _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 _mp3_preroll = collections.deque(maxlen=60) # Pre-roll (~2.5s at 192k) @@ -65,59 +64,35 @@ def _start_transcoder_if_needed(): if _ffmpeg_proc is not None and _ffmpeg_proc.poll() is None: return - 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', _current_bitrate, - '-tune', 'zerolatency', - '-flush_packets', '1', - '-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', _current_bitrate, - '-tune', 'zerolatency', - '-flush_packets', '1', - '-f', 'mp3', - 'pipe:1', - ] + # Local broadcast mode: input from pipe (Relay mode removed) + cmd = [ + 'ffmpeg', + '-hide_banner', + '-loglevel', 'error', + '-i', 'pipe:0', + '-vn', + '-acodec', 'libmp3lame', + '-b:a', _current_bitrate, + '-tune', 'zerolatency', + '-flush_packets', '1', + '-f', 'mp3', + 'pipe:1', + ] try: - 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, - ) + _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(f'đŸŽ›ī¸ ffmpeg transcoder started for /stream.mp3 ({ "remote relay" if _remote_stream_url else "local broadcast" })') + print(f'đŸŽ›ī¸ ffmpeg transcoder started for /stream.mp3 (local broadcast)') def _writer(): global _transcoder_last_error @@ -193,7 +168,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 or _remote_stream_url: + if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None: return _last_audio_chunk_ts = time.time() try: @@ -262,6 +237,31 @@ def setup_shared_routes(app): print(f"❌ Upload error: {e}") return jsonify({"success": False, "error": str(e)}), 500 + @app.route('/save_keymaps', methods=['POST']) + def save_keymaps(): + try: + data = request.get_json() + with open('keymaps.json', 'w', encoding='utf-8') as f: + json.dump(data, f, indent=4) + print("💾 Keymaps saved to keymaps.json") + return jsonify({"success": True}) + except Exception as e: + print(f"❌ Save keymaps error: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + + @app.route('/load_keymaps', methods=['GET']) + def load_keymaps(): + try: + if os.path.exists('keymaps.json'): + with open('keymaps.json', 'r', encoding='utf-8') as f: + data = json.load(f) + return jsonify({"success": True, "keymaps": data}) + else: + return jsonify({"success": True, "keymaps": None}) + except Exception as e: + print(f"❌ Load keymaps error: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + @app.route('/stream.mp3') def stream_mp3(): # Streaming response from the ffmpeg transcoder output. @@ -475,46 +475,6 @@ 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() - - global _remote_stream_url, _current_bitrate - _remote_stream_url = url - broadcast_state['active'] = True - broadcast_state['remote_relay'] = True - session['is_dj'] = True - - if data and 'bitrate' in data: - _current_bitrate = data['bitrate'] - print(f"📡 Setting relay bitrate to: {_current_bitrate}") - - 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='/') diff --git a/style.css b/style.css index 08f8ee6..4abe5bf 100644 --- a/style.css +++ b/style.css @@ -2500,6 +2500,114 @@ body.listening-active .landscape-prompt { display: flex; flex-direction: column; gap: 20px; + height: calc(100vh - 120px); + overflow-y: auto; +} + +/* Keyboard Mappings List */ +#keyboard-mappings-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.keyboard-mapping-item { + display: grid; + grid-template-columns: 80px 30px 1fr 80px; + align-items: center; + background: rgba(255, 255, 255, 0.03); + padding: 10px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.05); + transition: all 0.2s; +} + +.keyboard-mapping-item:hover { + background: rgba(0, 243, 255, 0.05); + border-color: rgba(0, 243, 255, 0.2); +} + +.keyboard-mapping-item.listening { + background: rgba(188, 19, 254, 0.1) !important; + border-color: var(--secondary-magenta) !important; + box-shadow: 0 0 15px rgba(188, 19, 254, 0.2); +} + +.key-display { + background: #222; + color: var(--primary-cyan); + padding: 4px 8px; + border-radius: 4px; + font-family: 'Orbitron', sans-serif; + font-size: 0.8rem; + text-align: center; + border: 1px solid #333; + min-width: 40px; +} + +.key-arrow { + text-align: center; + opacity: 0.4; + font-size: 0.8rem; +} + +.action-label { + font-size: 0.9rem; + color: #ccc; + font-weight: 500; +} + +.key-reassign-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid #444; + color: #888; + padding: 5px 10px; + border-radius: 4px; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s; +} + +.key-reassign-btn:hover { + background: rgba(255, 255, 255, 0.1); + color: #fff; + border-color: #666; +} + +.listening .key-reassign-btn { + color: var(--secondary-magenta); + border-color: var(--secondary-magenta); + background: transparent; + animation: blink-magenta 1s infinite; +} + +@keyframes blink-magenta { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } +} + +.btn-secondary { + background: #222; + border: 1px solid #444; + color: #eee; + padding: 8px 12px; + border-radius: 6px; + font-family: 'Orbitron', sans-serif; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s; +} + +.btn-secondary:hover { + background: #333; + border-color: #666; } .setting-item label { @@ -3646,4 +3754,4 @@ body.listening-active .landscape-prompt { position: relative; padding-left: 40px; padding-right: 40px; -} +} \ No newline at end of file