diff --git a/app/build.gradle b/app/build.gradle index 26d5f84c1e..e760fb3c53 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -133,6 +133,12 @@ android { path "src/main/jni/Android.mk" } } + + sourceSets { + main { + jniLibs.srcDirs = ['src/main/libs'] + } + } } dependencies { @@ -142,4 +148,6 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'org.jmdns:jmdns:3.5.9' implementation 'com.github.cgutman:ShieldControllerExtensions:1.0.1' + implementation group: 'commons-io', name: 'commons-io', version: '2.17.0' + } diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 1337627e2e..27387c3972 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -1,27 +1,5 @@ package com.limelight; -import java.io.IOException; -import java.io.StringReader; -import java.util.HashSet; -import java.util.List; - -import com.limelight.computers.ComputerManagerListener; -import com.limelight.computers.ComputerManagerService; -import com.limelight.grid.AppGridAdapter; -import com.limelight.nvstream.http.ComputerDetails; -import com.limelight.nvstream.http.NvApp; -import com.limelight.nvstream.http.NvHTTP; -import com.limelight.nvstream.http.PairingManager; -import com.limelight.preferences.PreferenceConfiguration; -import com.limelight.ui.AdapterFragment; -import com.limelight.ui.AdapterFragmentCallbacks; -import com.limelight.utils.CacheHelper; -import com.limelight.utils.Dialog; -import com.limelight.utils.ServerHelper; -import com.limelight.utils.ShortcutHelper; -import com.limelight.utils.SpinnerDialog; -import com.limelight.utils.UiHelper; - import android.app.Activity; import android.app.Service; import android.content.ComponentName; @@ -30,26 +8,66 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.Bitmap; +import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.preference.PreferenceManager; +import android.util.Log; import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.ContextMenu.ContextMenuInfo; +import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.CompoundButton; +import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; -import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.ToggleButton; + +import com.limelight.computers.ComputerManagerListener; +import com.limelight.computers.ComputerManagerService; +import com.limelight.grid.AppGridAdapter; +import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.NvApp; +import com.limelight.nvstream.http.NvHTTP; +import com.limelight.nvstream.http.PairingManager; +import com.limelight.preferences.PreferenceConfiguration; +import com.limelight.preferences.StreamSettings; +import com.limelight.ui.AdapterFragment; +import com.limelight.ui.AdapterFragmentCallbacks; +import com.limelight.utils.CacheHelper; +import com.limelight.utils.Dialog; +import com.limelight.utils.ServerHelper; +import com.limelight.utils.ShortcutHelper; +import com.limelight.utils.SpinnerDialog; +import com.limelight.utils.StringUtils; +import com.limelight.utils.UiHelper; + +import com.limelight.iperf3.cmd.CmdCallback; +import com.limelight.iperf3.cmd.Iperf3Cmd; import org.xmlpull.v1.XmlPullParserException; -public class AppView extends Activity implements AdapterFragmentCallbacks { +import java.io.IOException; +import java.io.StringReader; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +public class AppView extends Activity implements AdapterFragmentCallbacks, AdapterView.OnItemSelectedListener { + + public static final String TAG_IPERF_TEST = "IPERF_TEST"; + private AppGridAdapter appGridAdapter; private String uuidString; private ShortcutHelper shortcutHelper; @@ -63,6 +81,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { private boolean inForeground; private boolean showHiddenApps; private HashSet hiddenAppIds = new HashSet<>(); + private TextView tvNetworkSpeed; + private String computerIpAddress; private final static int START_OR_RESUME_ID = 1; private final static int QUIT_ID = 2; @@ -77,6 +97,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { public final static String UUID_EXTRA = "UUID"; public final static String NEW_PAIR_EXTRA = "NewPair"; public final static String SHOW_HIDDEN_APPS_EXTRA = "ShowHiddenApps"; + public final static String IP_ADDRESS = "IpAddress"; private ComputerManagerService.ComputerManagerBinder managerBinder; private final ServiceConnection serviceConnection = new ServiceConnection() { @@ -309,6 +330,7 @@ protected void onCreate(Bundle savedInstanceState) { } String computerName = getIntent().getStringExtra(NAME_EXTRA); + computerIpAddress = getIntent().getStringExtra(IP_ADDRESS); TextView label = findViewById(R.id.appListText); setTitle(computerName); @@ -317,6 +339,26 @@ protected void onCreate(Bundle savedInstanceState) { // Bind to the computer manager service bindService(new Intent(this, ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); + + // Setup the list view + ImageButton settingsButton = findViewById(R.id.settingsButton); + + settingsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startActivity(new Intent(AppView.this, StreamSettings.class)); + } + }); + + // 加载常用分辨率 + loadPopularResolutions(); + + // 测速 + tvNetworkSpeed = findViewById(R.id.tv_network_speed); + startIperf(); + tvNetworkSpeed.setOnClickListener(v -> { + startIperf(); + }); } private void updateHiddenApps(boolean hideImmediately) { @@ -645,6 +687,123 @@ public void onItemClick(AdapterView arg0, View arg1, int pos, listView.requestFocus(); } + /** + * 加载常用分辨率 + */ + private void loadPopularResolutions() { + // 反选设置的分辨率和帧率 + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(AppView.this); + String curSettingsResolution = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION); + String curSettingsFpg = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS); + + Spinner spinner = findViewById(R.id.spinner); + String[] popularResolutions = getResources().getStringArray(R.array.popular_resolutions); + String curResolution = String.format("%s %sHz", curSettingsResolution.replace("x", " "), curSettingsFpg); + String[] fullPopularResolutions; + int curResolutionIndex = -1; + for (int i = 0; i < popularResolutions.length; i++) { + if(popularResolutions[i].equals(curResolution)) { + curResolutionIndex = i; + break; + } + } + if(curResolutionIndex >= 0) { + fullPopularResolutions = popularResolutions; + } else { + curResolutionIndex = 0; + fullPopularResolutions = new String[popularResolutions.length + 1]; + fullPopularResolutions[0] = curResolution; + System.arraycopy(popularResolutions, 0, fullPopularResolutions, 1, popularResolutions.length); + } + ArrayAdapter adapter = new ArrayAdapter<>( + this, + R.layout.spinner_item_nv_layout, + R.id.spinner_item, + fullPopularResolutions); + adapter.setDropDownViewResource(R.layout.spinner_item_nv_layout); + spinner.setAdapter(adapter); + spinner.setOnItemSelectedListener(this); + spinner.setSelection(curResolutionIndex); + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String[] selected = parent.getItemAtPosition(position).toString().replace("Hz", "").split(" "); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(AppView.this); + prefs.edit().putString(PreferenceConfiguration.RESOLUTION_PREF_STRING, String.format("%sx%s", selected[0], selected[1])).apply(); + prefs.edit().putString(PreferenceConfiguration.FPS_PREF_STRING, selected[2]).apply(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + // Do nothing + } + // 加载常用分辨率-------------------------------------------------------- + + public void startIperf() { + tvNetworkSpeed.setText("Loading..."); + setTextBySpeed(tvNetworkSpeed, -1); + new Iperf3Cmd(this, new CmdCallback() { + @Override + public void onRawOutput(String rawOutputLine) { + } + + @Override + public void onConnecting(String destHost, int destPort) { + Log.d(TAG_IPERF_TEST, "onConnecting: " + destHost + " " + destPort); + } + + @Override + public void onConnected(String localAddr, int localPort, String destAddr, int destPort) { + Log.d(TAG_IPERF_TEST, "onConnected: " + localAddr + " " + localPort + " " + destAddr + " " + destPort); + } + + @Override + public void onInterval(double timeStart, double timeEnd, double transfer, double bitrate) { + Log.d(TAG_IPERF_TEST, "onInterval: " + timeStart + " " + timeEnd + " " + transfer + " " + bitrate); + runOnUiThread(() -> { + long speed = (long)(bitrate * 1024 * 1024 * 1.024); + tvNetworkSpeed.setText(StringUtils.bps2BpsStr(StringUtils.preferNum(speed))); + setTextBySpeed(tvNetworkSpeed, speed); + }); + } + + @Override + public void onResult(double timeStart, double timeEnd, double transfer, double bitrate) { + Log.d(TAG_IPERF_TEST, "onResult: " + timeStart + " " + timeEnd + " " + transfer + " " + bitrate); + runOnUiThread(() -> { + long speed = (long)(bitrate * 1024 * 1024 * 1.024); + tvNetworkSpeed.setText(StringUtils.bps2BpsStr(StringUtils.preferNum((long)(bitrate * 1024 * 1024 * 1.024)))); + setTextBySpeed(tvNetworkSpeed, speed); + }); + } + + @Override + public void onError(String errMsg) { + Log.d(TAG_IPERF_TEST, "onError: " + errMsg); + runOnUiThread(() -> { +// Toast.makeText(AppView.this, errMsg, Toast.LENGTH_SHORT).show(); + tvNetworkSpeed.setText("RETRY"); + setTextBySpeed(tvNetworkSpeed, 0); + }); + } + }).exec(new String[] {"-c", computerIpAddress, "-R", "-f", "m", "--tmp-template", getCacheDir().getAbsolutePath() + "/iperf3.XXXXXX"}); + } + + private void setTextBySpeed(TextView view, long speed) { + int color; + if(speed > 500 * 1000 * 1000) { + color = R.color.green_nvidia; + } else if (speed > 100 * 1000 * 1000) { + color = R.color.yellow; + } else if (speed >= 0) { + color = R.color.red; + } else { + color = R.color.grey; + } + view.setTextColor(getResources().getColor(color)); + } + public static class AppObject { public final NvApp app; public boolean isRunning; diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 5d214508a3..d06f39f763 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -23,7 +23,6 @@ import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvHTTP; -import com.limelight.nvstream.input.ControllerPacket; import com.limelight.nvstream.input.KeyboardPacket; import com.limelight.nvstream.input.MouseButtonPacket; import com.limelight.nvstream.jni.MoonBridge; @@ -52,6 +51,7 @@ import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.Typeface; import android.hardware.input.InputManager; import android.media.AudioManager; import android.net.ConnectivityManager; @@ -369,7 +369,7 @@ public boolean onCapturedPointer(View view, MotionEvent motionEvent) { } // Check if the user has enabled performance stats overlay - if (prefConfig.enablePerfOverlay) { + if (prefConfig.enablePerfOverlay || prefConfig.enableMinPerfOverlay) { performanceOverlayView.setVisibility(View.VISIBLE); } @@ -613,7 +613,7 @@ public void onConfigurationChanged(Configuration newConfig) { virtualController.show(); } - if (prefConfig.enablePerfOverlay) { + if (prefConfig.enablePerfOverlay || prefConfig.enableMinPerfOverlay) { performanceOverlayView.setVisibility(View.VISIBLE); } @@ -2646,6 +2646,16 @@ public void run() { }); } + @Override + public void onMinPerfUpdate(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + performanceOverlayView.setText(text); + } + }); + } + @Override public void onUsbPermissionPromptStarting() { // Disable PiP auto-enter while the USB permission prompt is on-screen. This prevents diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index 4ec6094f6e..d539304e83 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -65,6 +65,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { private ShortcutHelper shortcutHelper; private ComputerManagerService.ComputerManagerBinder managerBinder; private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled; + private long createStartTime; private final ServiceConnection serviceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder binder) { final ComputerManagerService.ComputerManagerBinder localBinder = @@ -231,6 +232,7 @@ public void onDrawFrame(GL10 gl10) { private void completeOnCreate() { completeOnCreateCalled = true; + createStartTime = System.currentTimeMillis(); shortcutHelper = new ShortcutHelper(this); @@ -258,6 +260,12 @@ public void notifyComputerUpdated(final ComputerDetails details) { @Override public void run() { updateComputer(details); + if(System.currentTimeMillis() - createStartTime < 5000 && + details.state == ComputerDetails.State.ONLINE && + details.pairState == PairState.PAIRED) { + createStartTime = 0; + doAppList(details, false, false); + } } }); @@ -597,6 +605,7 @@ private void doAppList(ComputerDetails computer, boolean newlyPaired, boolean sh i.putExtra(AppView.UUID_EXTRA, computer.uuid); i.putExtra(AppView.NEW_PAIR_EXTRA, newlyPaired); i.putExtra(AppView.SHOW_HIDDEN_APPS_EXTRA, showHiddenGames); + i.putExtra(AppView.IP_ADDRESS, computer.activeAddress.address); startActivity(i); } diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index d24ec0dc72..27efc206e1 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -19,6 +19,7 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.jni.MoonBridge; import com.limelight.preferences.PreferenceConfiguration; +import com.limelight.utils.StringUtils; import android.annotation.TargetApi; import android.app.Activity; @@ -28,11 +29,13 @@ import android.media.MediaFormat; import android.media.MediaCodec.BufferInfo; import android.media.MediaCodec.CodecException; +import android.net.TrafficStats; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Process; import android.os.SystemClock; +import android.util.Log; import android.util.Range; import android.view.Choreographer; import android.view.SurfaceHolder; @@ -1404,6 +1407,10 @@ public int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int dec if (lastFrameNumber == 0) { activeWindowVideoStats.measurementStartTimestamp = SystemClock.uptimeMillis(); + if(prefs.enablePerfOverlay || prefs.enableMinPerfOverlay) { + activeWindowVideoStats.measurementStartTotalRxBytes = TrafficStats.getTotalRxBytes(); + } + } else if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) { // We can receive the same "frame" multiple times if it's an IDR frame. // In that case, each frame start NALU is submitted independently. @@ -1423,48 +1430,69 @@ public int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int dec // Flip stats windows roughly every second if (SystemClock.uptimeMillis() >= activeWindowVideoStats.measurementStartTimestamp + 1000) { - if (prefs.enablePerfOverlay) { + if (prefs.enablePerfOverlay || prefs.enableMinPerfOverlay) { VideoStats lastTwo = new VideoStats(); lastTwo.add(lastWindowVideoStats); lastTwo.add(activeWindowVideoStats); VideoStatsFps fps = lastTwo.getFps(); - String decoder; - - if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) { - decoder = avcDecoder.getName(); - } else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) { - decoder = hevcDecoder.getName(); - } else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_AV1) != 0) { - decoder = av1Decoder.getName(); + + if(prefs.enableMinPerfOverlay) { + long rttInfo = MoonBridge.getEstimatedRttInfo(); + StringBuilder sb = new StringBuilder(); + + if(fps.totalFps - fps.renderedFps > 5) { + sb.append(String.format("%.0f/%.0f/%.0f FPS", fps.renderedFps, fps.receivedFps, fps.totalFps)); + } else { + sb.append(String.format("%.0f FPS", fps.renderedFps)); + } + sb.append(String.format(" %d ms", (int) (rttInfo >> 32))); + sb.append(String.format(" %s", StringUtils.byteps2BpsStr(activeWindowVideoStats.getByteRate()))); + perfListener.onMinPerfUpdate(sb.toString()); + } else { - decoder = "(unknown)"; - } + String decoder; + + if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) { + decoder = avcDecoder.getName(); + } else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) { + decoder = hevcDecoder.getName(); + } else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_AV1) != 0) { + decoder = av1Decoder.getName(); + } else { + decoder = "(unknown)"; + } + + float decodeTimeMs = (float) lastTwo.decoderTimeMs / lastTwo.totalFramesReceived; + long rttInfo = MoonBridge.getEstimatedRttInfo(); + StringBuilder sb = new StringBuilder(); + sb.append(context.getString(R.string.perf_overlay_streamdetails, initialWidth + "x" + initialHeight, fps.totalFps)).append('\n'); + sb.append(context.getString(R.string.perf_overlay_decoder, decoder)).append('\n'); + sb.append(context.getString(R.string.perf_overlay_incomingfps, fps.receivedFps)).append('\n'); + sb.append(context.getString(R.string.perf_overlay_renderingfps, fps.renderedFps)).append('\n'); + sb.append(context.getString(R.string.perf_overlay_netdrops, + (float) lastTwo.framesLost / lastTwo.totalFrames * 100)).append('\n'); + sb.append(context.getString(R.string.perf_overlay_netlatency, + (int) (rttInfo >> 32), (int) rttInfo)).append('\n'); + if (lastTwo.framesWithHostProcessingLatency > 0) { + sb.append(context.getString(R.string.perf_overlay_hostprocessinglatency, + (float) lastTwo.minHostProcessingLatency / 10, + (float) lastTwo.maxHostProcessingLatency / 10, + (float) lastTwo.totalHostProcessingLatency / 10 / lastTwo.framesWithHostProcessingLatency)).append('\n'); + } + sb.append(context.getString(R.string.perf_overlay_dectime, decodeTimeMs)).append('\n'); + sb.append(context.getString(R.string.perf_overlay_bitrate, StringUtils.byteps2BpsStr(activeWindowVideoStats.getByteRate()))); + perfListener.onPerfUpdate(sb.toString()); - float decodeTimeMs = (float)lastTwo.decoderTimeMs / lastTwo.totalFramesReceived; - long rttInfo = MoonBridge.getEstimatedRttInfo(); - StringBuilder sb = new StringBuilder(); - sb.append(context.getString(R.string.perf_overlay_streamdetails, initialWidth + "x" + initialHeight, fps.totalFps)).append('\n'); - sb.append(context.getString(R.string.perf_overlay_decoder, decoder)).append('\n'); - sb.append(context.getString(R.string.perf_overlay_incomingfps, fps.receivedFps)).append('\n'); - sb.append(context.getString(R.string.perf_overlay_renderingfps, fps.renderedFps)).append('\n'); - sb.append(context.getString(R.string.perf_overlay_netdrops, - (float)lastTwo.framesLost / lastTwo.totalFrames * 100)).append('\n'); - sb.append(context.getString(R.string.perf_overlay_netlatency, - (int)(rttInfo >> 32), (int)rttInfo)).append('\n'); - if (lastTwo.framesWithHostProcessingLatency > 0) { - sb.append(context.getString(R.string.perf_overlay_hostprocessinglatency, - (float)lastTwo.minHostProcessingLatency / 10, - (float)lastTwo.maxHostProcessingLatency / 10, - (float)lastTwo.totalHostProcessingLatency / 10 / lastTwo.framesWithHostProcessingLatency)).append('\n'); } - sb.append(context.getString(R.string.perf_overlay_dectime, decodeTimeMs)); - perfListener.onPerfUpdate(sb.toString()); } globalVideoStats.add(activeWindowVideoStats); lastWindowVideoStats.copy(activeWindowVideoStats); activeWindowVideoStats.clear(); activeWindowVideoStats.measurementStartTimestamp = SystemClock.uptimeMillis(); + if(prefs.enablePerfOverlay || prefs.enableMinPerfOverlay) { + activeWindowVideoStats.measurementStartTotalRxBytes = TrafficStats.getTotalRxBytes(); + } } boolean csdSubmittedForThisFrame = false; diff --git a/app/src/main/java/com/limelight/binding/video/PerfOverlayListener.java b/app/src/main/java/com/limelight/binding/video/PerfOverlayListener.java index 281f95a046..f60435e0c4 100644 --- a/app/src/main/java/com/limelight/binding/video/PerfOverlayListener.java +++ b/app/src/main/java/com/limelight/binding/video/PerfOverlayListener.java @@ -2,4 +2,5 @@ public interface PerfOverlayListener { void onPerfUpdate(final String text); + void onMinPerfUpdate(final String text); } diff --git a/app/src/main/java/com/limelight/binding/video/VideoStats.java b/app/src/main/java/com/limelight/binding/video/VideoStats.java index b65b897ecf..8dcabce7de 100644 --- a/app/src/main/java/com/limelight/binding/video/VideoStats.java +++ b/app/src/main/java/com/limelight/binding/video/VideoStats.java @@ -1,5 +1,6 @@ package com.limelight.binding.video; +import android.net.TrafficStats; import android.os.SystemClock; class VideoStats { @@ -16,6 +17,7 @@ class VideoStats { int totalHostProcessingLatency; int framesWithHostProcessingLatency; long measurementStartTimestamp; + long measurementStartTotalRxBytes; void add(VideoStats other) { this.decoderTimeMs += other.decoderTimeMs; @@ -55,6 +57,7 @@ void copy(VideoStats other) { this.totalHostProcessingLatency = other.totalHostProcessingLatency; this.framesWithHostProcessingLatency = other.framesWithHostProcessingLatency; this.measurementStartTimestamp = other.measurementStartTimestamp; + this.measurementStartTotalRxBytes = other.measurementStartTotalRxBytes; } void clear() { @@ -70,6 +73,7 @@ void clear() { this.totalHostProcessingLatency = 0; this.framesWithHostProcessingLatency = 0; this.measurementStartTimestamp = 0; + this.measurementStartTotalRxBytes = 0; } VideoStatsFps getFps() { @@ -83,6 +87,15 @@ VideoStatsFps getFps() { } return fps; } + + long getByteRate() { + if(this.measurementStartTotalRxBytes == 0) { + return 0; + } + + float elapsed = (SystemClock.uptimeMillis() - this.measurementStartTimestamp) / (float) 1000; + return (long) ((TrafficStats.getTotalRxBytes() - this.measurementStartTotalRxBytes) / elapsed); + } } class VideoStatsFps { diff --git a/app/src/main/java/com/limelight/iperf3/Iperf3Callback.java b/app/src/main/java/com/limelight/iperf3/Iperf3Callback.java new file mode 100644 index 0000000000..cd0eca4d7e --- /dev/null +++ b/app/src/main/java/com/limelight/iperf3/Iperf3Callback.java @@ -0,0 +1,18 @@ +package com.limelight.iperf3; + +/** + * @author shenyong + * @date 2020-11-17 + */ +public interface Iperf3Callback { + + void onConnecting(String destHost, int destPort); + + void onConnected(String localAddr, int localPort, String destAddr, int destPort); + + void onInterval(double timeStart, double timeEnd, double transfer, double bitrate); + + void onResult(double timeStart, double timeEnd, double transfer, double bitrate); + + void onError(String errMsg); +} diff --git a/app/src/main/java/com/limelight/iperf3/cmd/CmdCallback.java b/app/src/main/java/com/limelight/iperf3/cmd/CmdCallback.java new file mode 100644 index 0000000000..05be7b5927 --- /dev/null +++ b/app/src/main/java/com/limelight/iperf3/cmd/CmdCallback.java @@ -0,0 +1,14 @@ +package com.limelight.iperf3.cmd; + + +import com.limelight.iperf3.Iperf3Callback; + +/** + * + * @author shenyong + * @date 2020/12/3 + */ +public interface CmdCallback extends Iperf3Callback { + + void onRawOutput(String rawOutputLine); +} \ No newline at end of file diff --git a/app/src/main/java/com/limelight/iperf3/cmd/Iperf3Cmd.java b/app/src/main/java/com/limelight/iperf3/cmd/Iperf3Cmd.java new file mode 100644 index 0000000000..b0c61f5467 --- /dev/null +++ b/app/src/main/java/com/limelight/iperf3/cmd/Iperf3Cmd.java @@ -0,0 +1,239 @@ +package com.limelight.iperf3.cmd; + +import android.content.Context; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author shenyong + * @date 2020-11-10 + */ +public class Iperf3Cmd { + + public static final String TAG = "Iperf3Cmd"; + + private final Context context; + private final CmdCallback callback; + + public Iperf3Cmd(Context context, CmdCallback callback) { + this.context = context; + this.callback = callback; + } + + private static final String EXECUTABLE_NAME = "libIperf3.so"; + + private final Pattern CONNECTING_PATTERN = Pattern.compile("(Connecting to host (.*), port (\\d+))"); + private final Pattern CONNECTED_PATTERN = Pattern.compile("(local (.*) port (\\d+) connected to (.*) port (\\d+))"); + private final Pattern REPORT_PATTERN = Pattern.compile("(\\d{1,2}.\\d{2})-(\\d{1,2}.\\d{2})\\s+sec" + + "\\s+(\\d+(.\\d+)?) [KMGT]?Bytes\\s+(\\d+(.\\d+)?) Mbits/sec"); + private final Pattern UDP_LOSS = Pattern.compile("\\d+/\\d+ \\([\\d+-.e]+%\\)"); + private final Pattern TITLE_PATTERN = Pattern.compile("\\[\\s+ID\\]"); + private final Pattern ERR_PATTERN = Pattern.compile("iperf3: error"); + + private int parallels = 0; + private boolean isDown = false; + // title出现次数。title栏输出第一次之后的、第二次之前的,是中间结果,第二次之后的是最终平均速率 + private int titleCnt = 0; + + private String getCmdPath() { + return context.getApplicationInfo().nativeLibraryDir + "/" + EXECUTABLE_NAME; + } + + public void exec(String[] args) { + new Thread(() -> { + String[] cmdAndArgs = new String[args.length + 1]; + cmdAndArgs[0] = getCmdPath(); + System.arraycopy(args, 0, cmdAndArgs, 1, args.length); + + try { +// execCommand("ls -l " + context.getApplicationInfo().nativeLibraryDir); + Process process = Runtime.getRuntime().exec(cmdAndArgs); + Log.i(TAG, "command: " + String.join(" ", cmdAndArgs)); + parseArgs(cmdAndArgs); + + try ( + InputStreamReader in = new InputStreamReader(process.getInputStream()); + BufferedReader outReader = new BufferedReader(in); + BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + ) { + String line; + while ((line = outReader.readLine()) != null) { + parseToCallback(line); + } + while ((line = errReader.readLine()) != null) { + parseToCallback(line); + } + + // 等待进程结束 + process.waitFor(); + Log.i(TAG, "exitValue: " + process.waitFor()); + } + + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + callback.onError(e.getMessage()); + } + }).start(); + } + + private void parseArgs(final String[] cmdAndArgs) { + isDown = false; + titleCnt = 0; + for (int i = 0; i < cmdAndArgs.length; i++) { + String s = cmdAndArgs[i]; + if ("-P".equals(s)) { + parallels = Integer.parseInt(cmdAndArgs[i + 1]); + } else if ("-R".equals(s)) { + isDown = true; + } + } + // 如果命令中未设置-P参数,取默认值0 + // -P, --parallel n 要与服务器建立的同时连接数。默认值为 1。 + if(parallels == 0) { + parallels = 1; + } + } + + private void parseToCallback(String line) { + Log.d(TAG, "output: " + line); + callback.onRawOutput(line); + if (TITLE_PATTERN.matcher(line).find()) { + titleCnt++; + } + Matcher mr = CONNECTING_PATTERN.matcher(line); + if (mr.find()) { + String addr = mr.group(2); + int port = Integer.parseInt(mr.group(3)); + callback.onConnecting(addr, port); + } + mr = CONNECTED_PATTERN.matcher(line); + if (mr.find()) { + String laddr = mr.group(2); + int lport = Integer.parseInt(mr.group(3)); + String raddr = mr.group(4); + int rport = Integer.parseInt(mr.group(5)); + callback.onConnected(laddr, lport, raddr, rport); + } + // 并发连接数为1和>1时,速率报告有以下两种格式,通过正则捕获组来截取数据 + // [ 4] 9.00-10.00 sec 2.18 MBytes 18.3 Mbits/sec + // [SUM] 9.00-10.00 sec 1.85 MBytes 15.5 Mbits/sec + mr = REPORT_PATTERN.matcher(line); + if (mr.find()) { + double st = Double.parseDouble(mr.group(1)); + double et = Double.parseDouble(mr.group(2)); + double trans = Double.parseDouble(mr.group(3)); + double bw = Double.parseDouble(mr.group(5)); + if (isInterval(line)) { + callback.onInterval(st, et, trans, bw); + } else if (isResult(line)) { + callback.onResult(st, et, trans, bw); + } + } + if (ERR_PATTERN.matcher(line).find()) { + callback.onError(line); + } + } + + private boolean isInterval(String line) { + return isTcpInterval(line) || isUdpInterval(line); + } + + private boolean isResult(String line) { + return isTcpResult(line) || isUdpResult(line); + } + + private boolean isTcpInterval(String line) { + return titleCnt == 1 + && ((parallels == 1) + || (parallels > 1 && line.startsWith("[SUM]"))); + } + + private boolean isTcpResult(String line) { + //eg: + //[ ID] Interval Transfer Bandwidth Retr + //[ 4] 0.00-10.00 sec 19.5 MBytes 16.3 Mbits/sec 19 sender + //[ 4] 0.00-10.00 sec 19.0 MBytes 15.9 Mbits/sec receiver + boolean isLocalResult = (isDown && line.contains("receiver")) + || (!isDown && line.contains("sender")); + return titleCnt > 1 && isLocalResult + && ((parallels == 1) + || (parallels > 1 && line.startsWith("[SUM]"))); + } + + private boolean isUdpInterval(String line) { + // parallels == 1 eg: + //[ ID] Interval Transfer Bandwidth Total Datagrams + //[ 4] 0.00-1.00 sec 9.86 MBytes 82.7 Mbits/sec 1262 + // parallels > 1 eg: + //[ ID] Interval Transfer Bandwidth Total Datagrams + //[SUM] 0.00-1.00 sec 10.6 MBytes 88.5 Mbits/sec 1352 + boolean isUdpUpInterval = (titleCnt == 1 && !UDP_LOSS.matcher(line).find()) + && ((parallels == 1) + || (parallels > 1 && line.startsWith("[SUM]"))); + // parallels == 1 eg: + //[ ID] Interval Transfer Bandwidth Jitter Lost/Total Datagrams + //[ 4] 0.00-1.00 sec 240 KBytes 1.96 Mbits/sec 2594.444 ms 12595/12625 (1e+02%) + // parallels > 1 eg: + //[ ID] Interval Transfer Bandwidth Jitter Lost/Total Datagrams + //[SUM] 0.00-1.00 sec 264 KBytes 2.16 Mbits/sec 6458.650 ms 25698/25731 (1e+02%) + boolean inUdpDownInterval = (titleCnt == 1 && UDP_LOSS.matcher(line).find()) + && ((parallels == 1) + || (parallels > 1 && line.startsWith("[SUM]"))); + return isUdpUpInterval || inUdpDownInterval; + } + + private boolean isUdpResult(String line) { + //-------- up -------- + // parallels == 1 eg: + //[ ID] Interval Transfer Bandwidth Jitter Lost/Total Datagrams + //[ 4] 0.00-10.00 sec 95.3 MBytes 80.0 Mbits/sec 2.963 ms 11168/12029 (93%) + // parallels > 1 eg: + //[ ID] Interval Transfer Bandwidth Jitter Lost/Total Datagrams + //[SUM] 0.00-10.00 sec 104 MBytes 86.9 Mbits/sec 7.764 ms 11935/12613 (95%) + //-------- down -------- + // parallels == 1 eg: + //[ ID] Interval Transfer Bandwidth Jitter Lost/Total Datagrams + //[ 4] 0.00-10.00 sec 1.16 GBytes 996 Mbits/sec 4.518 ms 151121/151406 (1e+02%) + // parallels > 1 eg: + //[ ID] Interval Transfer Bandwidth Jitter Lost/Total Datagrams + //[SUM] 0.00-10.00 sec 2.33 GBytes 2002 Mbits/sec 55.278 ms 299008/299247 (1e+02%) + return (titleCnt > 1 && UDP_LOSS.matcher(line).find()) + && ((parallels == 1) + || (parallels > 1 && line.startsWith("[SUM]"))); + } + + public void execCommand(String command) { + Log.d(TAG, "exec: " + command); + try { + Runtime runtime = Runtime.getRuntime(); + Process proc = runtime.exec(command); + + BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream())); + BufferedReader stdError = new BufferedReader(new InputStreamReader(proc.getErrorStream())); + + // 读取标准输出流 + String s; + while ((s = stdInput.readLine()) != null) { + Log.d(TAG, s); + } + + // 读取标准错误流 + while ((s = stdError.readLine()) != null) { + Log.d(TAG, "Error: " + s); + } + + // 等待进程结束 + proc.waitFor(); + + // 打印退出值 + Log.d(TAG, "exit value = " + proc.exitValue()); + } catch (Exception e) { + Log.d(TAG, "Exception: " + e.getMessage()); + Log.e(TAG, e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index 8ed01e3610..8d68131273 100644 --- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java +++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java @@ -26,8 +26,9 @@ public enum AnalogStickForScrolling { private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps"; private static final String LEGACY_ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround"; - static final String RESOLUTION_PREF_STRING = "list_resolution"; - static final String FPS_PREF_STRING = "list_fps"; + static final String RESET_BITRATE_STRING = "checkbox_enable_reset_bitrate"; + public static final String RESOLUTION_PREF_STRING = "list_resolution"; + public static final String FPS_PREF_STRING = "list_fps"; static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps"; private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate"; private static final String STRETCH_PREF_STRING = "checkbox_stretch_video"; @@ -48,7 +49,8 @@ public enum AnalogStickForScrolling { private static final String LEGACY_DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop"; private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr"; private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip"; - private static final String ENABLE_PERF_OVERLAY_STRING = "checkbox_enable_perf_overlay"; + static final String ENABLE_PERF_OVERLAY_STRING = "checkbox_enable_perf_overlay"; + static final String ENABLE_MIN_PERF_OVERLAY_STRING = "checkbox_enable_min_perf_overlay"; private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all"; private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation"; private static final String ANALOG_SCROLLING_PREF_STRING = "analog_scrolling"; @@ -69,8 +71,9 @@ public enum AnalogStickForScrolling { private static final String GAMEPAD_MOTION_SENSORS_PREF_STRING = "checkbox_gamepad_motion_sensors"; private static final String GAMEPAD_MOTION_FALLBACK_PREF_STRING = "checkbox_gamepad_motion_fallback"; - static final String DEFAULT_RESOLUTION = "1280x720"; - static final String DEFAULT_FPS = "60"; + static final boolean DEFAULT_RESET_BITRATE = false; + public static final String DEFAULT_RESOLUTION = "1280x720"; + public static final String DEFAULT_FPS = "60"; private static final boolean DEFAULT_STRETCH = false; private static final boolean DEFAULT_SOPS = true; private static final boolean DEFAULT_DISABLE_TOASTS = false; @@ -88,6 +91,7 @@ public enum AnalogStickForScrolling { private static final boolean DEFAULT_ENABLE_HDR = false; private static final boolean DEFAULT_ENABLE_PIP = false; private static final boolean DEFAULT_ENABLE_PERF_OVERLAY = false; + private static final boolean DEFAULT_MIN_ENABLE_PERF_OVERLAY = false; private static final boolean DEFAULT_BIND_ALL_USB = false; private static final boolean DEFAULT_MOUSE_EMULATION = true; private static final String DEFAULT_ANALOG_STICK_FOR_SCROLLING = "right"; @@ -136,6 +140,7 @@ public enum AnalogStickForScrolling { public boolean enableHdr; public boolean enablePip; public boolean enablePerfOverlay; + public boolean enableMinPerfOverlay; public boolean enableLatencyToast; public boolean bindAllUsb; public boolean mouseEmulation; @@ -584,6 +589,7 @@ else if (audioConfig.equals("51")) { config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR) && !isShieldAtvFirmwareWithBrokenHdr(); config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP); config.enablePerfOverlay = prefs.getBoolean(ENABLE_PERF_OVERLAY_STRING, DEFAULT_ENABLE_PERF_OVERLAY); + config.enableMinPerfOverlay = prefs.getBoolean(ENABLE_MIN_PERF_OVERLAY_STRING, DEFAULT_MIN_ENABLE_PERF_OVERLAY); config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB); config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION); config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS); diff --git a/app/src/main/java/com/limelight/preferences/StreamSettings.java b/app/src/main/java/com/limelight/preferences/StreamSettings.java index 7070104168..88020fd5c0 100644 --- a/app/src/main/java/com/limelight/preferences/StreamSettings.java +++ b/app/src/main/java/com/limelight/preferences/StreamSettings.java @@ -26,6 +26,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; +import android.widget.Toast; import com.limelight.LimeLog; import com.limelight.PcView; @@ -36,6 +37,7 @@ import java.lang.reflect.Method; import java.util.Arrays; +import java.util.stream.Collectors; public class StreamSettings extends Activity { private PreferenceConfiguration previousPrefs; @@ -250,6 +252,10 @@ private void removeValue(String preferenceKey, String value, Runnable onMatched) } private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) { + if (!prefs.getBoolean(PreferenceConfiguration.RESET_BITRATE_STRING, PreferenceConfiguration.DEFAULT_RESET_BITRATE)) { + return; + } + if (res == null) { res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION); } @@ -663,6 +669,32 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { // Write the new bitrate value resetBitrateToDefault(prefs, null, valueStr); + // Allow the original preference change to take place + return true; + } + }); + findPreference(PreferenceConfiguration.ENABLE_MIN_PERF_OVERLAY_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean b = (Boolean) newValue; + if(b) { + CheckBoxPreference cb = (CheckBoxPreference) findPreference(PreferenceConfiguration.ENABLE_PERF_OVERLAY_STRING); + cb.setChecked(!b); + } + + // Allow the original preference change to take place + return true; + } + }); + findPreference(PreferenceConfiguration.ENABLE_PERF_OVERLAY_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean b = (Boolean) newValue; + if(b) { + CheckBoxPreference cb = (CheckBoxPreference) findPreference(PreferenceConfiguration.ENABLE_MIN_PERF_OVERLAY_STRING); + cb.setChecked(!b); + } + // Allow the original preference change to take place return true; } diff --git a/app/src/main/java/com/limelight/utils/DevUtils.java b/app/src/main/java/com/limelight/utils/DevUtils.java new file mode 100644 index 0000000000..aaa465b93f --- /dev/null +++ b/app/src/main/java/com/limelight/utils/DevUtils.java @@ -0,0 +1,38 @@ +package com.limelight.utils; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** + * + * @author shenyong + * @date 2020-11-10 + */ +public class DevUtils { + public static String abiInfo = ""; + public static String getCpuApi() { + if (abiInfo.isEmpty()) { + try ( + BufferedReader outReader = new BufferedReader(new InputStreamReader( + Runtime.getRuntime().exec("getprop ro.product.cpu.abi").getInputStream())); + ) { + abiInfo = outReader.readLine(); + } catch (Exception ignored) { + + }; + } + return abiInfo; + } + + public static boolean isArmAbi() { + return getCpuApi().startsWith("armeabi"); + } + + public static boolean isArm64Abi() { + return getCpuApi().startsWith("arm64"); + } + + public static boolean isX86Abi() { + return getCpuApi().startsWith("x86"); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/limelight/utils/StringUtils.java b/app/src/main/java/com/limelight/utils/StringUtils.java new file mode 100644 index 0000000000..730ea73bd2 --- /dev/null +++ b/app/src/main/java/com/limelight/utils/StringUtils.java @@ -0,0 +1,43 @@ +package com.limelight.utils; + +public class StringUtils { + + private static String[] BITRATE_UNITS = new String[]{"", "K", "M"}; + + public static String byteps2BpsStr(long byteps) { + return bps2BpsStr(byteps * 8); + } + + public static String bps2BpsStr(long byteps) { + float cur = byteps; + int unitIndex = 0; + while(cur >= 1000 && unitIndex < BITRATE_UNITS.length - 1) { + cur = cur / 1000; + unitIndex++; + } + return String.format((cur >= 10 || cur == 0 ? "%.0f" : "%.1f") + " %sbps", cur, BITRATE_UNITS[unitIndex]); + } + + public static long preferNum(long number) { + // 计算数字的位数 + int scale = (int) Math.floor(Math.log10(number)) + 1; + + // 根据位数确定步长 + int step = scale / 3; // 每三位确定一个步长 + if (step < 1) step = 1; // 至少有一个步长 + + // 计算步长值 + long factor = (long) Math.pow(10, step); + + // 向上取整 + long rounded = ((number + factor - 1) / factor) * factor; + + // 特殊处理,如果取整后的数字与原数字差距过大,则调整步长 + if (rounded - number > factor * 2) { + factor *= 2; + rounded = ((number + factor - 1) / factor) * factor; + } + + return rounded; + } +} diff --git a/app/src/main/libs/arm64-v8a/libIperf3.so b/app/src/main/libs/arm64-v8a/libIperf3.so new file mode 100644 index 0000000000..d37a3aab66 Binary files /dev/null and b/app/src/main/libs/arm64-v8a/libIperf3.so differ diff --git a/app/src/main/libs/armeabi-v7a/libIperf3.so b/app/src/main/libs/armeabi-v7a/libIperf3.so new file mode 100644 index 0000000000..61b85acb63 Binary files /dev/null and b/app/src/main/libs/armeabi-v7a/libIperf3.so differ diff --git a/app/src/main/libs/x86/libIperf3.so b/app/src/main/libs/x86/libIperf3.so new file mode 100644 index 0000000000..c41ebca52e Binary files /dev/null and b/app/src/main/libs/x86/libIperf3.so differ diff --git a/app/src/main/libs/x86_64/libIperf3.so b/app/src/main/libs/x86_64/libIperf3.so new file mode 100644 index 0000000000..92bb67051c Binary files /dev/null and b/app/src/main/libs/x86_64/libIperf3.so differ diff --git a/app/src/main/res/drawable/bg_text_selector.xml b/app/src/main/res/drawable/bg_text_selector.xml new file mode 100644 index 0000000000..5cb6b729d5 --- /dev/null +++ b/app/src/main/res/drawable/bg_text_selector.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/players.ttf b/app/src/main/res/font/players.ttf new file mode 100644 index 0000000000..4d05be36f0 Binary files /dev/null and b/app/src/main/res/font/players.ttf differ diff --git a/app/src/main/res/layout-land/activity_app_view.xml b/app/src/main/res/layout-land/activity_app_view.xml new file mode 100644 index 0000000000..48fe1bc15f --- /dev/null +++ b/app/src/main/res/layout-land/activity_app_view.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_app_view.xml b/app/src/main/res/layout/activity_app_view.xml index 37acf1e795..1620cf487d 100644 --- a/app/src/main/res/layout/activity_app_view.xml +++ b/app/src/main/res/layout/activity_app_view.xml @@ -14,18 +14,73 @@ android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_alignParentBottom="true" - android:layout_below="@+id/appListText"/> + android:layout_below="@+id/settingsButton"/> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_game.xml b/app/src/main/res/layout/activity_game.xml index 85a960d963..86a1862343 100644 --- a/app/src/main/res/layout/activity_game.xml +++ b/app/src/main/res/layout/activity_game.xml @@ -26,14 +26,14 @@ android:id="@+id/performanceOverlay" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="10dp" - android:layout_marginStart="10dp" - android:layout_marginTop="10dp" android:layout_gravity="left" + android:layout_margin="4dp" android:textAppearance="?android:attr/textAppearanceSmall" android:gravity="left" - android:background="#80000000" + android:textColor="#76b900" + android:textSize="12sp" android:preferKeepClear="true" + android:fontFamily="@font/players" android:visibility="gone" /> \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c03cd575fa..ab5af10f16 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -77,6 +77,7 @@ 渲染帧数: %1$.2f FPS 网络丢失帧: %1$.2f%% 平均解码时间: %1$.2f ms + 比特率: %1$s 正在连接电脑…… 恢复串流 @@ -284,4 +285,8 @@ 在鼠标模式下选择用哪个摇杆控制鼠标移动 增强或减弱设备的震动强度 默认 (两个摇杆都控制鼠标) + " 启用简化的性能信息 " + " 在串流中显示简化的实时性能信息 " + 性能信息 + 配置调整后重置比特率 \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 9bdeb036b7..6b6173eb3a 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -131,4 +131,9 @@ right left + + 1920 1080 60Hz + 1920 1080 120Hz + 3840 2160 60Hz + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000000..9927b9273a --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + #76b900 + #ffb900 + #f56c6c + #c1c1c1 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9cc2215aaa..831249e828 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -113,6 +113,7 @@ Frames dropped by your network connection: %1$.2f%% Average network latency: %1$d ms (variance: %2$d ms) Average decoding time: %1$.2f ms + Bitrate: %1$s Connecting to PC… @@ -144,7 +145,9 @@ That address doesn\'t look right. You must use your router\'s public IP address for streaming over the Internet. + Preferences Info Basic Settings + Reset bitrate after preferences changed. Video resolution Increase to improve image clarity. Decrease for better performance on lower end devices and slower networks. Native Resolution Warning @@ -252,6 +255,8 @@ This will cause loss of detail in light and dark areas if your device doesn\'t properly display full range video content. Show performance stats while streaming Display real-time stream performance information while streaming + Show min performance stats while streaming + Display real-time stream min performance information while streaming Show latency message after streaming Display a latency information message after the stream ends diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 5b09082b35..f8042b89b7 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -2,8 +2,25 @@ + + + + + -