Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 8 additions & 1 deletion app/src/main/java/com/limelight/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/com/limelight/Ps5StreamActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions app/src/main/java/com/limelight/home/BatteryCap.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading