Compare commits

...

43 Commits

Author SHA1 Message Date
Cameron Gutman e50b7076a1 Version 8.7 2019-12-04 18:21:11 -08:00
Cameron Gutman 36ab5aa1b6 Update common-c to fix logic error in audio duration selection 2019-12-01 20:31:39 -08:00
Cameron Gutman a0a2b299d9 Merge pull request #758 from duchuule/hotfix1
fix bug where touch hitbox of analog stick is not full circle
2019-12-01 22:29:02 -06:00
Cameron Gutman 14d354fc29 Whitelist all C2 decoders for direct submit and HEVC 2019-12-01 20:20:57 -08:00
Cameron Gutman 342515f916 Force remote streaming optimizations if a VPN is active 2019-12-01 20:05:09 -08:00
Cameron Gutman 5f5944c237 Improve low bandwidth audio performance and fix RTSP issues with broken PMTUD 2019-11-30 22:14:32 -06:00
Cameron Gutman c025432ad6 Support 20 ms audio frames 2019-11-29 18:04:57 -06:00
Duc Le 171a6437fe fix bug where touch hitbox of analog stick is not full circle 2019-11-26 04:40:22 -06:00
Cameron Gutman 11b3648fac Fix auto-comment line breaks 2019-11-16 12:23:27 -08:00
Cameron Gutman d1fae89d6d Don't change level_idc for high refresh rate streams 2019-11-10 18:29:31 -08:00
Cameron Gutman 5c06848fe9 Version 8.6 2019-11-10 18:18:11 -08:00
Cameron Gutman b50e506e58 Attempt to fix line breaks in auto-comment response 2019-11-09 16:34:25 -08:00
Cameron Gutman 59fafa163d Add configuration for auto-comment bot 2019-11-09 15:00:13 -08:00
Cameron Gutman 22d84b5763 Bind to the underlying network when a VPN is connected 2019-11-09 12:57:54 -08:00
Cameron Gutman 6d186892a8 Fix errant touch events after a cancelled gesture 2019-11-09 11:23:50 -08:00
Cameron Gutman 88d6143897 Display a placeholder box art bitmap while loading box art 2019-11-05 00:19:58 -08:00
Cameron Gutman b729fba75e Update AGP to 3.5.2 2019-11-04 20:59:56 -08:00
Cameron Gutman c0d3f9fa48 Abort pairing if another pairing attempt is in progress 2019-11-04 20:27:05 -08:00
Cameron Gutman af5e7a0e33 Calculate FPS using the actual display refresh rate rather than the requested one 2019-11-04 20:22:12 -08:00
Cameron Gutman 371d96ea65 Fix VPN check on KitKat and below 2019-11-04 19:05:34 -08:00
Cameron Gutman e9e332ff85 Don't update the external IP address when connected to a VPN 2019-11-04 19:00:29 -08:00
Cameron Gutman e133ac2815 Version 8.5 2019-10-29 22:06:28 -07:00
Cameron Gutman 1dba5d147e Add a hack for massive video latency on Pixel 4 after display mode change 2019-10-29 21:38:06 -07:00
Cameron Gutman 1616c0b022 Fix codec capabilities on devices launching with Q and C2 codecs 2019-10-24 20:20:26 -07:00
Cameron Gutman bcee2cf0e3 Update moonlight-common-c submodule 2019-10-24 19:57:03 -07:00
Cameron Gutman 3e7ddab0e9 Blacklist 59 FPS on BRAVIA_ATV3 due to crash reports 2019-10-20 00:06:17 -07:00
Cameron Gutman 5da0177356 Convert tabs to spaces 2019-10-19 23:59:33 -07:00
Cameron Gutman 7e21638811 Don't double count USB attached Xbox One controllers 2019-10-16 19:26:24 -07:00
Cameron Gutman db5b7ab867 Version 8.4.1 2019-10-16 19:10:56 -07:00
Cameron Gutman 3bcc1c84bb Fix crash on controllers with RX and RY but no Z and RZ axes 2019-10-16 19:02:51 -07:00
Cameron Gutman d46053f8d6 Preserve old DS4 detection behavior on Android 4.3 and below 2019-10-15 21:15:03 -07:00
Cameron Gutman 00a5fed9e9 Update AGP to 3.5.1 2019-10-15 20:58:03 -07:00
Cameron Gutman b6315a715a Improve support for DualShock 4 and Xbox One controllers on 4.14+ kernels 2019-10-15 20:57:33 -07:00
Cameron Gutman 0da8303468 Don't use the USB driver for Xbox One gamepads on 4.14+ kernels 2019-10-15 20:05:01 -07:00
Cameron Gutman c821c4684f Allow FFmpeg decoders on Android x86. Closes #630 2019-10-15 00:11:43 -07:00
Cameron Gutman 6bae33f822 Merge pull request #739 from vanitasvitae/patch-1
Fix German short_description
2019-10-15 00:06:12 -07:00
Cameron Gutman 08d4ab67a6 Update moonlight-common-c submodule 2019-10-12 19:50:30 -07:00
Paul Schaub 62203d2f21 Fix German short_description
fixed a typo
2019-10-06 12:44:40 +02:00
Cameron Gutman 4968dcc558 Version 8.3 2019-09-14 20:23:46 -07:00
Cameron Gutman 6d66d1371f Fix TV view padding on Android Q 2019-09-14 20:14:31 -07:00
Cameron Gutman b87ca71103 Treat all InputDevices as external on the Tinker Board 2019-09-14 20:08:26 -07:00
Cameron Gutman c251cd2e8f Fix control stream connection error on multi-homed hosts 2019-09-14 14:11:14 -07:00
Cameron Gutman 593616d2d9 Fix layout transitions on foldable devices 2019-09-08 11:11:02 -07:00
59 changed files with 3526 additions and 3174 deletions
+4
View File
@@ -0,0 +1,4 @@
issuesOpened: >
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://discord.gg/MySTSdq) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
Thank you, and happy streaming!
+2 -2
View File
@@ -7,8 +7,8 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionName "8.2" versionName "8.7"
versionCode = 199 versionCode = 206
} }
flavorDimensions "root" flavorDimensions "root"
+23 -2
View File
@@ -26,6 +26,7 @@ import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.os.Build; import android.os.Build;
@@ -96,8 +97,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
try { try {
appGridAdapter = new AppGridAdapter(AppView.this, appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode, PreferenceConfiguration.readPreferences(AppView.this),
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
computer, localBinder.getUniqueId()); computer, localBinder.getUniqueId());
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@@ -147,6 +147,27 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
} }
}; };
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// If appGridAdapter is initialized, let it know about the configuration change.
// If not, it will pick it up when it initializes.
if (appGridAdapter != null) {
// Update the app grid adapter to create grid items with the correct layout
appGridAdapter.updateLayoutWithPreferences(this, PreferenceConfiguration.readPreferences(this));
try {
// Reinflate the app grid itself to pick up the layout change
getFragmentManager().beginTransaction()
.replace(R.id.appFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
private void startComputerUpdates() { private void startComputerUpdates() {
// Don't start polling if we're not bound or in the foreground // Don't start polling if we're not bound or in the foreground
if (managerBinder == null || !inForeground) { if (managerBinder == null || !inForeground) {
+45 -25
View File
@@ -27,6 +27,7 @@ import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.GameGestures; import com.limelight.ui.GameGestures;
import com.limelight.ui.StreamView; import com.limelight.ui.StreamView;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.NetHelper;
import com.limelight.utils.ShortcutHelper; import com.limelight.utils.ShortcutHelper;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
@@ -397,28 +398,42 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Hopefully, we can get rid of this once someone comes up with a better way // Hopefully, we can get rid of this once someone comes up with a better way
// to track the state of the pipeline and time frames. // to track the state of the pipeline and time frames.
int roundedRefreshRate = Math.round(displayRefreshRate); int roundedRefreshRate = Math.round(displayRefreshRate);
if ((!prefConfig.disableFrameDrop || prefConfig.unlockFps) && prefConfig.fps >= roundedRefreshRate) { if (!prefConfig.disableFrameDrop || prefConfig.unlockFps) {
if (prefConfig.unlockFps) { if (Build.DEVICE.equals("coral") || Build.DEVICE.equals("flame")) {
// Use frame drops when rendering above the screen frame rate // HACK: Pixel 4 (XL) ignores the preferred display mode and lowers refresh rate,
decoderRenderer.enableLegacyFrameDropRendering(); // causing frame pacing issues. See https://issuetracker.google.com/issues/143401475
LimeLog.info("Using drop mode for FPS > Hz"); // To work around this, use frame drop mode if we want to stream at >= 60 FPS.
if (prefConfig.fps >= 60) {
LimeLog.info("Using Pixel 4 rendering hack");
decoderRenderer.enableLegacyFrameDropRendering();
}
} }
else if (roundedRefreshRate <= 49) { else if (prefConfig.fps >= roundedRefreshRate) {
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering if (prefConfig.unlockFps) {
decoderRenderer.enableLegacyFrameDropRendering(); // Use frame drops when rendering above the screen frame rate
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate); decoderRenderer.enableLegacyFrameDropRendering();
} LimeLog.info("Using drop mode for FPS > Hz");
// HACK: Avoid crashing on some MTK devices } else if (roundedRefreshRate <= 49) {
else if (roundedRefreshRate == 50 && decoderRenderer.is49FpsBlacklisted()) { // Let's avoid clearly bogus refresh rates and fall back to legacy rendering
// Use the old rendering strategy on these broken devices decoderRenderer.enableLegacyFrameDropRendering();
decoderRenderer.enableLegacyFrameDropRendering(); LimeLog.info("Bogus refresh rate: " + roundedRefreshRate);
} }
else { // HACK: Avoid crashing on some MTK devices
prefConfig.fps = roundedRefreshRate - 1; else if (decoderRenderer.isBlacklistedForFrameRate(roundedRefreshRate - 1)) {
LimeLog.info("Adjusting FPS target for screen to "+prefConfig.fps); // Use the old rendering strategy on these broken devices
decoderRenderer.enableLegacyFrameDropRendering();
} else {
prefConfig.fps = roundedRefreshRate - 1;
LimeLog.info("Adjusting FPS target for screen to " + prefConfig.fps);
}
} }
} }
boolean vpnActive = NetHelper.isActiveNetworkVpn(this);
if (vpnActive) {
LimeLog.info("Detected active network is a VPN");
}
StreamConfiguration config = new StreamConfiguration.Builder() StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height) .setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps) .setRefreshRate(prefConfig.fps)
@@ -426,8 +441,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
.setBitrate(prefConfig.bitrate) .setBitrate(prefConfig.bitrate)
.setEnableSops(prefConfig.enableSops) .setEnableSops(prefConfig.enableSops)
.enableLocalAudioPlayback(prefConfig.playHostAudio) .enableLocalAudioPlayback(prefConfig.playHostAudio)
.setMaxPacketSize(1392) .setMaxPacketSize(vpnActive ? 1024 : 1392) // Lower MTU on VPN
.setRemoteConfiguration(StreamConfiguration.STREAM_CFG_AUTO) .setRemoteConfiguration(vpnActive ? // Use remote optimizations on VPN
StreamConfiguration.STREAM_CFG_REMOTE :
StreamConfiguration.STREAM_CFG_AUTO)
.setHevcBitratePercentageMultiplier(75) .setHevcBitratePercentageMultiplier(75)
.setHevcSupported(decoderRenderer.isHevcSupported()) .setHevcSupported(decoderRenderer.isHevcSupported())
.setEnableHdr(willStreamHdr) .setEnableHdr(willStreamHdr)
@@ -578,7 +595,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private float prepareDisplayForRendering() { private float prepareDisplayForRendering() {
Display display = getWindowManager().getDefaultDisplay(); Display display = getWindowManager().getDefaultDisplay();
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes(); WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
float displayRefreshRate;
// On M, we can explicitly set the optimal display mode // On M, we can explicitly set the optimal display mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -622,7 +638,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+ LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate()); bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId(); windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
displayRefreshRate = bestMode.getRefreshRate();
} }
// On L, we can at least tell the OS that we want a refresh rate // On L, we can at least tell the OS that we want a refresh rate
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -643,12 +658,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
LimeLog.info("Selected refresh rate: "+bestRefreshRate); LimeLog.info("Selected refresh rate: "+bestRefreshRate);
windowLayoutParams.preferredRefreshRate = bestRefreshRate; windowLayoutParams.preferredRefreshRate = bestRefreshRate;
displayRefreshRate = bestRefreshRate;
} }
else { else {
// Otherwise, the active display refresh rate is just // Otherwise, the active display refresh rate is just
// whatever is currently in use. // whatever is currently in use.
displayRefreshRate = display.getRefreshRate();
} }
// Apply the display mode change // Apply the display mode change
@@ -685,7 +698,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height); streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
} }
return displayRefreshRate; // Use the actual refresh rate of the display, since the preferred refresh rate or mode
// may not actually be applied (ex: Pixel 4 with Smooth Display disabled).
return getWindowManager().getDefaultDisplay().getRefreshRate();
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
@@ -1257,6 +1272,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
} }
break; break;
case MotionEvent.ACTION_CANCEL:
for (TouchContext aTouchContext : touchContextMap) {
aTouchContext.cancelTouch();
}
break;
default: default:
return false; return false;
} }
+16 -16
View File
@@ -5,21 +5,21 @@ import java.util.logging.FileHandler;
import java.util.logging.Logger; import java.util.logging.Logger;
public class LimeLog { public class LimeLog {
private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName()); private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName());
public static void info(String msg) { public static void info(String msg) {
LOGGER.info(msg); LOGGER.info(msg);
} }
public static void warning(String msg) { public static void warning(String msg) {
LOGGER.warning(msg); LOGGER.warning(msg);
} }
public static void severe(String msg) { public static void severe(String msg) {
LOGGER.severe(msg); LOGGER.severe(msg);
} }
public static void setFileHandler(String fileName) throws IOException { public static void setFileHandler(String fileName) throws IOException {
LOGGER.addHandler(new FileHandler(fileName)); LOGGER.addHandler(new FileHandler(fileName));
} }
} }
+4 -3
View File
@@ -126,6 +126,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Set default preferences if we've never been run // Set default preferences if we've never been run
PreferenceManager.setDefaultValues(this, R.xml.preferences, false); PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
// Set the correct layout for the PC grid
pcGridAdapter.updateLayoutWithPreferences(this, PreferenceConfiguration.readPreferences(this));
// Setup the list view // Setup the list view
ImageButton settingsButton = findViewById(R.id.settingsButton); ImageButton settingsButton = findViewById(R.id.settingsButton);
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc); ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
@@ -223,9 +226,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection, bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE); Service.BIND_AUTO_CREATE);
pcGridAdapter = new PcGridAdapter(this, pcGridAdapter = new PcGridAdapter(this, PreferenceConfiguration.readPreferences(this));
PreferenceConfiguration.readPreferences(this).listMode,
PreferenceConfiguration.readPreferences(this).smallIconMode);
initializeViews(); initializeViews();
} }
@@ -14,10 +14,10 @@ public class AndroidAudioRenderer implements AudioRenderer {
private AudioTrack track; private AudioTrack track;
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) { private AudioTrack createAudioTrack(int channelConfig, int sampleRate, int bufferSize, boolean lowLatency) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return new AudioTrack(AudioManager.STREAM_MUSIC, return new AudioTrack(AudioManager.STREAM_MUSIC,
48000, sampleRate,
channelConfig, channelConfig,
AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_PCM_16BIT,
bufferSize, bufferSize,
@@ -28,7 +28,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
.setUsage(AudioAttributes.USAGE_GAME); .setUsage(AudioAttributes.USAGE_GAME);
AudioFormat format = new AudioFormat.Builder() AudioFormat format = new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(48000) .setSampleRate(sampleRate)
.setChannelMask(channelConfig) .setChannelMask(channelConfig)
.build(); .build();
@@ -64,7 +64,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
} }
@Override @Override
public int setup(int audioConfiguration) { public int setup(int audioConfiguration, int sampleRate, int samplesPerFrame) {
int channelConfig; int channelConfig;
int bytesPerFrame; int bytesPerFrame;
@@ -72,11 +72,11 @@ public class AndroidAudioRenderer implements AudioRenderer {
{ {
case MoonBridge.AUDIO_CONFIGURATION_STEREO: case MoonBridge.AUDIO_CONFIGURATION_STEREO:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO; channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
bytesPerFrame = 2 * 240 * 2; bytesPerFrame = 2 * samplesPerFrame * 2;
break; break;
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND: case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
bytesPerFrame = 6 * 240 * 2; bytesPerFrame = 6 * samplesPerFrame * 2;
break; break;
default: default:
LimeLog.severe("Decoder returned unhandled channel count"); LimeLog.severe("Decoder returned unhandled channel count");
@@ -122,7 +122,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
case 1: case 1:
case 3: case 3:
// Try the larger buffer size // Try the larger buffer size
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000, bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
channelConfig, channelConfig,
AudioFormat.ENCODING_PCM_16BIT), AudioFormat.ENCODING_PCM_16BIT),
bytesPerFrame * 2); bytesPerFrame * 2);
@@ -135,13 +135,13 @@ public class AndroidAudioRenderer implements AudioRenderer {
throw new IllegalStateException(); throw new IllegalStateException();
} }
// Skip low latency options if hardware sample rate isn't 48000Hz // Skip low latency options if hardware sample rate doesn't match the content
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) { if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != sampleRate && lowLatency) {
continue; continue;
} }
try { try {
track = createAudioTrack(channelConfig, bufferSize, lowLatency); track = createAudioTrack(channelConfig, sampleRate, bufferSize, lowLatency);
track.play(); track.play();
// Successfully created working AudioTrack. We're done here. // Successfully created working AudioTrack. We're done here.
@@ -170,14 +170,14 @@ public class AndroidAudioRenderer implements AudioRenderer {
@Override @Override
public void playDecodedAudio(short[] audioData) { public void playDecodedAudio(short[] audioData) {
// Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us. // Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us.
if (MoonBridge.getPendingAudioFrames() < 8) { if (MoonBridge.getPendingAudioDuration() < 40) {
// This will block until the write is completed. That can cause a backlog // This will block until the write is completed. That can cause a backlog
// of pending audio data, so we do the above check to be able to bound // of pending audio data, so we do the above check to be able to bound
// latency at 40 ms in that situation. // latency at 40 ms in that situation.
track.write(audioData, 0, audioData.length); track.write(audioData, 0, audioData.length);
} }
else { else {
LimeLog.info("Too many pending audio frames: " + MoonBridge.getPendingAudioFrames()); LimeLog.info("Too much pending audio data: " + MoonBridge.getPendingAudioDuration() +" ms");
} }
} }
@@ -215,9 +215,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if (PreferenceConfiguration.readPreferences(context).usbDriver) { if (PreferenceConfiguration.readPreferences(context).usbDriver) {
UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
for (UsbDevice dev : usbManager.getDeviceList().values()) { for (UsbDevice dev : usbManager.getDeviceList().values()) {
// We explicitly ask not to claim devices that appear as InputDevices // We explicitly check not to claim devices that appear as InputDevices
// otherwise we will double count them. // otherwise we will double count them.
if (UsbDriverService.shouldClaimDevice(dev, false)) { if (UsbDriverService.shouldClaimDevice(dev, false) &&
!UsbDriverService.isRecognizedInputDevice(dev)) {
LimeLog.info("Counting UsbDevice: "+dev.getDeviceName()); LimeLog.info("Counting UsbDevice: "+dev.getDeviceName());
mask |= 1 << count++; mask |= 1 << count++;
} }
@@ -335,6 +336,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
} }
private static boolean isExternal(InputDevice dev) { private static boolean isExternal(InputDevice dev) {
// The ASUS Tinker Board inaccurately reports Bluetooth gamepads as internal,
// causing shouldIgnoreBack() to believe it should pass through back as a
// navigation event for any attached gamepads.
if (Build.MODEL.equals("Tinker Board")) {
return true;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q // Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q
return dev.isExternal(); return dev.isExternal();
@@ -418,6 +426,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
String devName = dev.getName(); String devName = dev.getName();
LimeLog.info("Creating controller context for device: "+devName); LimeLog.info("Creating controller context for device: "+devName);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
LimeLog.info("Vendor ID: "+dev.getVendorId());
LimeLog.info("Product ID: "+dev.getProductId());
}
LimeLog.info(dev.toString()); LimeLog.info(dev.toString());
context.name = devName; context.name = devName;
@@ -464,25 +476,45 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX); InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY); InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
if (rxRange != null && ryRange != null && devName != null) { if (rxRange != null && ryRange != null && devName != null) {
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
// Xbox controllers use RX and RY for right stick if (dev.getVendorId() == 0x054c) { // Sony
if (dev.hasKeys(KeyEvent.KEYCODE_BUTTON_C)[0]) {
LimeLog.info("Detected non-standard DualShock 4 mapping");
context.isNonStandardDualShock4 = true;
}
else {
LimeLog.info("Detected DualShock 4 (Linux standard mapping)");
context.usesLinuxGamepadStandardFaceButtons = true;
}
}
}
else if (!devName.contains("Xbox") && !devName.contains("XBox") && !devName.contains("X-Box")) {
LimeLog.info("Assuming non-standard DualShock 4 mapping on < 4.4");
context.isNonStandardDualShock4 = true;
}
if (context.isNonStandardDualShock4) {
// The old DS4 driver uses RX and RY for triggers
context.leftTriggerAxis = MotionEvent.AXIS_RX;
context.rightTriggerAxis = MotionEvent.AXIS_RY;
}
else {
// If it's not a non-standard DS4 controller, it's probably an Xbox controller or
// other sane controller that uses RX and RY for right stick and Z and RZ for triggers.
context.rightStickXAxis = MotionEvent.AXIS_RX; context.rightStickXAxis = MotionEvent.AXIS_RX;
context.rightStickYAxis = MotionEvent.AXIS_RY; context.rightStickYAxis = MotionEvent.AXIS_RY;
// Xbox controllers use Z and RZ for triggers // While it's likely that Z and RZ are triggers, we may have digital trigger buttons
context.leftTriggerAxis = MotionEvent.AXIS_Z; // instead. We must check that we actually have Z and RZ axes before assigning them.
context.rightTriggerAxis = MotionEvent.AXIS_RZ; if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z) != null &&
context.triggersIdleNegative = true; getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ) != null) {
context.isXboxController = true; context.leftTriggerAxis = MotionEvent.AXIS_Z;
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
}
} }
else {
// DS4 controller uses RX and RY for triggers
context.leftTriggerAxis = MotionEvent.AXIS_RX;
context.rightTriggerAxis = MotionEvent.AXIS_RY;
context.triggersIdleNegative = true;
context.isDualShock4 = true; // Triggers always idle negative on axes that are centered at zero
} context.triggersIdleNegative = true;
} }
} }
@@ -588,7 +620,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// required fixup is ignoring the select button. // required fixup is ignoring the select button.
else if (devName.equals("Xbox Wireless Controller")) { else if (devName.equals("Xbox Wireless Controller")) {
if (gasRange == null) { if (gasRange == null) {
context.isXboxBtController = true; context.isNonStandardXboxBtController = true;
} }
} }
} }
@@ -767,7 +799,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
} }
} }
if (context.isDualShock4) { if (context.usesLinuxGamepadStandardFaceButtons) {
// Android's Generic.kl swaps BTN_NORTH and BTN_WEST
switch (event.getScanCode()) {
case 304:
return KeyEvent.KEYCODE_BUTTON_A;
case 305:
return KeyEvent.KEYCODE_BUTTON_B;
case 307:
return KeyEvent.KEYCODE_BUTTON_Y;
case 308:
return KeyEvent.KEYCODE_BUTTON_X;
}
}
if (context.isNonStandardDualShock4) {
switch (event.getScanCode()) { switch (event.getScanCode()) {
case 304: case 304:
return KeyEvent.KEYCODE_BUTTON_X; return KeyEvent.KEYCODE_BUTTON_X;
@@ -812,7 +858,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return KeyEvent.KEYCODE_BUTTON_START; return KeyEvent.KEYCODE_BUTTON_START;
} }
} }
else if (context.isXboxBtController) { else if (context.isNonStandardXboxBtController) {
switch (event.getScanCode()) { switch (event.getScanCode()) {
case 306: case 306:
return KeyEvent.KEYCODE_BUTTON_X; return KeyEvent.KEYCODE_BUTTON_X;
@@ -1531,9 +1577,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public int hatXAxis = -1; public int hatXAxis = -1;
public int hatYAxis = -1; public int hatYAxis = -1;
public boolean isDualShock4; public boolean isNonStandardDualShock4;
public boolean isXboxController; public boolean usesLinuxGamepadStandardFaceButtons;
public boolean isXboxBtController; public boolean isNonStandardXboxBtController;
public boolean isServal; public boolean isServal;
public boolean backIsStart; public boolean backIsStart;
public boolean modeIsSelect; public boolean modeIsSelect;
@@ -8,290 +8,290 @@ import android.view.KeyEvent;
* @author Cameron Gutman * @author Cameron Gutman
*/ */
public class KeyboardTranslator { public class KeyboardTranslator {
/** /**
* GFE's prefix for every key code * GFE's prefix for every key code
*/ */
private static final short KEY_PREFIX = (short) 0x80; private static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48; public static final int VK_0 = 48;
public static final int VK_9 = 57; public static final int VK_9 = 57;
public static final int VK_A = 65; public static final int VK_A = 65;
public static final int VK_Z = 90; public static final int VK_Z = 90;
public static final int VK_NUMPAD0 = 96; public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92; public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20; public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12; public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44; public static final int VK_COMMA = 44;
public static final int VK_BACK_SPACE = 8; public static final int VK_BACK_SPACE = 8;
public static final int VK_EQUALS = 61; public static final int VK_EQUALS = 61;
public static final int VK_ESCAPE = 27; public static final int VK_ESCAPE = 27;
public static final int VK_F1 = 112; public static final int VK_F1 = 112;
public static final int VK_END = 35; public static final int VK_END = 35;
public static final int VK_HOME = 36; public static final int VK_HOME = 36;
public static final int VK_NUM_LOCK = 144; public static final int VK_NUM_LOCK = 144;
public static final int VK_PAGE_UP = 33; public static final int VK_PAGE_UP = 33;
public static final int VK_PAGE_DOWN = 34; public static final int VK_PAGE_DOWN = 34;
public static final int VK_PLUS = 521; public static final int VK_PLUS = 521;
public static final int VK_CLOSE_BRACKET = 93; public static final int VK_CLOSE_BRACKET = 93;
public static final int VK_SCROLL_LOCK = 145; public static final int VK_SCROLL_LOCK = 145;
public static final int VK_SEMICOLON = 59; public static final int VK_SEMICOLON = 59;
public static final int VK_SLASH = 47; public static final int VK_SLASH = 47;
public static final int VK_SPACE = 32; public static final int VK_SPACE = 32;
public static final int VK_PRINTSCREEN = 154; public static final int VK_PRINTSCREEN = 154;
public static final int VK_TAB = 9; public static final int VK_TAB = 9;
public static final int VK_LEFT = 37; public static final int VK_LEFT = 37;
public static final int VK_RIGHT = 39; public static final int VK_RIGHT = 39;
public static final int VK_UP = 38; public static final int VK_UP = 38;
public static final int VK_DOWN = 40; public static final int VK_DOWN = 40;
public static final int VK_BACK_QUOTE = 192; public static final int VK_BACK_QUOTE = 192;
public static final int VK_QUOTE = 222; public static final int VK_QUOTE = 222;
public static final int VK_PAUSE = 19; public static final int VK_PAUSE = 19;
public static boolean needsShift(int keycode) { public static boolean needsShift(int keycode) {
switch (keycode) switch (keycode)
{ {
case KeyEvent.KEYCODE_AT: case KeyEvent.KEYCODE_AT:
case KeyEvent.KEYCODE_POUND: case KeyEvent.KEYCODE_POUND:
case KeyEvent.KEYCODE_PLUS: case KeyEvent.KEYCODE_PLUS:
case KeyEvent.KEYCODE_STAR: case KeyEvent.KEYCODE_STAR:
return true; return true;
default: default:
return false; return false;
} }
} }
/** /**
* Translates the given keycode and returns the GFE keycode * Translates the given keycode and returns the GFE keycode
* @param keycode the code to be translated * @param keycode the code to be translated
* @return a GFE keycode for the given keycode * @return a GFE keycode for the given keycode
*/ */
public static short translate(int keycode) { public static short translate(int keycode) {
int translated; int translated;
// This is a poor man's mapping between Android key codes // This is a poor man's mapping between Android key codes
// and Windows VK_* codes. For all defined VK_ codes, see: // and Windows VK_* codes. For all defined VK_ codes, see:
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx // https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
if (keycode >= KeyEvent.KEYCODE_0 && if (keycode >= KeyEvent.KEYCODE_0 &&
keycode <= KeyEvent.KEYCODE_9) { keycode <= KeyEvent.KEYCODE_9) {
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0; translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
} }
else if (keycode >= KeyEvent.KEYCODE_A && else if (keycode >= KeyEvent.KEYCODE_A &&
keycode <= KeyEvent.KEYCODE_Z) { keycode <= KeyEvent.KEYCODE_Z) {
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A; translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
} }
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 && else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
keycode <= KeyEvent.KEYCODE_NUMPAD_9) { keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0; translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
} }
else if (keycode >= KeyEvent.KEYCODE_F1 && else if (keycode >= KeyEvent.KEYCODE_F1 &&
keycode <= KeyEvent.KEYCODE_F12) { keycode <= KeyEvent.KEYCODE_F12) {
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1; translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
} }
else { else {
switch (keycode) { switch (keycode) {
case KeyEvent.KEYCODE_ALT_LEFT: case KeyEvent.KEYCODE_ALT_LEFT:
translated = 0xA4; translated = 0xA4;
break; break;
case KeyEvent.KEYCODE_ALT_RIGHT: case KeyEvent.KEYCODE_ALT_RIGHT:
translated = 0xA5; translated = 0xA5;
break; break;
case KeyEvent.KEYCODE_BACKSLASH: case KeyEvent.KEYCODE_BACKSLASH:
translated = 0xdc; translated = 0xdc;
break; break;
case KeyEvent.KEYCODE_CAPS_LOCK: case KeyEvent.KEYCODE_CAPS_LOCK:
translated = VK_CAPS_LOCK; translated = VK_CAPS_LOCK;
break; break;
case KeyEvent.KEYCODE_CLEAR: case KeyEvent.KEYCODE_CLEAR:
translated = VK_CLEAR; translated = VK_CLEAR;
break; break;
case KeyEvent.KEYCODE_COMMA: case KeyEvent.KEYCODE_COMMA:
translated = 0xbc; translated = 0xbc;
break; break;
case KeyEvent.KEYCODE_CTRL_LEFT: case KeyEvent.KEYCODE_CTRL_LEFT:
translated = 0xA2; translated = 0xA2;
break; break;
case KeyEvent.KEYCODE_CTRL_RIGHT: case KeyEvent.KEYCODE_CTRL_RIGHT:
translated = 0xA3; translated = 0xA3;
break; break;
case KeyEvent.KEYCODE_DEL: case KeyEvent.KEYCODE_DEL:
translated = VK_BACK_SPACE; translated = VK_BACK_SPACE;
break; break;
case KeyEvent.KEYCODE_ENTER: case KeyEvent.KEYCODE_ENTER:
translated = 0x0d; translated = 0x0d;
break; break;
case KeyEvent.KEYCODE_PLUS: case KeyEvent.KEYCODE_PLUS:
case KeyEvent.KEYCODE_EQUALS: case KeyEvent.KEYCODE_EQUALS:
translated = 0xbb; translated = 0xbb;
break; break;
case KeyEvent.KEYCODE_ESCAPE: case KeyEvent.KEYCODE_ESCAPE:
translated = VK_ESCAPE; translated = VK_ESCAPE;
break; break;
case KeyEvent.KEYCODE_FORWARD_DEL: case KeyEvent.KEYCODE_FORWARD_DEL:
translated = 0x2e; translated = 0x2e;
break; break;
case KeyEvent.KEYCODE_INSERT: case KeyEvent.KEYCODE_INSERT:
translated = 0x2d; translated = 0x2d;
break; break;
case KeyEvent.KEYCODE_LEFT_BRACKET: case KeyEvent.KEYCODE_LEFT_BRACKET:
translated = 0xdb; translated = 0xdb;
break; break;
case KeyEvent.KEYCODE_META_LEFT: case KeyEvent.KEYCODE_META_LEFT:
translated = 0x5b; translated = 0x5b;
break; break;
case KeyEvent.KEYCODE_META_RIGHT: case KeyEvent.KEYCODE_META_RIGHT:
translated = 0x5c; translated = 0x5c;
break; break;
case KeyEvent.KEYCODE_MINUS: case KeyEvent.KEYCODE_MINUS:
translated = 0xbd; translated = 0xbd;
break; break;
case KeyEvent.KEYCODE_MOVE_END: case KeyEvent.KEYCODE_MOVE_END:
translated = VK_END; translated = VK_END;
break; break;
case KeyEvent.KEYCODE_MOVE_HOME: case KeyEvent.KEYCODE_MOVE_HOME:
translated = VK_HOME; translated = VK_HOME;
break; break;
case KeyEvent.KEYCODE_NUM_LOCK: case KeyEvent.KEYCODE_NUM_LOCK:
translated = VK_NUM_LOCK; translated = VK_NUM_LOCK;
break; break;
case KeyEvent.KEYCODE_PAGE_DOWN: case KeyEvent.KEYCODE_PAGE_DOWN:
translated = VK_PAGE_DOWN; translated = VK_PAGE_DOWN;
break; break;
case KeyEvent.KEYCODE_PAGE_UP: case KeyEvent.KEYCODE_PAGE_UP:
translated = VK_PAGE_UP; translated = VK_PAGE_UP;
break; break;
case KeyEvent.KEYCODE_PERIOD: case KeyEvent.KEYCODE_PERIOD:
translated = 0xbe; translated = 0xbe;
break; break;
case KeyEvent.KEYCODE_RIGHT_BRACKET: case KeyEvent.KEYCODE_RIGHT_BRACKET:
translated = 0xdd; translated = 0xdd;
break; break;
case KeyEvent.KEYCODE_SCROLL_LOCK: case KeyEvent.KEYCODE_SCROLL_LOCK:
translated = VK_SCROLL_LOCK; translated = VK_SCROLL_LOCK;
break; break;
case KeyEvent.KEYCODE_SEMICOLON: case KeyEvent.KEYCODE_SEMICOLON:
translated = 0xba; translated = 0xba;
break; break;
case KeyEvent.KEYCODE_SHIFT_LEFT: case KeyEvent.KEYCODE_SHIFT_LEFT:
translated = 0xA0; translated = 0xA0;
break; break;
case KeyEvent.KEYCODE_SHIFT_RIGHT: case KeyEvent.KEYCODE_SHIFT_RIGHT:
translated = 0xA1; translated = 0xA1;
break; break;
case KeyEvent.KEYCODE_SLASH: case KeyEvent.KEYCODE_SLASH:
translated = 0xbf; translated = 0xbf;
break; break;
case KeyEvent.KEYCODE_SPACE: case KeyEvent.KEYCODE_SPACE:
translated = VK_SPACE; translated = VK_SPACE;
break; break;
case KeyEvent.KEYCODE_SYSRQ: case KeyEvent.KEYCODE_SYSRQ:
// Android defines this as SysRq/PrntScrn // Android defines this as SysRq/PrntScrn
translated = VK_PRINTSCREEN; translated = VK_PRINTSCREEN;
break; break;
case KeyEvent.KEYCODE_TAB: case KeyEvent.KEYCODE_TAB:
translated = VK_TAB; translated = VK_TAB;
break; break;
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
translated = VK_LEFT; translated = VK_LEFT;
break; break;
case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_RIGHT:
translated = VK_RIGHT; translated = VK_RIGHT;
break; break;
case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_UP:
translated = VK_UP; translated = VK_UP;
break; break;
case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN:
translated = VK_DOWN; translated = VK_DOWN;
break; break;
case KeyEvent.KEYCODE_GRAVE: case KeyEvent.KEYCODE_GRAVE:
translated = VK_BACK_QUOTE; translated = VK_BACK_QUOTE;
break; break;
case KeyEvent.KEYCODE_APOSTROPHE: case KeyEvent.KEYCODE_APOSTROPHE:
translated = 0xde; translated = 0xde;
break; break;
case KeyEvent.KEYCODE_BREAK: case KeyEvent.KEYCODE_BREAK:
translated = VK_PAUSE; translated = VK_PAUSE;
break; break;
case KeyEvent.KEYCODE_NUMPAD_DIVIDE: case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
translated = 0x6F; translated = 0x6F;
break; break;
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY: case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
translated = 0x6A; translated = 0x6A;
break; break;
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT: case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
translated = 0x6D; translated = 0x6D;
break; break;
case KeyEvent.KEYCODE_NUMPAD_ADD: case KeyEvent.KEYCODE_NUMPAD_ADD:
translated = 0x6B; translated = 0x6B;
break; break;
case KeyEvent.KEYCODE_NUMPAD_DOT: case KeyEvent.KEYCODE_NUMPAD_DOT:
translated = 0x6E; translated = 0x6E;
break; break;
case KeyEvent.KEYCODE_AT: case KeyEvent.KEYCODE_AT:
translated = 2 + VK_0; translated = 2 + VK_0;
break; break;
case KeyEvent.KEYCODE_POUND: case KeyEvent.KEYCODE_POUND:
translated = 3 + VK_0; translated = 3 + VK_0;
break; break;
case KeyEvent.KEYCODE_STAR: case KeyEvent.KEYCODE_STAR:
translated = 8 + VK_0; translated = 8 + VK_0;
break; break;
default: default:
System.out.println("No key for "+keycode); System.out.println("No key for "+keycode);
return 0; return 0;
} }
} }
return (short) ((KEY_PREFIX << 8) | translated); return (short) ((KEY_PREFIX << 8) | translated);
} }
} }
@@ -166,7 +166,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
} }
} }
private static boolean isRecognizedInputDevice(UsbDevice device) { public static boolean isRecognizedInputDevice(UsbDevice device) {
// On KitKat and later, we can determine if this VID and PID combo // On KitKat and later, we can determine if this VID and PID combo
// matches an existing input device and defer to the built-in controller // matches an existing input device and defer to the built-in controller
// support in that case. Prior to KitKat, we'll always return true to be safe. // support in that case. Prior to KitKat, we'll always return true to be safe.
@@ -191,10 +191,32 @@ public class UsbDriverService extends Service implements UsbDriverListener {
} }
} }
public static boolean kernelSupportsXboxOne() {
String kernelVersion = System.getProperty("os.version");
LimeLog.info("Kernel Version: "+kernelVersion);
if (kernelVersion == null) {
// We'll assume this is some newer version of Android
// that doesn't let you read the kernel version this way.
return true;
}
else if (kernelVersion.startsWith("2.") || kernelVersion.startsWith("3.")) {
// These are old kernels that definitely don't support Xbox One controllers properly
return false;
}
else if (kernelVersion.startsWith("4.4.") || kernelVersion.startsWith("4.9.")) {
// These aren't guaranteed to have backported kernel patches for proper Xbox One
// support (though some devices will).
return false;
}
else {
// The next AOSP common kernel is 4.14 which has working Xbox One controller support
return true;
}
}
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) { public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
// We always bind to XB1 controllers but only bind to XB360 controllers return ((!kernelSupportsXboxOne() || !isRecognizedInputDevice(device) || claimAllAvailable) && XboxOneController.canClaimDevice(device)) ||
// if we know the kernel isn't already driving this device.
return XboxOneController.canClaimDevice(device) ||
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device)); ((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
} }
@@ -293,12 +293,12 @@ public class AnalogStick extends VirtualControllerElement {
movement_radius = getMovementRadius(relative_x, relative_y); movement_radius = getMovementRadius(relative_x, relative_y);
movement_angle = getAngle(relative_x, relative_y); movement_angle = getAngle(relative_x, relative_y);
// chop radius if out of outer circle and already pressed // pass touch event to parent if out of outer circle
if (movement_radius > radius_complete && !isPressed())
return false;
// chop radius if out of outer circle or near the edge
if (movement_radius > (radius_complete - radius_analog_stick)) { if (movement_radius > (radius_complete - radius_analog_stick)) {
// not pressed already, so ignore event from outer circle
if (!isPressed()) {
return false;
}
movement_radius = radius_complete - radius_analog_stick; movement_radius = radius_complete - radius_analog_stick;
} }
@@ -306,7 +306,7 @@ public class VirtualControllerConfigurationLoader {
prefEditor.apply(); prefEditor.apply();
} }
public static void loadFromPreferences(final VirtualController controller, final Context context) { public static void loadFromPreferences(final VirtualController controller, final Context context) {
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE); SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
for (VirtualControllerElement element : controller.getElements()) { for (VirtualControllerElement element : controller.getElements()) {
@@ -324,5 +324,5 @@ public class VirtualControllerConfigurationLoader {
} }
} }
} }
} }
} }
@@ -112,34 +112,34 @@ public abstract class VirtualControllerElement extends View {
/* /*
protected void actionShowNormalColorChooser() { protected void actionShowNormalColorChooser() {
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() { AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
@Override @Override
public void onCancel(AmbilWarnaDialog dialog) public void onCancel(AmbilWarnaDialog dialog)
{} {}
@Override @Override
public void onOk(AmbilWarnaDialog dialog, int color) { public void onOk(AmbilWarnaDialog dialog, int color) {
normalColor = color; normalColor = color;
invalidate(); invalidate();
} }
}); });
colorDialog.show(); colorDialog.show();
} }
protected void actionShowPressedColorChooser() { protected void actionShowPressedColorChooser() {
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() { AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
@Override @Override
public void onCancel(AmbilWarnaDialog dialog) { public void onCancel(AmbilWarnaDialog dialog) {
} }
@Override @Override
public void onOk(AmbilWarnaDialog dialog, int color) { public void onOk(AmbilWarnaDialog dialog, int color) {
pressedColor = color; pressedColor = color;
invalidate(); invalidate();
} }
}); });
colorDialog.show(); colorDialog.show();
} }
*/ */
protected void actionEnableMove() { protected void actionEnableMove() {
@@ -195,11 +195,11 @@ public abstract class VirtualControllerElement extends View {
break; break;
} }
/* /*
case 2: { // set default color case 2: { // set default color
actionShowNormalColorChooser(); actionShowNormalColorChooser();
break; break;
} }
case 3: { // set pressed color case 3: { // set pressed color
actionShowPressedColorChooser(); actionShowPressedColorChooser();
break; break;
} }
@@ -190,8 +190,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return avcDecoder != null; return avcDecoder != null;
} }
public boolean is49FpsBlacklisted() { public boolean isBlacklistedForFrameRate(int frameRate) {
return avcDecoder != null && MediaCodecHelper.decoderBlacklistedFor49Fps(avcDecoder.getName()); return avcDecoder != null && MediaCodecHelper.decoderBlacklistedForFrameRate(avcDecoder.getName(), frameRate);
} }
public void enableLegacyFrameDropRendering() { public void enableLegacyFrameDropRendering() {
@@ -683,17 +683,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// for known resolution combinations. Reference frame invalidation may need // for known resolution combinations. Reference frame invalidation may need
// these, so leave them be for those decoders. // these, so leave them be for those decoders.
if (!refFrameInvalidationActive) { if (!refFrameInvalidationActive) {
if (initialWidth <= 720 && initialHeight <= 480) { if (initialWidth <= 720 && initialHeight <= 480 && refreshRate <= 60) {
// Max 5 buffered frames at 720x480x60 // Max 5 buffered frames at 720x480x60
LimeLog.info("Patching level_idc to 31"); LimeLog.info("Patching level_idc to 31");
sps.levelIdc = 31; sps.levelIdc = 31;
} }
else if (initialWidth <= 1280 && initialHeight <= 720) { else if (initialWidth <= 1280 && initialHeight <= 720 && refreshRate <= 60) {
// Max 5 buffered frames at 1280x720x60 // Max 5 buffered frames at 1280x720x60
LimeLog.info("Patching level_idc to 32"); LimeLog.info("Patching level_idc to 32");
sps.levelIdc = 32; sps.levelIdc = 32;
} }
else if (initialWidth <= 1920 && initialHeight <= 1080) { else if (initialWidth <= 1920 && initialHeight <= 1080 && refreshRate <= 60) {
// Max 4 buffered frames at 1920x1080x64 // Max 4 buffered frames at 1920x1080x64
LimeLog.info("Patching level_idc to 42"); LimeLog.info("Patching level_idc to 42");
sps.levelIdc = 42; sps.levelIdc = 42;
File diff suppressed because it is too large Load Diff
@@ -10,6 +10,8 @@ import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
@@ -22,13 +24,19 @@ import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.mdns.MdnsComputer; import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener; import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper; import com.limelight.utils.CacheHelper;
import com.limelight.utils.NetHelper;
import com.limelight.utils.ServerHelper; import com.limelight.utils.ServerHelper;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Binder; import android.os.Binder;
import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
@@ -54,6 +62,7 @@ public class ComputerManagerService extends Service {
private ComputerManagerListener listener = null; private ComputerManagerListener listener = null;
private final AtomicInteger activePolls = new AtomicInteger(0); private final AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false; private boolean pollingActive = false;
private final Lock defaultNetworkLock = new ReentrantLock();
private DiscoveryService.DiscoveryBinder discoveryBinder; private DiscoveryService.DiscoveryBinder discoveryBinder;
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() { private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
@@ -294,6 +303,66 @@ public class ComputerManagerService extends Service {
return false; return false;
} }
private void populateExternalAddress(ComputerDetails details) {
boolean boundToNetwork = false;
boolean activeNetworkIsVpn = NetHelper.isActiveNetworkVpn(this);
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
// Check if we're currently connected to a VPN which may send our
// STUN request from an unexpected interface
if (activeNetworkIsVpn) {
// Acquire the default network lock since we could be changing global process state
defaultNetworkLock.lock();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// On Lollipop or later, we can bind our process to the underlying interface
// to ensure our STUN request goes out on that interface or not at all (which is
// preferable to getting a VPN endpoint address back).
Network[] networks = connMgr.getAllNetworks();
for (Network net : networks) {
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(net);
if (netCaps != null) {
if (!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
// This network looks like an underlying multicast-capable transport,
// so let's guess that it's probably where our mDNS response came from.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (connMgr.bindProcessToNetwork(net)) {
boundToNetwork = true;
break;
}
}
else if (ConnectivityManager.setProcessDefaultNetwork(net)) {
boundToNetwork = true;
break;
}
}
}
}
}
}
// Perform the STUN request if we're not on a VPN or if we bound to a network
if (!activeNetworkIsVpn || boundToNetwork) {
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
}
// Unbind from the network
if (boundToNetwork) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
connMgr.bindProcessToNetwork(null);
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ConnectivityManager.setProcessDefaultNetwork(null);
}
}
// Unlock the network state
if (activeNetworkIsVpn) {
defaultNetworkLock.unlock();
}
}
private MdnsDiscoveryListener createDiscoveryListener() { private MdnsDiscoveryListener createDiscoveryListener() {
return new MdnsDiscoveryListener() { return new MdnsDiscoveryListener() {
@Override @Override
@@ -308,7 +377,7 @@ public class ComputerManagerService extends Service {
// our WAN address, which is also very likely the WAN address // our WAN address, which is also very likely the WAN address
// of the PC. We can use this later to connect remotely. // of the PC. We can use this later to connect remotely.
if (computer.getLocalAddress() instanceof Inet4Address) { if (computer.getLocalAddress() instanceof Inet4Address) {
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478); populateExternalAddress(details);
} }
} }
if (computer.getIpv6Address() != null) { if (computer.getIpv6Address() != null) {
@@ -1,6 +1,7 @@
package com.limelight.grid; package com.limelight.grid;
import android.app.Activity; import android.content.Context;
import android.graphics.BitmapFactory;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
@@ -13,6 +14,7 @@ import com.limelight.grid.assets.DiskAssetLoader;
import com.limelight.grid.assets.MemoryAssetLoader; import com.limelight.grid.assets.MemoryAssetLoader;
import com.limelight.grid.assets.NetworkAssetLoader; import com.limelight.grid.assets.NetworkAssetLoader;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@@ -23,15 +25,37 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private static final int SMALL_WIDTH_DP = 100; private static final int SMALL_WIDTH_DP = 100;
private static final int LARGE_WIDTH_DP = 150; private static final int LARGE_WIDTH_DP = 150;
private final CachedAppAssetLoader loader; private final ComputerDetails computer;
private final String uniqueId;
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) { private CachedAppAssetLoader loader;
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item));
int dpi = activity.getResources().getDisplayMetrics().densityDpi; public AppGridAdapter(Context context, PreferenceConfiguration prefs, ComputerDetails computer, String uniqueId) {
super(context, getLayoutIdForPreferences(prefs));
this.computer = computer;
this.uniqueId = uniqueId;
updateLayoutWithPreferences(context, prefs);
}
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
if (prefs.listMode) {
return R.layout.simple_row;
}
else if (prefs.smallIconMode) {
return R.layout.app_grid_item_small;
}
else {
return R.layout.app_grid_item;
}
}
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
int dpi = context.getResources().getDisplayMetrics().densityDpi;
int dp; int dp;
if (small) { if (prefs.smallIconMode) {
dp = SMALL_WIDTH_DP; dp = SMALL_WIDTH_DP;
} }
else { else {
@@ -45,10 +69,19 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
LimeLog.info("Art scaling divisor: " + scalingDivisor); LimeLog.info("Art scaling divisor: " + scalingDivisor);
if (loader != null) {
// Cancel operations on the old loader
cancelQueuedOperations();
}
this.loader = new CachedAppAssetLoader(computer, scalingDivisor, this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId), new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(), new MemoryAssetLoader(),
new DiskAssetLoader(context)); new DiskAssetLoader(context),
BitmapFactory.decodeResource(context.getResources(), R.drawable.no_app_image));
// This will trigger the view to reload with the new layout
setLayoutId(getLayoutIdForPreferences(prefs));
} }
public void cancelQueuedOperations() { public void cancelQueuedOperations() {
@@ -10,22 +10,32 @@ import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import com.limelight.R; import com.limelight.R;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.ArrayList; import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter { public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected final Context context; protected final Context context;
protected final int layoutId; private int layoutId;
protected final ArrayList<T> itemList = new ArrayList<>(); final ArrayList<T> itemList = new ArrayList<>();
protected final LayoutInflater inflater; private final LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId) { GenericGridAdapter(Context context, int layoutId) {
this.context = context; this.context = context;
this.layoutId = layoutId; this.layoutId = layoutId;
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
} }
void setLayoutId(int layoutId) {
if (layoutId != this.layoutId) {
this.layoutId = layoutId;
// Force the view to be redrawn with the new layout
notifyDataSetInvalidated();
}
}
public void clear() { public void clear() {
itemList.clear(); itemList.clear();
} }
@@ -10,14 +10,32 @@ import com.limelight.PcView;
import com.limelight.R; import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.PairingManager; import com.limelight.nvstream.http.PairingManager;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> { public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context, boolean listMode, boolean small) { public PcGridAdapter(Context context, PreferenceConfiguration prefs) {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item)); super(context, getLayoutIdForPreferences(prefs));
}
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
if (prefs.listMode) {
return R.layout.simple_row;
}
else if (prefs.smallIconMode) {
return R.layout.pc_grid_item_small;
}
else {
return R.layout.pc_grid_item;
}
}
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
// This will trigger the view to reload with the new layout
setLayoutId(getLayoutIdForPreferences(prefs));
} }
public void addComputer(PcView.ComputerObject computer) { public void addComputer(PcView.ComputerObject computer) {
@@ -52,15 +52,17 @@ public class CachedAppAssetLoader {
private final MemoryAssetLoader memoryLoader; private final MemoryAssetLoader memoryLoader;
private final DiskAssetLoader diskLoader; private final DiskAssetLoader diskLoader;
private final Bitmap placeholderBitmap; private final Bitmap placeholderBitmap;
private final Bitmap noAppImageBitmap;
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider, public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader, NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
DiskAssetLoader diskLoader) { DiskAssetLoader diskLoader, Bitmap noAppImageBitmap) {
this.computer = computer; this.computer = computer;
this.scalingDivider = scalingDivider; this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader; this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader; this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader; this.diskLoader = diskLoader;
this.noAppImageBitmap = noAppImageBitmap;
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
} }
@@ -188,9 +190,10 @@ public class CachedAppAssetLoader {
prgView.setVisibility(View.VISIBLE); prgView.setVisibility(View.VISIBLE);
} }
// Set off another loader task on the network executor // Set off another loader task on the network executor. This time our AsyncDrawable
// will use the app image placeholder bitmap, rather than an empty bitmap.
LoaderTask task = new LoaderTask(imageView, prgView, false); LoaderTask task = new LoaderTask(imageView, prgView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task); AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), noAppImageBitmap, task);
imageView.setVisibility(View.VISIBLE); imageView.setVisibility(View.VISIBLE);
imageView.setImageDrawable(asyncDrawable); imageView.setImageDrawable(asyncDrawable);
task.executeOnExecutor(networkExecutor, tuple); task.executeOnExecutor(networkExecutor, tuple);
@@ -5,20 +5,20 @@ import java.security.cert.X509Certificate;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
public class ConnectionContext { public class ConnectionContext {
public String serverAddress; public String serverAddress;
public X509Certificate serverCert; public X509Certificate serverCert;
public StreamConfiguration streamConfig; public StreamConfiguration streamConfig;
public NvConnectionListener connListener; public NvConnectionListener connListener;
public SecretKey riKey; public SecretKey riKey;
public int riKeyId; public int riKeyId;
// This is the version quad from the appversion tag of /serverinfo // This is the version quad from the appversion tag of /serverinfo
public String serverAppVersion; public String serverAppVersion;
public String serverGfeVersion; public String serverGfeVersion;
public int negotiatedWidth, negotiatedHeight; public int negotiatedWidth, negotiatedHeight;
public int negotiatedFps; public int negotiatedFps;
public boolean negotiatedHdr; public boolean negotiatedHdr;
public int videoCapabilities; public int videoCapabilities;
} }
@@ -26,320 +26,320 @@ import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.nvstream.jni.MoonBridge; import com.limelight.nvstream.jni.MoonBridge;
public class NvConnection { public class NvConnection {
// Context parameters // Context parameters
private String host; private String host;
private LimelightCryptoProvider cryptoProvider; private LimelightCryptoProvider cryptoProvider;
private String uniqueId; private String uniqueId;
private ConnectionContext context; private ConnectionContext context;
private static Semaphore connectionAllowed = new Semaphore(1); private static Semaphore connectionAllowed = new Semaphore(1);
private final boolean isMonkey; private final boolean isMonkey;
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert) public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
{ {
this.host = host; this.host = host;
this.cryptoProvider = cryptoProvider; this.cryptoProvider = cryptoProvider;
this.uniqueId = uniqueId; this.uniqueId = uniqueId;
this.context = new ConnectionContext(); this.context = new ConnectionContext();
this.context.streamConfig = config; this.context.streamConfig = config;
this.context.serverCert = serverCert; this.context.serverCert = serverCert;
try { try {
// This is unique per connection // This is unique per connection
this.context.riKey = generateRiAesKey(); this.context.riKey = generateRiAesKey();
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
} }
this.context.riKeyId = generateRiKeyId(); this.context.riKeyId = generateRiKeyId();
this.isMonkey = ActivityManager.isUserAMonkey(); this.isMonkey = ActivityManager.isUserAMonkey();
} }
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException { private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
KeyGenerator keyGen = KeyGenerator.getInstance("AES"); KeyGenerator keyGen = KeyGenerator.getInstance("AES");
// RI keys are 128 bits // RI keys are 128 bits
keyGen.init(128); keyGen.init(128);
return keyGen.generateKey(); return keyGen.generateKey();
} }
private static int generateRiKeyId() { private static int generateRiKeyId() {
return new SecureRandom().nextInt(); return new SecureRandom().nextInt();
} }
public void stop() { public void stop() {
// Interrupt any pending connection. This is thread-safe. // Interrupt any pending connection. This is thread-safe.
MoonBridge.interruptConnection(); MoonBridge.interruptConnection();
// Moonlight-core is not thread-safe with respect to connection start and stop, so // Moonlight-core is not thread-safe with respect to connection start and stop, so
// we must not invoke that functionality in parallel. // we must not invoke that functionality in parallel.
synchronized (MoonBridge.class) { synchronized (MoonBridge.class) {
MoonBridge.stopConnection(); MoonBridge.stopConnection();
MoonBridge.cleanupBridge(); MoonBridge.cleanupBridge();
} }
// Now a pending connection can be processed // Now a pending connection can be processed
connectionAllowed.release(); connectionAllowed.release();
} }
private boolean startApp() throws XmlPullParserException, IOException private boolean startApp() throws XmlPullParserException, IOException
{ {
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider); NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
String serverInfo = h.getServerInfo(); String serverInfo = h.getServerInfo();
context.serverAppVersion = h.getServerVersion(serverInfo); context.serverAppVersion = h.getServerVersion(serverInfo);
if (context.serverAppVersion == null) { if (context.serverAppVersion == null) {
context.connListener.displayMessage("Server version malformed"); context.connListener.displayMessage("Server version malformed");
return false; return false;
} }
// May be missing for older servers // May be missing for older servers
context.serverGfeVersion = h.getGfeVersion(serverInfo); context.serverGfeVersion = h.getGfeVersion(serverInfo);
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) { if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
context.connListener.displayMessage("Device not paired with computer"); context.connListener.displayMessage("Device not paired with computer");
return false; return false;
} }
context.negotiatedHdr = context.streamConfig.getEnableHdr(); context.negotiatedHdr = context.streamConfig.getEnableHdr();
if ((h.getServerCodecModeSupport(serverInfo) & 0x200) == 0 && context.negotiatedHdr) { if ((h.getServerCodecModeSupport(serverInfo) & 0x200) == 0 && context.negotiatedHdr) {
context.connListener.displayTransientMessage("Your GPU does not support streaming HDR. The stream will be SDR."); context.connListener.displayTransientMessage("Your GPU does not support streaming HDR. The stream will be SDR.");
context.negotiatedHdr = false; context.negotiatedHdr = false;
} }
// //
// Decide on negotiated stream parameters now // Decide on negotiated stream parameters now
// //
// Check for a supported stream resolution // Check for a supported stream resolution
if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) { if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
// Client wants 4K but the server can't do it // Client wants 4K but the server can't do it
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p."); context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
// Lower resolution to 1080p // Lower resolution to 1080p
context.negotiatedWidth = 1920; context.negotiatedWidth = 1920;
context.negotiatedHeight = 1080; context.negotiatedHeight = 1080;
context.negotiatedFps = context.streamConfig.getRefreshRate(); context.negotiatedFps = context.streamConfig.getRefreshRate();
} }
else { else {
// Take what the client wanted // Take what the client wanted
context.negotiatedWidth = context.streamConfig.getWidth(); context.negotiatedWidth = context.streamConfig.getWidth();
context.negotiatedHeight = context.streamConfig.getHeight(); context.negotiatedHeight = context.streamConfig.getHeight();
context.negotiatedFps = context.streamConfig.getRefreshRate(); context.negotiatedFps = context.streamConfig.getRefreshRate();
} }
// //
// Video stream format will be decided during the RTSP handshake // Video stream format will be decided during the RTSP handshake
// //
NvApp app = context.streamConfig.getApp(); NvApp app = context.streamConfig.getApp();
// If the client did not provide an exact app ID, do a lookup with the applist // If the client did not provide an exact app ID, do a lookup with the applist
if (!context.streamConfig.getApp().isInitialized()) { if (!context.streamConfig.getApp().isInitialized()) {
LimeLog.info("Using deprecated app lookup method - Please specify an app ID in your StreamConfiguration instead"); LimeLog.info("Using deprecated app lookup method - Please specify an app ID in your StreamConfiguration instead");
app = h.getAppByName(context.streamConfig.getApp().getAppName()); app = h.getAppByName(context.streamConfig.getApp().getAppName());
if (app == null) { if (app == null) {
context.connListener.displayMessage("The app " + context.streamConfig.getApp().getAppName() + " is not in GFE app list"); context.connListener.displayMessage("The app " + context.streamConfig.getApp().getAppName() + " is not in GFE app list");
return false; return false;
} }
} }
// If there's a game running, resume it // If there's a game running, resume it
if (h.getCurrentGame(serverInfo) != 0) { if (h.getCurrentGame(serverInfo) != 0) {
try { try {
if (h.getCurrentGame(serverInfo) == app.getAppId()) { if (h.getCurrentGame(serverInfo) == app.getAppId()) {
if (!h.resumeApp(context)) { if (!h.resumeApp(context)) {
context.connListener.displayMessage("Failed to resume existing session"); context.connListener.displayMessage("Failed to resume existing session");
return false; return false;
} }
} else { } else {
return quitAndLaunch(h, context); return quitAndLaunch(h, context);
} }
} catch (GfeHttpResponseException e) { } catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 470) { if (e.getErrorCode() == 470) {
// This is the error you get when you try to resume a session that's not yours. // This is the error you get when you try to resume a session that's not yours.
// Because this is fairly common, we'll display a more detailed message. // Because this is fairly common, we'll display a more detailed message.
context.connListener.displayMessage("This session wasn't started by this device," + context.connListener.displayMessage("This session wasn't started by this device," +
" so it cannot be resumed. End streaming on the original " + " so it cannot be resumed. End streaming on the original " +
"device or the PC itself and try again. (Error code: "+e.getErrorCode()+")"); "device or the PC itself and try again. (Error code: "+e.getErrorCode()+")");
return false; return false;
} }
else if (e.getErrorCode() == 525) { else if (e.getErrorCode() == 525) {
context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " + context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " +
"quit the session and start streaming again."); "quit the session and start streaming again.");
return false; return false;
} else { } else {
throw e; throw e;
} }
} }
LimeLog.info("Resumed existing game session"); LimeLog.info("Resumed existing game session");
return true; return true;
} }
else { else {
return launchNotRunningApp(h, context); return launchNotRunningApp(h, context);
} }
} }
protected boolean quitAndLaunch(NvHTTP h, ConnectionContext context) throws IOException, protected boolean quitAndLaunch(NvHTTP h, ConnectionContext context) throws IOException,
XmlPullParserException { XmlPullParserException {
try { try {
if (!h.quitApp()) { if (!h.quitApp()) {
context.connListener.displayMessage("Failed to quit previous session! You must quit it manually"); context.connListener.displayMessage("Failed to quit previous session! You must quit it manually");
return false; return false;
} }
} catch (GfeHttpResponseException e) { } catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 599) { if (e.getErrorCode() == 599) {
context.connListener.displayMessage("This session wasn't started by this device," + context.connListener.displayMessage("This session wasn't started by this device," +
" so it cannot be quit. End streaming on the original " + " so it cannot be quit. End streaming on the original " +
"device or the PC itself. (Error code: "+e.getErrorCode()+")"); "device or the PC itself. (Error code: "+e.getErrorCode()+")");
return false; return false;
} }
else { else {
throw e; throw e;
} }
} }
return launchNotRunningApp(h, context); return launchNotRunningApp(h, context);
} }
private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context) private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context)
throws IOException, XmlPullParserException { throws IOException, XmlPullParserException {
// Launch the app since it's not running // Launch the app since it's not running
if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) { if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) {
context.connListener.displayMessage("Failed to launch application"); context.connListener.displayMessage("Failed to launch application");
return false; return false;
} }
LimeLog.info("Launched new game session"); LimeLog.info("Launched new game session");
return true; return true;
} }
public void start(final AudioRenderer audioRenderer, final VideoDecoderRenderer videoDecoderRenderer, final NvConnectionListener connectionListener) public void start(final AudioRenderer audioRenderer, final VideoDecoderRenderer videoDecoderRenderer, final NvConnectionListener connectionListener)
{ {
new Thread(new Runnable() { new Thread(new Runnable() {
public void run() { public void run() {
context.connListener = connectionListener; context.connListener = connectionListener;
context.videoCapabilities = videoDecoderRenderer.getCapabilities(); context.videoCapabilities = videoDecoderRenderer.getCapabilities();
String appName = context.streamConfig.getApp().getAppName(); String appName = context.streamConfig.getApp().getAppName();
context.serverAddress = host; context.serverAddress = host;
context.connListener.stageStarting(appName); context.connListener.stageStarting(appName);
try { try {
if (!startApp()) { if (!startApp()) {
context.connListener.stageFailed(appName, 0); context.connListener.stageFailed(appName, 0);
return; return;
} }
context.connListener.stageComplete(appName); context.connListener.stageComplete(appName);
} catch (XmlPullParserException | IOException e) { } catch (XmlPullParserException | IOException e) {
e.printStackTrace(); e.printStackTrace();
context.connListener.displayMessage(e.getMessage()); context.connListener.displayMessage(e.getMessage());
context.connListener.stageFailed(appName, 0); context.connListener.stageFailed(appName, 0);
return; return;
} }
ByteBuffer ib = ByteBuffer.allocate(16); ByteBuffer ib = ByteBuffer.allocate(16);
ib.putInt(context.riKeyId); ib.putInt(context.riKeyId);
// Acquire the connection semaphore to ensure we only have one // Acquire the connection semaphore to ensure we only have one
// connection going at once. // connection going at once.
try { try {
connectionAllowed.acquire(); connectionAllowed.acquire();
} catch (InterruptedException e) { } catch (InterruptedException e) {
context.connListener.displayMessage(e.getMessage()); context.connListener.displayMessage(e.getMessage());
context.connListener.stageFailed(appName, 0); context.connListener.stageFailed(appName, 0);
return; return;
} }
// Moonlight-core is not thread-safe with respect to connection start and stop, so // Moonlight-core is not thread-safe with respect to connection start and stop, so
// we must not invoke that functionality in parallel. // we must not invoke that functionality in parallel.
synchronized (MoonBridge.class) { synchronized (MoonBridge.class) {
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener); MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
int ret = MoonBridge.startConnection(context.serverAddress, int ret = MoonBridge.startConnection(context.serverAddress,
context.serverAppVersion, context.serverGfeVersion, context.serverAppVersion, context.serverGfeVersion,
context.negotiatedWidth, context.negotiatedHeight, context.negotiatedWidth, context.negotiatedHeight,
context.negotiatedFps, context.streamConfig.getBitrate(), context.negotiatedFps, context.streamConfig.getBitrate(),
context.streamConfig.getMaxPacketSize(), context.streamConfig.getMaxPacketSize(),
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration(), context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration(),
context.streamConfig.getHevcSupported(), context.streamConfig.getHevcSupported(),
context.negotiatedHdr, context.negotiatedHdr,
context.streamConfig.getHevcBitratePercentageMultiplier(), context.streamConfig.getHevcBitratePercentageMultiplier(),
context.streamConfig.getClientRefreshRateX100(), context.streamConfig.getClientRefreshRateX100(),
context.riKey.getEncoded(), ib.array(), context.riKey.getEncoded(), ib.array(),
context.videoCapabilities); context.videoCapabilities);
if (ret != 0) { if (ret != 0) {
// LiStartConnection() failed, so the caller is not expected // LiStartConnection() failed, so the caller is not expected
// to stop the connection themselves. We need to release their // to stop the connection themselves. We need to release their
// semaphore count for them. // semaphore count for them.
connectionAllowed.release(); connectionAllowed.release();
} }
} }
} }
}).start(); }).start();
} }
public void sendMouseMove(final short deltaX, final short deltaY) public void sendMouseMove(final short deltaX, final short deltaY)
{ {
if (!isMonkey) { if (!isMonkey) {
MoonBridge.sendMouseMove(deltaX, deltaY); MoonBridge.sendMouseMove(deltaX, deltaY);
} }
} }
public void sendMouseButtonDown(final byte mouseButton) public void sendMouseButtonDown(final byte mouseButton)
{ {
if (!isMonkey) { if (!isMonkey) {
MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton); MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton);
} }
} }
public void sendMouseButtonUp(final byte mouseButton) public void sendMouseButtonUp(final byte mouseButton)
{ {
if (!isMonkey) { if (!isMonkey) {
MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton); MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton);
} }
} }
public void sendControllerInput(final short controllerNumber, public void sendControllerInput(final short controllerNumber,
final short activeGamepadMask, final short buttonFlags, final short activeGamepadMask, final short buttonFlags,
final byte leftTrigger, final byte rightTrigger, final byte leftTrigger, final byte rightTrigger,
final short leftStickX, final short leftStickY, final short leftStickX, final short leftStickY,
final short rightStickX, final short rightStickY) final short rightStickX, final short rightStickY)
{ {
if (!isMonkey) { if (!isMonkey) {
MoonBridge.sendMultiControllerInput(controllerNumber, activeGamepadMask, buttonFlags, MoonBridge.sendMultiControllerInput(controllerNumber, activeGamepadMask, buttonFlags,
leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY); leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY);
} }
} }
public void sendControllerInput(final short buttonFlags, public void sendControllerInput(final short buttonFlags,
final byte leftTrigger, final byte rightTrigger, final byte leftTrigger, final byte rightTrigger,
final short leftStickX, final short leftStickY, final short leftStickX, final short leftStickY,
final short rightStickX, final short rightStickY) final short rightStickX, final short rightStickY)
{ {
if (!isMonkey) { if (!isMonkey) {
MoonBridge.sendControllerInput(buttonFlags, leftTrigger, rightTrigger, leftStickX, MoonBridge.sendControllerInput(buttonFlags, leftTrigger, rightTrigger, leftStickX,
leftStickY, rightStickX, rightStickY); leftStickY, rightStickX, rightStickY);
} }
} }
public void sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier) { public void sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier) {
if (!isMonkey) { if (!isMonkey) {
MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier); MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier);
} }
} }
public void sendMouseScroll(final byte scrollClicks) { public void sendMouseScroll(final byte scrollClicks) {
if (!isMonkey) { if (!isMonkey) {
MoonBridge.sendMouseScroll(scrollClicks); MoonBridge.sendMouseScroll(scrollClicks);
} }
} }
public static String findExternalAddressForMdns(String stunHostname, int stunPort) { public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort); return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
} }
} }
@@ -1,16 +1,16 @@
package com.limelight.nvstream; package com.limelight.nvstream;
public interface NvConnectionListener { public interface NvConnectionListener {
void stageStarting(String stage); void stageStarting(String stage);
void stageComplete(String stage); void stageComplete(String stage);
void stageFailed(String stage, long errorCode); void stageFailed(String stage, long errorCode);
void connectionStarted(); void connectionStarted();
void connectionTerminated(long errorCode); void connectionTerminated(long errorCode);
void connectionStatusUpdate(int connectionStatus); void connectionStatusUpdate(int connectionStatus);
void displayMessage(String message); void displayMessage(String message);
void displayTransientMessage(String message); void displayTransientMessage(String message);
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor); void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
} }
@@ -4,230 +4,230 @@ import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.jni.MoonBridge; import com.limelight.nvstream.jni.MoonBridge;
public class StreamConfiguration { public class StreamConfiguration {
public static final int INVALID_APP_ID = 0; public static final int INVALID_APP_ID = 0;
public static final int STREAM_CFG_LOCAL = 0; public static final int STREAM_CFG_LOCAL = 0;
public static final int STREAM_CFG_REMOTE = 1; public static final int STREAM_CFG_REMOTE = 1;
public static final int STREAM_CFG_AUTO = 2; public static final int STREAM_CFG_AUTO = 2;
private static final int CHANNEL_COUNT_STEREO = 2; private static final int CHANNEL_COUNT_STEREO = 2;
private static final int CHANNEL_COUNT_5_1 = 6; private static final int CHANNEL_COUNT_5_1 = 6;
private static final int CHANNEL_MASK_STEREO = 0x3; private static final int CHANNEL_MASK_STEREO = 0x3;
private static final int CHANNEL_MASK_5_1 = 0xFC; private static final int CHANNEL_MASK_5_1 = 0xFC;
private NvApp app; private NvApp app;
private int width, height; private int width, height;
private int refreshRate; private int refreshRate;
private int clientRefreshRateX100; private int clientRefreshRateX100;
private int bitrate; private int bitrate;
private boolean sops; private boolean sops;
private boolean enableAdaptiveResolution; private boolean enableAdaptiveResolution;
private boolean playLocalAudio; private boolean playLocalAudio;
private int maxPacketSize; private int maxPacketSize;
private int remote; private int remote;
private int audioChannelMask; private int audioChannelMask;
private int audioChannelCount; private int audioChannelCount;
private int audioConfiguration; private int audioConfiguration;
private boolean supportsHevc; private boolean supportsHevc;
private int hevcBitratePercentageMultiplier; private int hevcBitratePercentageMultiplier;
private boolean enableHdr; private boolean enableHdr;
private int attachedGamepadMask; private int attachedGamepadMask;
public static class Builder { public static class Builder {
private StreamConfiguration config = new StreamConfiguration(); private StreamConfiguration config = new StreamConfiguration();
public StreamConfiguration.Builder setApp(NvApp app) { public StreamConfiguration.Builder setApp(NvApp app) {
config.app = app; config.app = app;
return this; return this;
} }
public StreamConfiguration.Builder setRemoteConfiguration(int remote) { public StreamConfiguration.Builder setRemoteConfiguration(int remote) {
config.remote = remote; config.remote = remote;
return this; return this;
} }
public StreamConfiguration.Builder setResolution(int width, int height) { public StreamConfiguration.Builder setResolution(int width, int height) {
config.width = width; config.width = width;
config.height = height; config.height = height;
return this; return this;
} }
public StreamConfiguration.Builder setRefreshRate(int refreshRate) { public StreamConfiguration.Builder setRefreshRate(int refreshRate) {
config.refreshRate = refreshRate; config.refreshRate = refreshRate;
return this; return this;
} }
public StreamConfiguration.Builder setBitrate(int bitrate) { public StreamConfiguration.Builder setBitrate(int bitrate) {
config.bitrate = bitrate; config.bitrate = bitrate;
return this; return this;
} }
public StreamConfiguration.Builder setEnableSops(boolean enable) { public StreamConfiguration.Builder setEnableSops(boolean enable) {
config.sops = enable; config.sops = enable;
return this; return this;
} }
public StreamConfiguration.Builder enableAdaptiveResolution(boolean enable) { public StreamConfiguration.Builder enableAdaptiveResolution(boolean enable) {
config.enableAdaptiveResolution = enable; config.enableAdaptiveResolution = enable;
return this; return this;
} }
public StreamConfiguration.Builder enableLocalAudioPlayback(boolean enable) { public StreamConfiguration.Builder enableLocalAudioPlayback(boolean enable) {
config.playLocalAudio = enable; config.playLocalAudio = enable;
return this; return this;
} }
public StreamConfiguration.Builder setMaxPacketSize(int maxPacketSize) { public StreamConfiguration.Builder setMaxPacketSize(int maxPacketSize) {
config.maxPacketSize = maxPacketSize; config.maxPacketSize = maxPacketSize;
return this; return this;
} }
public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) { public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) {
config.hevcBitratePercentageMultiplier = multiplier; config.hevcBitratePercentageMultiplier = multiplier;
return this; return this;
} }
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) { public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
config.enableHdr = enableHdr; config.enableHdr = enableHdr;
return this; return this;
} }
public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) { public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) {
config.attachedGamepadMask = attachedGamepadMask; config.attachedGamepadMask = attachedGamepadMask;
return this; return this;
} }
public StreamConfiguration.Builder setAttachedGamepadMaskByCount(int gamepadCount) { public StreamConfiguration.Builder setAttachedGamepadMaskByCount(int gamepadCount) {
config.attachedGamepadMask = 0; config.attachedGamepadMask = 0;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (gamepadCount > i) { if (gamepadCount > i) {
config.attachedGamepadMask |= 1 << i; config.attachedGamepadMask |= 1 << i;
} }
} }
return this; return this;
} }
public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) { public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) {
config.clientRefreshRateX100 = refreshRateX100; config.clientRefreshRateX100 = refreshRateX100;
return this; return this;
} }
public StreamConfiguration.Builder setAudioConfiguration(int audioConfig) { public StreamConfiguration.Builder setAudioConfiguration(int audioConfig) {
if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_STEREO) { if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_STEREO) {
config.audioChannelCount = CHANNEL_COUNT_STEREO; config.audioChannelCount = CHANNEL_COUNT_STEREO;
config.audioChannelMask = CHANNEL_MASK_STEREO; config.audioChannelMask = CHANNEL_MASK_STEREO;
} }
else if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_51_SURROUND) { else if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_51_SURROUND) {
config.audioChannelCount = CHANNEL_COUNT_5_1; config.audioChannelCount = CHANNEL_COUNT_5_1;
config.audioChannelMask = CHANNEL_MASK_5_1; config.audioChannelMask = CHANNEL_MASK_5_1;
} }
else { else {
throw new IllegalArgumentException("Invalid audio configuration"); throw new IllegalArgumentException("Invalid audio configuration");
} }
config.audioConfiguration = audioConfig; config.audioConfiguration = audioConfig;
return this; return this;
} }
public StreamConfiguration.Builder setHevcSupported(boolean supportsHevc) { public StreamConfiguration.Builder setHevcSupported(boolean supportsHevc) {
config.supportsHevc = supportsHevc; config.supportsHevc = supportsHevc;
return this; return this;
} }
public StreamConfiguration build() { public StreamConfiguration build() {
return config; return config;
} }
} }
private StreamConfiguration() { private StreamConfiguration() {
// Set default attributes // Set default attributes
this.app = new NvApp("Steam"); this.app = new NvApp("Steam");
this.width = 1280; this.width = 1280;
this.height = 720; this.height = 720;
this.refreshRate = 60; this.refreshRate = 60;
this.bitrate = 10000; this.bitrate = 10000;
this.maxPacketSize = 1024; this.maxPacketSize = 1024;
this.remote = STREAM_CFG_AUTO; this.remote = STREAM_CFG_AUTO;
this.sops = true; this.sops = true;
this.enableAdaptiveResolution = false; this.enableAdaptiveResolution = false;
this.audioChannelCount = CHANNEL_COUNT_STEREO; this.audioChannelCount = CHANNEL_COUNT_STEREO;
this.audioChannelMask = CHANNEL_MASK_STEREO; this.audioChannelMask = CHANNEL_MASK_STEREO;
this.supportsHevc = false; this.supportsHevc = false;
this.enableHdr = false; this.enableHdr = false;
this.attachedGamepadMask = 0; this.attachedGamepadMask = 0;
} }
public int getWidth() { public int getWidth() {
return width; return width;
} }
public int getHeight() { public int getHeight() {
return height; return height;
} }
public int getRefreshRate() { public int getRefreshRate() {
return refreshRate; return refreshRate;
} }
public int getBitrate() { public int getBitrate() {
return bitrate; return bitrate;
} }
public int getMaxPacketSize() { public int getMaxPacketSize() {
return maxPacketSize; return maxPacketSize;
} }
public NvApp getApp() { public NvApp getApp() {
return app; return app;
} }
public boolean getSops() { public boolean getSops() {
return sops; return sops;
} }
public boolean getAdaptiveResolutionEnabled() { public boolean getAdaptiveResolutionEnabled() {
return enableAdaptiveResolution; return enableAdaptiveResolution;
} }
public boolean getPlayLocalAudio() { public boolean getPlayLocalAudio() {
return playLocalAudio; return playLocalAudio;
} }
public int getRemote() { public int getRemote() {
return remote; return remote;
} }
public int getAudioChannelCount() { public int getAudioChannelCount() {
return audioChannelCount; return audioChannelCount;
} }
public int getAudioChannelMask() { public int getAudioChannelMask() {
return audioChannelMask; return audioChannelMask;
} }
public int getAudioConfiguration() { public int getAudioConfiguration() {
return audioConfiguration; return audioConfiguration;
} }
public boolean getHevcSupported() { public boolean getHevcSupported() {
return supportsHevc; return supportsHevc;
} }
public int getHevcBitratePercentageMultiplier() { public int getHevcBitratePercentageMultiplier() {
return hevcBitratePercentageMultiplier; return hevcBitratePercentageMultiplier;
} }
public boolean getEnableHdr() { public boolean getEnableHdr() {
return enableHdr; return enableHdr;
} }
public int getAttachedGamepadMask() { public int getAttachedGamepadMask() {
return attachedGamepadMask; return attachedGamepadMask;
} }
public int getClientRefreshRateX100() { public int getClientRefreshRateX100() {
return clientRefreshRateX100; return clientRefreshRateX100;
} }
} }
@@ -1,57 +1,57 @@
package com.limelight.nvstream.av; package com.limelight.nvstream.av;
public class ByteBufferDescriptor { public class ByteBufferDescriptor {
public byte[] data; public byte[] data;
public int offset; public int offset;
public int length; public int length;
public ByteBufferDescriptor nextDescriptor; public ByteBufferDescriptor nextDescriptor;
public ByteBufferDescriptor(byte[] data, int offset, int length) public ByteBufferDescriptor(byte[] data, int offset, int length)
{ {
this.data = data; this.data = data;
this.offset = offset; this.offset = offset;
this.length = length; this.length = length;
} }
public ByteBufferDescriptor(ByteBufferDescriptor desc) public ByteBufferDescriptor(ByteBufferDescriptor desc)
{ {
this.data = desc.data; this.data = desc.data;
this.offset = desc.offset; this.offset = desc.offset;
this.length = desc.length; this.length = desc.length;
} }
public void reinitialize(byte[] data, int offset, int length) public void reinitialize(byte[] data, int offset, int length)
{ {
this.data = data; this.data = data;
this.offset = offset; this.offset = offset;
this.length = length; this.length = length;
this.nextDescriptor = null; this.nextDescriptor = null;
} }
public void print() public void print()
{ {
print(offset, length); print(offset, length);
} }
public void print(int length) public void print(int length)
{ {
print(this.offset, length); print(this.offset, length);
} }
public void print(int offset, int length) public void print(int offset, int length)
{ {
for (int i = offset; i < offset+length;) { for (int i = offset; i < offset+length;) {
if (i + 8 <= offset+length) { if (i + 8 <= offset+length) {
System.out.printf("%x: %02x %02x %02x %02x %02x %02x %02x %02x\n", i, System.out.printf("%x: %02x %02x %02x %02x %02x %02x %02x %02x\n", i,
data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]); data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]);
i += 8; i += 8;
} }
else { else {
System.out.printf("%x: %02x \n", i, data[i]); System.out.printf("%x: %02x \n", i, data[i]);
i++; i++;
} }
} }
System.out.println(); System.out.println();
} }
} }
@@ -1,13 +1,13 @@
package com.limelight.nvstream.av.audio; package com.limelight.nvstream.av.audio;
public interface AudioRenderer { public interface AudioRenderer {
int setup(int audioConfiguration); int setup(int audioConfiguration, int sampleRate, int samplesPerFrame);
void start(); void start();
void stop(); void stop();
void playDecodedAudio(short[] audioData); void playDecodedAudio(short[] audioData);
void cleanup(); void cleanup();
} }
@@ -1,18 +1,18 @@
package com.limelight.nvstream.av.video; package com.limelight.nvstream.av.video;
public abstract class VideoDecoderRenderer { public abstract class VideoDecoderRenderer {
public abstract int setup(int format, int width, int height, int redrawRate); public abstract int setup(int format, int width, int height, int redrawRate);
public abstract void start(); public abstract void start();
public abstract void stop(); public abstract void stop();
// This is called once for each frame-start NALU. This means it will be called several times // This is called once for each frame-start NALU. This means it will be called several times
// for an IDR frame which contains several parameter sets and the I-frame data. // for an IDR frame which contains several parameter sets and the I-frame data.
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType, public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
int frameNumber, long receiveTimeMs); int frameNumber, long receiveTimeMs);
public abstract void cleanup(); public abstract void cleanup();
public abstract int getCapabilities(); public abstract int getCapabilities();
} }
@@ -4,82 +4,82 @@ import java.security.cert.X509Certificate;
public class ComputerDetails { public class ComputerDetails {
public enum State { public enum State {
ONLINE, OFFLINE, UNKNOWN ONLINE, OFFLINE, UNKNOWN
} }
// Persistent attributes // Persistent attributes
public String uuid; public String uuid;
public String name; public String name;
public String localAddress; public String localAddress;
public String remoteAddress; public String remoteAddress;
public String manualAddress; public String manualAddress;
public String ipv6Address; public String ipv6Address;
public String macAddress; public String macAddress;
public X509Certificate serverCert; public X509Certificate serverCert;
// Transient attributes // Transient attributes
public State state; public State state;
public String activeAddress; public String activeAddress;
public PairingManager.PairState pairState; public PairingManager.PairState pairState;
public int runningGameId; public int runningGameId;
public String rawAppList; public String rawAppList;
public ComputerDetails() { public ComputerDetails() {
// Use defaults // Use defaults
state = State.UNKNOWN; state = State.UNKNOWN;
} }
public ComputerDetails(ComputerDetails details) { public ComputerDetails(ComputerDetails details) {
// Copy details from the other computer // Copy details from the other computer
update(details); update(details);
} }
public void update(ComputerDetails details) { public void update(ComputerDetails details) {
this.state = details.state; this.state = details.state;
this.name = details.name; this.name = details.name;
this.uuid = details.uuid; this.uuid = details.uuid;
if (details.activeAddress != null) { if (details.activeAddress != null) {
this.activeAddress = details.activeAddress; this.activeAddress = details.activeAddress;
} }
// We can get IPv4 loopback addresses with GS IPv6 Forwarder // We can get IPv4 loopback addresses with GS IPv6 Forwarder
if (details.localAddress != null && !details.localAddress.startsWith("127.")) { if (details.localAddress != null && !details.localAddress.startsWith("127.")) {
this.localAddress = details.localAddress; this.localAddress = details.localAddress;
} }
if (details.remoteAddress != null) { if (details.remoteAddress != null) {
this.remoteAddress = details.remoteAddress; this.remoteAddress = details.remoteAddress;
} }
if (details.manualAddress != null) { if (details.manualAddress != null) {
this.manualAddress = details.manualAddress; this.manualAddress = details.manualAddress;
} }
if (details.ipv6Address != null) { if (details.ipv6Address != null) {
this.ipv6Address = details.ipv6Address; this.ipv6Address = details.ipv6Address;
} }
if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) { if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) {
this.macAddress = details.macAddress; this.macAddress = details.macAddress;
} }
if (details.serverCert != null) { if (details.serverCert != null) {
this.serverCert = details.serverCert; this.serverCert = details.serverCert;
} }
this.pairState = details.pairState; this.pairState = details.pairState;
this.runningGameId = details.runningGameId; this.runningGameId = details.runningGameId;
this.rawAppList = details.rawAppList; this.rawAppList = details.rawAppList;
} }
@Override @Override
public String toString() { public String toString() {
StringBuilder str = new StringBuilder(); StringBuilder str = new StringBuilder();
str.append("State: ").append(state).append("\n"); str.append("State: ").append(state).append("\n");
str.append("Active Address: ").append(activeAddress).append("\n"); str.append("Active Address: ").append(activeAddress).append("\n");
str.append("Name: ").append(name).append("\n"); str.append("Name: ").append(name).append("\n");
str.append("UUID: ").append(uuid).append("\n"); str.append("UUID: ").append(uuid).append("\n");
str.append("Local Address: ").append(localAddress).append("\n"); str.append("Local Address: ").append(localAddress).append("\n");
str.append("Remote Address: ").append(remoteAddress).append("\n"); str.append("Remote Address: ").append(remoteAddress).append("\n");
str.append("IPv6 Address: ").append(ipv6Address).append("\n"); str.append("IPv6 Address: ").append(ipv6Address).append("\n");
str.append("Manual Address: ").append(manualAddress).append("\n"); str.append("Manual Address: ").append(manualAddress).append("\n");
str.append("MAC Address: ").append(macAddress).append("\n"); str.append("MAC Address: ").append(macAddress).append("\n");
str.append("Pair State: ").append(pairState).append("\n"); str.append("Pair State: ").append(pairState).append("\n");
str.append("Running Game ID: ").append(runningGameId).append("\n"); str.append("Running Game ID: ").append(runningGameId).append("\n");
return str.toString(); return str.toString();
} }
} }
@@ -3,26 +3,26 @@ package com.limelight.nvstream.http;
import java.io.IOException; import java.io.IOException;
public class GfeHttpResponseException extends IOException { public class GfeHttpResponseException extends IOException {
private static final long serialVersionUID = 1543508830807804222L; private static final long serialVersionUID = 1543508830807804222L;
private int errorCode; private int errorCode;
private String errorMsg; private String errorMsg;
public GfeHttpResponseException(int errorCode, String errorMsg) { public GfeHttpResponseException(int errorCode, String errorMsg) {
this.errorCode = errorCode; this.errorCode = errorCode;
this.errorMsg = errorMsg; this.errorMsg = errorMsg;
} }
public int getErrorCode() { public int getErrorCode() {
return errorCode; return errorCode;
} }
public String getErrorMessage() { public String getErrorMessage() {
return errorMsg; return errorMsg;
} }
@Override @Override
public String getMessage() { public String getMessage() {
return "GeForce Experience returned error: "+errorMsg+" (Error code: "+errorCode+")"; return "GeForce Experience returned error: "+errorMsg+" (Error code: "+errorCode+")";
} }
} }
@@ -4,8 +4,8 @@ import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPrivateKey;
public interface LimelightCryptoProvider { public interface LimelightCryptoProvider {
X509Certificate getClientCertificate(); X509Certificate getClientCertificate();
RSAPrivateKey getClientPrivateKey(); RSAPrivateKey getClientPrivateKey();
byte[] getPemEncodedClientCertificate(); byte[] getPemEncodedClientCertificate();
String encodeBase64String(byte[] data); String encodeBase64String(byte[] data);
} }
@@ -3,59 +3,59 @@ package com.limelight.nvstream.http;
import com.limelight.LimeLog; import com.limelight.LimeLog;
public class NvApp { public class NvApp {
private String appName = ""; private String appName = "";
private int appId; private int appId;
private boolean initialized; private boolean initialized;
private boolean hdrSupported; private boolean hdrSupported;
public NvApp() {} public NvApp() {}
public NvApp(String appName) { public NvApp(String appName) {
this.appName = appName; this.appName = appName;
} }
public NvApp(String appName, int appId, boolean hdrSupported) { public NvApp(String appName, int appId, boolean hdrSupported) {
this.appName = appName; this.appName = appName;
this.appId = appId; this.appId = appId;
this.hdrSupported = hdrSupported; this.hdrSupported = hdrSupported;
this.initialized = true; this.initialized = true;
} }
public void setAppName(String appName) { public void setAppName(String appName) {
this.appName = appName; this.appName = appName;
} }
public void setAppId(String appId) { public void setAppId(String appId) {
try { try {
this.appId = Integer.parseInt(appId); this.appId = Integer.parseInt(appId);
this.initialized = true; this.initialized = true;
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
LimeLog.warning("Malformed app ID: "+appId); LimeLog.warning("Malformed app ID: "+appId);
} }
} }
public void setAppId(int appId) { public void setAppId(int appId) {
this.appId = appId; this.appId = appId;
this.initialized = true; this.initialized = true;
} }
public void setHdrSupported(boolean hdrSupported) { public void setHdrSupported(boolean hdrSupported) {
this.hdrSupported = hdrSupported; this.hdrSupported = hdrSupported;
} }
public String getAppName() { public String getAppName() {
return this.appName; return this.appName;
} }
public int getAppId() { public int getAppId() {
return this.appId; return this.appId;
} }
public boolean isHdrSupported() { public boolean isHdrSupported() {
return this.hdrSupported; return this.hdrSupported;
} }
public boolean isInitialized() { public boolean isInitialized() {
return this.initialized; return this.initialized;
} }
} }
File diff suppressed because it is too large Load Diff
@@ -18,323 +18,324 @@ import java.util.Random;
public class PairingManager { public class PairingManager {
private NvHTTP http; private NvHTTP http;
private PrivateKey pk; private PrivateKey pk;
private X509Certificate cert; private X509Certificate cert;
private SecretKey aesKey; private SecretKey aesKey;
private byte[] pemCertBytes; private byte[] pemCertBytes;
private X509Certificate serverCert; private X509Certificate serverCert;
public enum PairState { public enum PairState {
NOT_PAIRED, NOT_PAIRED,
PAIRED, PAIRED,
PIN_WRONG, PIN_WRONG,
FAILED, FAILED,
ALREADY_IN_PROGRESS ALREADY_IN_PROGRESS
} }
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) { public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
this.http = http; this.http = http;
this.cert = cryptoProvider.getClientCertificate(); this.cert = cryptoProvider.getClientCertificate();
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate(); this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
this.pk = cryptoProvider.getClientPrivateKey(); this.pk = cryptoProvider.getClientPrivateKey();
} }
final private static char[] hexArray = "0123456789ABCDEF".toCharArray(); final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
private static String bytesToHex(byte[] bytes) { private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2]; char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) { for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF; int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F]; hexChars[j * 2 + 1] = hexArray[v & 0x0F];
} }
return new String(hexChars); return new String(hexChars);
} }
private static byte[] hexToBytes(String s) { private static byte[] hexToBytes(String s) {
int len = s.length(); int len = s.length();
byte[] data = new byte[len / 2]; byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) { for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16)); + Character.digit(s.charAt(i+1), 16));
} }
return data; return data;
} }
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
{ {
String certText = NvHTTP.getXmlString(text, "plaincert"); String certText = NvHTTP.getXmlString(text, "plaincert");
if (certText != null) { if (certText != null) {
byte[] certBytes = hexToBytes(certText); byte[] certBytes = hexToBytes(certText);
try { try {
CertificateFactory cf = CertificateFactory.getInstance("X.509"); CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes)); return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
} catch (CertificateException e) { } catch (CertificateException e) {
e.printStackTrace(); e.printStackTrace();
return null; return null;
} }
} }
else { else {
return null; return null;
} }
} }
private byte[] generateRandomBytes(int length) private byte[] generateRandomBytes(int length)
{ {
byte[] rand = new byte[length]; byte[] rand = new byte[length];
new SecureRandom().nextBytes(rand); new SecureRandom().nextBytes(rand);
return rand; return rand;
} }
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException { private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
byte[] saltedPin = new byte[salt.length + pin.length()]; byte[] saltedPin = new byte[salt.length + pin.length()];
System.arraycopy(salt, 0, saltedPin, 0, salt.length); System.arraycopy(salt, 0, saltedPin, 0, salt.length);
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length()); System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
return saltedPin; return saltedPin;
} }
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) { private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
try { try {
Signature sig = Signature.getInstance("SHA256withRSA"); Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(cert.getPublicKey()); sig.initVerify(cert.getPublicKey());
sig.update(data); sig.update(data);
return sig.verify(signature); return sig.verify(signature);
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) { } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
e.printStackTrace(); e.printStackTrace();
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private static byte[] signData(byte[] data, PrivateKey key) { private static byte[] signData(byte[] data, PrivateKey key) {
try { try {
Signature sig = Signature.getInstance("SHA256withRSA"); Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(key); sig.initSign(key);
sig.update(data); sig.update(data);
byte[] signature = new byte[256]; byte[] signature = new byte[256];
sig.sign(signature, 0, signature.length); sig.sign(signature, 0, signature.length);
return signature; return signature;
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) { } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
e.printStackTrace(); e.printStackTrace();
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) { private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) {
try { try {
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16; int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize); byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
byte[] fullDecrypted = new byte[blockRoundedSize]; byte[] fullDecrypted = new byte[blockRoundedSize];
cipher.init(Cipher.DECRYPT_MODE, secretKey); cipher.init(Cipher.DECRYPT_MODE, secretKey);
cipher.doFinal(blockRoundedEncrypted, 0, cipher.doFinal(blockRoundedEncrypted, 0,
blockRoundedSize, fullDecrypted); blockRoundedSize, fullDecrypted);
return fullDecrypted; return fullDecrypted;
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
e.printStackTrace(); e.printStackTrace();
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private static byte[] encryptAes(byte[] data, SecretKey secretKey) { private static byte[] encryptAes(byte[] data, SecretKey secretKey) {
try { try {
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
int blockRoundedSize = ((data.length + 15) / 16) * 16; int blockRoundedSize = ((data.length + 15) / 16) * 16;
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize); byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
cipher.init(Cipher.ENCRYPT_MODE, secretKey); cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(blockRoundedData); return cipher.doFinal(blockRoundedData);
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
e.printStackTrace(); e.printStackTrace();
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) { private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16); byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
return new SecretKeySpec(aesTruncated, "AES"); return new SecretKeySpec(aesTruncated, "AES");
} }
private static byte[] concatBytes(byte[] a, byte[] b) { private static byte[] concatBytes(byte[] a, byte[] b) {
byte[] c = new byte[a.length + b.length]; byte[] c = new byte[a.length + b.length];
System.arraycopy(a, 0, c, 0, a.length); System.arraycopy(a, 0, c, 0, a.length);
System.arraycopy(b, 0, c, a.length, b.length); System.arraycopy(b, 0, c, a.length, b.length);
return c; return c;
} }
public static String generatePinString() { public static String generatePinString() {
Random r = new Random(); Random r = new Random();
return String.format((Locale)null, "%d%d%d%d", return String.format((Locale)null, "%d%d%d%d",
r.nextInt(10), r.nextInt(10), r.nextInt(10), r.nextInt(10),
r.nextInt(10), r.nextInt(10)); r.nextInt(10), r.nextInt(10));
} }
public X509Certificate getPairedCert() { public X509Certificate getPairedCert() {
return serverCert; return serverCert;
} }
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException { public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
PairingHashAlgorithm hashAlgo; PairingHashAlgorithm hashAlgo;
int serverMajorVersion = http.getServerMajorVersion(serverInfo); int serverMajorVersion = http.getServerMajorVersion(serverInfo);
LimeLog.info("Pairing with server generation: "+serverMajorVersion); LimeLog.info("Pairing with server generation: "+serverMajorVersion);
if (serverMajorVersion >= 7) { if (serverMajorVersion >= 7) {
// Gen 7+ uses SHA-256 hashing // Gen 7+ uses SHA-256 hashing
hashAlgo = new Sha256PairingHash(); hashAlgo = new Sha256PairingHash();
} }
else { else {
// Prior to Gen 7, SHA-1 is used // Prior to Gen 7, SHA-1 is used
hashAlgo = new Sha1PairingHash(); hashAlgo = new Sha1PairingHash();
} }
// Generate a salt for hashing the PIN // Generate a salt for hashing the PIN
byte[] salt = generateRandomBytes(16); byte[] salt = generateRandomBytes(16);
// Combine the salt and pin, then create an AES key from them // Combine the salt and pin, then create an AES key from them
byte[] saltAndPin = saltPin(salt, pin); byte[] saltAndPin = saltPin(salt, pin);
aesKey = generateAesKey(hashAlgo, saltAndPin); aesKey = generateAesKey(hashAlgo, saltAndPin);
// Send the salt and get the server cert. This doesn't have a read timeout // Send the salt and get the server cert. This doesn't have a read timeout
// because the user must enter the PIN before the server responds // because the user must enter the PIN before the server responds
String getCert = http.openHttpConnectionToString(http.baseUrlHttp + String getCert = http.openHttpConnectionToString(http.baseUrlHttp +
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+ "/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes), bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
false); false);
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) { if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
return PairState.FAILED; return PairState.FAILED;
} }
// Save this cert for retrieval later // Save this cert for retrieval later
serverCert = extractPlainCert(getCert); serverCert = extractPlainCert(getCert);
if (serverCert == null) { if (serverCert == null) {
// Attempting to pair while another device is pairing will cause GFE // Attempting to pair while another device is pairing will cause GFE
// to give an empty cert in the response. // to give an empty cert in the response.
return PairState.ALREADY_IN_PROGRESS; http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
} return PairState.ALREADY_IN_PROGRESS;
}
// Require this cert for TLS to this host // Require this cert for TLS to this host
http.setServerCert(serverCert); http.setServerCert(serverCert);
// Generate a random challenge and encrypt it with our AES key // Generate a random challenge and encrypt it with our AES key
byte[] randomChallenge = generateRandomBytes(16); byte[] randomChallenge = generateRandomBytes(16);
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey); byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
// Send the encrypted challenge to the server // Send the encrypted challenge to the server
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp + String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge), "/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
true); true);
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) { if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true); http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
return PairState.FAILED; return PairState.FAILED;
} }
// Decode the server's response and subsequent challenge // Decode the server's response and subsequent challenge
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse")); byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey); byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength()); byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16); byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge // Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
byte[] clientSecret = generateRandomBytes(16); byte[] clientSecret = generateRandomBytes(16);
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret)); byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey); byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp + String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted), "/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
true); true);
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) { if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true); http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
return PairState.FAILED; return PairState.FAILED;
} }
// Get the server's signed secret // Get the server's signed secret
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret")); byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16); byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272); byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
// Ensure the authenticity of the data // Ensure the authenticity of the data
if (!verifySignature(serverSecret, serverSignature, serverCert)) { if (!verifySignature(serverSecret, serverSignature, serverCert)) {
// Cancel the pairing process // Cancel the pairing process
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true); http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
// Looks like a MITM // Looks like a MITM
return PairState.FAILED; return PairState.FAILED;
} }
// Ensure the server challenge matched what we expected (aka the PIN was correct) // Ensure the server challenge matched what we expected (aka the PIN was correct)
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret)); byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) { if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
// Cancel the pairing process // Cancel the pairing process
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true); http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
// Probably got the wrong PIN // Probably got the wrong PIN
return PairState.PIN_WRONG; return PairState.PIN_WRONG;
} }
// Send the server our signed secret // Send the server our signed secret
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk)); byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp + String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret), "/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
true); true);
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) { if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true); http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
return PairState.FAILED; return PairState.FAILED;
} }
// Do the initial challenge (seems neccessary for us to show as paired) // Do the initial challenge (seems neccessary for us to show as paired)
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps + String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true); "/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) { if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true); http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
return PairState.FAILED; return PairState.FAILED;
} }
return PairState.PAIRED; return PairState.PAIRED;
} }
private interface PairingHashAlgorithm { private interface PairingHashAlgorithm {
int getHashLength(); int getHashLength();
byte[] hashData(byte[] data); byte[] hashData(byte[] data);
} }
private static class Sha1PairingHash implements PairingHashAlgorithm { private static class Sha1PairingHash implements PairingHashAlgorithm {
public int getHashLength() { public int getHashLength() {
return 20; return 20;
} }
public byte[] hashData(byte[] data) { public byte[] hashData(byte[] data) {
try { try {
MessageDigest md = MessageDigest.getInstance("SHA-1"); MessageDigest md = MessageDigest.getInstance("SHA-1");
return md.digest(data); return md.digest(data);
} }
catch (NoSuchAlgorithmException e) { catch (NoSuchAlgorithmException e) {
// Shouldn't ever happen // Shouldn't ever happen
e.printStackTrace(); e.printStackTrace();
return null; return null;
} }
} }
} }
private static class Sha256PairingHash implements PairingHashAlgorithm { private static class Sha256PairingHash implements PairingHashAlgorithm {
public int getHashLength() { public int getHashLength() {
return 32; return 32;
} }
public byte[] hashData(byte[] data) { public byte[] hashData(byte[] data) {
try { try {
MessageDigest md = MessageDigest.getInstance("SHA-256"); MessageDigest md = MessageDigest.getInstance("SHA-256");
return md.digest(data); return md.digest(data);
} }
catch (NoSuchAlgorithmException e) { catch (NoSuchAlgorithmException e) {
// Shouldn't ever happen // Shouldn't ever happen
e.printStackTrace(); e.printStackTrace();
return null; return null;
} }
} }
} }
} }
@@ -1,19 +1,19 @@
package com.limelight.nvstream.input; package com.limelight.nvstream.input;
public class ControllerPacket { public class ControllerPacket {
public static final short A_FLAG = 0x1000; public static final short A_FLAG = 0x1000;
public static final short B_FLAG = 0x2000; public static final short B_FLAG = 0x2000;
public static final short X_FLAG = 0x4000; public static final short X_FLAG = 0x4000;
public static final short Y_FLAG = (short)0x8000; public static final short Y_FLAG = (short)0x8000;
public static final short UP_FLAG = 0x0001; public static final short UP_FLAG = 0x0001;
public static final short DOWN_FLAG = 0x0002; public static final short DOWN_FLAG = 0x0002;
public static final short LEFT_FLAG = 0x0004; public static final short LEFT_FLAG = 0x0004;
public static final short RIGHT_FLAG = 0x0008; public static final short RIGHT_FLAG = 0x0008;
public static final short LB_FLAG = 0x0100; public static final short LB_FLAG = 0x0100;
public static final short RB_FLAG = 0x0200; public static final short RB_FLAG = 0x0200;
public static final short PLAY_FLAG = 0x0010; public static final short PLAY_FLAG = 0x0010;
public static final short BACK_FLAG = 0x0020; public static final short BACK_FLAG = 0x0020;
public static final short LS_CLK_FLAG = 0x0040; public static final short LS_CLK_FLAG = 0x0040;
public static final short RS_CLK_FLAG = 0x0080; public static final short RS_CLK_FLAG = 0x0080;
public static final short SPECIAL_BUTTON_FLAG = 0x0400; public static final short SPECIAL_BUTTON_FLAG = 0x0400;
} }
@@ -1,10 +1,10 @@
package com.limelight.nvstream.input; package com.limelight.nvstream.input;
public class KeyboardPacket { public class KeyboardPacket {
public static final byte KEY_DOWN = 0x03; public static final byte KEY_DOWN = 0x03;
public static final byte KEY_UP = 0x04; public static final byte KEY_UP = 0x04;
public static final byte MODIFIER_SHIFT = 0x01; public static final byte MODIFIER_SHIFT = 0x01;
public static final byte MODIFIER_CTRL = 0x02; public static final byte MODIFIER_CTRL = 0x02;
public static final byte MODIFIER_ALT = 0x04; public static final byte MODIFIER_ALT = 0x04;
} }
@@ -1,12 +1,12 @@
package com.limelight.nvstream.input; package com.limelight.nvstream.input;
public class MouseButtonPacket { public class MouseButtonPacket {
public static final byte PRESS_EVENT = 0x07; public static final byte PRESS_EVENT = 0x07;
public static final byte RELEASE_EVENT = 0x08; public static final byte RELEASE_EVENT = 0x08;
public static final byte BUTTON_LEFT = 0x01; public static final byte BUTTON_LEFT = 0x01;
public static final byte BUTTON_MIDDLE = 0x02; public static final byte BUTTON_MIDDLE = 0x02;
public static final byte BUTTON_RIGHT = 0x03; public static final byte BUTTON_RIGHT = 0x03;
public static final byte BUTTON_X1 = 0x04; public static final byte BUTTON_X1 = 0x04;
public static final byte BUTTON_X2 = 0x05; public static final byte BUTTON_X2 = 0x05;
} }
@@ -84,9 +84,9 @@ public class MoonBridge {
} }
} }
public static int bridgeArInit(int audioConfiguration) { public static int bridgeArInit(int audioConfiguration, int sampleRate, int samplesPerFrame) {
if (audioRenderer != null) { if (audioRenderer != null) {
return audioRenderer.setup(audioConfiguration); return audioRenderer.setup(audioConfiguration, sampleRate, samplesPerFrame);
} }
else { else {
return -1; return -1;
@@ -208,7 +208,7 @@ public class MoonBridge {
public static native String findExternalAddressIP4(String stunHostName, int stunPort); public static native String findExternalAddressIP4(String stunHostName, int stunPort);
public static native int getPendingAudioFrames(); public static native int getPendingAudioDuration();
public static native int getPendingVideoFrames(); public static native int getPendingVideoFrames();
@@ -4,62 +4,62 @@ import java.net.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
public class MdnsComputer { public class MdnsComputer {
private InetAddress localAddr; private InetAddress localAddr;
private Inet6Address v6Addr; private Inet6Address v6Addr;
private String name; private String name;
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr) { public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr) {
this.name = name; this.name = name;
this.localAddr = localAddress; this.localAddr = localAddress;
this.v6Addr = v6Addr; this.v6Addr = v6Addr;
} }
public String getName() { public String getName() {
return name; return name;
} }
public InetAddress getLocalAddress() { public InetAddress getLocalAddress() {
return localAddr; return localAddr;
} }
public Inet6Address getIpv6Address() { public Inet6Address getIpv6Address() {
return v6Addr; return v6Addr;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return name.hashCode(); return name.hashCode();
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o instanceof MdnsComputer) { if (o instanceof MdnsComputer) {
MdnsComputer other = (MdnsComputer)o; MdnsComputer other = (MdnsComputer)o;
if (!other.name.equals(name)) { if (!other.name.equals(name)) {
return false; return false;
} }
if ((other.localAddr != null && localAddr == null) || if ((other.localAddr != null && localAddr == null) ||
(other.localAddr == null && localAddr != null) || (other.localAddr == null && localAddr != null) ||
(other.localAddr != null && !other.localAddr.equals(localAddr))) { (other.localAddr != null && !other.localAddr.equals(localAddr))) {
return false; return false;
} }
if ((other.v6Addr != null && v6Addr == null) || if ((other.v6Addr != null && v6Addr == null) ||
(other.v6Addr == null && v6Addr != null) || (other.v6Addr == null && v6Addr != null) ||
(other.v6Addr != null && !other.v6Addr.equals(v6Addr))) { (other.v6Addr != null && !other.v6Addr.equals(v6Addr))) {
return false; return false;
} }
return true; return true;
} }
return false; return false;
} }
@Override @Override
public String toString() { public String toString() {
return "["+name+" - "+localAddr+" - "+v6Addr+"]"; return "["+name+" - "+localAddr+" - "+v6Addr+"]";
} }
} }
@@ -21,388 +21,388 @@ import javax.jmdns.impl.NetworkTopologyDiscoveryImpl;
import com.limelight.LimeLog; import com.limelight.LimeLog;
public class MdnsDiscoveryAgent implements ServiceListener { public class MdnsDiscoveryAgent implements ServiceListener {
public static final String SERVICE_TYPE = "_nvstream._tcp.local."; public static final String SERVICE_TYPE = "_nvstream._tcp.local.";
private MdnsDiscoveryListener listener; private MdnsDiscoveryListener listener;
private Thread discoveryThread; private Thread discoveryThread;
private HashMap<InetAddress, MdnsComputer> computers = new HashMap<InetAddress, MdnsComputer>(); private HashMap<InetAddress, MdnsComputer> computers = new HashMap<InetAddress, MdnsComputer>();
private HashSet<String> pendingResolution = new HashSet<String>(); private HashSet<String> pendingResolution = new HashSet<String>();
// The resolver factory's instance member has a static lifetime which // The resolver factory's instance member has a static lifetime which
// means our ref count and listener must be static also. // means our ref count and listener must be static also.
private static int resolverRefCount = 0; private static int resolverRefCount = 0;
private static HashSet<ServiceListener> listeners = new HashSet<ServiceListener>(); private static HashSet<ServiceListener> listeners = new HashSet<ServiceListener>();
private static ServiceListener nvstreamListener = new ServiceListener() { private static ServiceListener nvstreamListener = new ServiceListener() {
@Override @Override
public void serviceAdded(ServiceEvent event) { public void serviceAdded(ServiceEvent event) {
HashSet<ServiceListener> localListeners; HashSet<ServiceListener> localListeners;
// Copy the listener set into a new set so we can invoke // Copy the listener set into a new set so we can invoke
// the callbacks without holding the listeners monitor the // the callbacks without holding the listeners monitor the
// whole time. // whole time.
synchronized (listeners) { synchronized (listeners) {
localListeners = new HashSet<ServiceListener>(listeners); localListeners = new HashSet<ServiceListener>(listeners);
} }
for (ServiceListener listener : localListeners) { for (ServiceListener listener : localListeners) {
listener.serviceAdded(event); listener.serviceAdded(event);
} }
} }
@Override @Override
public void serviceRemoved(ServiceEvent event) { public void serviceRemoved(ServiceEvent event) {
HashSet<ServiceListener> localListeners; HashSet<ServiceListener> localListeners;
// Copy the listener set into a new set so we can invoke // Copy the listener set into a new set so we can invoke
// the callbacks without holding the listeners monitor the // the callbacks without holding the listeners monitor the
// whole time. // whole time.
synchronized (listeners) { synchronized (listeners) {
localListeners = new HashSet<ServiceListener>(listeners); localListeners = new HashSet<ServiceListener>(listeners);
} }
for (ServiceListener listener : localListeners) { for (ServiceListener listener : localListeners) {
listener.serviceRemoved(event); listener.serviceRemoved(event);
} }
} }
@Override @Override
public void serviceResolved(ServiceEvent event) { public void serviceResolved(ServiceEvent event) {
HashSet<ServiceListener> localListeners; HashSet<ServiceListener> localListeners;
// Copy the listener set into a new set so we can invoke // Copy the listener set into a new set so we can invoke
// the callbacks without holding the listeners monitor the // the callbacks without holding the listeners monitor the
// whole time. // whole time.
synchronized (listeners) { synchronized (listeners) {
localListeners = new HashSet<ServiceListener>(listeners); localListeners = new HashSet<ServiceListener>(listeners);
} }
for (ServiceListener listener : localListeners) { for (ServiceListener listener : localListeners) {
listener.serviceResolved(event); listener.serviceResolved(event);
} }
} }
}; };
public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl { public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
@Override @Override
public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) { public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
// This is an copy of jmDNS's implementation, except we omit the multicast check, since // This is an copy of jmDNS's implementation, except we omit the multicast check, since
// it seems at least some devices lie about interfaces not supporting multicast when they really do. // it seems at least some devices lie about interfaces not supporting multicast when they really do.
try { try {
if (!networkInterface.isUp()) { if (!networkInterface.isUp()) {
return false; return false;
} }
/* /*
if (!networkInterface.supportsMulticast()) { if (!networkInterface.supportsMulticast()) {
return false; return false;
} }
*/ */
if (networkInterface.isLoopback()) { if (networkInterface.isLoopback()) {
return false; return false;
} }
return true; return true;
} catch (Exception exception) { } catch (Exception exception) {
return false; return false;
} }
} }
}; };
static { static {
// Override jmDNS's default topology discovery class with ours // Override jmDNS's default topology discovery class with ours
NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() { NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
@Override @Override
public NetworkTopologyDiscovery newNetworkTopologyDiscovery() { public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
return new MyNetworkTopologyDiscovery(); return new MyNetworkTopologyDiscovery();
} }
}); });
} }
private static JmmDNS referenceResolver() { private static JmmDNS referenceResolver() {
synchronized (MdnsDiscoveryAgent.class) { synchronized (MdnsDiscoveryAgent.class) {
JmmDNS instance = JmmDNS.Factory.getInstance(); JmmDNS instance = JmmDNS.Factory.getInstance();
if (++resolverRefCount == 1) { if (++resolverRefCount == 1) {
// This will cause the listener to be invoked for known hosts immediately. // This will cause the listener to be invoked for known hosts immediately.
// JmDNS only supports one listener per service, so we have to do this here // JmDNS only supports one listener per service, so we have to do this here
// with a static listener. // with a static listener.
instance.addServiceListener(SERVICE_TYPE, nvstreamListener); instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
} }
return instance; return instance;
} }
} }
private static void dereferenceResolver() { private static void dereferenceResolver() {
synchronized (MdnsDiscoveryAgent.class) { synchronized (MdnsDiscoveryAgent.class) {
if (--resolverRefCount == 0) { if (--resolverRefCount == 0) {
try { try {
JmmDNS.Factory.close(); JmmDNS.Factory.close();
} catch (IOException e) {} } catch (IOException e) {}
} }
} }
} }
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) { public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
this.listener = listener; this.listener = listener;
} }
private void handleResolvedServiceInfo(ServiceInfo info) { private void handleResolvedServiceInfo(ServiceInfo info) {
synchronized (pendingResolution) { synchronized (pendingResolution) {
pendingResolution.remove(info.getName()); pendingResolution.remove(info.getName());
} }
try { try {
handleServiceInfo(info); handleServiceInfo(info);
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
// Invalid DNS response // Invalid DNS response
LimeLog.info("mDNS: Invalid response for machine: "+info.getName()); LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
return; return;
} }
} }
private Inet6Address getLocalAddress(Inet6Address[] addresses) { private Inet6Address getLocalAddress(Inet6Address[] addresses) {
for (Inet6Address addr : addresses) { for (Inet6Address addr : addresses) {
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) { if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
return addr; return addr;
} }
// fc00::/7 - ULAs // fc00::/7 - ULAs
else if ((addr.getAddress()[0] & 0xfe) == 0xfc) { else if ((addr.getAddress()[0] & 0xfe) == 0xfc) {
return addr; return addr;
} }
} }
return null; return null;
} }
private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) { private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
for (Inet6Address addr : addresses) { for (Inet6Address addr : addresses) {
if (addr.isLinkLocalAddress()) { if (addr.isLinkLocalAddress()) {
LimeLog.info("Found link-local address: "+addr.getHostAddress()); LimeLog.info("Found link-local address: "+addr.getHostAddress());
return addr; return addr;
} }
} }
return null; return null;
} }
private Inet6Address getBestIpv6Address(Inet6Address[] addresses) { private Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
// First try to find a link local address, so we can match the interface identifier // First try to find a link local address, so we can match the interface identifier
// with a global address (this will work for SLAAC but not DHCPv6). // with a global address (this will work for SLAAC but not DHCPv6).
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses); Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
// We will try once to match a SLAAC interface suffix, then // We will try once to match a SLAAC interface suffix, then
// pick the first matching address // pick the first matching address
for (int tries = 0; tries < 2; tries++) { for (int tries = 0; tries < 2; tries++) {
// We assume the addresses are already sorted in descending order // We assume the addresses are already sorted in descending order
// of preference from Bonjour. // of preference from Bonjour.
for (Inet6Address addr : addresses) { for (Inet6Address addr : addresses) {
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isLoopbackAddress()) { if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
// Link-local, site-local, and loopback aren't global // Link-local, site-local, and loopback aren't global
LimeLog.info("Ignoring non-global address: "+addr.getHostAddress()); LimeLog.info("Ignoring non-global address: "+addr.getHostAddress());
continue; continue;
} }
byte[] addrBytes = addr.getAddress(); byte[] addrBytes = addr.getAddress();
// 2002::/16 // 2002::/16
if (addrBytes[0] == 0x20 && addrBytes[1] == 0x02) { if (addrBytes[0] == 0x20 && addrBytes[1] == 0x02) {
// 6to4 has horrible performance // 6to4 has horrible performance
LimeLog.info("Ignoring 6to4 address: "+addr.getHostAddress()); LimeLog.info("Ignoring 6to4 address: "+addr.getHostAddress());
continue; continue;
} }
// 2001::/32 // 2001::/32
else if (addrBytes[0] == 0x20 && addrBytes[1] == 0x01 && addrBytes[2] == 0x00 && addrBytes[3] == 0x00) { else if (addrBytes[0] == 0x20 && addrBytes[1] == 0x01 && addrBytes[2] == 0x00 && addrBytes[3] == 0x00) {
// Teredo also has horrible performance // Teredo also has horrible performance
LimeLog.info("Ignoring Teredo address: "+addr.getHostAddress()); LimeLog.info("Ignoring Teredo address: "+addr.getHostAddress());
continue; continue;
} }
// fc00::/7 // fc00::/7
else if ((addrBytes[0] & 0xfe) == 0xfc) { else if ((addrBytes[0] & 0xfe) == 0xfc) {
// ULAs aren't global // ULAs aren't global
LimeLog.info("Ignoring ULA: "+addr.getHostAddress()); LimeLog.info("Ignoring ULA: "+addr.getHostAddress());
continue; continue;
} }
// Compare the final 64-bit interface identifier and skip the address // Compare the final 64-bit interface identifier and skip the address
// if it doesn't match our link-local address. // if it doesn't match our link-local address.
if (linkLocalAddr != null && tries == 0) { if (linkLocalAddr != null && tries == 0) {
boolean matched = true; boolean matched = true;
for (int i = 8; i < 16; i++) { for (int i = 8; i < 16; i++) {
if (linkLocalAddr.getAddress()[i] != addr.getAddress()[i]) { if (linkLocalAddr.getAddress()[i] != addr.getAddress()[i]) {
matched = false; matched = false;
break; break;
} }
} }
if (!matched) { if (!matched) {
LimeLog.info("Ignoring non-matching global address: "+addr.getHostAddress()); LimeLog.info("Ignoring non-matching global address: "+addr.getHostAddress());
continue; continue;
} }
} }
return addr; return addr;
} }
} }
return null; return null;
} }
private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException { private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
Inet4Address v4Addrs[] = info.getInet4Addresses(); Inet4Address v4Addrs[] = info.getInet4Addresses();
Inet6Address v6Addrs[] = info.getInet6Addresses(); Inet6Address v6Addrs[] = info.getInet6Addresses();
LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses"); LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses");
LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses"); LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses");
Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs); Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
// Add a computer object for each IPv4 address reported by the PC // Add a computer object for each IPv4 address reported by the PC
for (Inet4Address v4Addr : v4Addrs) { for (Inet4Address v4Addr : v4Addrs) {
synchronized (computers) { synchronized (computers) {
MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr); MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr);
if (computers.put(computer.getLocalAddress(), computer) == null) { if (computers.put(computer.getLocalAddress(), computer) == null) {
// This was a new entry // This was a new entry
listener.notifyComputerAdded(computer); listener.notifyComputerAdded(computer);
} }
} }
} }
// If there were no IPv4 addresses, use IPv6 for registration // If there were no IPv4 addresses, use IPv6 for registration
if (v4Addrs.length == 0) { if (v4Addrs.length == 0) {
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs); Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
if (v6LocalAddr != null || v6GlobalAddr != null) { if (v6LocalAddr != null || v6GlobalAddr != null) {
MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr); MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr);
if (computers.put(v6LocalAddr != null ? if (computers.put(v6LocalAddr != null ?
computer.getLocalAddress() : computer.getIpv6Address(), computer) == null) { computer.getLocalAddress() : computer.getIpv6Address(), computer) == null) {
// This was a new entry // This was a new entry
listener.notifyComputerAdded(computer); listener.notifyComputerAdded(computer);
} }
} }
} }
} }
public void startDiscovery(final int discoveryIntervalMs) { public void startDiscovery(final int discoveryIntervalMs) {
// Kill any existing discovery before starting a new one // Kill any existing discovery before starting a new one
stopDiscovery(); stopDiscovery();
// Add our listener to the set // Add our listener to the set
synchronized (listeners) { synchronized (listeners) {
listeners.add(MdnsDiscoveryAgent.this); listeners.add(MdnsDiscoveryAgent.this);
} }
discoveryThread = new Thread() { discoveryThread = new Thread() {
@Override @Override
public void run() { public void run() {
// This may result in listener callbacks so we must register // This may result in listener callbacks so we must register
// our listener first. // our listener first.
JmmDNS resolver = referenceResolver(); JmmDNS resolver = referenceResolver();
try { try {
while (!Thread.interrupted()) { while (!Thread.interrupted()) {
// Start an mDNS request // Start an mDNS request
resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs); resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
// Run service resolution again for pending machines // Run service resolution again for pending machines
ArrayList<String> pendingNames; ArrayList<String> pendingNames;
synchronized (pendingResolution) { synchronized (pendingResolution) {
pendingNames = new ArrayList<String>(pendingResolution); pendingNames = new ArrayList<String>(pendingResolution);
} }
for (String name : pendingNames) { for (String name : pendingNames) {
LimeLog.info("mDNS: Retrying service resolution for machine: "+name); LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500); ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
if (infos != null && infos.length != 0) { if (infos != null && infos.length != 0) {
LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries"); LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
for (ServiceInfo svcinfo : infos) { for (ServiceInfo svcinfo : infos) {
handleResolvedServiceInfo(svcinfo); handleResolvedServiceInfo(svcinfo);
} }
} }
} }
// Wait for the next polling interval // Wait for the next polling interval
try { try {
Thread.sleep(discoveryIntervalMs); Thread.sleep(discoveryIntervalMs);
} catch (InterruptedException e) { } catch (InterruptedException e) {
break; break;
} }
} }
} }
finally { finally {
// Dereference the resolver // Dereference the resolver
dereferenceResolver(); dereferenceResolver();
} }
} }
}; };
discoveryThread.setName("mDNS Discovery Thread"); discoveryThread.setName("mDNS Discovery Thread");
discoveryThread.start(); discoveryThread.start();
} }
public void stopDiscovery() { public void stopDiscovery() {
// Remove our listener from the set // Remove our listener from the set
synchronized (listeners) { synchronized (listeners) {
listeners.remove(MdnsDiscoveryAgent.this); listeners.remove(MdnsDiscoveryAgent.this);
} }
// If there's already a running thread, interrupt it // If there's already a running thread, interrupt it
if (discoveryThread != null) { if (discoveryThread != null) {
discoveryThread.interrupt(); discoveryThread.interrupt();
discoveryThread = null; discoveryThread = null;
} }
} }
public List<MdnsComputer> getComputerSet() { public List<MdnsComputer> getComputerSet() {
synchronized (computers) { synchronized (computers) {
return new ArrayList<MdnsComputer>(computers.values()); return new ArrayList<MdnsComputer>(computers.values());
} }
} }
@Override @Override
public void serviceAdded(ServiceEvent event) { public void serviceAdded(ServiceEvent event) {
LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName()); LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500); ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
if (info == null) { if (info == null) {
// This machine is pending resolution // This machine is pending resolution
synchronized (pendingResolution) { synchronized (pendingResolution) {
pendingResolution.add(event.getInfo().getName()); pendingResolution.add(event.getInfo().getName());
} }
return; return;
} }
LimeLog.info("mDNS: Resolved (blocking)"); LimeLog.info("mDNS: Resolved (blocking)");
handleResolvedServiceInfo(info); handleResolvedServiceInfo(info);
} }
@Override @Override
public void serviceRemoved(ServiceEvent event) { public void serviceRemoved(ServiceEvent event) {
LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName()); LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
Inet4Address v4Addrs[] = event.getInfo().getInet4Addresses(); Inet4Address v4Addrs[] = event.getInfo().getInet4Addresses();
for (Inet4Address addr : v4Addrs) { for (Inet4Address addr : v4Addrs) {
synchronized (computers) { synchronized (computers) {
MdnsComputer computer = computers.remove(addr); MdnsComputer computer = computers.remove(addr);
if (computer != null) { if (computer != null) {
listener.notifyComputerRemoved(computer); listener.notifyComputerRemoved(computer);
break; break;
} }
} }
} }
Inet6Address v6Addrs[] = event.getInfo().getInet6Addresses(); Inet6Address v6Addrs[] = event.getInfo().getInet6Addresses();
for (Inet6Address addr : v6Addrs) { for (Inet6Address addr : v6Addrs) {
synchronized (computers) { synchronized (computers) {
MdnsComputer computer = computers.remove(addr); MdnsComputer computer = computers.remove(addr);
if (computer != null) { if (computer != null) {
listener.notifyComputerRemoved(computer); listener.notifyComputerRemoved(computer);
break; break;
} }
} }
} }
} }
@Override @Override
public void serviceResolved(ServiceEvent event) { public void serviceResolved(ServiceEvent event) {
// We handle this synchronously // We handle this synchronously
} }
} }
@@ -1,7 +1,7 @@
package com.limelight.nvstream.mdns; package com.limelight.nvstream.mdns;
public interface MdnsDiscoveryListener { public interface MdnsDiscoveryListener {
void notifyComputerAdded(MdnsComputer computer); void notifyComputerAdded(MdnsComputer computer);
void notifyComputerRemoved(MdnsComputer computer); void notifyComputerRemoved(MdnsComputer computer);
void notifyDiscoveryFailure(Exception e); void notifyDiscoveryFailure(Exception e);
} }
@@ -10,90 +10,90 @@ import com.limelight.LimeLog;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
public class WakeOnLanSender { public class WakeOnLanSender {
private static final int[] PORTS_TO_TRY = new int[] { private static final int[] PORTS_TO_TRY = new int[] {
7, 9, // Standard WOL ports 7, 9, // Standard WOL ports
47998, 47999, 48000, 48002, 48010 // Ports opened by GFE 47998, 47999, 48000, 48002, 48010 // Ports opened by GFE
}; };
public static void sendWolPacket(ComputerDetails computer) throws IOException { public static void sendWolPacket(ComputerDetails computer) throws IOException {
DatagramSocket sock = new DatagramSocket(0); DatagramSocket sock = new DatagramSocket(0);
byte[] payload = createWolPayload(computer); byte[] payload = createWolPayload(computer);
IOException lastException = null; IOException lastException = null;
boolean sentWolPacket = false; boolean sentWolPacket = false;
try { try {
// Try all resolved remote and local addresses and IPv4 broadcast address. // Try all resolved remote and local addresses and IPv4 broadcast address.
// The broadcast address is required to avoid stale ARP cache entries // The broadcast address is required to avoid stale ARP cache entries
// making the sleeping machine unreachable. // making the sleeping machine unreachable.
for (String unresolvedAddress : new String[] { for (String unresolvedAddress : new String[] {
computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255" computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255"
}) { }) {
if (unresolvedAddress == null) { if (unresolvedAddress == null) {
continue; continue;
} }
try { try {
for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) { for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) {
// Try all the ports for each resolved address // Try all the ports for each resolved address
for (int port : PORTS_TO_TRY) { for (int port : PORTS_TO_TRY) {
DatagramPacket dp = new DatagramPacket(payload, payload.length); DatagramPacket dp = new DatagramPacket(payload, payload.length);
dp.setAddress(resolvedAddress); dp.setAddress(resolvedAddress);
dp.setPort(port); dp.setPort(port);
sock.send(dp); sock.send(dp);
sentWolPacket = true; sentWolPacket = true;
} }
} }
} catch (IOException e) { } catch (IOException e) {
// We may have addresses that don't resolve on this subnet, // We may have addresses that don't resolve on this subnet,
// but don't throw and exit the whole function if that happens. // but don't throw and exit the whole function if that happens.
// We'll throw it at the end if we didn't send a single packet. // We'll throw it at the end if we didn't send a single packet.
e.printStackTrace(); e.printStackTrace();
lastException = e; lastException = e;
} }
} }
} finally { } finally {
sock.close(); sock.close();
} }
// Propagate the DNS resolution exception if we didn't // Propagate the DNS resolution exception if we didn't
// manage to get a single packet out to the host. // manage to get a single packet out to the host.
if (!sentWolPacket && lastException != null) { if (!sentWolPacket && lastException != null) {
throw lastException; throw lastException;
} }
} }
private static byte[] macStringToBytes(String macAddress) { private static byte[] macStringToBytes(String macAddress) {
byte[] macBytes = new byte[6]; byte[] macBytes = new byte[6];
@SuppressWarnings("resource") @SuppressWarnings("resource")
Scanner scan = new Scanner(macAddress).useDelimiter(":"); Scanner scan = new Scanner(macAddress).useDelimiter(":");
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) { for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
try { try {
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16); macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
LimeLog.warning("Malformed MAC address: "+macAddress+" (index: "+i+")"); LimeLog.warning("Malformed MAC address: "+macAddress+" (index: "+i+")");
break; break;
} }
} }
scan.close(); scan.close();
return macBytes; return macBytes;
} }
private static byte[] createWolPayload(ComputerDetails computer) { private static byte[] createWolPayload(ComputerDetails computer) {
byte[] payload = new byte[102]; byte[] payload = new byte[102];
byte[] macAddress = macStringToBytes(computer.macAddress); byte[] macAddress = macStringToBytes(computer.macAddress);
int i; int i;
// 6 bytes of FF // 6 bytes of FF
for (i = 0; i < 6; i++) { for (i = 0; i < 6; i++) {
payload[i] = (byte)0xFF; payload[i] = (byte)0xFF;
} }
// 16 repetitions of the MAC address // 16 repetitions of the MAC address
for (int j = 0; j < 16; j++) { for (int j = 0; j < 16; j++) {
System.arraycopy(macAddress, 0, payload, i, macAddress.length); System.arraycopy(macAddress, 0, payload, i, macAddress.length);
i += macAddress.length; i += macAddress.length;
} }
return payload; return payload;
} }
} }
@@ -0,0 +1,32 @@
package com.limelight.utils;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.Build;
public class NetHelper {
public static boolean isActiveNetworkVpn(Context context) {
ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network activeNetwork = connMgr.getActiveNetwork();
if (activeNetwork != null) {
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(activeNetwork);
if (netCaps != null) {
return netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
!netCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
}
}
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
if (activeNetworkInfo != null) {
return activeNetworkInfo.getType() == ConnectivityManager.TYPE_VPN;
}
}
return false;
}
}
@@ -68,17 +68,6 @@ public class UiHelper {
View rootView = activity.findViewById(android.R.id.content); View rootView = activity.findViewById(android.R.id.content);
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE); UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
{
// Increase view padding on TVs
float scale = activity.getResources().getDisplayMetrics().density;
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Allow this non-streaming activity to layout under notches. // Allow this non-streaming activity to layout under notches.
// //
@@ -89,7 +78,16 @@ public class UiHelper {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
// Increase view padding on TVs
float scale = activity.getResources().getDisplayMetrics().density;
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Draw under the status bar on Android Q devices // Draw under the status bar on Android Q devices
// Using getDecorView() here breaks the translucent status/navigation bar when gestures are disabled // Using getDecorView() here breaks the translucent status/navigation bar when gestures are disabled
@@ -1,47 +1,47 @@
package com.limelight.utils; package com.limelight.utils;
public class Vector2d { public class Vector2d {
private float x; private float x;
private float y; private float y;
private double magnitude; private double magnitude;
public static final Vector2d ZERO = new Vector2d(); public static final Vector2d ZERO = new Vector2d();
public Vector2d() { public Vector2d() {
initialize(0, 0); initialize(0, 0);
} }
public void initialize(float x, float y) { public void initialize(float x, float y) {
this.x = x; this.x = x;
this.y = y; this.y = y;
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
} }
public double getMagnitude() { public double getMagnitude() {
return magnitude; return magnitude;
} }
public void getNormalized(Vector2d vector) { public void getNormalized(Vector2d vector) {
vector.initialize((float)(x / magnitude), (float)(y / magnitude)); vector.initialize((float)(x / magnitude), (float)(y / magnitude));
} }
public void scalarMultiply(double factor) { public void scalarMultiply(double factor) {
initialize((float)(x * factor), (float)(y * factor)); initialize((float)(x * factor), (float)(y * factor));
} }
public void setX(float x) { public void setX(float x) {
initialize(x, this.y); initialize(x, this.y);
} }
public void setY(float y) { public void setY(float y) {
initialize(this.x, y); initialize(this.x, y);
} }
public float getX() { public float getX() {
return x; return x;
} }
public float getY() { public float getY() {
return y; return y;
} }
} }
+3 -2
View File
@@ -80,7 +80,7 @@ Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) {
BridgeDrStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStop", "()V"); BridgeDrStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStop", "()V");
BridgeDrCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrCleanup", "()V"); BridgeDrCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrCleanup", "()V");
BridgeDrSubmitDecodeUnitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrSubmitDecodeUnit", "([BIIIJ)I"); BridgeDrSubmitDecodeUnitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrSubmitDecodeUnit", "([BIIIJ)I");
BridgeArInitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArInit", "(I)I"); BridgeArInitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArInit", "(III)I");
BridgeArStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStart", "()V"); BridgeArStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStart", "()V");
BridgeArStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStop", "()V"); BridgeArStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStop", "()V");
BridgeArCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArCleanup", "()V"); BridgeArCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArCleanup", "()V");
@@ -206,7 +206,7 @@ int BridgeArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusCon
return -1; return -1;
} }
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeArInitMethod, audioConfiguration); err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeArInitMethod, audioConfiguration, opusConfig->sampleRate, opusConfig->samplesPerFrame);
if ((*env)->ExceptionCheck(env)) { if ((*env)->ExceptionCheck(env)) {
err = -1; err = -1;
} }
@@ -382,6 +382,7 @@ static AUDIO_RENDERER_CALLBACKS BridgeAudioRendererCallbacks = {
.stop = BridgeArStop, .stop = BridgeArStop,
.cleanup = BridgeArCleanup, .cleanup = BridgeArCleanup,
.decodeAndPlaySample = BridgeArDecodeAndPlaySample, .decodeAndPlaySample = BridgeArDecodeAndPlaySample,
.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION
}; };
static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = { static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = {
+2 -2
View File
@@ -83,8 +83,8 @@ Java_com_limelight_nvstream_jni_MoonBridge_findExternalAddressIP4(JNIEnv *env, j
} }
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Java_com_limelight_nvstream_jni_MoonBridge_getPendingAudioFrames(JNIEnv *env, jclass clazz) { Java_com_limelight_nvstream_jni_MoonBridge_getPendingAudioDuration(JNIEnv *env, jclass clazz) {
return LiGetPendingAudioFrames(); return LiGetPendingAudioDuration();
} }
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+8 -8
View File
@@ -8,14 +8,6 @@
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="75dp"
android:layout_height="75dp"
android:indeterminate="true">
</ProgressBar>
<ImageView <ImageView
android:id="@+id/grid_image" android:id="@+id/grid_image"
android:cropToPadding="false" android:cropToPadding="false"
@@ -24,6 +16,14 @@
android:layout_width="150dp" android:layout_width="150dp"
android:layout_height="175dp"> android:layout_height="175dp">
</ImageView> </ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="75dp"
android:layout_height="75dp"
android:indeterminate="true">
</ProgressBar>
<ImageView <ImageView
android:id="@+id/grid_overlay" android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
@@ -8,14 +8,6 @@
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="50dp"
android:layout_height="50dp"
android:indeterminate="true">
</ProgressBar>
<ImageView <ImageView
android:id="@+id/grid_image" android:id="@+id/grid_image"
android:cropToPadding="false" android:cropToPadding="false"
@@ -24,6 +16,14 @@
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="117dp"> android:layout_height="117dp">
</ImageView> </ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="50dp"
android:layout_height="50dp"
android:indeterminate="true">
</ProgressBar>
<ImageView <ImageView
android:id="@+id/grid_overlay" android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
@@ -1,5 +1,5 @@
package com.limelight; package com.limelight;
public class LimelightBuildProps { public class LimelightBuildProps {
public static final boolean ROOT_BUILD = false; public static final boolean ROOT_BUILD = false;
} }
@@ -1,5 +1,5 @@
package com.limelight; package com.limelight;
public class LimelightBuildProps { public class LimelightBuildProps {
public static final boolean ROOT_BUILD = true; public static final boolean ROOT_BUILD = true;
} }
+1 -1
View File
@@ -5,7 +5,7 @@ buildscript {
google() google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.0' classpath 'com.android.tools.build:gradle:3.5.2'
} }
} }
@@ -1 +1 @@
Spiele vom deinem PC auf Android spielen (nur NVIDIA) Spiele von deinem PC auf Android spielen (nur NVIDIA)
@@ -0,0 +1,4 @@
- Fixed various UI bugs on foldable Android devices
- Fixed connecting to a PC with multiple network connections
- Fixed overscan padding on Android TV 10
- Fixed gamepad back buttons not working on the ASUS Tinker Board
@@ -0,0 +1,4 @@
- Fixed DualShock 4 mapping on devices running 4.14+ kernels
- Improved support for wired Xbox 360/One controllers
- Fixed crash using certain controllers without analog triggers
- Enabled streaming on Android-x86
@@ -0,0 +1,4 @@
- Optimized for new devices launching with Android 10
- Added a workaround to avoid video lag on the Pixel 4
- Fixed duplicate gamepads being created when using a USB Xbox One gamepad
- Fixed crashes on Sony Bravia Android TV devices
@@ -0,0 +1,3 @@
- Fixed false touch events when using the back gesture on Android 10
- Fixed external IP address detection with certain VPN apps
- Display a placeholder image when box art is loading
@@ -0,0 +1,4 @@
- Fixed RTSP handshake error when streaming from certain networks
- Improved performance when streaming over a VPN
- Reduced audio bandwidth usage when streaming over low speed connections
- Fixed hitbox of on-screen analog sticks