diff --git a/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt b/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt index 884369a..93d00ac 100644 --- a/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt +++ b/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt @@ -229,6 +229,7 @@ class MainActivity : AppCompatActivity() { }.sorted().joinToString("|") val changed = assignmentSig != zoneManager?.lastAssignmentSig + com.remotedisplay.player.util.DebugLog.i("Player", "Layout: MULTI-ZONE (${layoutZones.length()} zones, layout=$layoutId), ${assignments.length()} assignments") if (zoneManager?.hasZones() != true || layoutId != currentLayoutId) { Log.i("MainActivity", "Multi-zone layout with ${layoutZones.length()} zones (layout=$layoutId, was=$currentLayoutId)") handler.post { @@ -252,6 +253,7 @@ class MainActivity : AppCompatActivity() { } } else { // Single-zone mode - use PlaylistController (existing behavior) + com.remotedisplay.player.util.DebugLog.i("Player", "Layout: SINGLE/FULLSCREEN (${layoutZones?.length() ?: 0} zones), ${assignments.length()} assignments") if (zoneManager?.hasZones() == true) handler.post { zoneManager?.cleanup() } playlistController.updatePlaylist(assignments) } @@ -420,6 +422,7 @@ class MainActivity : AppCompatActivity() { private fun playItem(item: PlaylistItem) { hideStatus() + com.remotedisplay.player.util.DebugLog.i("Player", "playItem: ${item.filename} mime=${item.mimeType} widget=${item.widgetId ?: "-"} zone=fullscreen") // Widget content - render fullscreen in a WebView (single-zone / fullscreen // layouts; multi-zone widgets go through ZoneManager). Previously unhandled, diff --git a/android/app/src/main/java/com/remotedisplay/player/player/ZoneManager.kt b/android/app/src/main/java/com/remotedisplay/player/player/ZoneManager.kt index 73ddb35..c98892c 100644 --- a/android/app/src/main/java/com/remotedisplay/player/player/ZoneManager.kt +++ b/android/app/src/main/java/com/remotedisplay/player/player/ZoneManager.kt @@ -117,6 +117,7 @@ class ZoneManager( val widgetConfig = if (firstAssignment.isNull("widget_config")) null else firstAssignment.optString("widget_config", null) val contentId = if (firstAssignment.isNull("content_id")) null else firstAssignment.optString("content_id", null) val filepath = firstAssignment.optString("filepath", "") + com.remotedisplay.player.util.DebugLog.i("Zone", "Zone '${zone.name}' (${zone.widthPercent.toInt()}x${zone.heightPercent.toInt()}%) -> ${widgetType?.let { "widget:$it" } ?: mimeType.ifEmpty { "empty" }}") val isMuted = firstAssignment.optInt("muted", 0) == 1 when { diff --git a/android/app/src/main/java/com/remotedisplay/player/service/WebSocketService.kt b/android/app/src/main/java/com/remotedisplay/player/service/WebSocketService.kt index 1574556..225116f 100644 --- a/android/app/src/main/java/com/remotedisplay/player/service/WebSocketService.kt +++ b/android/app/src/main/java/com/remotedisplay/player/service/WebSocketService.kt @@ -269,6 +269,21 @@ class WebSocketService : Service() { "screen_on" -> { Thread { try { Runtime.getRuntime().exec(arrayOf("input", "keyevent", "224")).waitFor() } catch (_: Exception) {} }.start() } + "set_debug" -> { + val on = payload?.optBoolean("enabled", false) ?: false + // Point the sink at this socket, then flip the flag. When on, + // DebugLog.* mirrors player/zone lines to the dashboard. + com.remotedisplay.player.util.DebugLog.sink = { tag, level, msg -> + try { + socket?.emit("device:log", JSONObject().apply { + put("tag", tag); put("level", level); put("message", msg) + }) + } catch (_: Throwable) {} + } + com.remotedisplay.player.util.DebugLog.enabled = on + Log.i("WebSocketService", "Remote debug logging ${if (on) "ENABLED" else "disabled"}") + com.remotedisplay.player.util.DebugLog.i("Debug", "Remote debug logging ${if (on) "ON" else "OFF"}") + } else -> handler.post { try { onCommand?.invoke(type, payload) } catch (e: Throwable) { Log.e("WebSocketService", "onCommand cb: ${e.message}") } } } } diff --git a/android/app/src/main/java/com/remotedisplay/player/util/DebugLog.kt b/android/app/src/main/java/com/remotedisplay/player/util/DebugLog.kt new file mode 100644 index 0000000..7b105af --- /dev/null +++ b/android/app/src/main/java/com/remotedisplay/player/util/DebugLog.kt @@ -0,0 +1,25 @@ +package com.remotedisplay.player.util + +import android.util.Log + +/** + * Lightweight player debug logger. Always writes to logcat; when remote debug is + * enabled (toggled from the dashboard device-detail screen via a `set_debug` + * command), it ALSO streams the line to the server over the device socket so it + * can be watched live without adb. Off by default; no network when disabled. + */ +object DebugLog { + @Volatile var enabled = false + // Set by WebSocketService: (tag, level, message) -> emit over the device socket. + @Volatile var sink: ((String, String, String) -> Unit)? = null + + fun d(tag: String, msg: String) { Log.d(tag, msg); send(tag, "d", msg) } + fun i(tag: String, msg: String) { Log.i(tag, msg); send(tag, "i", msg) } + fun w(tag: String, msg: String) { Log.w(tag, msg); send(tag, "w", msg) } + fun e(tag: String, msg: String) { Log.e(tag, msg); send(tag, "e", msg) } + + private fun send(tag: String, level: String, msg: String) { + if (!enabled) return + try { sink?.invoke(tag, level, msg) } catch (_: Throwable) {} + } +} diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index 9c0cb00..9e665b2 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -286,6 +286,8 @@ export default { 'device.form.default_content_none': 'None (show "Waiting...")', 'device.form.notes_label': 'Notes', 'device.form.notes_placeholder': 'Location, setup details, etc.', + 'device.debug.toggle': 'Debug logging (live)', + 'device.debug.hint': 'Streams player/zone logs from this device in real time. Turns off on its own when the device reconnects.', 'device.form.save_settings': 'Save Settings', // Control buttons 'device.ctl.reboot_device': 'Reboot Device', diff --git a/frontend/js/socket.js b/frontend/js/socket.js index f961bd4..c1a2163 100644 --- a/frontend/js/socket.js +++ b/frontend/js/socket.js @@ -52,6 +52,12 @@ export function connectSocket() { emit('playback-state', data); }); + // Live device debug log line (device-detail screen streams these when the + // per-device "Debug logging" checkbox is on). + dashboardSocket.on('dashboard:device-log', (data) => { + emit('device-log', data); + }); + // Playback progress (play_start with duration — drives device-card progress bars) dashboardSocket.on('dashboard:playback-progress', (data) => { emit('playback-progress', data); diff --git a/frontend/js/views/device-detail.js b/frontend/js/views/device-detail.js index 07d4436..b79a93c 100644 --- a/frontend/js/views/device-detail.js +++ b/frontend/js/views/device-detail.js @@ -8,6 +8,7 @@ let currentDevice = null; let statusHandler = null; let screenshotHandler = null; let playbackHandler = null; +let logHandler = null; let screenshotInterval = null; let remoteActive = false; @@ -99,9 +100,24 @@ export function render(container, deviceId) { } }; + // Live debug log lines streamed from the device (when the Debug logging + // checkbox is on). Appended via textContent — no HTML injection. + logHandler = (data) => { + if (data.device_id !== deviceId) return; + const panel = document.getElementById('debugLogPanel'); + if (!panel) return; + const line = document.createElement('div'); + const time = new Date(data.ts || Date.now()).toLocaleTimeString(); + line.textContent = `${time} [${data.tag || ''}] ${data.message || ''}`; + panel.appendChild(line); + while (panel.childElementCount > 500) panel.removeChild(panel.firstChild); + panel.scrollTop = panel.scrollHeight; + }; + on('device-status', statusHandler); on('screenshot-ready', screenshotHandler); on('playback-state', playbackHandler); + on('device-log', logHandler); } async function loadDevice(deviceId, activeTab = null) { @@ -324,6 +340,15 @@ async function loadDevice(deviceId, activeTab = null) { + +