Skip to content

Commit f8231c9

Browse files
committed
add per-game settings
1 parent f10085f commit f8231c9

5 files changed

Lines changed: 274 additions & 0 deletions

File tree

app/src/main/java/com/limelight/AppView.java

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
import com.limelight.utils.UiHelper;
2424

2525
import android.app.Activity;
26+
import android.app.AlertDialog;
2627
import android.app.Service;
2728
import android.content.ComponentName;
29+
import android.content.DialogInterface;
2830
import android.content.Intent;
2931
import android.content.ServiceConnection;
3032
import android.content.SharedPreferences;
@@ -35,18 +37,24 @@
3537
import android.os.Bundle;
3638
import android.os.IBinder;
3739
import android.view.ContextMenu;
40+
import android.view.LayoutInflater;
3841
import android.view.Menu;
3942
import android.view.MenuItem;
4043
import android.view.View;
4144
import android.view.ContextMenu.ContextMenuInfo;
4245
import android.widget.AbsListView;
4346
import android.widget.AdapterView;
4447
import android.widget.AdapterView.OnItemClickListener;
48+
import android.widget.ArrayAdapter;
4549
import android.widget.ImageView;
50+
import android.widget.SeekBar;
51+
import android.widget.Spinner;
4652
import android.widget.TextView;
4753
import android.widget.Toast;
4854
import android.widget.AdapterView.AdapterContextMenuInfo;
4955

56+
import com.limelight.preferences.PerAppConfiguration;
57+
5058
import org.xmlpull.v1.XmlPullParserException;
5159

5260
public class AppView extends Activity implements AdapterFragmentCallbacks {
@@ -70,6 +78,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
7078
private final static int VIEW_DETAILS_ID = 5;
7179
private final static int CREATE_SHORTCUT_ID = 6;
7280
private final static int HIDE_APP_ID = 7;
81+
private final static int APP_SETTINGS_ID = 8;
7382

7483
public final static String HIDDEN_APPS_PREF_FILENAME = "HiddenApps";
7584

@@ -415,6 +424,7 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuIn
415424
}
416425

417426
menu.add(Menu.NONE, VIEW_DETAILS_ID, 4, getResources().getString(R.string.applist_menu_details));
427+
menu.add(Menu.NONE, APP_SETTINGS_ID, 5, getResources().getString(R.string.applist_menu_streaming_settings));
418428

419429
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
420430
// Only add an option to create shortcut if box art is loaded
@@ -500,6 +510,10 @@ public void run() {
500510
}
501511
return true;
502512

513+
case APP_SETTINGS_ID:
514+
showPerAppSettingsDialog(app);
515+
return true;
516+
503517
default:
504518
return super.onContextItemSelected(item);
505519
}
@@ -645,6 +659,117 @@ public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
645659
listView.requestFocus();
646660
}
647661

