mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
Silence a backgrounded player, and rebuild zones when a layout is edited in place
Two more from #234, both Android-only. 1. "I closed the app and I can still hear the sound." Nothing in the Android lifecycle pauses a WebView, and MainActivity had no onStop at all, so a YouTube embed kept playing with the app in the background and the panel kept making noise with the app apparently closed. onStop rather than onPause: onPause also fires for a transient dialog or a permission prompt, and pausing playback for those would be a visible stutter on a wall. Pauses via the IFrame-API bridge that already exists for live mute, so returning to the foreground resumes in place instead of restarting the clip. 2. "I added 4 zones and they dont appear on the screen. I had 3 zones before and they appeared." The zone rebuild fired only when the layout ID changed. Editing a layout in place keeps its id, so setupZones never ran: the geometry stayed at three zones and only the assignments re-rendered into the old ones, which is why it took a force-stop to appear. The rebuild now also triggers on a signature of the zones themselves (id, position, size, z-index, type, fit). Compiles clean; NOT yet verified on hardware — both need a device to prove, unlike the audio-on- item-switch fix which was measured before and after on an emulator. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
parent
4cc750ba3a
commit
abdb3b434d
|
|
@ -575,8 +575,21 @@ class MainActivity : AppCompatActivity() {
|
||||||
}.sorted().joinToString("|")
|
}.sorted().joinToString("|")
|
||||||
val changed = assignmentSig != zoneManager?.lastAssignmentSig
|
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")
|
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)")
|
Log.i("MainActivity", "Multi-zone layout with ${layoutZones.length()} zones (layout=$layoutId, was=$currentLayoutId)")
|
||||||
handler.post {
|
handler.post {
|
||||||
hideStatus()
|
hideStatus()
|
||||||
|
|
@ -587,6 +600,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
zoneManager?.setupZones(layoutZones, layoutId)
|
zoneManager?.setupZones(layoutZones, layoutId)
|
||||||
zoneManager?.renderAssignments(assignments, config.serverUrl, contentCache, config.deviceId)
|
zoneManager?.renderAssignments(assignments, config.serverUrl, contentCache, config.deviceId)
|
||||||
zoneManager?.lastAssignmentSig = assignmentSig
|
zoneManager?.lastAssignmentSig = assignmentSig
|
||||||
|
zoneManager?.lastZoneSig = zoneSig
|
||||||
}
|
}
|
||||||
} else if (changed) {
|
} else if (changed) {
|
||||||
Log.i("MainActivity", "Multi-zone assignments changed, re-rendering")
|
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() {
|
override fun onDestroy() {
|
||||||
remoteStreaming = false
|
remoteStreaming = false
|
||||||
// Kill the wall/group leader tick BEFORE releasing media. The Handler is on the main looper
|
// Kill the wall/group leader tick BEFORE releasing media. The Handler is on the main looper
|
||||||
|
|
|
||||||
|
|
@ -209,13 +209,42 @@ class MediaPlayerManager(
|
||||||
// would restart the video and flicker. Main thread only (WebView access).
|
// would restart the video and flicker. Main thread only (WebView access).
|
||||||
private fun setYoutubeMuted(muted: Boolean) {
|
private fun setYoutubeMuted(muted: Boolean) {
|
||||||
youtubeMuted = muted
|
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');" +
|
val js = "(function(){try{var f=document.querySelector('iframe');" +
|
||||||
"if(f&&f.contentWindow){f.contentWindow.postMessage(" +
|
"if(f&&f.contentWindow){f.contentWindow.postMessage(" +
|
||||||
"JSON.stringify({event:'command',func:'$func',args:[]}),'*');}}catch(e){}})()"
|
"JSON.stringify({event:'command',func:'$func',args:[]}),'*');}}catch(e){}})()"
|
||||||
youtubeWebView?.let { wv -> wv.post { try { wv.evaluateJavascript(js, null) } catch (_: Throwable) {} } }
|
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
|
// Fullscreen widget render (single-zone / "fullscreen" layouts). Reuses the
|
||||||
// full-screen WebView; ZoneManager handles widgets in multi-zone layouts.
|
// full-screen WebView; ZoneManager handles widgets in multi-zone layouts.
|
||||||
fun showWidget(url: String) {
|
fun showWidget(url: String) {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ class ZoneManager(
|
||||||
var currentLayoutId: String? = null
|
var currentLayoutId: String? = null
|
||||||
private set
|
private set
|
||||||
var lastAssignmentSig: String? = null
|
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.
|
// #74/#75: device-effective IANA timezone for per-item schedule evaluation.
|
||||||
@Volatile private var effectiveTimezone: String? = null
|
@Volatile private var effectiveTimezone: String? = null
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue