Fix OrbitHive blue screen on the box, darken the Horror ident sky
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 1m41s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m32s

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) <noreply@anthropic.com>
This commit is contained in:
2026-11-02 19:18:20 +00:00
co-authored by Claude Opus 5.5
parent 022d27c0d8
commit 1ed2b6ade4
10 changed files with 50 additions and 12 deletions
+5
View File
@@ -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.
+2 -2
View File
@@ -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 }
@@ -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);
@@ -7,7 +7,7 @@
<style>
@font-face{font-family:Barlow;font-weight:100 900;src:url(barlow-semibold.ttf)}
html,body{margin:0;height:100%;background:#071632;overflow:hidden;touch-action:none;user-select:none}
canvas{display:block;width:100vw;height:100vh;outline:none}
canvas{display:block;position:fixed;left:0;top:0;width:100%;height:100%;outline:none}
</style>
</head>
<body>
@@ -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)
@@ -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<WebView?>(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() } }
@@ -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()) {
+2 -2
View File
@@ -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')
+7 -2
View File
@@ -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 <frames folder> <logo png> <out.mp4> [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"
+4
View File
@@ -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.