From 1ed2b6ade4c1664be7f3291c379ba83c18b575dd Mon Sep 17 00:00:00 2001 From: Pauljd Date: Mon, 2 Nov 2026 19:18:20 +0000 Subject: [PATCH] Fix OrbitHive blue screen on the box, darken the Horror ident sky The game's canvas had no height in the app's WebView because 100vh read as 0. Screenshots stay allowed while the game is open. Horror ident logo no longer sits on a flat red sky; compose script gains an optional logo shadow. Co-Authored-By: Claude Opus 5.5 (1M context) --- VERIFICATION-0.79.3.md | 5 ++++ android/app/build.gradle.kts | 4 ++-- android/app/src/main/assets/orbithive/game.js | 6 +++-- .../app/src/main/assets/orbithive/index.html | 2 +- .../main/java/tv/orbit/classic/EmbyClient.kt | 2 +- .../main/java/tv/orbit/classic/OrbitArcade.kt | 24 ++++++++++++++++++- .../tv/orbit/classic/SecureMediaDataSource.kt | 2 +- design/channels/blender/build_3d_ident.py | 4 ++-- design/channels/blender/compose_ident.sh | 9 +++++-- docs/refinement-notes.md | 4 ++++ 10 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 VERIFICATION-0.79.3.md diff --git a/VERIFICATION-0.79.3.md b/VERIFICATION-0.79.3.md new file mode 100644 index 0000000..707f852 --- /dev/null +++ b/VERIFICATION-0.79.3.md @@ -0,0 +1,5 @@ +# Orbit TV 0.79.3 + +- OrbitHive drew nothing on the box: blue screen with sound. Log showed canvas box 960x0 with real game pixels inside. +- Fix: canvas sized with position:fixed and 100%, size re-checked each frame. WebView can read app assets. Window not secure while the game is open. +- Confirmed on the TV by Paul before the version bump: full screen and working. Temporary pixel logging removed for this release. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 852c9e9..d967765 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,8 +9,8 @@ android { applicationId = "tv.orbit.classic" minSdk = 26 targetSdk = 36 - versionCode = 86 - versionName = "0.79.0-orbithive" + versionCode = 89 + versionName = "0.79.3-orbithive-canvas" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/assets/orbithive/game.js b/android/app/src/main/assets/orbithive/game.js index c34b309..52f4aa8 100644 --- a/android/app/src/main/assets/orbithive/game.js +++ b/android/app/src/main/assets/orbithive/game.js @@ -36,9 +36,9 @@ // ---------- canvas ---------- const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); - let scale = 1, ox = 0, oy = 0, dpr = 1; + let scale = 1, ox = 0, oy = 0, dpr = Math.min(window.devicePixelRatio || 1, 2); function resize() { - dpr = Math.min(window.devicePixelRatio || 1, 2); + dpr = 1; canvas.width = Math.round(innerWidth * dpr); canvas.height = Math.round(innerHeight * dpr); scale = Math.min(canvas.width / W, canvas.height / H); @@ -1110,6 +1110,8 @@ // ---------- main loop ---------- let last = performance.now(); function frame(now) { + // In the app the page can start before it has a size, and may not be told when it gets one. + if (innerWidth > 0 && (canvas.width !== Math.round(innerWidth * dpr) || canvas.height !== Math.round(innerHeight * dpr))) resize(); const dt = Math.min(0.05, (now - last) / 1000); last = now; update(dt); diff --git a/android/app/src/main/assets/orbithive/index.html b/android/app/src/main/assets/orbithive/index.html index 382f8dc..bd1f873 100644 --- a/android/app/src/main/assets/orbithive/index.html +++ b/android/app/src/main/assets/orbithive/index.html @@ -7,7 +7,7 @@ diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt index 0b47c10..d23335a 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt @@ -66,7 +66,7 @@ internal class HttpsEmbyTransport(internal val deviceId: String = UUID.randomUUI connection.connectTimeout = 10_000 connection.readTimeout = request.readTimeoutMs ?: if (request.path == "/Sessions/Logout") 5_000 else 15_000 connection.setRequestProperty("Accept", "application/json") - connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.79.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.79.3\"") request.token?.let { connection.setRequestProperty("X-Emby-Token", it) } if (request.method == "POST") { val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8) diff --git a/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt b/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt index 703b626..3e55758 100644 --- a/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt +++ b/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt @@ -42,7 +42,10 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView +import android.webkit.ConsoleMessage import android.webkit.JavascriptInterface +import android.webkit.WebChromeClient +import android.webkit.WebViewClient import android.webkit.WebView import androidx.tv.material3.Text import kotlinx.coroutines.delay @@ -260,14 +263,33 @@ private fun SnakeScreen(best: Int, onBest: (Int) -> Unit, onLeave: () -> Unit) { @Composable private fun OrbitHiveScreen(onLeave: () -> Unit) { var web by remember { mutableStateOf(null) } + // No protected picture here, and leaving the window open to screenshots lets the game be checked on the box. + val activity = androidx.activity.compose.LocalActivity.current + androidx.compose.runtime.DisposableEffect(Unit) { + activity?.window?.clearFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE) + onDispose { activity?.window?.addFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE) } + } BackHandler { web?.evaluateJavascript("window.orbitBack()", null) } AndroidView(modifier = Modifier.fillMaxSize(), factory = { context -> WebView(context).apply { setBackgroundColor(android.graphics.Color.rgb(7, 22, 50)) settings.javaScriptEnabled = true settings.domStorageEnabled = true - settings.blockNetworkLoads = true + settings.allowFileAccess = true settings.mediaPlaybackRequiresUserGesture = false + // The page is the app's own and asks for nothing outside it. Its problems go to the log. + webChromeClient = object : WebChromeClient() { + override fun onConsoleMessage(message: ConsoleMessage): Boolean { + android.util.Log.i("OrbitHive", "${message.messageLevel()} ${message.message()} line ${message.lineNumber()}") + return true + } + } + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView, url: String) { android.util.Log.i("OrbitHive", "loaded size=${view.width}x${view.height}") } + override fun onReceivedError(view: WebView, request: android.webkit.WebResourceRequest, error: android.webkit.WebResourceError) { + android.util.Log.w("OrbitHive", "failed ${request.url.lastPathSegment}: ${error.description}") + } + } isFocusable = true; isFocusableInTouchMode = true addJavascriptInterface(object { @JavascriptInterface fun leave() { post { onLeave() } } diff --git a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt index 8a34f89..bc93542 100644 --- a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt +++ b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt @@ -33,7 +33,7 @@ internal class SecureMediaDataSource(private val session: EmbySampleSession) : B next.instanceFollowRedirects = false next.connectTimeout = 10_000 next.readTimeout = 20_000 - next.setRequestProperty("User-Agent", "OrbitTV/0.79.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.79.3") next.setRequestProperty("Accept-Encoding", "identity") session.mediaHeaders(address).forEach { (key, value) -> next.setRequestProperty(key, value) } if (dataSpec.position != 0L || dataSpec.length != C.LENGTH_UNSET.toLong()) { diff --git a/design/channels/blender/build_3d_ident.py b/design/channels/blender/build_3d_ident.py index fdc3b79..fd66797 100644 --- a/design/channels/blender/build_3d_ident.py +++ b/design/channels/blender/build_3d_ident.py @@ -504,12 +504,12 @@ def haunted(windows_colour='#FF2A1E'): moon, n, l = material('moon'); out = n['Material Output']; n.remove(n['Principled BSDF']) coord = n.new('ShaderNodeTexCoord'); tone = noise_ramp(n, l, coord.outputs['Object'], .22, 10, [(.32, '#7A0A10'), (.7, '#E8261E')]) emit = n.new('ShaderNodeEmission'); l.new(tone.outputs['Color'], emit.inputs['Color']); emit.inputs['Strength'].default_value = 2.4; l.new(emit.outputs[0], out.inputs['Surface']) - ball = sphere('moon', 26); ball.location = (46, 150, 40); ball.data.materials.append(moon); ball.visible_shadow = False + ball = sphere('moon', 26); ball.location = (92, 150, 44); ball.data.materials.append(moon); ball.visible_shadow = False data = bpy.data.lights.new('rim', 'SUN'); data.energy = 1.5; data.color = hexrgb('#FF7A6A')[:3]; rim = bpy.data.objects.new('rim', data); bpy.context.collection.objects.link(rim); rim.rotation_euler = Vector((.45, .75, -.5)).to_track_quat('-Z', 'Y').to_euler() scene.world = bpy.data.worlds.new('fog'); scene.world.use_nodes = True bg = scene.world.node_tree.nodes['Background']; bg.inputs['Strength'].default_value = 1.1 w = scene.world.node_tree; tex = w.nodes.new('ShaderNodeTexCoord'); sep = w.nodes.new('ShaderNodeSeparateXYZ'); w.links.new(tex.outputs['Generated'], sep.inputs[0]) - sky = ramp(w.nodes, [(.47, '#7A2030'), (.56, '#3A1020'), (.72, '#0E0614')]); w.links.new(sep.outputs['Z'], sky.inputs['Fac']); w.links.new(sky.outputs['Color'], bg.inputs['Color']) + sky = ramp(w.nodes, [(0, '#5E1622'), (.1, '#2A0A14'), (.32, '#120A1A')]); w.links.new(sep.outputs['Z'], sky.inputs['Fac']); w.links.new(sky.outputs['Color'], bg.inputs['Color']) if SCENE == 'orbit-1': world_tint = ('#061A3C', '#0E3A6E') diff --git a/design/channels/blender/compose_ident.sh b/design/channels/blender/compose_ident.sh index 9b48d5c..2f844ba 100755 --- a/design/channels/blender/compose_ident.sh +++ b/design/channels/blender/compose_ident.sh @@ -1,12 +1,17 @@ #!/bin/bash # Frames from build_3d_ident.py into a finished ident: a soft start, then the channel's logo fading in over the end. # compose_ident.sh [overlay folder of over_%04d.png, drawn on top] +# LOGO_SHADOW=0.8 lays a soft dark shadow under the logo, for scenes busy behind it. +LOGO="[1:v]scale=992:-1,format=rgba,fade=t=in:st=4.4:d=0.8:alpha=1[logo];[0:v]fade=t=in:st=0:d=0.3[scene];[scene][logo]overlay=x=(W-w)/2-110:y=(H-h)/2-30:shortest=1" +if [ -n "$LOGO_SHADOW" ]; then + LOGO="[1:v]scale=992:-1,format=rgba,fade=t=in:st=4.4:d=0.8:alpha=1,split[logo][under];[under]colorchannelmixer=rr=0:gg=0:bb=0:aa=$LOGO_SHADOW,boxblur=26:2[shadow];[0:v]fade=t=in:st=0:d=0.3[scene];[scene][shadow]overlay=x=(W-w)/2-110:y=(H-h)/2-30:shortest=1[dim];[dim][logo]overlay=x=(W-w)/2-110:y=(H-h)/2-30:shortest=1" +fi if [ -n "$4" ]; then ffmpeg -nostdin -y -loglevel error -framerate 50 -i "$1/frame_%04d.png" -loop 1 -i "$2" -framerate 50 -i "$4/over_%04d.png" -filter_complex \ - "[1:v]scale=992:-1,format=rgba,fade=t=in:st=4.4:d=0.8:alpha=1[logo];[0:v]fade=t=in:st=0:d=0.3[scene];[scene][logo]overlay=x=(W-w)/2-110:y=(H-h)/2-30:shortest=1[lit];[lit][2:v]overlay=0:0:shortest=1,format=yuv420p" \ + "$LOGO[lit];[lit][2:v]overlay=0:0:shortest=1,format=yuv420p" \ -t 7 -c:v libx264 -preset slow -crf 18 -movflags +faststart -an "$3" exit fi ffmpeg -nostdin -y -loglevel error -framerate 50 -i "$1/frame_%04d.png" -loop 1 -i "$2" -filter_complex \ - "[1:v]scale=992:-1,format=rgba,fade=t=in:st=4.4:d=0.8:alpha=1[logo];[0:v]fade=t=in:st=0:d=0.3[scene];[scene][logo]overlay=x=(W-w)/2-110:y=(H-h)/2-30:shortest=1,format=yuv420p" \ + "$LOGO,format=yuv420p" \ -t 7 -c:v libx264 -preset slow -crf 18 -movflags +faststart -an "$3" diff --git a/docs/refinement-notes.md b/docs/refinement-notes.md index a2e04ac..369d04f 100644 --- a/docs/refinement-notes.md +++ b/docs/refinement-notes.md @@ -51,3 +51,7 @@ Paul, 2 November 2026: keep an eye on optimisation, size and cache management. - **One screen at a time:** sign in, channels ticked from what the library holds, then the main menu. - **Smart schedules on by default.** Today a fresh sign-in starts every channel off, and needs 29 presses. - **Numbers in blocks like Sky** from the start, so there's never a move to make. The one move we made missed two channels until a test caught it. + +## OrbitHive blue screen (0.79.3) + +Inside the app's WebView on the box, `100vh` came out as 0, so the game's canvas had no height while the game ran and played sound behind it. Only the page's blue showed. Canvas sized with `position:fixed` and `100%` instead. Don't use viewport units in pages hosted in the app's WebView. Screenshots stay allowed while OrbitHive is open, because they are what gets a future problem like this checked on the box.