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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user