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 7859d45..97eedce 100644 --- a/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt +++ b/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt @@ -575,8 +575,21 @@ class MainActivity : AppCompatActivity() { }.sorted().joinToString("|") val changed = assignmentSig != zoneManager?.lastAssignmentSig + // The ZONES themselves can change without the layout id changing — editing a layout + // in place (adding a 4th zone to a 3-zone layout) keeps the same id. Rebuilding only + // on an id change meant the new zone never appeared: the geometry stayed as it was + // and only the assignments re-rendered into the OLD zones, so the change looked like + // it had been ignored until the app was force-stopped. Reported on #234. + val zoneSig = (0 until layoutZones.length()).map { i -> + val z = layoutZones.getJSONObject(i) + "${z.optString("id")}:${z.optDouble("x_percent", -1.0)}:${z.optDouble("y_percent", -1.0)}:" + + "${z.optDouble("width_percent", -1.0)}:${z.optDouble("height_percent", -1.0)}:" + + "${z.optInt("z_index", 0)}:${z.optString("zone_type")}:${z.optString("fit_mode")}" + }.sorted().joinToString("|") + val zonesChanged = zoneSig != zoneManager?.lastZoneSig + com.remotedisplay.player.util.DebugLog.i("Player", "Layout: MULTI-ZONE (${layoutZones.length()} zones, layout=$layoutId), ${assignments.length()} assignments") - if (zoneManager?.hasZones() != true || layoutId != currentLayoutId) { + if (zoneManager?.hasZones() != true || layoutId != currentLayoutId || zonesChanged) { Log.i("MainActivity", "Multi-zone layout with ${layoutZones.length()} zones (layout=$layoutId, was=$currentLayoutId)") handler.post { hideStatus() @@ -587,6 +600,7 @@ class MainActivity : AppCompatActivity() { zoneManager?.setupZones(layoutZones, layoutId) zoneManager?.renderAssignments(assignments, config.serverUrl, contentCache, config.deviceId) zoneManager?.lastAssignmentSig = assignmentSig + zoneManager?.lastZoneSig = zoneSig } } else if (changed) { Log.i("MainActivity", "Multi-zone assignments changed, re-rendering") @@ -1287,6 +1301,22 @@ class MainActivity : AppCompatActivity() { ) } + /** + * Nothing in the Android lifecycle pauses a WebView, so a YouTube embed kept playing with the + * app in the background and the panel kept making noise with the app "closed". Reported on + * #234. onStop (not onPause) is the right hook: onPause also fires for a transient dialog or a + * permission prompt, and pausing playback for those would be a visible stutter on a wall. + */ + override fun onStop() { + super.onStop() + if (::mediaPlayer.isInitialized) mediaPlayer.onAppBackgrounded() + } + + override fun onStart() { + super.onStart() + if (::mediaPlayer.isInitialized) mediaPlayer.onAppForegrounded() + } + override fun onDestroy() { remoteStreaming = false // Kill the wall/group leader tick BEFORE releasing media. The Handler is on the main looper diff --git a/android/app/src/main/java/com/remotedisplay/player/player/MediaPlayerManager.kt b/android/app/src/main/java/com/remotedisplay/player/player/MediaPlayerManager.kt index 5858c72..401f6ae 100644 --- a/android/app/src/main/java/com/remotedisplay/player/player/MediaPlayerManager.kt +++ b/android/app/src/main/java/com/remotedisplay/player/player/MediaPlayerManager.kt @@ -209,13 +209,42 @@ class MediaPlayerManager( // would restart the video and flicker. Main thread only (WebView access). private fun setYoutubeMuted(muted: Boolean) { youtubeMuted = muted - val func = if (muted) "mute" else "unMute" + postYoutubeCommand(if (muted) "mute" else "unMute") + } + + /** Send one IFrame-API command to the embed. Main thread only (WebView access). */ + private fun postYoutubeCommand(func: String) { val js = "(function(){try{var f=document.querySelector('iframe');" + "if(f&&f.contentWindow){f.contentWindow.postMessage(" + "JSON.stringify({event:'command',func:'$func',args:[]}),'*');}}catch(e){}})()" youtubeWebView?.let { wv -> wv.post { try { wv.evaluateJavascript(js, null) } catch (_: Throwable) {} } } } + /** + * The app is going to the background. Stop making noise. + * + * A WebView keeps running when its Activity stops — nothing in the lifecycle pauses it — so a + * YouTube embed carried on playing with the app closed and the audio kept coming out of the + * panel: "I closed the app and I can still hear the sound... I force stop the app and then open + * again." A signage player that is not on screen must be silent. + * + * Pause rather than blank, so returning to the foreground resumes in place instead of + * restarting the clip. pauseTimers() is process-wide, which is fine here (one WebView) and is + * what actually stops the embed's own scripted playback. + */ + fun onAppBackgrounded() { + if (currentType == MediaType.YOUTUBE) postYoutubeCommand("pauseVideo") + youtubeWebView?.let { wv -> wv.post { try { wv.onPause(); wv.pauseTimers() } catch (_: Throwable) {} } } + exoPlayer?.pause() + } + + /** Back in the foreground: undo onAppBackgrounded. */ + fun onAppForegrounded() { + youtubeWebView?.let { wv -> wv.post { try { wv.resumeTimers(); wv.onResume() } catch (_: Throwable) {} } } + if (currentType == MediaType.YOUTUBE) postYoutubeCommand("playVideo") + if (currentType == MediaType.VIDEO) exoPlayer?.play() + } + // Fullscreen widget render (single-zone / "fullscreen" layouts). Reuses the // full-screen WebView; ZoneManager handles widgets in multi-zone layouts. fun showWidget(url: String) { 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 135b479..b2a8a96 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 @@ -51,6 +51,9 @@ class ZoneManager( var currentLayoutId: String? = null private set var lastAssignmentSig: String? = null + // Geometry of the zones currently built. Editing a layout in place keeps its id, so the id + // alone cannot tell "same layout, same zones" from "same layout, zones changed". + var lastZoneSig: String? = null // #74/#75: device-effective IANA timezone for per-item schedule evaluation. @Volatile private var effectiveTimezone: String? = null