662+
private void showPerAppSettingsDialog(final AppObject app) {
663+
View view = LayoutInflater.from(this).inflate(R.layout.dialog_per_app_settings, null);
664+
final Spinner resSpinner = view.findViewById(R.id.resolution_spinner);
665+
final Spinner fpsSpinner = view.findViewById(R.id.fps_spinner);
666+
final SeekBar bitrateSeekBar = view.findViewById(R.id.bitrate_seekbar);
667+
final TextView bitrateLabel = view.findViewById(R.id.bitrate_label);
668+
669+
final String[] resEntries = {
670+
getString(R.string.per_app_settings_global_default),
671+
"360p (640×360)", "480p (854×480)", "720p (1280×720)",
672+
"1080p (1920×1080)", "1440p (2560×1440)", "4K (3840×2160)"
673+
};
674+
final int[][] resValues = {
675+
{0, 0}, {640, 360}, {854, 480}, {1280, 720},
676+
{1920, 1080}, {2560, 1440}, {3840, 2160}
677+
};
678+
679+
final int[] fpsValues = {0, 30, 60, 90, 120, 144, 240};
680+
final String[] fpsEntries = new String[fpsValues.length];
681+
fpsEntries[0] = getString(R.string.per_app_settings_global_default);
682+
for (int i = 1; i < fpsValues.length; i++) {
683+
fpsEntries[i] = fpsValues[i] + " FPS";
684+
}
685+
686+
ArrayAdapter<String> resAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, resEntries);
687+
resAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
688+
resSpinner.setAdapter(resAdapter);
689+
690+
ArrayAdapter<String> fpsAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, fpsEntries);
691+
fpsAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
692+
fpsSpinner.setAdapter(fpsAdapter);
693+
694+
// SeekBar: progress 0 = default, progress N = N * 500 kbps (max 150 Mbps)
695+
final int bitrateStep = 500;
696+
bitrateSeekBar.setMax(150000 / bitrateStep);
697+
bitrateSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
698+
@Override
699+
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
700+
if (progress == 0) {
701+
bitrateLabel.setText(getString(R.string.per_app_settings_bitrate_default));
702+
} else {
703+
bitrateLabel.setText(String.format("%.1f Mbps", progress * bitrateStep / 1000.0));
704+
}
705+
}
706+
707+
@Override
708+
public void onStartTrackingTouch(SeekBar seekBar) {
709+
}
710+
711+
@Override
712+
public void onStopTrackingTouch(SeekBar seekBar) {
713+
}
714+
});
715+
716+
// Load existing overrides
717+
PerAppConfiguration override = PerAppConfiguration.readOverride(this, uuidString, app.app.getAppId());
718+
719+
int resIndex = 0;
720+
if (override.width > 0) {
721+
for (int i = 1; i < resValues.length; i++) {
722+
if (resValues[i][0] == override.width && resValues[i][1] == override.height) {
723+
resIndex = i;
724+
break;
725+
}
726+
}
727+
}
728+
resSpinner.setSelection(resIndex);
729+
730+
int fpsIndex = 0;
731+
if (override.fps > 0) {
732+
for (int i = 1; i < fpsValues.length; i++) {
733+
if (fpsValues[i] == override.fps) {
734+
fpsIndex = i;
735+
break;
736+
}
737+
}
738+
}
739+
fpsSpinner.setSelection(fpsIndex);
740+
741+
int bitrateProgress = override.bitrate > 0 ? override.bitrate / bitrateStep : 0;
742+
bitrateSeekBar.setProgress(bitrateProgress);
743+
bitrateLabel.setText(bitrateProgress == 0
744+
? getString(R.string.per_app_settings_bitrate_default)
745+
: String.format("%.1f Mbps", bitrateProgress * bitrateStep / 1000.0));
746+
747+
new AlertDialog.Builder(this)
748+
.setTitle(getString(R.string.per_app_settings_title) + ": " + app.app.getAppName())
749+
.setView(view)
750+
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
751+
@Override
752+
public void onClick(DialogInterface dialog, int which) {
753+
PerAppConfiguration newOverride = new PerAppConfiguration();
754+
int ri = resSpinner.getSelectedItemPosition();
755+
newOverride.width = resValues[ri][0];
756+
newOverride.height = resValues[ri][1];
757+
newOverride.fps = fpsValues[fpsSpinner.getSelectedItemPosition()];
758+
int progress = bitrateSeekBar.getProgress();
759+
newOverride.bitrate = progress > 0 ? progress * bitrateStep : 0;
760+
PerAppConfiguration.saveOverride(AppView.this, uuidString, app.app.getAppId(), newOverride);
761+
}
762+
})
763+
.setNegativeButton(android.R.string.cancel, null)
764+
.setNeutralButton(R.string.per_app_settings_reset, new DialogInterface.OnClickListener() {
765+
@Override
766+
public void onClick(DialogInterface dialog, int which) {
767+
PerAppConfiguration.deleteOverride(AppView.this, uuidString, app.app.getAppId());
768+
}
769+
})
770+
.show();
771+
}
772+
648773
public static class AppObject {
649774
public final NvApp app;
650775
public boolean isRunning;

app/src/main/java/com/limelight/Game.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.limelight.nvstream.input.MouseButtonPacket;
2929
import com.limelight.nvstream.jni.MoonBridge;
3030
import com.limelight.preferences.GlPreferences;
31+
import com.limelight.preferences.PerAppConfiguration;
3132
import com.limelight.preferences.PreferenceConfiguration;
3233
import com.limelight.ui.GameGestures;
3334
import com.limelight.ui.StreamView;
@@ -320,6 +321,11 @@ public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
320321

321322
app = new NvApp(appName != null ? appName : "app", appId, appSupportsHdr);
322323

324+
String pcUuid = getIntent().getStringExtra(EXTRA_PC_UUID);
325+
if (pcUuid != null) {
326+
PerAppConfiguration.readOverride(this, pcUuid, appId).applyTo(prefConfig);
327+
}
328+
323329
X509Certificate serverCert = null;
324330
try {
325331
if (derCertData != null) {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.limelight.preferences;
2+
3+
import android.content.Context;
4+
import android.content.SharedPreferences;
5+
6+
public class PerAppConfiguration {
7+
private static final String PREFS_NAME = "PerAppSettings";
8+
9+
public int width; // 0 = no override
10+
public int height; // 0 = no override
11+
public int fps; // 0 = no override
12+
public int bitrate; // 0 = no override (kbps)
13+
14+
private static String prefix(String uuid, int appId) {
15+
return uuid + "_" + appId;
16+
}
17+
18+
public static PerAppConfiguration readOverride(Context context, String uuid, int appId) {
19+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
20+
String keyPrefix = prefix(uuid, appId);
21+
PerAppConfiguration config = new PerAppConfiguration();
22+
config.width = prefs.getInt(keyPrefix + "_w", 0);
23+
config.height = prefs.getInt(keyPrefix + "_h", 0);
24+
config.fps = prefs.getInt(keyPrefix + "_fps", 0);
25+
config.bitrate = prefs.getInt(keyPrefix + "_bitrate", 0);
26+
return config;
27+
}
28+
29+
public static void saveOverride(Context context, String uuid, int appId, PerAppConfiguration override) {
30+
SharedPreferences.Editor editor = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit();
31+
String keyPrefix = prefix(uuid, appId);
32+
if (override.width > 0) {
33+
editor.putInt(keyPrefix + "_w", override.width);
34+
editor.putInt(keyPrefix + "_h", override.height);
35+
} else {
36+
editor.remove(keyPrefix + "_w");
37+
editor.remove(keyPrefix + "_h");
38+
}
39+
if (override.fps > 0) {
40+
editor.putInt(keyPrefix + "_fps", override.fps);
41+
} else {
42+
editor.remove(keyPrefix + "_fps");
43+
}
44+
if (override.bitrate > 0) {
45+
editor.putInt(keyPrefix + "_bitrate", override.bitrate);
46+
} else {
47+
editor.remove(keyPrefix + "_bitrate");
48+
}
49+
editor.apply();
50+
}
51+
52+
public static void deleteOverride(Context context, String uuid, int appId) {
53+
String keyPrefix = prefix(uuid, appId);
54+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
55+
.remove(keyPrefix + "_w")
56+
.remove(keyPrefix + "_h")
57+
.remove(keyPrefix + "_fps")
58+
.remove(keyPrefix + "_bitrate")
59+
.apply();
60+
}
61+
62+
public void applyTo(PreferenceConfiguration config) {
63+
if (width > 0 && height > 0) {
64+
config.width = width;
65+
config.height = height;
66+
}
67+
if (fps > 0) {
68+
config.fps = fps;
69+
}
70+
if (bitrate > 0) {
71+
config.bitrate = bitrate;
72+
}
73+
}
74+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:layout_width="match_parent"
4+
android:layout_height="wrap_content">
5+
6+
<LinearLayout
7+
android:layout_width="match_parent"
8+
android:layout_height="wrap_content"
9+
android:orientation="vertical"
10+
android:padding="16dp">
11+
12+
<TextView
13+
android:layout_width="match_parent"
14+
android:layout_height="wrap_content"
15+
android:text="@string/title_resolution_list"
16+
android:textAppearance="?android:textAppearanceSmall" />
17+
18+
<Spinner
19+
android:id="@+id/resolution_spinner"
20+
android:layout_width="match_parent"
21+
android:layout_height="wrap_content"
22+
android:layout_marginBottom="16dp" />
23+
24+
<TextView
25+
android:layout_width="match_parent"
26+
android:layout_height="wrap_content"
27+
android:text="@string/title_fps_list"
28+
android:textAppearance="?android:textAppearanceSmall" />
29+
30+
<Spinner
31+
android:id="@+id/fps_spinner"
32+
android:layout_width="match_parent"
33+
android:layout_height="wrap_content"
34+
android:layout_marginBottom="16dp" />
35+
36+
<LinearLayout
37+
android:layout_width="match_parent"
38+
android:layout_height="wrap_content"
39+
android:orientation="horizontal">
40+
41+
<TextView
42+
android:layout_width="0dp"
43+
android:layout_height="wrap_content"
44+
android:layout_weight="1"
45+
android:text="@string/title_seekbar_bitrate"
46+
android:textAppearance="?android:textAppearanceSmall" />
47+
48+
<TextView
49+
android:id="@+id/bitrate_label"
50+
android:layout_width="wrap_content"
51+
android:layout_height="wrap_content"
52+
android:minWidth="80dp"
53+
android:gravity="end" />
54+
55+
</LinearLayout>
56+
57+
<SeekBar
58+
android:id="@+id/bitrate_seekbar"
59+
android:layout_width="match_parent"
60+
android:layout_height="wrap_content" />
61+
62+
</LinearLayout>
63+
64+
</ScrollView>

app/src/main/res/values/strings.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@
124124
<string name="applist_menu_scut">Create Shortcut</string>
125125
<string name="applist_menu_tv_channel">Add to Channel</string>
126126
<string name="applist_menu_hide_app">Hide App</string>
127+
<string name="applist_menu_streaming_settings">Streaming Settings…</string>
128+
<string name="per_app_settings_title">Streaming Settings</string>
129+
<string name="per_app_settings_reset">Reset to Defaults</string>
130+
<string name="per_app_settings_global_default">Default (global setting)</string>
131+
<string name="per_app_settings_bitrate_default">Default</string>
127132
<string name="applist_refresh_title">App List</string>
128133
<string name="applist_refresh_msg">Refreshing apps…</string>
129134
<string name="applist_refresh_error_title">Error</string>

0 commit comments

Comments
 (0)