diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ccfc2796a..fd65827be2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0-test237 (2026-10-21) +- Handheld, "On battery, stream at" (Settings, Handheld) now works (#65). It was saved but nothing read it. Unplugged, PC and PS5 streams go no higher than the chosen height (1080p, 720p or 540p); plugged in, the Picture settings apply as before. + - PC: the size keeps the stream's shape (Native 2448 × 1696 at 720p becomes 1040 × 720). The fold check compares the screen with the size before the cap, so a capped stream doesn't keep reconnecting at the screen's size. + - PS5: the lower of the PS5's own setting and the cap. + - Xbox: not affected; its resolution comes from the Xbox service. + ## 1.0-test236 (2026-10-21) - Found on the JAL TV (AQUOS, Android 11, MediaTek with a Mali GPU), PS5 over PlayStation Network at 720p: - Super resolution didn't start there ("eglChooseConfig"): the GPU has no OpenGL ES 3 setup for both a window and an off-screen surface. FSR now tries several setups, and without an off-screen surface it works surfaceless. Before, it fell back to the plain picture, as designed. diff --git a/app/src/handheld/java/com/limelight/handheld/HandheldHomeActivity.kt b/app/src/handheld/java/com/limelight/handheld/HandheldHomeActivity.kt index 1276e6a9b5..5220942c51 100644 --- a/app/src/handheld/java/com/limelight/handheld/HandheldHomeActivity.kt +++ b/app/src/handheld/java/com/limelight/handheld/HandheldHomeActivity.kt @@ -1202,7 +1202,7 @@ class HandheldHomeActivity : Activity() { choice(list, prefs, "hh_screen_size", "Screen size", "Auto fits this screen. Compact and Large make everything bigger", listOf("Auto" to "auto", "Compact" to "compact", "Normal" to "normal", "Large" to "large"), "auto") { recreate() } appToggle(list, "hh_rotate", "Screen turns with the device", "Portrait when you hold it upright", true) { applyRotation() } - choice(list, prefs, "hh_battery_res", "On battery, stream at", "Plugged in, the Picture settings apply", + choice(list, prefs, com.limelight.home.BatteryCap.PREF, "On battery, stream at", "Unplugged, PC and PS5 streams go no higher than this. Plugged in, the Picture settings apply", listOf("Same" to "same", "1080p" to "1080", "720p" to "720", "540p" to "540"), "same") appToggle(list, "hh_dim_on_tv", "Dim the screen on a TV", "While this ${deviceShort()} is the controller for a TV", true) toggle(list, "checkbox_ultra_low_latency", "Ultra low latency", "Experimental. This device: ${chipName()}", false) diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 7ff4d1ecc6..cd0aee4793 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -223,6 +223,10 @@ protected void onCreate(Bundle savedInstanceState) { // Read the stream preferences prefConfig = PreferenceConfiguration.readPreferences(this); + // #65: on battery, Settings, Handheld, "On battery, stream at" caps the size. The fold checks compare the + // screen with the size before the cap, so a capped stream isn't taken for a wrong-size one. + uncappedSize = prefConfig.width + "x" + prefConfig.height; + com.limelight.home.BatteryCap.apply(this, prefConfig); trace(foldLabel(getResources().getConfiguration()) + ": stream " + prefConfig.width + "x" + prefConfig.height + ", Native " + PreferenceConfiguration.nativeWholeScreen(this)); tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0); @@ -2865,9 +2869,12 @@ private void reconnectHere(String why) { /** With Native on, the size this screen streams at now differs from this stream's. */ private boolean sizeChanged() { return prefConfig != null && PreferenceConfiguration.nativeWholeScreen(this) - && !PreferenceConfiguration.nativeResolution(this).equals(prefConfig.width + "x" + prefConfig.height); + && !PreferenceConfiguration.nativeResolution(this).equals(uncappedSize != null ? uncappedSize : prefConfig.width + "x" + prefConfig.height); } + /** The stream's size before the battery cap (#65). */ + private String uncappedSize; + @Override public void mouseMove(int deltaX, int deltaY) { conn.sendMouseMove((short) deltaX, (short) deltaY); diff --git a/app/src/main/java/com/limelight/Ps5StreamActivity.kt b/app/src/main/java/com/limelight/Ps5StreamActivity.kt index bcb38a1af5..fcb0a5273d 100644 --- a/app/src/main/java/com/limelight/Ps5StreamActivity.kt +++ b/app/src/main/java/com/limelight/Ps5StreamActivity.kt @@ -147,6 +147,15 @@ class Ps5StreamActivity : Activity(), SurfaceHolder.Callback, com.limelight.home away && awayQuality == "1080" -> VideoResolutionPreset.RES_1080P weak || (!away && prefs.getString("ps5_resolution", "1080") == "720") -> VideoResolutionPreset.RES_720P else -> VideoResolutionPreset.RES_1080P + }.let { chosen -> + // #65: on battery, Settings, Handheld, "On battery, stream at" caps it + val cap = com.limelight.home.BatteryCap.height(this) + val h = when (chosen) { VideoResolutionPreset.RES_540P -> 540; VideoResolutionPreset.RES_720P -> 720; else -> 1080 } + when { + cap in 1 until h && cap <= 540 -> VideoResolutionPreset.RES_540P + cap in 1 until h -> VideoResolutionPreset.RES_720P + else -> chosen + }.also { if (it != chosen) Log.i(TAG, "on battery: $chosen capped to $it") } } val h264 = !ps5 || (if (away) prefs.getString("away_codec", "h265") else prefs.getString("ps5_codec", "h265")) == "h264" // Frame rate: the PS5 streams at 30 or 60 fps only; any other rate broke the picture (#39). A Custom value diff --git a/app/src/main/java/com/limelight/home/BatteryCap.kt b/app/src/main/java/com/limelight/home/BatteryCap.kt new file mode 100644 index 0000000000..64885615d5 --- /dev/null +++ b/app/src/main/java/com/limelight/home/BatteryCap.kt @@ -0,0 +1,36 @@ +package com.limelight.home + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.util.Log +import com.limelight.preferences.PreferenceConfiguration + +/** + * Settings, Handheld, "On battery, stream at" (#65): while the device isn't plugged in, streams are capped at that + * height. Plugged in, the Picture settings apply. Only the handheld app sets it; elsewhere it stays "same". + */ +object BatteryCap { + const val PREF = "hh_battery_res" + + /** The height to cap a stream at now, or 0: no cap (the setting is Same, or the device is plugged in). */ + @JvmStatic + fun height(context: Context): Int { + val cap = context.getSharedPreferences("mc_tv", Context.MODE_PRIVATE).getString(PREF, "same")?.toIntOrNull() ?: return 0 + val status = try { context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) } catch (e: RuntimeException) { null } ?: return 0 + return if (status.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) == 0) cap else 0 + } + + /** Caps a PC stream's size, keeping its shape (width a multiple of 8). True if it was capped. */ + @JvmStatic + fun apply(context: Context, config: PreferenceConfiguration): Boolean { + val cap = height(context) + if (cap <= 0 || config.height <= cap) return false + val w = (Math.round(config.width.toDouble() * cap / config.height / 8.0) * 8).toInt() + Log.i("MoonCast", "on battery: stream ${config.width}x${config.height} capped to ${w}x$cap") + config.width = w + config.height = cap + return true + } +}