Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e50b7076a1 | |||
| 36ab5aa1b6 | |||
| a0a2b299d9 | |||
| 14d354fc29 | |||
| 342515f916 | |||
| 5f5944c237 | |||
| c025432ad6 | |||
| 171a6437fe | |||
| 11b3648fac | |||
| d1fae89d6d | |||
| 5c06848fe9 | |||
| b50e506e58 | |||
| 59fafa163d | |||
| 22d84b5763 | |||
| 6d186892a8 | |||
| 88d6143897 | |||
| b729fba75e | |||
| c0d3f9fa48 | |||
| af5e7a0e33 | |||
| 371d96ea65 | |||
| e9e332ff85 | |||
| e133ac2815 | |||
| 1dba5d147e | |||
| 1616c0b022 | |||
| bcee2cf0e3 | |||
| 3e7ddab0e9 | |||
| 5da0177356 | |||
| 7e21638811 | |||
| db5b7ab867 | |||
| 3bcc1c84bb | |||
| d46053f8d6 | |||
| 00a5fed9e9 | |||
| b6315a715a | |||
| 0da8303468 | |||
| c821c4684f | |||
| 6bae33f822 | |||
| 08d4ab67a6 | |||
| 62203d2f21 | |||
| 4968dcc558 | |||
| 6d66d1371f | |||
| b87ca71103 | |||
| c251cd2e8f | |||
| 593616d2d9 |
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-27
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Submodule app/src/main/jni/moonlight-core/moonlight-common-c updated: 438b4f87d3...f5ae5df5d0
@@ -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,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
@@ -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
|
||||||
Reference in New Issue
Block a user