From 101e8fbf1b277da74a3510435e28fd4bcaa6edae Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 27 Oct 2026 11:47:35 +0100 Subject: fix(android): keep the screen on while a video plays The WebView has no Screen Wake Lock API, so the player's request did nothing. The bridge now supplies navigator.wakeLock over a native screen:keep-on channel (the WebView's keepScreenOn). Co-Authored-By: Claude Opus 5.5 --- .../app/src/main/assets/bridge/meshbay-bridge.js | 39 ++++++++++++++++++++++ .../main/kotlin/org/meshbay/client/MainActivity.kt | 1 + .../kotlin/org/meshbay/client/bridge/Channels.kt | 2 ++ 3 files changed, 42 insertions(+) (limited to 'packages/meshbay-android/app/src') diff --git a/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js index e8b40b2d..e78a2337 100644 --- a/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js +++ b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js @@ -46,6 +46,45 @@ port.postMessage(JSON.stringify({ id, ch: channel, args })); }); + // The Screen Wake Lock API, which Chrome has and the WebView does not: the + // video player (and music, when asked) holds it so the screen does not time + // out mid-film. Held natively as the WebView's keepScreenOn, and released + // when the page hides, as the API does. Cleared first, for a reload that + // left one held. + if (!('wakeLock' in navigator)) { + const held = new Set(); + const sync = () => call('screen:keep-on', held.size > 0).catch(() => {}); + class WakeLockSentinel extends EventTarget { + constructor() { super(); this.type = 'screen'; this.released = false; this.onrelease = null; } + release() { + if (!this.released) { + this.released = true; + held.delete(this); + sync(); + const event = new Event('release'); + if (typeof this.onrelease === 'function') this.onrelease(event); + this.dispatchEvent(event); + } + return Promise.resolve(); + } + } + const request = (type) => { + if (type !== 'screen') return Promise.reject(new TypeError('Unsupported wake lock type')); + if (document.visibilityState !== 'visible') { + return Promise.reject(new DOMException('The page is not visible', 'NotAllowedError')); + } + const sentinel = new WakeLockSentinel(); + held.add(sentinel); + sync(); + return Promise.resolve(sentinel); + }; + document.addEventListener('visibilitychange', () => { + if (document.visibilityState !== 'visible') [...held].forEach((s) => s.release()); + }); + Object.defineProperty(navigator, 'wakeLock', { value: Object.freeze({ request }) }); + sync(); + } + // A write is a binary message: "MBB1" | id | channel | 0 | handle | bytes, // big-endian — one copy, no JSON, no base64 (spike S-3: 136 MB/s awaited // per 48 KB chunk). Without ArrayBuffer messages, base64 over JSON. diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt index 75ac0156..e60535b9 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt @@ -134,6 +134,7 @@ class MainActivity : Activity() { channels = Channels(hub, onHubChanged = { runOnUiThread { reloadForHub() } }, hasCatalogue = { code -> hasAsset("ui/locales/$code.js") }, keys = keys, saves = saves, cast = cast, onPlayback = { on -> runOnUiThread { playing = on; keepAlive() } }, + onKeepScreenOn = { on -> runOnUiThread { web.keepScreenOn = on } }, push = PushChannels(this, PushState(getSharedPreferences(PushState.PREFS, Context.MODE_PRIVATE)), channelNames = { mapOf( Notifier.CHANNEL_CHAT to text.get("push.channel_chat", channels.locale), diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/bridge/Channels.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/bridge/Channels.kt index 19b0f3c2..1563810b 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/bridge/Channels.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/bridge/Channels.kt @@ -32,6 +32,7 @@ class Channels( private val saves: SaveSinks? = null, private val cast: CastChannels? = null, private val onPlayback: (Boolean) -> Unit = {}, + private val onKeepScreenOn: (Boolean) -> Unit = {}, private val push: PushChannels? = null, private val photos: PhotoChannels? = null, private val destinations: DestinationChannels? = null, @@ -48,6 +49,7 @@ class Channels( "ice:resolve-stun" -> resolveStun(args.optJSONArray(0) ?: JSONArray()) "ui:locale" -> setLocale(args.optString(0, "")) "playback:keep-alive" -> { onPlayback(args.optBoolean(0, false)); true } + "screen:keep-on" -> { onKeepScreenOn(args.optBoolean(0, false)); true } // Where downloads go, chosen once; a display name, never a URI. "folder:choose" -> saves().chooseFolder() -- cgit v1.2.3